Merge remote-tracking branch 'origin/main' into codex/block-definition-command-line

# Conflicts:
#	src/app/commands/inquiry.rs
#	src/modules/draw/modify/block_edit.rs
This commit is contained in:
ramox81 committed 2026-10-14 17:51:56 +03:00
commit 99e30f1548
33 files changed
+1660 -199

No files matched your search

+14 -6
View File
@@ -434,6 +434,9 @@ const CODES: &[(&str, &str, &str)] = &[
("block_editor", "Open and Save", "BO"),
("block_editor", "BEDIT", "BE"),
("block_editor", "BSAVE", "BS"),
("block_editor", "BTESTBLOCK", "TB"),
("draw", "Close", "TC"),
("draw", "BCLOSETESTBLOCK", "CT"),
("block_editor", "ATTDEF", "AD"),
("block_editor", "AUTOCONSTRAIN", "AC"),
("block_editor", "CCONSTRAINT", "CI"),
@@ -631,7 +634,7 @@ impl Ribbon {
};
let scope = module.id();
let mut tips = Vec::new();
for group in module.ribbon_groups() {
for group in self.groups_for(index) {
for item in &group.tools {
match item {
RibbonItem::Tool(tool)
@@ -709,11 +712,16 @@ impl Ribbon {
fn item_tips(&self, index: usize, open: &str) -> Vec<Tip> {
let mut items = draw_panel::keytip_items(open);
if items.is_empty() {
let groups = self.modules.get(index).map(|m| m.ribbon_groups()).unwrap_or(&[]);
for item in groups.iter().flat_map(|group| &group.tools) {
if let RibbonItem::Dropdown { id, items: list, .. }
| RibbonItem::LabeledDropdown { id, items: list, .. }
| RibbonItem::LargeDropdown { id, items: list, .. } = item
for item in self.groups_for(index).flat_map(|group| &group.tools) {
if let RibbonItem::Dropdown {
id, items: list, ..
}
| RibbonItem::LabeledDropdown {
id, items: list, ..
}
| RibbonItem::LargeDropdown {
id, items: list, ..
} = item
{
if *id == open {
items = list
+6 -15
View File
@@ -68,15 +68,11 @@ impl Ribbon {
fn entries(&self) -> impl Iterator<Item = (usize, Entry<'_>)> {
let order = std::iter::once(self.active)
.chain((0..self.modules.len()).filter(move |i| *i != self.active));
order
.filter_map(|i| self.modules.get(i).map(|m| (i, m)))
.flat_map(|(i, module)| {
module
.ribbon_groups()
.iter()
.flat_map(group_entries)
.map(move |entry| (i, entry))
})
order.flat_map(|i| {
self.groups_for(i)
.flat_map(group_entries)
.map(move |entry| (i, entry))
})
}
/// The button id that runs the command line `cmd`: an exact match first,
@@ -111,12 +107,7 @@ impl Ribbon {
/// module has no tab (its tools are on the side toolbar).
pub(super) fn tab_holding_active_tool(&self) -> Option<usize> {
let id = self.active_tool.as_deref()?;
let holds = |i: usize| {
self.modules[i]
.ribbon_groups()
.iter()
.any(|group| group_holds(group, id))
};
let holds = |i: usize| self.groups_for(i).any(|group| group_holds(group, id));
if self
.modules
.get(self.active)
+101 -33
View File
@@ -126,6 +126,7 @@ pub struct Ribbon {
/// A block editor session is open: the tab that was active before the
/// Block Editor tab took over.
block_editing: Option<usize>,
block_testing: Option<usize>,
/// XDWGFADECTL as the Reference slide-out shows it (negative = off).
pub xref_fade: i32,
/// Open key tips (Alt).
@@ -231,6 +232,7 @@ impl Ribbon {
xref_ctx: false,
point_cloud_ctx: None,
block_editing: None,
block_testing: None,
xref_fade: 50,
keytips: None,
}
@@ -328,6 +330,40 @@ impl Ribbon {
}
}
pub fn set_block_testing(&mut self, testing: bool) {
if testing != self.block_testing.is_some() {
self.close_dropdown();
let _ = self.hide_keytips();
}
let Some(index) = self.modules.iter().position(|module| module.id() == "draw") else {
return;
};
match (testing, self.block_testing) {
(true, None) => {
self.block_testing = Some(self.active);
self.active = index;
}
(false, Some(previous)) => {
self.block_testing = None;
if self.active == index {
self.active = previous.min(self.modules.len().saturating_sub(1));
}
}
_ => {}
}
}
/// Contextual panels share their normal tab with the drawing tools.
fn groups_for(&self, index: usize) -> impl Iterator<Item = &RibbonGroup> {
let testing = self.block_testing.is_some();
self.modules.get(index).into_iter().flat_map(move |module| {
module
.ribbon_groups()
.iter()
.filter(move |group| module.id() != "draw" || group.title != "Close" || testing)
})
}
pub fn select(&mut self, index: usize) {
if index < self.modules.len() {
self.active = index;
@@ -733,38 +769,36 @@ impl Ribbon {
} else {
self.active
};
let tool_area: Element<'_, Message> =
if let Some(module) = self.modules.get(effective_active) {
let groups = module.ribbon_groups();
let style_ctx = StyleContext {
text_style_names: &self.text_style_names,
active_text_style: &self.active_text_style,
dim_style_names: &self.dim_style_names,
active_dim_style: &self.active_dim_style,
mleader_style_names: &self.mleader_style_names,
active_mleader_style: &self.active_mleader_style,
table_style_names: &self.table_style_names,
active_table_style: &self.active_table_style,
};
// Adaptive tool area: panels sit on one row; when they don't all
// fit they degrade from the right — a panel's large buttons first
// shrink to compact icon columns, then it collapses to a ▾ flyout
// button. See `CollapsePanels`.
let ts = self.toggle_state(show_block_palette);
let panels: Vec<Panel<'_>> = groups
.iter()
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
.into()
} else {
text("").into()
let tool_area: Element<'_, Message> = if self.modules.get(effective_active).is_some() {
let groups = self.groups_for(effective_active);
let style_ctx = StyleContext {
text_style_names: &self.text_style_names,
active_text_style: &self.active_text_style,
dim_style_names: &self.dim_style_names,
active_dim_style: &self.active_dim_style,
mleader_style_names: &self.mleader_style_names,
active_mleader_style: &self.active_mleader_style,
table_style_names: &self.table_style_names,
active_table_style: &self.active_table_style,
};
// Adaptive tool area: panels sit on one row; when they don't all
// fit they degrade from the right — a panel's large buttons first
// shrink to compact icon columns, then it collapses to a ▾ flyout
// button. See `CollapsePanels`.
let ts = self.toggle_state(show_block_palette);
let panels: Vec<Panel<'_>> = groups
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
.into()
} else {
text("").into()
};
let tool_bar: Element<'_, Message> = container(tool_area)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weakest.color)),
@@ -931,8 +965,7 @@ impl Ribbon {
return Some(ov);
}
let module = self.modules.get(self.active)?;
let groups = module.ribbon_groups();
let groups = self.groups_for(self.active);
let mut items_list: Option<Vec<(&'static str, &'static str, IconKind)>> = None;
let mut dd_default = "";
let mut dd_id: &'static str = "";
@@ -1161,7 +1194,7 @@ impl Ribbon {
/// Draw-tab dropdowns — so the list grows to fit its entries instead of
/// being clipped to the ribbon-row height. (#153)
fn style_combo_overlay(&self, open_id: &str, win: (f32, f32)) -> Option<Element<'_, Message>> {
let groups = self.modules.get(self.active)?.ribbon_groups();
let groups = self.groups_for(self.active);
// Locate the open style combo; capture its style key + manager command.
let mut found: Option<(crate::modules::StyleKey, Option<&'static str>)> = None;
@@ -1677,6 +1710,41 @@ mod tests {
use super::widgets::is_active_tool;
use super::Ribbon;
#[test]
fn block_test_close_panel_extends_draw_only_while_testing() {
let mut ribbon = Ribbon::default();
let draw = ribbon
.modules
.iter()
.position(|module| module.id() == "draw")
.unwrap();
assert!(!ribbon
.modules
.iter()
.any(|module| module.id() == "block_test"));
let ordinary: Vec<_> = ribbon.groups_for(draw).map(|group| group.title).collect();
assert!(!ordinary.contains(&"Close"));
ribbon.set_block_editing(true);
ribbon.set_block_editing(false);
ribbon.set_block_testing(true);
assert_eq!(ribbon.active, draw);
let testing: Vec<_> = ribbon.groups_for(draw).map(|group| group.title).collect();
assert_eq!(&testing[..ordinary.len()], ordinary.as_slice());
assert_eq!(testing.last(), Some(&"Close"));
ribbon.toggle_collapsed_panel("Close");
ribbon.set_block_testing(false);
assert!(ribbon.collapsed_open.is_none());
assert_eq!(
ribbon
.groups_for(draw)
.map(|group| group.title)
.collect::<Vec<_>>(),
ordinary
);
ribbon.set_block_editing(true);
assert_eq!(ribbon.modules[ribbon.active].id(), "block_editor");
}
#[test]
fn block_palette_highlight_follows_the_app_state_passed_to_the_view() {
// The Block Palette button highlight is derived from the app's
+122
View File
@@ -0,0 +1,122 @@
use crate::app::Message;
use crate::scene::WireModel;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{hdivider, vsep};
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Element, Fill, Length, Theme};
pub(crate) struct State {
pub document_id: u64,
pub name: String,
pub existing_names: Vec<String>,
pub preview: Vec<WireModel>,
pub description: String,
pub error: Option<String>,
}
fn list_item_style(selected: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |theme: &Theme, status| {
if selected {
button::primary(theme, status)
} else {
button::subtle(theme, status)
}
}
}
pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let can_apply = state.error.is_none() && !state.name.trim().is_empty();
// ── Left: Block name field and list of existing blocks ─────────────────────
let name_input = text_input(crate::t!("Block name").as_ref(), &state.name)
.on_input(Message::BlockEditPickerName)
.on_submit_maybe(can_apply.then_some(Message::BlockEditPickerApply))
.style(crate::ui::style::form::field_style);
let list_items: Vec<Element<'_, Message>> = state
.existing_names
.iter()
.map(|name| {
let is_sel = name.eq_ignore_ascii_case(state.name.trim());
button(text(name.clone()).size(12))
.on_press(Message::BlockEditPickerSelect(name.clone()))
.style(list_item_style(is_sel))
.padding([4, 8])
.width(Fill)
.into()
})
.collect();
let list_scrollable = container(scrollable(column(list_items).spacing(2)).height(Fill))
.style(container::bordered_box)
.padding(2)
.height(Fill);
let left_col = column![
text(crate::t!("Block to create or edit").into_owned()).size(12),
name_input,
list_scrollable,
]
.spacing(8)
.width(Length::Fixed(240.0))
.height(Fill);
// ── Right: Preview and Description ─────────────────────────────────────────
let preview_box = column![
text(crate::t!("Preview").into_owned()).size(12),
container(super::block_palette::preview_canvas(&state.preview, 150.0))
.style(container::bordered_box)
.width(Fill)
.height(Length::Fixed(150.0)),
]
.spacing(4);
let desc_text = if state.description.is_empty() {
crate::t!("(No description)").into_owned()
} else {
state.description.clone()
};
let desc_box = column![
text(crate::t!("Description").into_owned()).size(12),
container(scrollable(text(desc_text).size(11).style(muted_style)).height(Fill))
.style(container::bordered_box)
.padding(6)
.width(Fill)
.height(Fill),
]
.spacing(4)
.height(Fill);
let right_col = column![preview_box, desc_box]
.spacing(8)
.width(Fill)
.height(Fill);
// ── Body ───────────────────────────────────────────────────────────────────
let body = row![left_col, vsep(sizing.height), right_col]
.spacing(12)
.height(Fill);
// ── Bottom: Error message and OK / Cancel buttons ───────────────────────────
let error_text = text(state.error.clone().unwrap_or_default())
.size(11)
.style(text::danger);
let buttons = row![
error_text,
Space::new().width(Fill),
button(text(crate::t!("OK").into_owned()))
.on_press_maybe(can_apply.then_some(Message::BlockEditPickerApply))
.style(button::primary),
button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal),
]
.spacing(8)
.align_y(iced::Center);
container(column![body, hdivider(sizing.width), buttons].spacing(10))
.padding(16)
.width(sizing.width)
.height(sizing.height)
.into()
}
+10
View File
@@ -44,6 +44,7 @@ pub struct DraftingSettingsState {
pub polar_increment_deg: f32,
// Object Snap
pub osnap_on: bool,
pub osnap_aperture_input: String,
pub otrack_on: bool,
pub snap_modes: rustc_hash::FxHashSet<SnapType>,
// 3D Object Snap
@@ -78,6 +79,7 @@ impl DraftingSettingsState {
|| self.ortho_on != saved.ortho_on
|| (self.polar_increment_deg - saved.polar_increment_deg).abs() > 0.001
|| self.osnap_on != saved.osnap_on
|| self.osnap_aperture_input != saved.osnap_aperture_input
|| self.otrack_on != saved.otrack_on
|| self.snap_modes != saved.snap_modes
|| self.osnap3d_on != saved.osnap3d_on
@@ -508,6 +510,14 @@ pub fn view_window<'a>(
column![
row![osnap_on_toggle, Space::new().width(24), otrack_on_toggle].align_y(iced::Center),
row![
text("OSNAP capture radius (pixels)").size(11),
text_input("25", &state.osnap_aperture_input)
.on_input(Message::DraftingSettingsOsnapApertureChanged)
.width(70),
]
.spacing(10)
.align_y(iced::Center),
action_buttons,
Space::new().height(4),
snap_modes_group,
+1
View File
@@ -5,6 +5,7 @@ pub mod attdef_dialog;
pub mod attribute_editor;
pub mod auto_constrain_settings;
pub mod block_definition;
pub mod block_edit_picker;
pub mod block_save_as;
pub mod block_palette;
pub mod browser;