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:
parent
8164e14a19
commit
73b05e1dec
14 changed files with 946 additions and 689 deletions
|
|
@ -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> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ fn handle_host_request(
|
|||
Ok(groups) => HostResponse::Ribbon(
|
||||
groups
|
||||
.ribbon_groups()
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(OwnedRibbonGroup::from)
|
||||
.collect(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
})],
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}),
|
||||
],
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ impl CadModule for AnnotateModule {
|
|||
"Annotate"
|
||||
}
|
||||
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
fn ribbon_groups(&self) -> &[RibbonGroup] {
|
||||
use crate::modules::draw::draw::{revcloud, wipeout};
|
||||
|
||||
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
|
||||
GROUPS.get_or_init(|| {
|
||||
vec![
|
||||
// ── Text ─────────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
|
|
@ -213,7 +215,9 @@ impl CadModule for AnnotateModule {
|
|||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/scale_list.svg"
|
||||
)),
|
||||
event: crate::modules::ModuleEvent::Command("SCALELISTEDIT".to_string()),
|
||||
event: crate::modules::ModuleEvent::Command(
|
||||
"SCALELISTEDIT".to_string(),
|
||||
),
|
||||
}),
|
||||
RibbonItem::Tool(crate::modules::ToolDef {
|
||||
id: "SYNCPVIEWPORTS",
|
||||
|
|
@ -221,10 +225,13 @@ impl CadModule for AnnotateModule {
|
|||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/sync.svg"
|
||||
)),
|
||||
event: crate::modules::ModuleEvent::Command("SYNCPVIEWPORTS".to_string()),
|
||||
event: crate::modules::ModuleEvent::Command(
|
||||
"SYNCPVIEWPORTS".to_string(),
|
||||
),
|
||||
}),
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +43,8 @@ impl CadModule for DrawModule {
|
|||
};
|
||||
use properties::match_prop;
|
||||
|
||||
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
|
||||
GROUPS.get_or_init(|| {
|
||||
vec![
|
||||
RibbonGroup {
|
||||
title: "Draw",
|
||||
|
|
@ -228,5 +230,6 @@ impl CadModule for DrawModule {
|
|||
// Support group lives on the Start tab now (see view.rs:
|
||||
// start_page_view). Removed from the Draw ribbon to declutter.
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ impl CadModule for InsertModule {
|
|||
"Insert"
|
||||
}
|
||||
|
||||
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![
|
||||
// ── Reference ────────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
|
|
@ -121,5 +123,6 @@ impl CadModule for InsertModule {
|
|||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ impl CadModule for LayoutModule {
|
|||
"Layout"
|
||||
}
|
||||
|
||||
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: "Viewport",
|
||||
|
|
@ -48,7 +50,9 @@ impl CadModule for LayoutModule {
|
|||
ToolDef {
|
||||
id: "PAGESETUP",
|
||||
label: "Page Setup",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/pagesetup.svg")),
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/pagesetup.svg"
|
||||
)),
|
||||
event: ModuleEvent::Command("PAGESETUP".to_string()),
|
||||
}
|
||||
.into(),
|
||||
|
|
@ -62,5 +66,6 @@ impl CadModule for LayoutModule {
|
|||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ impl CadModule for ManageModule {
|
|||
"Manage"
|
||||
}
|
||||
|
||||
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![
|
||||
// ── Customization ─────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
|
|
@ -74,5 +76,6 @@ impl CadModule for ManageModule {
|
|||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ impl CadModule for ModelModule {
|
|||
"Model"
|
||||
}
|
||||
|
||||
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: "Model",
|
||||
|
|
@ -69,5 +71,6 @@ impl CadModule for ModelModule {
|
|||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ impl CadModule for ViewModule {
|
|||
"View"
|
||||
}
|
||||
|
||||
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![
|
||||
// ── Viewport Tools ───────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
|
|
@ -136,5 +138,6 @@ impl CadModule for ViewModule {
|
|||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue