From b1695873f5d0031f51a8d5e7ec036fced6f000e2 Mon Sep 17 00:00:00 2001 From: DuBearte <130190089+DuBearte@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:45:13 -0400 Subject: [PATCH] Browser: show given names as typed, not translated as labels (#1228) (#1264) Browser rows that are names someone gave (bodies, sketches, components, joints, views...) show as typed instead of being translated as UI labels; default names like Sketch1 are still translated. Fixes #1228. --- crates/ui-egui/src/browser.rs | 27 +++++++++++++++++--- crates/ui-egui/src/dialogs_assembly.rs | 11 +++++++- crates/ui-egui/src/i18n.rs | 35 +++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index d7102275..894a080e 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -268,6 +268,9 @@ pub(crate) struct Row<'a> { pub(crate) eye: Option, pub(crate) icon: &'a str, pub(crate) label: &'a str, + /// The label is a name someone gave (a body, sketch, component, …): it is shown as typed, + /// not translated when it happens to match a UI label. + pub(crate) name: bool, pub(crate) selected: bool, pub(crate) dim: bool, pub(crate) color: Option, @@ -375,7 +378,7 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp { x += 21.0; } let col = row.color.unwrap_or(if row.dim { t.text_dim } else { t.text }); - let label_display = crate::i18n::tr_dyn(row.label); + let label_display = if row.name { crate::i18n::tr_name(row.label) } else { crate::i18n::tr_dyn(row.label) }; let galley = ui.painter().layout_no_wrap(label_display, FontId::proportional(12.5), col); let lw = galley.size().x; ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, col); @@ -596,6 +599,7 @@ fn component_rows(app: &mut SolveApp, ui: &mut egui::Ui, id: u64, depth: usize, eye: (id != 0).then_some(visible), icon: "component", label: &name, + name: true, selected: app.tree.picked_components.contains(&id), dim: faded, radio: Some(active), @@ -699,6 +703,7 @@ fn occurrence_rows(app: &mut SolveApp, ui: &mut egui::Ui, path: Vec, depth: eye: Some(own_visible), icon: "component", label: &placement.name, + name: true, selected, dim: faded || !visible, radio: Some(active), @@ -891,7 +896,11 @@ fn named_view_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &m } let names: Vec = app.session.doc.named_views.iter().map(|v| v.name.clone()).collect(); for name in names { - let r = draw_row(ui, ui.id().with(("view", &name)), &Row { depth: depth + 1, icon: "perspective", label: &name, ..Default::default() }); + let r = draw_row( + ui, + ui.id().with(("view", &name)), + &Row { depth: depth + 1, icon: "perspective", label: &name, name: true, ..Default::default() }, + ); if r.double { restore_view(app, &name); } @@ -1031,7 +1040,7 @@ fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A let r = draw_row( ui, ui.id().with(("canvas", id)), - &Row { depth: depth + 1, eye: Some(visible), icon: "canvas", label: &name, selected, dim: !visible, ..Default::default() }, + &Row { depth: depth + 1, eye: Some(visible), icon: "canvas", label: &name, name: true, selected, dim: !visible, ..Default::default() }, ); if r.eye { let _ = app.run("canvas.edit", json!({ "canvas": id, "visible": !visible })); @@ -1461,7 +1470,16 @@ fn folder_rows_at(app: &mut SolveApp, ui: &mut egui::Ui, comp: u64, path: Option let r = draw_row( ui, ui.id().with(&gkey), - &Row { depth: depth + 1, fold: Some(gopen), eye: Some(gvis), icon: "folder", label: &label, dim: !gvis, ..Default::default() }, + &Row { + depth: depth + 1, + fold: Some(gopen), + eye: Some(gvis), + icon: "folder", + label: &label, + name: true, + dim: !gvis, + ..Default::default() + }, ); if r.fold || r.clicked { toggle(app, &gkey, true); @@ -1517,6 +1535,7 @@ fn entry_row( eye: Some(e.visible), icon: e.icon, label: &e.label, + name: true, selected: e.selected, dim: !e.visible, color: e.color, diff --git a/crates/ui-egui/src/dialogs_assembly.rs b/crates/ui-egui/src/dialogs_assembly.rs index 4ef4a39e..60fbf3a3 100644 --- a/crates/ui-egui/src/dialogs_assembly.rs +++ b/crates/ui-egui/src/dialogs_assembly.rs @@ -1119,7 +1119,16 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) { let r = draw_row( ui, ui.id().with(("joint", id)), - &Row { depth: depth + 1, eye: Some(visible), icon: "joint", label: &name, selected, dim: suppressed || !visible, ..Default::default() }, + &Row { + depth: depth + 1, + eye: Some(visible), + icon: "joint", + label: &name, + name: true, + selected, + dim: suppressed || !visible, + ..Default::default() + }, ); if let Some(resp) = &r.resp { resp.clone().on_hover_text(format!("{kind:?} joint")); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 6ba7ef6b..28c548c0 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -77,16 +77,30 @@ impl Locale { interpolate(self.text(id), args) } - /// Translate generated browser names and labels, preserving custom names otherwise. - pub fn dynamic(self, text: &str) -> String { + /// The default names SolidCraft gives (`Sketch1`, `Body2`, `Component3`), translated. + fn default_name(self, text: &str) -> Option { for (prefix, id) in [("Sketch", "browser.sketch_name"), ("Body", "browser.body_name"), ("Component", "browser.component_name")] { if let Some(number) = text.strip_prefix(prefix) && !number.is_empty() && number.chars().all(|c| c.is_ascii_digit()) { - return self.format(id, &[("number", number)]); + return Some(self.format(id, &[("number", number)])); } } + None + } + + /// A name someone gave (a body, sketch, component, …): shown as typed, even when it matches + /// a UI label ("Fillet", "Boss"); only the default names are translated. + pub fn given_name(self, text: &str) -> String { + self.default_name(text).unwrap_or_else(|| text.to_string()) + } + + /// Translate generated browser names and labels, preserving custom names otherwise. + pub fn dynamic(self, text: &str) -> String { + if let Some(name) = self.default_name(text) { + return name; + } for (prefix, id, arg) in [("Units: ", "browser.units", "units"), ("Precision: ", "browser.precision", "precision")] { if let Some(value) = text.strip_prefix(prefix) { return self.format(id, &[(arg, value)]); @@ -166,6 +180,9 @@ pub fn format(id: &str, args: &[(&str, &str)]) -> String { pub fn tr_dyn(text: &str) -> String { CURRENT.with(|current| current.get().dynamic(text)) } +pub fn tr_name(text: &str) -> String { + CURRENT.with(|current| current.get().given_name(text)) +} #[cfg(test)] mod tests { @@ -402,6 +419,18 @@ mod tests { assert_eq!(Locale::for_language("en").dynamic("Sketch1"), "Sketch1"); } + #[test] + fn given_names_are_not_translated_as_labels() { + let de = Locale::for_language("de"); + // "Boss" and "Fillet" are also UI labels, which `dynamic` translates. + assert_ne!(de.dynamic("Boss"), "Boss"); + assert_ne!(de.dynamic("Fillet"), "Fillet"); + assert_eq!(de.given_name("Boss"), "Boss"); + assert_eq!(de.given_name("Fillet"), "Fillet"); + assert_eq!(de.given_name("Sketch3"), de.dynamic("Sketch3")); + assert_ne!(de.given_name("Sketch3"), "Sketch3"); + } + #[test] fn korean_catalog_translates_keys_labels_and_browser_patterns() { let ko = Locale::for_language("ko");