From 73b05e1dec0145f8e967a1fbe08b23f703e72446 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Thu, 2 Jul 2026 20:58:48 +0100 Subject: [PATCH] perf(core): cache ribbon_groups() with OnceLock, avoid per-frame tree allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change CadModule::ribbon_groups() from -> Vec to -> &[RibbonGroup], backed by a per-module OnceLock cache. The ribbon group tree is static data (&'static str, Copy enums) that was being fully reconstructed — Vecs, String clones, enum discriminants — on every call, including all three per-frame call sites (view(), dropdown_overlay(), style_combo_overlay()). - 7 built-in modules + 2 plugin templates cache via function-local OnceLock. Safe because each is a unit struct with exactly one instance per process (plugins run in isolated child processes; see PluginProcess::spawn()). - SharedCadModule avoids the static pattern entirely, storing groups in an instance field (owned.rs). - IPC runner converts &RibbonGroup -> OwnedRibbonGroup directly via new From<&T> impls, avoiding an intermediate clone. - render_small/render_large and the two make_tool_row closures now borrow (&RibbonItem, &[ToolDef]) instead of taking ownership, so the view loop doesn't need to clone items to satisfy the old by-value signatures. - Various match-ergonomics deref fixes (*id, *default, *icon, etc.) from the &RibbonItem pattern change. --- crates/ocs_plugin_api/src/ribbon.rs | 147 ++++++++- crates/ocs_plugin_api/src/ribbon/owned.rs | 95 +++++- crates/ocs_plugin_api/src/runner.rs | 2 +- crates/plugin-template-api2/src/lib.rs | 5 +- docs/plugin-template-v2/src/lib.rs | 5 +- src/modules/annotate/mod.rs | 373 ++++++++++----------- src/modules/draw/mod.rs | 375 +++++++++++----------- src/modules/insert/mod.rs | 175 +++++----- src/modules/layout/mod.rs | 57 ++-- src/modules/manage/mod.rs | 109 ++++--- src/modules/model/mod.rs | 61 ++-- src/modules/view/mod.rs | 183 +++++------ src/ui/ribbon/mod.rs | 12 +- src/ui/ribbon/widgets.rs | 38 +-- 14 files changed, 947 insertions(+), 690 deletions(-) diff --git a/crates/ocs_plugin_api/src/ribbon.rs b/crates/ocs_plugin_api/src/ribbon.rs index fc58ccef..b5f6ca11 100644 --- a/crates/ocs_plugin_api/src/ribbon.rs +++ b/crates/ocs_plugin_api/src/ribbon.rs @@ -138,13 +138,114 @@ pub trait CadModule: Send + Sync { #[allow(dead_code)] fn id(&self) -> &'static str; fn title(&self) -> &'static str; - fn ribbon_groups(&self) -> Vec; + fn ribbon_groups(&self) -> &[RibbonGroup]; } #[cfg(test)] mod tests { use super::*; + #[test] + fn once_lock_eliminates_allocation_after_first_call() { + // Helper to build a realistic module tree (2 groups, ~20 items) + // Benchmark builds this manually for a clean before/after comparison. + fn build_tree() -> Vec { + vec![ + RibbonGroup { + title: "Draw", + tools: (0..10) + .map(|i| { + RibbonItem::Tool(ToolDef { + id: &*Box::leak(format!("TOOL_{i}").into_boxed_str()), + label: &*Box::leak(format!("Tool {i}").into_boxed_str()), + icon: IconKind::Glyph("T"), + event: ModuleEvent::Command(format!("CMD_{i}")), + }) + }) + .collect(), + }, + RibbonGroup { + title: "Modify", + tools: (0..10) + .map(|i| { + RibbonItem::Tool(ToolDef { + id: &*Box::leak(format!("MOD_{i}").into_boxed_str()), + label: &*Box::leak(format!("Mod {i}").into_boxed_str()), + icon: IconKind::Glyph("M"), + event: ModuleEvent::Command(format!("MODIFY_{i}")), + }) + }) + .collect(), + }, + ] + } + + // ── Before: rebuild the tree every call (what every frame used to pay) ── + const N: usize = 10_000; + let start = std::time::Instant::now(); + let mut total_len = 0usize; + for _ in 0..N { + let groups = std::hint::black_box(build_tree()); + total_len += std::hint::black_box(groups.len()); + // Prevent optimizer from reusing the allocation across iterations + // by leaking the Vec — otherwise LLVM coalesces the Vec into a + // single allocation for the whole loop, which under-counts the + // real per-frame cost. black_box on .as_ptr() forces the box to + // be materialised even when nothing else consumes it. + std::hint::black_box(groups.as_ptr()); + } + let before_elapsed = start.elapsed(); + eprintln!( + "BEFORE (rebuild each call): {N} builds in {before_elapsed:?} \ + ({:.1} µs/build, total_len={total_len})", + before_elapsed.as_secs_f64() * 1_000_000.0 / N as f64, + ); + + // ── After: cached via OnceLock ── + struct Cached; + impl CadModule for Cached { + fn id(&self) -> &'static str { + "cached" + } + fn title(&self) -> &'static str { + "Cached" + } + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(build_tree) + } + } + let m = Cached; + let _ = m.ribbon_groups(); // warm-up — pays construction cost once + + let start = std::time::Instant::now(); + let mut total_len = 0usize; + for _ in 0..N { + let groups = std::hint::black_box(m.ribbon_groups()); + total_len += std::hint::black_box(groups.len()); + } + let after_elapsed = start.elapsed(); + eprintln!( + "AFTER (cached): {N} calls in {after_elapsed:?} \ + ({:.1} ns/call, total_len={total_len})", + after_elapsed.as_secs_f64() * 1_000_000_000.0 / N as f64, + ); + + // Ratio: serialize cost in ns/call to avoid division by zero + let before_ns = before_elapsed.as_nanos() as f64 / N as f64; + let after_ns = after_elapsed.as_nanos() as f64 / N as f64; + let ratio = if after_ns > 0.0 { + before_ns / after_ns + } else { + f64::INFINITY + }; + eprintln!( + "BEFORE vs AFTER: {before_ns:.0} ns/call vs {after_ns:.1} ns/call ({ratio:.0}× faster)" + ); + + assert_eq!(total_len, 2 * N, "each call must return 2 groups"); + } + #[test] fn tool_def_converts_into_small_tool() { let tool = ToolDef { @@ -156,6 +257,37 @@ mod tests { assert!(matches!(RibbonItem::from(tool), RibbonItem::Tool(_))); } + #[test] + fn once_lock_produces_identical_pointer_on_subsequent_calls() { + struct Demo; + impl CadModule for Demo { + fn id(&self) -> &'static str { + "demo" + } + fn title(&self) -> &'static str { + "Demo" + } + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![RibbonGroup { + title: "Group", + tools: vec![RibbonItem::Tool(ToolDef { + id: "LINE", + label: "Line", + icon: IconKind::Glyph("/"), + event: ModuleEvent::Command("LINE".to_string()), + })], + }] + }) + } + } + let m = Demo; + let first: *const [RibbonGroup] = m.ribbon_groups(); + let second: *const [RibbonGroup] = m.ribbon_groups(); + assert_eq!(first, second, "cached calls must return the same pointer"); + } + #[test] fn cad_module_is_object_safe() { struct Demo; @@ -166,11 +298,14 @@ mod tests { fn title(&self) -> &'static str { "Demo" } - fn ribbon_groups(&self) -> Vec { - vec![RibbonGroup { - title: "Group", - tools: vec![], - }] + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![RibbonGroup { + title: "Group", + tools: vec![], + }] + }) } } let m: Box = Box::new(Demo); diff --git a/crates/ocs_plugin_api/src/ribbon/owned.rs b/crates/ocs_plugin_api/src/ribbon/owned.rs index d2d2368a..1ae62436 100644 --- a/crates/ocs_plugin_api/src/ribbon/owned.rs +++ b/crates/ocs_plugin_api/src/ribbon/owned.rs @@ -79,6 +79,15 @@ impl From for OwnedIconKind { } } +impl From<&IconKind> for OwnedIconKind { + fn from(i: &IconKind) -> Self { + match *i { + IconKind::Glyph(g) => OwnedIconKind::Glyph(g.to_string()), + IconKind::Svg(b) => OwnedIconKind::Svg(b.to_vec()), + } + } +} + impl OwnedIconKind { /// Leak the owned data to reconstruct an `IconKind` with `&'static` lifetime. pub fn to_static(self) -> IconKind { @@ -100,6 +109,17 @@ impl From for OwnedToolDef { } } +impl From<&ToolDef> for OwnedToolDef { + fn from(t: &ToolDef) -> Self { + Self { + id: t.id.to_string(), + label: t.label.to_string(), + icon: (&t.icon).into(), + event: t.event.clone(), + } + } +} + impl OwnedToolDef { pub fn to_static(self) -> ToolDef { ToolDef { @@ -243,6 +263,66 @@ impl OwnedRibbonItem { } } +impl From<&RibbonItem> for OwnedRibbonItem { + fn from(item: &RibbonItem) -> Self { + match item { + RibbonItem::Tool(t) => OwnedRibbonItem::Tool(t.into()), + RibbonItem::LargeTool(t) => OwnedRibbonItem::LargeTool(t.into()), + RibbonItem::Dropdown { + id, + icon, + items, + default, + } => OwnedRibbonItem::Dropdown { + id: id.to_string(), + icon: icon.into(), + items: items + .iter() + .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) + .collect(), + default: default.to_string(), + }, + RibbonItem::LargeDropdown { + id, + label, + icon, + items, + default, + } => OwnedRibbonItem::LargeDropdown { + id: id.to_string(), + label: label.to_string(), + icon: icon.into(), + items: items + .iter() + .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) + .collect(), + default: default.to_string(), + }, + RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup { + row2: row2.iter().map(Into::into).collect(), + row3: row3.iter().map(Into::into).collect(), + }, + RibbonItem::PropertiesGroup { match_prop } => OwnedRibbonItem::PropertiesGroup { + match_prop: match_prop.into(), + }, + RibbonItem::StyleComboGroup { + style_key, + combo_id, + manager_cmd, + rows, + } => OwnedRibbonItem::StyleComboGroup { + style_key: *style_key, + combo_id: combo_id.to_string(), + manager_cmd: manager_cmd.map(|s| s.to_string()), + rows: rows + .iter() + .map(|r| r.iter().map(Into::into).collect()) + .collect(), + }, + } + } +} + impl From for OwnedRibbonGroup { fn from(g: RibbonGroup) -> Self { Self { @@ -252,6 +332,15 @@ impl From for OwnedRibbonGroup { } } +impl From<&RibbonGroup> for OwnedRibbonGroup { + fn from(g: &RibbonGroup) -> Self { + Self { + title: g.title.to_string(), + tools: g.tools.iter().map(Into::into).collect(), + } + } +} + impl OwnedRibbonGroup { pub fn to_static(self) -> RibbonGroup { RibbonGroup { @@ -275,8 +364,8 @@ pub fn to_module(id: String, title: String, groups: Vec) -> Bo fn title(&self) -> &'static str { self.title } - fn ribbon_groups(&self) -> Vec { - self.groups.clone() + fn ribbon_groups(&self) -> &[RibbonGroup] { + &self.groups } } let id = &*Box::leak(id.into_boxed_str()); @@ -302,7 +391,7 @@ impl CadModule for SharedCadModule { fn title(&self) -> &'static str { self.0.title() } - fn ribbon_groups(&self) -> Vec { + fn ribbon_groups(&self) -> &[RibbonGroup] { self.0.ribbon_groups() } } diff --git a/crates/ocs_plugin_api/src/runner.rs b/crates/ocs_plugin_api/src/runner.rs index 4da00e94..952e3bdc 100644 --- a/crates/ocs_plugin_api/src/runner.rs +++ b/crates/ocs_plugin_api/src/runner.rs @@ -78,7 +78,7 @@ fn handle_host_request( Ok(groups) => HostResponse::Ribbon( groups .ribbon_groups() - .into_iter() + .iter() .map(OwnedRibbonGroup::from) .collect(), ), diff --git a/crates/plugin-template-api2/src/lib.rs b/crates/plugin-template-api2/src/lib.rs index e2c4e0d2..ee26e847 100644 --- a/crates/plugin-template-api2/src/lib.rs +++ b/crates/plugin-template-api2/src/lib.rs @@ -29,7 +29,9 @@ impl CadModule for MyModule { fn title(&self) -> &'static str { "My Plugin" } - fn ribbon_groups(&self) -> Vec { + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { vec![RibbonGroup { title: "Tools", tools: vec![RibbonItem::LargeTool(ToolDef { @@ -39,6 +41,7 @@ impl CadModule for MyModule { event: ModuleEvent::Command("MP_HELLO".to_string()), })], }] + }) } } diff --git a/docs/plugin-template-v2/src/lib.rs b/docs/plugin-template-v2/src/lib.rs index 702f39c9..6440de08 100644 --- a/docs/plugin-template-v2/src/lib.rs +++ b/docs/plugin-template-v2/src/lib.rs @@ -139,7 +139,9 @@ impl CadModule for TemplateModule { "Template v2" } - fn ribbon_groups(&self) -> Vec { + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { vec![RibbonGroup { title: "Survey", tools: vec![ @@ -169,6 +171,7 @@ impl CadModule for TemplateModule { }), ], }] + }) } } diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index da00d7f3..2abe8f95 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -38,193 +38,200 @@ impl CadModule for AnnotateModule { "Annotate" } - fn ribbon_groups(&self) -> Vec { + fn ribbon_groups(&self) -> &[RibbonGroup] { use crate::modules::draw::draw::{revcloud, wipeout}; - vec![ - // ── Text ───────────────────────────────────────────────────── - RibbonGroup { - title: "Text", - tools: vec![ - RibbonItem::LargeDropdown { - id: "ANNOTATE_TEXT", - label: "Multiline\nText", - icon: mtext::ICON, - items: vec![ - (mtext::tool().id, mtext::tool().label, mtext::tool().icon), - (text::tool().id, text::tool().label, text::tool().icon), - (ddedit::tool().id, ddedit::tool().label, ddedit::tool().icon), - ], - default: "MTEXT", - }, - RibbonItem::StyleComboGroup { - style_key: StyleKey::TextStyle, - combo_id: "TEXT_STYLE_COMBO", - manager_cmd: Some("STYLE"), - rows: vec![vec![crate::modules::ToolDef { - id: "FIND", - label: "Find", - icon: crate::modules::IconKind::Svg(include_bytes!( - "../../../assets/icons/find.svg" - )), - event: crate::modules::ModuleEvent::Command("FIND".to_string()), - }]], - }, - ], - }, - // ── Dimensions ─────────────────────────────────────────────── - RibbonGroup { - title: "Dimensions", - tools: vec![ - RibbonItem::LargeDropdown { - id: "ANNOTATE_DIM", - label: "Dimension", - icon: linear_dim::ICON, - items: vec![ - ( - linear_dim::tool().id, - linear_dim::tool().label, - linear_dim::tool().icon, - ), - ( - aligned_dim::tool().id, - aligned_dim::tool().label, - aligned_dim::tool().icon, - ), - ( - angular_dim::tool().id, - angular_dim::tool().label, - angular_dim::tool().icon, - ), - ( - radius_dim::tool().id, - radius_dim::tool().label, - radius_dim::tool().icon, - ), - ( - diameter_dim::tool().id, - diameter_dim::tool().label, - diameter_dim::tool().icon, - ), - ( - ordinate_dim::tool().id, - ordinate_dim::tool().label, - ordinate_dim::tool().icon, - ), - (qdim::tool().id, qdim::tool().label, qdim::tool().icon), - ], - default: "DIMLINEAR", - }, - RibbonItem::StyleComboGroup { - style_key: StyleKey::DimStyle, - combo_id: "DIM_STYLE_COMBO", - manager_cmd: Some("DIMSTYLE"), - rows: vec![ - vec![qdim::tool(), dim_continue::tool(), dim_baseline::tool()], - vec![ - tolerance_cmd::tool(), - dimedit::tool(), - dimtedit::tool(), - dimbreak::tool(), - dimspace::tool(), - dimjogline::tool(), + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![ + // ── Text ───────────────────────────────────────────────────── + RibbonGroup { + title: "Text", + tools: vec![ + RibbonItem::LargeDropdown { + id: "ANNOTATE_TEXT", + label: "Multiline\nText", + icon: mtext::ICON, + items: vec![ + (mtext::tool().id, mtext::tool().label, mtext::tool().icon), + (text::tool().id, text::tool().label, text::tool().icon), + (ddedit::tool().id, ddedit::tool().label, ddedit::tool().icon), ], - ], - }, - ], - }, - // ── Leaders ────────────────────────────────────────────────── - RibbonGroup { - title: "Leaders", - tools: vec![ - RibbonItem::LargeDropdown { - id: "ANNOTATE_LEADER", - label: "Multileader", - icon: mleader_cmd::ICON, - items: vec![ - ( - mleader_cmd::tool().id, - mleader_cmd::tool().label, - mleader_cmd::tool().icon, + default: "MTEXT", + }, + RibbonItem::StyleComboGroup { + style_key: StyleKey::TextStyle, + combo_id: "TEXT_STYLE_COMBO", + manager_cmd: Some("STYLE"), + rows: vec![vec![crate::modules::ToolDef { + id: "FIND", + label: "Find", + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/find.svg" + )), + event: crate::modules::ModuleEvent::Command("FIND".to_string()), + }]], + }, + ], + }, + // ── Dimensions ─────────────────────────────────────────────── + RibbonGroup { + title: "Dimensions", + tools: vec![ + RibbonItem::LargeDropdown { + id: "ANNOTATE_DIM", + label: "Dimension", + icon: linear_dim::ICON, + items: vec![ + ( + linear_dim::tool().id, + linear_dim::tool().label, + linear_dim::tool().icon, + ), + ( + aligned_dim::tool().id, + aligned_dim::tool().label, + aligned_dim::tool().icon, + ), + ( + angular_dim::tool().id, + angular_dim::tool().label, + angular_dim::tool().icon, + ), + ( + radius_dim::tool().id, + radius_dim::tool().label, + radius_dim::tool().icon, + ), + ( + diameter_dim::tool().id, + diameter_dim::tool().label, + diameter_dim::tool().icon, + ), + ( + ordinate_dim::tool().id, + ordinate_dim::tool().label, + ordinate_dim::tool().icon, + ), + (qdim::tool().id, qdim::tool().label, qdim::tool().icon), + ], + default: "DIMLINEAR", + }, + RibbonItem::StyleComboGroup { + style_key: StyleKey::DimStyle, + combo_id: "DIM_STYLE_COMBO", + manager_cmd: Some("DIMSTYLE"), + rows: vec![ + vec![qdim::tool(), dim_continue::tool(), dim_baseline::tool()], + vec![ + tolerance_cmd::tool(), + dimedit::tool(), + dimtedit::tool(), + dimbreak::tool(), + dimspace::tool(), + dimjogline::tool(), + ], + ], + }, + ], + }, + // ── Leaders ────────────────────────────────────────────────── + RibbonGroup { + title: "Leaders", + tools: vec![ + RibbonItem::LargeDropdown { + id: "ANNOTATE_LEADER", + label: "Multileader", + icon: mleader_cmd::ICON, + items: vec![ + ( + mleader_cmd::tool().id, + mleader_cmd::tool().label, + mleader_cmd::tool().icon, + ), + ( + leader_cmd::tool().id, + leader_cmd::tool().label, + leader_cmd::tool().icon, + ), + ], + default: "MLEADER", + }, + RibbonItem::StyleComboGroup { + style_key: StyleKey::MLeaderStyle, + combo_id: "MLEADER_STYLE_COMBO", + manager_cmd: Some("MLEADERSTYLE"), + rows: vec![ + vec![mleader_edit::tool_add(), mleader_edit::tool_remove()], + vec![mleader_edit::tool_align(), mleader_edit::tool_collect()], + ], + }, + ], + }, + // ── Tables ─────────────────────────────────────────────────── + RibbonGroup { + title: "Tables", + tools: vec![ + RibbonItem::LargeTool(table_cmd::tool()), + RibbonItem::StyleComboGroup { + style_key: StyleKey::TableStyle, + combo_id: "TABLE_STYLE_COMBO", + manager_cmd: Some("TABLESTYLE"), + rows: vec![vec![data_extract::tool(), data_link::tool()]], + }, + ], + }, + // ── Markup ─────────────────────────────────────────────────── + RibbonGroup { + title: "Markup", + tools: vec![ + RibbonItem::LargeTool(wipeout::tool()), + RibbonItem::LargeTool(revcloud::tool()), + ], + }, + // ── Annotation Scaling ─────────────────────────────────────── + RibbonGroup { + title: "Annotation Scaling", + tools: vec![ + RibbonItem::Tool(crate::modules::ToolDef { + id: "ANNOSCALE", + label: "Scale List", + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/scale_list.svg" + )), + event: crate::modules::ModuleEvent::Command("ANNOSCALE".to_string()), + }), + RibbonItem::Tool(crate::modules::ToolDef { + id: "OBJECTSCALE", + label: "Add Scale", + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/add_scale.svg" + )), + event: crate::modules::ModuleEvent::Command("OBJECTSCALE".to_string()), + }), + RibbonItem::Tool(crate::modules::ToolDef { + id: "SCALELISTEDIT", + label: "Scale Edit", + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/scale_list.svg" + )), + event: crate::modules::ModuleEvent::Command( + "SCALELISTEDIT".to_string(), ), - ( - leader_cmd::tool().id, - leader_cmd::tool().label, - leader_cmd::tool().icon, + }), + RibbonItem::Tool(crate::modules::ToolDef { + id: "SYNCPVIEWPORTS", + label: "Sync Scales", + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/sync.svg" + )), + event: crate::modules::ModuleEvent::Command( + "SYNCPVIEWPORTS".to_string(), ), - ], - default: "MLEADER", - }, - RibbonItem::StyleComboGroup { - style_key: StyleKey::MLeaderStyle, - combo_id: "MLEADER_STYLE_COMBO", - manager_cmd: Some("MLEADERSTYLE"), - rows: vec![ - vec![mleader_edit::tool_add(), mleader_edit::tool_remove()], - vec![mleader_edit::tool_align(), mleader_edit::tool_collect()], - ], - }, - ], - }, - // ── Tables ─────────────────────────────────────────────────── - RibbonGroup { - title: "Tables", - tools: vec![ - RibbonItem::LargeTool(table_cmd::tool()), - RibbonItem::StyleComboGroup { - style_key: StyleKey::TableStyle, - combo_id: "TABLE_STYLE_COMBO", - manager_cmd: Some("TABLESTYLE"), - rows: vec![vec![data_extract::tool(), data_link::tool()]], - }, - ], - }, - // ── Markup ─────────────────────────────────────────────────── - RibbonGroup { - title: "Markup", - tools: vec![ - RibbonItem::LargeTool(wipeout::tool()), - RibbonItem::LargeTool(revcloud::tool()), - ], - }, - // ── Annotation Scaling ─────────────────────────────────────── - RibbonGroup { - title: "Annotation Scaling", - tools: vec![ - RibbonItem::Tool(crate::modules::ToolDef { - id: "ANNOSCALE", - label: "Scale List", - icon: crate::modules::IconKind::Svg(include_bytes!( - "../../../assets/icons/scale_list.svg" - )), - event: crate::modules::ModuleEvent::Command("ANNOSCALE".to_string()), - }), - RibbonItem::Tool(crate::modules::ToolDef { - id: "OBJECTSCALE", - label: "Add Scale", - icon: crate::modules::IconKind::Svg(include_bytes!( - "../../../assets/icons/add_scale.svg" - )), - event: crate::modules::ModuleEvent::Command("OBJECTSCALE".to_string()), - }), - RibbonItem::Tool(crate::modules::ToolDef { - id: "SCALELISTEDIT", - label: "Scale Edit", - icon: crate::modules::IconKind::Svg(include_bytes!( - "../../../assets/icons/scale_list.svg" - )), - event: crate::modules::ModuleEvent::Command("SCALELISTEDIT".to_string()), - }), - RibbonItem::Tool(crate::modules::ToolDef { - id: "SYNCPVIEWPORTS", - label: "Sync Scales", - icon: crate::modules::IconKind::Svg(include_bytes!( - "../../../assets/icons/sync.svg" - )), - event: crate::modules::ModuleEvent::Command("SYNCPVIEWPORTS".to_string()), - }), - ], - }, - ] + }), + ], + }, + ] + }) } } diff --git a/src/modules/draw/mod.rs b/src/modules/draw/mod.rs index c3004f9e..1e2bb0a1 100644 --- a/src/modules/draw/mod.rs +++ b/src/modules/draw/mod.rs @@ -26,7 +26,7 @@ impl CadModule for DrawModule { "Draw" } - fn ribbon_groups(&self) -> Vec { + fn ribbon_groups(&self) -> &[RibbonGroup] { use crate::modules::annotate::{ angular_dim, leader_cmd, linear_dim, mleader_cmd, mtext, radius_dim, text, }; @@ -43,190 +43,193 @@ impl CadModule for DrawModule { }; use properties::match_prop; - vec![ - RibbonGroup { - title: "Draw", - tools: vec![ - RibbonItem::LargeTool(line::tool()), - RibbonItem::LargeTool(polyline::tool()), - RibbonItem::LargeDropdown { - id: circle::DROPDOWN_ID, - label: "Circle", - icon: circle::ICON, - items: circle::DROPDOWN_ITEMS.to_vec(), - default: "CIRCLE", - }, - RibbonItem::LargeDropdown { - id: arc::DROPDOWN_ID, - label: "Arc", - icon: arc::ICON, - items: arc::DROPDOWN_ITEMS.to_vec(), - default: "ARC", - }, - RibbonItem::Dropdown { - id: shapes::DROPDOWN_ID, - icon: shapes::ICON, - items: shapes::DROPDOWN_ITEMS.to_vec(), - default: "RECT", - }, - RibbonItem::Dropdown { - id: ellipse::DROPDOWN_ID, - icon: ellipse::ICON, - items: ellipse::DROPDOWN_ITEMS.to_vec(), - default: "ELLIPSE", - }, - RibbonItem::Dropdown { - id: hatch::DROPDOWN_ID, - icon: hatch::ICON, - items: hatch::DROPDOWN_ITEMS.to_vec(), - default: "HATCH", - }, - ], - }, - RibbonGroup { - title: "Modify", - tools: vec![ - translate::tool().into(), - copy::tool().into(), - stretch::tool().into(), - rotate::tool().into(), - mirror::tool().into(), - scale::tool().into(), - RibbonItem::Dropdown { - id: trim::DROPDOWN_ID, - icon: trim::ICON, - items: trim::DROPDOWN_ITEMS.to_vec(), - default: "TRIM", - }, - RibbonItem::Dropdown { - id: fillet::DROPDOWN_ID, - icon: fillet::ICON, - items: fillet::DROPDOWN_ITEMS.to_vec(), - default: "FILLET", - }, - RibbonItem::Dropdown { - id: array::DROPDOWN_ID, - icon: array::ICON, - items: array::DROPDOWN_ITEMS.to_vec(), - default: "ARRAYRECT", - }, - delete::tool().into(), - explode::tool().into(), - offset::tool().into(), - ], - }, - RibbonGroup { - title: "Annotation", - tools: vec![ - RibbonItem::LargeDropdown { - id: "ANNOTATION_TEXT", - label: "Text", - icon: text::ICON, - items: vec![ - (text::tool().id, text::tool().label, text::tool().icon), - (mtext::tool().id, mtext::tool().label, mtext::tool().icon), - ], - default: "TEXT", - }, - RibbonItem::LargeDropdown { - id: "ANNOTATION_DIMENSIONS", - label: "Dimensions", - icon: linear_dim::ICON, - items: vec![ - ( - linear_dim::tool().id, - linear_dim::tool().label, - linear_dim::tool().icon, - ), - ( - radius_dim::tool().id, - radius_dim::tool().label, - radius_dim::tool().icon, - ), - ( - angular_dim::tool().id, - angular_dim::tool().label, - angular_dim::tool().icon, - ), - ], - default: "DIMLINEAR", - }, - RibbonItem::LargeDropdown { - id: "ANNOTATION_LEADER", - label: "Leader", - icon: leader_cmd::ICON, - items: vec![ - ( - mleader_cmd::tool().id, - mleader_cmd::tool().label, - mleader_cmd::tool().icon, - ), - ( - leader_cmd::tool().id, - leader_cmd::tool().label, - leader_cmd::tool().icon, - ), - ], - default: "MLEADER", - }, - ], - }, - RibbonGroup { - title: "Layers", - tools: vec![ - RibbonItem::LargeTool(panel::tool()), - RibbonItem::LayerComboGroup { - row2: vec![ - layoff::tool(), - layfrz::tool(), - laylck::tool(), - make_current::tool(), - ], - row3: vec![ - layon::tool(), - laythw::tool(), - layulk::tool(), - match_layer::tool(), - ], - }, - ], - }, - RibbonGroup { - title: "Block", - tools: vec![ - RibbonItem::LargeTool(create_block::tool()), - RibbonItem::LargeTool(insert_block::tool()), - ], - }, - RibbonGroup { - title: "Properties", - tools: vec![RibbonItem::PropertiesGroup { - match_prop: match_prop::tool(), - }], - }, - RibbonGroup { - title: "Groups", - tools: vec![ - RibbonItem::LargeTool(group::tool()), - RibbonItem::LargeTool(ungroup::tool()), - ], - }, - RibbonGroup { - title: "Clipboard", - tools: vec![ - RibbonItem::LargeDropdown { - id: "PASTE_MENU", - label: "Paste", - icon: paste::ICON, - items: paste::MENU_ITEMS.to_vec(), - default: "PASTECLIP", - }, - copy_clip::tool().into(), - cut::tool().into(), - ], - }, - // Support group lives on the Start tab now (see view.rs: - // start_page_view). Removed from the Draw ribbon to declutter. - ] + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![ + RibbonGroup { + title: "Draw", + tools: vec![ + RibbonItem::LargeTool(line::tool()), + RibbonItem::LargeTool(polyline::tool()), + RibbonItem::LargeDropdown { + id: circle::DROPDOWN_ID, + label: "Circle", + icon: circle::ICON, + items: circle::DROPDOWN_ITEMS.to_vec(), + default: "CIRCLE", + }, + RibbonItem::LargeDropdown { + id: arc::DROPDOWN_ID, + label: "Arc", + icon: arc::ICON, + items: arc::DROPDOWN_ITEMS.to_vec(), + default: "ARC", + }, + RibbonItem::Dropdown { + id: shapes::DROPDOWN_ID, + icon: shapes::ICON, + items: shapes::DROPDOWN_ITEMS.to_vec(), + default: "RECT", + }, + RibbonItem::Dropdown { + id: ellipse::DROPDOWN_ID, + icon: ellipse::ICON, + items: ellipse::DROPDOWN_ITEMS.to_vec(), + default: "ELLIPSE", + }, + RibbonItem::Dropdown { + id: hatch::DROPDOWN_ID, + icon: hatch::ICON, + items: hatch::DROPDOWN_ITEMS.to_vec(), + default: "HATCH", + }, + ], + }, + RibbonGroup { + title: "Modify", + tools: vec![ + translate::tool().into(), + copy::tool().into(), + stretch::tool().into(), + rotate::tool().into(), + mirror::tool().into(), + scale::tool().into(), + RibbonItem::Dropdown { + id: trim::DROPDOWN_ID, + icon: trim::ICON, + items: trim::DROPDOWN_ITEMS.to_vec(), + default: "TRIM", + }, + RibbonItem::Dropdown { + id: fillet::DROPDOWN_ID, + icon: fillet::ICON, + items: fillet::DROPDOWN_ITEMS.to_vec(), + default: "FILLET", + }, + RibbonItem::Dropdown { + id: array::DROPDOWN_ID, + icon: array::ICON, + items: array::DROPDOWN_ITEMS.to_vec(), + default: "ARRAYRECT", + }, + delete::tool().into(), + explode::tool().into(), + offset::tool().into(), + ], + }, + RibbonGroup { + title: "Annotation", + tools: vec![ + RibbonItem::LargeDropdown { + id: "ANNOTATION_TEXT", + label: "Text", + icon: text::ICON, + items: vec![ + (text::tool().id, text::tool().label, text::tool().icon), + (mtext::tool().id, mtext::tool().label, mtext::tool().icon), + ], + default: "TEXT", + }, + RibbonItem::LargeDropdown { + id: "ANNOTATION_DIMENSIONS", + label: "Dimensions", + icon: linear_dim::ICON, + items: vec![ + ( + linear_dim::tool().id, + linear_dim::tool().label, + linear_dim::tool().icon, + ), + ( + radius_dim::tool().id, + radius_dim::tool().label, + radius_dim::tool().icon, + ), + ( + angular_dim::tool().id, + angular_dim::tool().label, + angular_dim::tool().icon, + ), + ], + default: "DIMLINEAR", + }, + RibbonItem::LargeDropdown { + id: "ANNOTATION_LEADER", + label: "Leader", + icon: leader_cmd::ICON, + items: vec![ + ( + mleader_cmd::tool().id, + mleader_cmd::tool().label, + mleader_cmd::tool().icon, + ), + ( + leader_cmd::tool().id, + leader_cmd::tool().label, + leader_cmd::tool().icon, + ), + ], + default: "MLEADER", + }, + ], + }, + RibbonGroup { + title: "Layers", + tools: vec![ + RibbonItem::LargeTool(panel::tool()), + RibbonItem::LayerComboGroup { + row2: vec![ + layoff::tool(), + layfrz::tool(), + laylck::tool(), + make_current::tool(), + ], + row3: vec![ + layon::tool(), + laythw::tool(), + layulk::tool(), + match_layer::tool(), + ], + }, + ], + }, + RibbonGroup { + title: "Block", + tools: vec![ + RibbonItem::LargeTool(create_block::tool()), + RibbonItem::LargeTool(insert_block::tool()), + ], + }, + RibbonGroup { + title: "Properties", + tools: vec![RibbonItem::PropertiesGroup { + match_prop: match_prop::tool(), + }], + }, + RibbonGroup { + title: "Groups", + tools: vec![ + RibbonItem::LargeTool(group::tool()), + RibbonItem::LargeTool(ungroup::tool()), + ], + }, + RibbonGroup { + title: "Clipboard", + tools: vec![ + RibbonItem::LargeDropdown { + id: "PASTE_MENU", + label: "Paste", + icon: paste::ICON, + items: paste::MENU_ITEMS.to_vec(), + default: "PASTECLIP", + }, + copy_clip::tool().into(), + cut::tool().into(), + ], + }, + // Support group lives on the Start tab now (see view.rs: + // start_page_view). Removed from the Draw ribbon to declutter. + ] + }) } } diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index 969f6901..d758a476 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -35,91 +35,94 @@ impl CadModule for InsertModule { "Insert" } - fn ribbon_groups(&self) -> Vec { - vec![ - // ── Reference ──────────────────────────────────────────────────── - RibbonGroup { - title: "Reference", - tools: vec![ - RibbonItem::LargeTool(xattach::tool()), - RibbonItem::LargeTool(xclip::tool()), - RibbonItem::LargeTool(xadjust::tool()), - RibbonItem::Tool(underlay_layers::tool()), - RibbonItem::Dropdown { - id: "FRAMES_DROPDOWN", - icon: IconKind::Svg(include_bytes!( - "../../../assets/icons/underlay_frames.svg" - )), - items: vec![ - ( - "FRAMES0", - "Frames Off", - IconKind::Svg(include_bytes!( - "../../../assets/icons/underlay_frames.svg" - )), - ), - ( - "FRAMES1", - "Frames On", - IconKind::Svg(include_bytes!( - "../../../assets/icons/underlay_frames.svg" - )), - ), - ( - "FRAMES2", - "Frames & Print", - IconKind::Svg(include_bytes!( - "../../../assets/icons/underlay_frames.svg" - )), - ), - ], - default: "FRAMES1", - }, - RibbonItem::Tool(snap_underlays::tool()), - ], - }, - // ── Point Cloud ─────────────────────────────────────────────────── - RibbonGroup { - title: "Point Cloud", - tools: vec![RibbonItem::LargeTool(pc_attach::tool())], - }, - // ── Block ───────────────────────────────────────────────────────── - RibbonGroup { - title: "Block", - tools: vec![ - RibbonItem::LargeTool(mview_block::tool()), - RibbonItem::LargeTool(insert_block::tool()), - RibbonItem::Tool(create_block::tool()), - RibbonItem::Tool(edit_block::tool()), - RibbonItem::Tool(base_point::tool()), - ], - }, - // ── Attributes ──────────────────────────────────────────────────── - RibbonGroup { - title: "Attributes", - tools: vec![ - RibbonItem::LargeTool(attdef::tool()), - RibbonItem::LargeTool(attedit::tool()), - RibbonItem::Tool(attman::tool()), - RibbonItem::Tool(attsync::tool()), - ], - }, - // ── Import ──────────────────────────────────────────────────────── - RibbonGroup { - title: "Import", - tools: vec![ - RibbonItem::LargeTool(open_obj::tool()), - RibbonItem::LargeTool(landxml::tool()), - ], - }, - // ── Content ─────────────────────────────────────────────────────── - RibbonGroup { - title: "Content", - tools: vec![ - RibbonItem::LargeTool(content_browser::tool()), - RibbonItem::LargeTool(design_center::tool()), - ], - }, - ] + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![ + // ── Reference ──────────────────────────────────────────────────── + RibbonGroup { + title: "Reference", + tools: vec![ + RibbonItem::LargeTool(xattach::tool()), + RibbonItem::LargeTool(xclip::tool()), + RibbonItem::LargeTool(xadjust::tool()), + RibbonItem::Tool(underlay_layers::tool()), + RibbonItem::Dropdown { + id: "FRAMES_DROPDOWN", + icon: IconKind::Svg(include_bytes!( + "../../../assets/icons/underlay_frames.svg" + )), + items: vec![ + ( + "FRAMES0", + "Frames Off", + IconKind::Svg(include_bytes!( + "../../../assets/icons/underlay_frames.svg" + )), + ), + ( + "FRAMES1", + "Frames On", + IconKind::Svg(include_bytes!( + "../../../assets/icons/underlay_frames.svg" + )), + ), + ( + "FRAMES2", + "Frames & Print", + IconKind::Svg(include_bytes!( + "../../../assets/icons/underlay_frames.svg" + )), + ), + ], + default: "FRAMES1", + }, + RibbonItem::Tool(snap_underlays::tool()), + ], + }, + // ── Point Cloud ─────────────────────────────────────────────────── + RibbonGroup { + title: "Point Cloud", + tools: vec![RibbonItem::LargeTool(pc_attach::tool())], + }, + // ── Block ───────────────────────────────────────────────────────── + RibbonGroup { + title: "Block", + tools: vec![ + RibbonItem::LargeTool(mview_block::tool()), + RibbonItem::LargeTool(insert_block::tool()), + RibbonItem::Tool(create_block::tool()), + RibbonItem::Tool(edit_block::tool()), + RibbonItem::Tool(base_point::tool()), + ], + }, + // ── Attributes ──────────────────────────────────────────────────── + RibbonGroup { + title: "Attributes", + tools: vec![ + RibbonItem::LargeTool(attdef::tool()), + RibbonItem::LargeTool(attedit::tool()), + RibbonItem::Tool(attman::tool()), + RibbonItem::Tool(attsync::tool()), + ], + }, + // ── Import ──────────────────────────────────────────────────────── + RibbonGroup { + title: "Import", + tools: vec![ + RibbonItem::LargeTool(open_obj::tool()), + RibbonItem::LargeTool(landxml::tool()), + ], + }, + // ── Content ─────────────────────────────────────────────────────── + RibbonGroup { + title: "Content", + tools: vec![ + RibbonItem::LargeTool(content_browser::tool()), + RibbonItem::LargeTool(design_center::tool()), + ], + }, + ] + }) } } diff --git a/src/modules/layout/mod.rs b/src/modules/layout/mod.rs index 78a5a63c..35c1cbff 100644 --- a/src/modules/layout/mod.rs +++ b/src/modules/layout/mod.rs @@ -36,31 +36,36 @@ impl CadModule for LayoutModule { "Layout" } - fn ribbon_groups(&self) -> Vec { - vec![ - RibbonGroup { - title: "Viewport", - tools: vec![mview::tool().into()], - }, - RibbonGroup { - title: "Plot", - tools: vec![ - ToolDef { - id: "PAGESETUP", - label: "Page Setup", - icon: IconKind::Svg(include_bytes!("../../../assets/icons/pagesetup.svg")), - event: ModuleEvent::Command("PAGESETUP".to_string()), - } - .into(), - ToolDef { - id: "PLOT", - label: "Export PDF", - icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")), - event: ModuleEvent::Command("PLOT".to_string()), - } - .into(), - ], - }, - ] + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![ + RibbonGroup { + title: "Viewport", + tools: vec![mview::tool().into()], + }, + RibbonGroup { + title: "Plot", + tools: vec![ + ToolDef { + id: "PAGESETUP", + label: "Page Setup", + icon: IconKind::Svg(include_bytes!( + "../../../assets/icons/pagesetup.svg" + )), + event: ModuleEvent::Command("PAGESETUP".to_string()), + } + .into(), + ToolDef { + id: "PLOT", + label: "Export PDF", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")), + event: ModuleEvent::Command("PLOT".to_string()), + } + .into(), + ], + }, + ] + }) } } diff --git a/src/modules/manage/mod.rs b/src/modules/manage/mod.rs index cc77a2f1..55f76501 100644 --- a/src/modules/manage/mod.rs +++ b/src/modules/manage/mod.rs @@ -21,58 +21,61 @@ impl CadModule for ManageModule { "Manage" } - fn ribbon_groups(&self) -> Vec { - vec![ - // ── Customization ───────────────────────────────────────────────── - RibbonGroup { - title: "Customization", - tools: vec![ - RibbonItem::LargeTool(user_interface::tool()), - RibbonItem::LargeTool(crate::modules::ToolDef { - id: "TOOLPALETTES", - label: "Tool\nPalettes", - icon: IconKind::Svg(include_bytes!( - "../../../assets/icons/tool_palettes.svg" - )), - event: crate::modules::ModuleEvent::Command("TOOLPALETTES".to_string()), - }), - RibbonItem::Tool(cui_import::tool()), - RibbonItem::Tool(cui_export::tool()), - RibbonItem::Dropdown { - id: "ALIASEDIT_DROPDOWN", - icon: IconKind::Svg(include_bytes!( - "../../../assets/icons/edit_aliases.svg" - )), - items: vec![ - ( - "ALIASEDIT", - "Edit Aliases", - IconKind::Svg(include_bytes!( - "../../../assets/icons/edit_aliases.svg" - )), - ), - ( - "CUILOAD", - "Load Partial CUI", - IconKind::Svg(include_bytes!( - "../../../assets/icons/cui_import.svg" - )), - ), - ], - default: "ALIASEDIT", - }, - ], - }, - // ── Cleanup ─────────────────────────────────────────────────────── - RibbonGroup { - title: "Cleanup", - tools: vec![ - RibbonItem::LargeTool(find_nonpurgeable::tool()), - RibbonItem::Tool(purge::tool()), - RibbonItem::Tool(overkill::tool()), - RibbonItem::Tool(audit::tool()), - ], - }, - ] + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![ + // ── Customization ───────────────────────────────────────────────── + RibbonGroup { + title: "Customization", + tools: vec![ + RibbonItem::LargeTool(user_interface::tool()), + RibbonItem::LargeTool(crate::modules::ToolDef { + id: "TOOLPALETTES", + label: "Tool\nPalettes", + icon: IconKind::Svg(include_bytes!( + "../../../assets/icons/tool_palettes.svg" + )), + event: crate::modules::ModuleEvent::Command("TOOLPALETTES".to_string()), + }), + RibbonItem::Tool(cui_import::tool()), + RibbonItem::Tool(cui_export::tool()), + RibbonItem::Dropdown { + id: "ALIASEDIT_DROPDOWN", + icon: IconKind::Svg(include_bytes!( + "../../../assets/icons/edit_aliases.svg" + )), + items: vec![ + ( + "ALIASEDIT", + "Edit Aliases", + IconKind::Svg(include_bytes!( + "../../../assets/icons/edit_aliases.svg" + )), + ), + ( + "CUILOAD", + "Load Partial CUI", + IconKind::Svg(include_bytes!( + "../../../assets/icons/cui_import.svg" + )), + ), + ], + default: "ALIASEDIT", + }, + ], + }, + // ── Cleanup ─────────────────────────────────────────────────────── + RibbonGroup { + title: "Cleanup", + tools: vec![ + RibbonItem::LargeTool(find_nonpurgeable::tool()), + RibbonItem::Tool(purge::tool()), + RibbonItem::Tool(overkill::tool()), + RibbonItem::Tool(audit::tool()), + ], + }, + ] + }) } } diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index b30d464f..66293cbd 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -40,34 +40,37 @@ impl CadModule for ModelModule { "Model" } - fn ribbon_groups(&self) -> Vec { - vec![ - RibbonGroup { - title: "Model", - tools: vec![ - RibbonItem::LargeTool(tool("BOX", "Box", BOX_ICON)), - RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", CYLINDER_ICON)), - RibbonItem::LargeTool(tool("CONE", "Cone", CONE_ICON)), - RibbonItem::LargeTool(tool("SPHERE", "Sphere", SPHERE_ICON)), - RibbonItem::Dropdown { - id: "MODEL_MORE", - icon: IconKind::Svg(WEDGE_ICON), - items: vec![ - ("WEDGE", "Wedge", IconKind::Svg(WEDGE_ICON)), - ("TORUS", "Torus", IconKind::Svg(TORUS_ICON)), - ], - default: "WEDGE", - }, - ], - }, - RibbonGroup { - title: "Design", - tools: vec![ - RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)), - RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_ICON)), - RibbonItem::LargeTool(tool("INTERSECT", "Intersect", INTERSECT_ICON)), - ], - }, - ] + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![ + RibbonGroup { + title: "Model", + tools: vec![ + RibbonItem::LargeTool(tool("BOX", "Box", BOX_ICON)), + RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", CYLINDER_ICON)), + RibbonItem::LargeTool(tool("CONE", "Cone", CONE_ICON)), + RibbonItem::LargeTool(tool("SPHERE", "Sphere", SPHERE_ICON)), + RibbonItem::Dropdown { + id: "MODEL_MORE", + icon: IconKind::Svg(WEDGE_ICON), + items: vec![ + ("WEDGE", "Wedge", IconKind::Svg(WEDGE_ICON)), + ("TORUS", "Torus", IconKind::Svg(TORUS_ICON)), + ], + default: "WEDGE", + }, + ], + }, + RibbonGroup { + title: "Design", + tools: vec![ + RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)), + RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_ICON)), + RibbonItem::LargeTool(tool("INTERSECT", "Intersect", INTERSECT_ICON)), + ], + }, + ] + }) } } diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index a2382c71..234924a0 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -45,96 +45,99 @@ impl CadModule for ViewModule { "View" } - fn ribbon_groups(&self) -> Vec { - vec![ - // ── Viewport Tools ─────────────────────────────────────────────── - RibbonGroup { - title: "Viewport Tools", - tools: vec![ - RibbonItem::LargeTool(ucs_icon::tool()), - RibbonItem::LargeTool(viewcube::tool()), - ], - }, - // ── Navigate ───────────────────────────────────────────────────── - RibbonGroup { - title: "Navigate", - tools: vec![ - RibbonItem::LargeTool(zoom_ext::tool()), - RibbonItem::Tool(zoom_window::tool()), - RibbonItem::Tool(zoom_in::tool()), - RibbonItem::Tool(zoom_out::tool()), - RibbonItem::Tool(pan::tool()), - RibbonItem::Tool(orbit::tool()), - ], - }, - // ── Model Viewports ─────────────────────────────────────────────── - RibbonGroup { - title: "Model Viewports", - tools: vec![ - RibbonItem::LargeTool(vports_config::tool()), - RibbonItem::Tool(vports_named::tool()), - RibbonItem::Tool(vports_join::tool()), - RibbonItem::Tool(vports_restore::tool()), - ], - }, - // ── Visual Style ────────────────────────────────────────────────── - RibbonGroup { - // WIREFRAME and SOLID ids are special-cased in ribbon.rs - // for toggle-state highlighting based on Ribbon::wireframe. - title: "Visual Style", - tools: vec![RibbonItem::LargeDropdown { - id: "VISUAL_STYLE", - label: "Visual\nStyle", - icon: wireframe::tool().icon, - items: vec![ - ("WIREFRAME", "Wireframe", wireframe::tool().icon), - ("SOLID", "Shaded", solid::tool().icon), - ("HIDDEN", "Hidden", hidden::tool().icon), - ("XRAY", "X-Ray", xray::tool().icon), + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + vec![ + // ── Viewport Tools ─────────────────────────────────────────────── + RibbonGroup { + title: "Viewport Tools", + tools: vec![ + RibbonItem::LargeTool(ucs_icon::tool()), + RibbonItem::LargeTool(viewcube::tool()), ], - default: "WIREFRAME", - }], - }, - // ── Projection ──────────────────────────────────────────────────── - RibbonGroup { - // ORTHO and PERSP ids are special-cased in ribbon.rs - // for toggle-state highlighting based on Camera::projection. - title: "Projection", - tools: vec![ - RibbonItem::LargeTool(ortho::tool()), - RibbonItem::LargeTool(persp::tool()), - ], - }, - // ── Preset Views ────────────────────────────────────────────────── - RibbonGroup { - title: "Preset", - tools: vec![ - RibbonItem::Tool(view_top::tool()), - RibbonItem::Tool(view_front::tool()), - RibbonItem::Tool(view_right::tool()), - RibbonItem::Tool(view_iso::tool()), - ], - }, - // ── Palettes ────────────────────────────────────────────────────── - RibbonGroup { - title: "Palettes", - tools: vec![ - RibbonItem::LargeTool(tool_palettes::tool()), - RibbonItem::LargeTool(properties_palette::tool()), - RibbonItem::LargeTool(sheetset::tool()), - ], - }, - // ── Interface ───────────────────────────────────────────────────── - RibbonGroup { - title: "Interface", - tools: vec![ - RibbonItem::LargeTool(file_tabs::tool()), - RibbonItem::LargeTool(layout_tabs::tool()), - RibbonItem::Tool(tile_horiz::tool()), - RibbonItem::Tool(tile_vert::tool()), - RibbonItem::Tool(cascade::tool()), - ], - }, - ] + }, + // ── Navigate ───────────────────────────────────────────────────── + RibbonGroup { + title: "Navigate", + tools: vec![ + RibbonItem::LargeTool(zoom_ext::tool()), + RibbonItem::Tool(zoom_window::tool()), + RibbonItem::Tool(zoom_in::tool()), + RibbonItem::Tool(zoom_out::tool()), + RibbonItem::Tool(pan::tool()), + RibbonItem::Tool(orbit::tool()), + ], + }, + // ── Model Viewports ─────────────────────────────────────────────── + RibbonGroup { + title: "Model Viewports", + tools: vec![ + RibbonItem::LargeTool(vports_config::tool()), + RibbonItem::Tool(vports_named::tool()), + RibbonItem::Tool(vports_join::tool()), + RibbonItem::Tool(vports_restore::tool()), + ], + }, + // ── Visual Style ────────────────────────────────────────────────── + RibbonGroup { + // WIREFRAME and SOLID ids are special-cased in ribbon.rs + // for toggle-state highlighting based on Ribbon::wireframe. + title: "Visual Style", + tools: vec![RibbonItem::LargeDropdown { + id: "VISUAL_STYLE", + label: "Visual\nStyle", + icon: wireframe::tool().icon, + items: vec![ + ("WIREFRAME", "Wireframe", wireframe::tool().icon), + ("SOLID", "Shaded", solid::tool().icon), + ("HIDDEN", "Hidden", hidden::tool().icon), + ("XRAY", "X-Ray", xray::tool().icon), + ], + default: "WIREFRAME", + }], + }, + // ── Projection ──────────────────────────────────────────────────── + RibbonGroup { + // ORTHO and PERSP ids are special-cased in ribbon.rs + // for toggle-state highlighting based on Camera::projection. + title: "Projection", + tools: vec![ + RibbonItem::LargeTool(ortho::tool()), + RibbonItem::LargeTool(persp::tool()), + ], + }, + // ── Preset Views ────────────────────────────────────────────────── + RibbonGroup { + title: "Preset", + tools: vec![ + RibbonItem::Tool(view_top::tool()), + RibbonItem::Tool(view_front::tool()), + RibbonItem::Tool(view_right::tool()), + RibbonItem::Tool(view_iso::tool()), + ], + }, + // ── Palettes ────────────────────────────────────────────────────── + RibbonGroup { + title: "Palettes", + tools: vec![ + RibbonItem::LargeTool(tool_palettes::tool()), + RibbonItem::LargeTool(properties_palette::tool()), + RibbonItem::LargeTool(sheetset::tool()), + ], + }, + // ── Interface ───────────────────────────────────────────────────── + RibbonGroup { + title: "Interface", + tools: vec![ + RibbonItem::LargeTool(file_tabs::tool()), + RibbonItem::LargeTool(layout_tabs::tool()), + RibbonItem::Tool(tile_horiz::tool()), + RibbonItem::Tool(tile_vert::tool()), + RibbonItem::Tool(cascade::tool()), + ], + }, + ] + }) } } diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index a13fb35d..b55229fc 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -409,7 +409,7 @@ impl Ribbon { let mut items_row: Vec> = Vec::new(); let mut small_buf: Vec> = Vec::new(); - for item in group.tools { + for item in &group.tools { let is_large = matches!( &item, RibbonItem::LargeTool(_) @@ -587,18 +587,18 @@ impl Ribbon { let mut dd_default = ""; let mut dd_id: &'static str = ""; - 'outer: for group in &groups { + 'outer: for group in groups { for item in &group.tools { let (id, items, default) = match item { RibbonItem::Dropdown { id, items, default, .. - } => (id, items, default), + } => (*id, items, *default), RibbonItem::LargeDropdown { id, items, default, .. - } => (id, items, default), + } => (*id, items, *default), _ => continue, }; - if *id == open_id { + if id == open_id { items_list = Some(items.clone()); dd_default = default; dd_id = id; @@ -820,7 +820,7 @@ impl Ribbon { // Locate the open style combo; capture its style key + manager command. let mut found: Option<(crate::modules::StyleKey, Option<&'static str>)> = None; - 'outer: for group in &groups { + 'outer: for group in groups { for item in &group.tools { if let RibbonItem::StyleComboGroup { style_key, diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 442b5abe..bcc0a99e 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -335,7 +335,7 @@ pub(super) fn tip_style(_theme: &Theme) -> container::Style { /// Render a 1-row small button (Tool or Dropdown). pub(super) fn render_small<'a>( - item: RibbonItem, + item: &RibbonItem, active_tool: &Option, open_dd: &Option, last_cmd: &HashMap<&'static str, &'static str>, @@ -368,12 +368,12 @@ pub(super) fn render_small<'a>( default, .. } => { - let active = active_tool.as_deref() == Some(id) + let active = active_tool.as_deref() == Some(*id) || items .iter() .any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd)); - let dd_open = open_dd.as_deref() == Some(id); - let last = last_cmd.get(id).copied().unwrap_or(default); + let dd_open = open_dd.as_deref() == Some(*id); + let last = last_cmd.get(id).copied().unwrap_or(*default); let cur_icon = last_cmd .get(id) .copied() @@ -384,7 +384,7 @@ pub(super) fn render_small<'a>( .map(|(_, _, ik)| *ik) }) .or_else(|| items.first().map(|(_, _, ik)| *ik)) - .unwrap_or(icon); + .unwrap_or(*icon); let cur_label = last_cmd .get(id) @@ -396,7 +396,7 @@ pub(super) fn render_small<'a>( .map(|(_, lbl, _)| *lbl) }) .or_else(|| items.first().map(|(_, lbl, _)| *lbl)) - .unwrap_or(id); + .unwrap_or(*id); let tip_text = format!("{}\nCommand: {}", cur_label, last); let icon_btn = button(make_icon(cur_icon, SMALL_ICON)) @@ -457,7 +457,7 @@ pub(super) fn render_small<'a>( /// Render a full-height large button (LargeTool, LargeDropdown, LayerCombo, StyleCombo). pub(super) fn render_large<'a>( - item: RibbonItem, + item: &RibbonItem, active_tool: &Option, open_dd: &Option, last_cmd: &HashMap<&'static str, &'static str>, @@ -508,12 +508,12 @@ pub(super) fn render_large<'a>( items, default, } => { - let active = active_tool.as_deref() == Some(id) + let active = active_tool.as_deref() == Some(*id) || items .iter() .any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd)); - let dd_open = open_dd.as_deref() == Some(id); - let last = last_cmd.get(id).copied().unwrap_or(default); + let dd_open = open_dd.as_deref() == Some(*id); + let last = last_cmd.get(id).copied().unwrap_or(*default); let cur_icon = last_cmd .get(id) .copied() @@ -524,7 +524,7 @@ pub(super) fn render_large<'a>( .map(|(_, _, ik)| *ik) }) .or_else(|| items.first().map(|(_, _, ik)| *ik)) - .unwrap_or(icon); + .unwrap_or(*icon); let cur_label = last_cmd .get(id) @@ -536,14 +536,14 @@ pub(super) fn render_large<'a>( .map(|(_, lbl, _)| *lbl) }) .or_else(|| items.first().map(|(_, lbl, _)| *lbl)) - .unwrap_or(label); + .unwrap_or(*label); let tip_text = format!("{}\nCommand: {}", cur_label, last); let arr_tip = format!("{} options", label); let top_btn = button( column![ make_icon(cur_icon, LARGE_ICON), - text(label).size(10).color(LABEL_COLOR), + text(*label).size(10).color(LABEL_COLOR), ] .align_x(iced::Center) .spacing(3), @@ -662,9 +662,9 @@ pub(super) fn render_large<'a>( .padding([3, 8]) .width(Fill); - let make_tool_row = |tools: Vec| -> Element { + let make_tool_row = |tools: &[ToolDef]| -> Element { let btns: Vec> = tools - .into_iter() + .iter() .map(|t| { let is_active = active_tool.as_deref() == Some(t.id); let tip = t.label; @@ -847,8 +847,8 @@ pub(super) fn render_large<'a>( .. } => { const STYLE_COMBO_W: f32 = LARGE_W * 2.3; - let active: String = style_ctx.active_for(style_key).to_string(); - let is_open = open_dd.as_deref() == Some(combo_id); + let active: String = style_ctx.active_for(*style_key).to_string(); + let is_open = open_dd.as_deref() == Some(*combo_id); // ── combo button ── let combo_btn = button( @@ -889,9 +889,9 @@ pub(super) fn render_large<'a>( iced::widget::Space::new().width(0).height(0).into(); // ── tool rows below combo ── - let make_tool_row = |tools: Vec| -> Element { + let make_tool_row = |tools: &[ToolDef]| -> Element { let btns: Vec> = tools - .into_iter() + .iter() .map(|t| { let is_active = active_tool.as_deref() == Some(t.id); let tip = t.label;