perf(core): cache ribbon_groups() with OnceLock, avoid per-frame tree allocations

Change CadModule::ribbon_groups() from -> Vec<RibbonGroup> 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.
This commit is contained in:
Karim Jerbi 2026-07-02 20:58:48 +01:00
commit 73b05e1dec
14 changed files with 946 additions and 689 deletions

View file

@ -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<RibbonGroup>;
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<RibbonGroup> {
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<Vec<RibbonGroup>> = 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<Vec<RibbonGroup>> = 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<RibbonGroup> {
vec![RibbonGroup {
title: "Group",
tools: vec![],
}]
fn ribbon_groups(&self) -> &[RibbonGroup] {
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
GROUPS.get_or_init(|| {
vec![RibbonGroup {
title: "Group",
tools: vec![],
}]
})
}
}
let m: Box<dyn CadModule> = Box::new(Demo);

View file

@ -79,6 +79,15 @@ impl From<IconKind> 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<ToolDef> 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<RibbonGroup> for OwnedRibbonGroup {
fn from(g: RibbonGroup) -> Self {
Self {
@ -252,6 +332,15 @@ impl From<RibbonGroup> 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<OwnedRibbonGroup>) -> Bo
fn title(&self) -> &'static str {
self.title
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<RibbonGroup> {
fn ribbon_groups(&self) -> &[RibbonGroup] {
self.0.ribbon_groups()
}
}

View file

@ -78,7 +78,7 @@ fn handle_host_request(
Ok(groups) => HostResponse::Ribbon(
groups
.ribbon_groups()
.into_iter()
.iter()
.map(OwnedRibbonGroup::from)
.collect(),
),

View file

@ -29,7 +29,9 @@ impl CadModule for MyModule {
fn title(&self) -> &'static str {
"My Plugin"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
fn ribbon_groups(&self) -> &[RibbonGroup] {
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = 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()),
})],
}]
})
}
}

View file

@ -139,7 +139,9 @@ impl CadModule for TemplateModule {
"Template v2"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
fn ribbon_groups(&self) -> &[RibbonGroup] {
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
GROUPS.get_or_init(|| {
vec![RibbonGroup {
title: "Survey",
tools: vec![
@ -169,6 +171,7 @@ impl CadModule for TemplateModule {
}),
],
}]
})
}
}

View file

@ -38,193 +38,200 @@ impl CadModule for AnnotateModule {
"Annotate"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<Vec<RibbonGroup>> = 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()),
}),
],
},
]
}),
],
},
]
})
}
}

View file

@ -26,7 +26,7 @@ impl CadModule for DrawModule {
"Draw"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<Vec<RibbonGroup>> = 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.
]
})
}
}

View file

@ -35,91 +35,94 @@ impl CadModule for InsertModule {
"Insert"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<Vec<RibbonGroup>> = 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()),
],
},
]
})
}
}

View file

@ -36,31 +36,36 @@ impl CadModule for LayoutModule {
"Layout"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<Vec<RibbonGroup>> = 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(),
],
},
]
})
}
}

View file

@ -21,58 +21,61 @@ impl CadModule for ManageModule {
"Manage"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<Vec<RibbonGroup>> = 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()),
],
},
]
})
}
}

View file

@ -40,34 +40,37 @@ impl CadModule for ModelModule {
"Model"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<Vec<RibbonGroup>> = 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)),
],
},
]
})
}
}

View file

@ -45,96 +45,99 @@ impl CadModule for ViewModule {
"View"
}
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
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<Vec<RibbonGroup>> = 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()),
],
},
]
})
}
}

View file

@ -409,7 +409,7 @@ impl Ribbon {
let mut items_row: Vec<Element<Message>> = Vec::new();
let mut small_buf: Vec<Element<Message>> = 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,

View file

@ -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<String>,
open_dd: &Option<String>,
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<String>,
open_dd: &Option<String>,
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<ToolDef>| -> Element<Message> {
let make_tool_row = |tools: &[ToolDef]| -> Element<Message> {
let btns: Vec<Element<Message>> = 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<ToolDef>| -> Element<Message> {
let make_tool_row = |tools: &[ToolDef]| -> Element<Message> {
let btns: Vec<Element<Message>> = tools
.into_iter()
.iter()
.map(|t| {
let is_active = active_tool.as_deref() == Some(t.id);
let tip = t.label;