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 committed 2026-07-02 21:00:57 +01:00
1 parent 8164e14a19
commit 73b05e1dec
14 files changed
+947 -690

No files matched your search

+6 -6
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,
+19 -19
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;