forked from 1b-agent/opencadstudio-upstream
feat(block-editor): add Test Block preview
Preview the current unsaved block in a disposable drawing with independent history. Keep normal Draw tools available, append Close Test Block, and return to the original editor without saving. Guard persistence paths and handle refresh and parent-close lifecycle by stable document identity.
This commit is contained in:
1 parent
0a29be07f7
commit
9632979d09
22 files changed
+801
-65
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
|
||||
|
||||
Reference in new issue
Block a user