forked from 1b-agent/opencadstudio-upstream
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:
commit
99e30f1548
33 files changed
+1660
-199
No files matched your search
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user