diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 45574f52..12fa00f4 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1201,6 +1201,12 @@ fn ime_output(app: &mut VectorcraftApp, ctx: &egui::Context, xf: &Xf) { }); } +/// Is overlay label `text` the Artboard tool's "01 - "? It holds a name, so it is +/// shown as it is; the tools' other labels ("anchor", "path", Puppet Warp's warning) are ours. +fn names_an_artboard(text: &str) -> bool { + text.split_once(" - ").is_some_and(|(n, _)| n.len() >= 2 && n.bytes().all(|b| b.is_ascii_digit())) +} + fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens) { for o in overlays { match o { @@ -1236,7 +1242,13 @@ fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens) { } Overlay::Label { p: pt, text, color } => { let sp = xf.to_screen(*pt) + vec2(8.0, -14.0); - p.text(sp, egui::Align2::LEFT_TOP, tl!(text), egui::FontId::proportional(11.0), c32(*color)); + p.text( + sp, + egui::Align2::LEFT_TOP, + crate::panels::label_or_name(text, !names_an_artboard(text)), + egui::FontId::proportional(11.0), + c32(*color), + ); } Overlay::Highlight { quad, color } => { let c = Color32::from_rgba_unmultiplied(color[0], color[1], color[2], color[3]); @@ -1415,6 +1427,14 @@ mod tests { out.textures_delta.clear(); } + /// The Artboard tool's label holds the artboard's name (never translated); the tools' own + /// labels are interface text. + #[test] + fn artboard_labels_name_the_artboard() { + assert!(names_an_artboard("01 - Layers") && names_an_artboard("12 - Artboard 12 - copy")); + assert!(!names_an_artboard("anchor") && !names_an_artboard("1 - x") && !names_an_artboard("Off the mesh - move closer")); + } + fn middle(pos: Pos2, pressed: bool) -> egui::Event { egui::Event::PointerButton { pos, button: egui::PointerButton::Middle, pressed, modifiers: Default::default() } } diff --git a/crates/ui-egui/src/find_font.rs b/crates/ui-egui/src/find_font.rs index 6d969f7e..743b57ac 100644 --- a/crates/ui-egui/src/find_font.rs +++ b/crates/ui-egui/src/find_font.rs @@ -76,10 +76,13 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { d.fields.insert("style".into(), json!("")); } let styles = vectorcraft_text::FontDb::global().styles(&d.str("family")); - let mut opts: Vec<&str> = vec!["(closest)"]; + // "(closest)" is ours; the font's style names are shown as they are. + let closest = tl!("(closest)"); + let mut opts: Vec<&str> = vec![closest]; opts.extend(styles.iter().map(String::as_str)); - let cur = if d.str("style").is_empty() { "(closest)".to_string() } else { d.str("style") }; - if let Some(i) = widgets::dropdown(ui, "ff-style", &cur, &opts, 120.0) { + let style = d.str("style"); + let cur = if style.is_empty() { closest } else { style.as_str() }; + if let Some(i) = widgets::dropdown_names(ui, "ff-style", cur, &opts, 120.0) { d.fields.insert("style".into(), json!(if i == 0 { String::new() } else { styles[i - 1].clone() })); } }); diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d3a3ba7d..4a0642b6 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -20,6 +20,7 @@ — missing — 遺失 to search every command 以搜尋所有指令 + (closest) (最接近) + Add Scale + 新增縮放比例 0.2 (quality) to 100 (speed) 0.2 (品質) 至 100 (速度) 1 Color Job… 1 色印刷作業… diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index 32d0d990..87f440da 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -154,6 +154,20 @@ pub fn effect_label(id: &str) -> String { .unwrap_or_else(|| id.rsplit('.').next().unwrap_or(id).to_string()) } +/// Is `id` one of our own effects? A plug-in's effect name and parameters come from the plug-in +/// and are shown as they are. +fn built_in_effect(id: &str) -> bool { + catalog().iter().any(|c| c.0 == id) +} + +/// [`effect_label`] as shown: a built-in effect's in the UI language, a plug-in's as it is. +fn shown_effect_label(id: &str) -> String { + match catalog().iter().find(|c| c.0 == id) { + Some(c) => tl!(&c.1).to_string(), + None => effect_label(id), + } +} + /// "Opacity: Default" or "Opacity: 50% Multiply". pub fn opacity_text(opacity: f32, blend: BlendMode) -> String { if (opacity - 1.0).abs() < 1e-4 && blend == BlendMode::Normal { @@ -320,7 +334,7 @@ fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) { let fx = |ui: &mut Ui, e: &Effect, id: (Option, usize)| { let (r, _) = row(ui, false); eye(ui, r, ("ap-def-fx", id), e.visible, false); - text(ui, pos2(r.left() + EYE_W + 24.0 + if id.0.is_some() { 12.0 } else { 0.0 }, r.center().y), tl!(&effect_label(&e.id)), false); + text(ui, pos2(r.left() + EYE_W + 24.0 + if id.0.is_some() { 12.0 } else { 0.0 }, r.center().y), &shown_effect_label(&e.id), false); icons::paint(ui, "dc-fx", Rect::from_center_size(r.right_center() - vec2(14.0, 0.0), vec2(16.0, 16.0)), t.icon); }; for (i, it) in ap.items.iter().enumerate().rev() { @@ -686,7 +700,7 @@ fn effect_row(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: usi set_pstate(ui.ctx(), &open_key, !open); } let lx = r.left() + EYE_W + 24.0; - if link(ui, pos2(lx, r.center().y), ("ap-fx-link", item, k), tl!(&effect_label(&e.id))) { + if link(ui, pos2(lx, r.center().y), ("ap-fx-link", item, k), &shown_effect_label(&e.id)) { select_row(app, ui.ctx(), this); if has_options(&e.id) { app.run("effect.dialog", json!({"effect": e.id, "index": k, "item": item})).ok(); @@ -724,6 +738,7 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: } } let mut change: Option<(String, Value)> = None; + let built_in = built_in_effect(&e.id); let (unit, relative) = (app.session.general_unit(), params.get("relative").and_then(Value::as_bool).unwrap_or(false)); egui::Frame::NONE.fill(t.panel_darker).inner_margin(egui::Margin { left: (EYE_W + 12.0) as i8, right: 6, top: 4, bottom: 4 }).show(ui, |ui| { ui.spacing_mut().item_spacing.y = 3.0; @@ -732,8 +747,10 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: } for (key, v) in ¶ms { ui.horizontal(|ui| { + // A plug-in's parameter names are its own. let label = humanize(key); - ui.add_sized(vec2(84.0, 22.0), egui::Label::new(egui::RichText::new(tl!(&label)).size(11.5).color(t.text)).truncate()); + let label = super::label_or_name(&label, built_in); + ui.add_sized(vec2(84.0, 22.0), egui::Label::new(egui::RichText::new(label).size(11.5).color(t.text)).truncate()); if let Some(cur) = widgets::blend_param(key, v) { if let Some(m) = widgets::blend_param_dropdown(ui, ("fx-blend", item, k, key.as_str()), cur) { change = Some((key.clone(), m)); @@ -874,18 +891,20 @@ fn delete_selected(app: &mut VectorcraftApp, ui: &Ui, node: Option<&Node>, sel: pub(crate) fn fx_menu(app: &mut VectorcraftApp, ui: &mut Ui) { let mut groups: Vec<(String, Vec<(String, String)>)> = vec![]; let plugins: Vec<_> = vectorcraft_effects::plugin_effects().into_iter().map(catalog_entry).collect(); - for (id, label, menu) in catalog().iter().chain(&plugins) { + // Built-in effects in the UI language; a plug-in's by the name it gives. + let built_in = catalog().iter().map(|(id, label, menu)| (id, tl!(label), menu)); + for (id, label, menu) in built_in.chain(plugins.iter().map(|(id, label, menu)| (id, label.as_str(), menu))) { let g = menu.get(1).cloned().unwrap_or_else(|| "Other".into()); match groups.iter_mut().find(|(n, _)| *n == g) { - Some((_, v)) => v.push((id.clone(), label.clone())), - None => groups.push((g, vec![(id.clone(), label.clone())])), + Some((_, v)) => v.push((id.clone(), label.to_string())), + None => groups.push((g, vec![(id.clone(), label.to_string())])), } } for (g, items) in groups { ui.menu_button(tl!(&g), |ui| { crate::widgets::menu_scroll(ui, |ui| { for (id, label) in items { - if ui.button(format!("{}…", tl!(&label))).clicked() { + if ui.button(format!("{label}…")).clicked() { app.run("effect.dialog", json!({"effect": id})).ok(); ui.close(); } @@ -932,7 +951,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let redefine = style .as_ref() .map_or_else(|| tl!("Redefine Graphic Style").into(), |n| crate::i18n::fmt(tl!("Redefine Graphic Style “{name}”"), &[("name", n)])); - if menu_item(ui, &redefine, style.is_some(), false) { + // Translated above, around the style's name. + if widgets::menu_item_name(ui, &redefine, style.is_some(), false) { app.run("graphicStyle.redefine", json!({})).ok(); } if menu_item(ui, tl!("Show All Hidden Attributes"), node.as_ref().is_some_and(|n| n.appearance.has_hidden()), false) { @@ -957,6 +977,9 @@ mod tests { assert_eq!(humanize("blur"), "Blur"); assert!(!effect_label("stylize.dropShadow").contains('…')); assert_eq!(effect_label("x.unknownThing"), "unknownThing"); + // Only our own effects are interface labels; a plug-in's name is its own. + assert!(built_in_effect("stylize.dropShadow") && !built_in_effect("plugin.example.glow")); + assert_eq!(shown_effect_label("plugin.example.glow"), effect_label("plugin.example.glow")); } #[test] diff --git a/crates/ui-egui/src/panels/attributes.rs b/crates/ui-egui/src/panels/attributes.rs index 19239733..58fdfb7b 100644 --- a/crates/ui-egui/src/panels/attributes.rs +++ b/crates/ui-egui/src/panels/attributes.rs @@ -140,7 +140,7 @@ fn recent_urls(app: &mut VectorcraftApp, ui: &mut Ui) { egui::Popup::menu(&resp).show(|ui| { ui.set_min_width(220.0); for u in &app.session.recent_urls { - if menu_item(ui, u, true, false) { + if widgets::menu_item_name(ui, u, true, false) { chosen = Some(u.clone()); } } diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index 8853f968..2e9d3fac 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -246,7 +246,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } let styles = vectorcraft_text::FontDb::global().styles(&s.font_family); let snames: Vec<&str> = styles.iter().map(String::as_str).collect(); - if let Some(i) = widgets::dropdown(ui, "ch-style", &s.font_style, &snames, w - 4.0) { + if let Some(i) = widgets::dropdown_names(ui, "ch-style", &s.font_style, &snames, w - 4.0) { style(app, json!({"style": snames[i]})); } ui.add_space(2.0); diff --git a/crates/ui-egui/src/panels/color_guide.rs b/crates/ui-egui/src/panels/color_guide.rs index 89e4f5a1..c9c8a478 100644 --- a/crates/ui-egui/src/panels/color_guide.rs +++ b/crates/ui-egui/src/panels/color_guide.rs @@ -200,7 +200,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } let (left, right) = app.ui.color_guide.variation.sides(); - let limit = limit_name(app, &app.ui.color_guide_limit); + let key = &app.ui.color_guide_limit; + let limit = limit_name(app, key).map(|n| library_panel::library_name(key, &n).to_string()); ui.horizontal(|ui| { widgets::dim_label(ui, left); // The library the colours are limited to, centred between the ends. @@ -214,7 +215,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { Some(name) => crate::i18n::fmt(tl!("Limit colors to swatch library: {name}"), &[("name", name)]), None => tl!("Limit colors to swatch library").into(), }; - let lr = widgets::icon_button(ui, "library", &tip, limit.is_some(), 24.0); + // The tip is translated around the library's name, not looked up whole. + let lr = widgets::icon_button(ui, "library", "", limit.is_some(), 24.0).on_hover_text(tip); egui::Popup::menu(&lr).show(|ui| limit_menu(app, ui)); ui.add_space((ui.available_width() - 3.0 * 28.0).max(0.0)); // Recolor Artwork with the harmony colours as the new colours: the selected art's, or diff --git a/crates/ui-egui/src/panels/color_themes.rs b/crates/ui-egui/src/panels/color_themes.rs index 29caf04f..751f8dfd 100644 --- a/crates/ui-egui/src/panels/color_themes.rs +++ b/crates/ui-egui/src/panels/color_themes.rs @@ -211,7 +211,8 @@ fn create(app: &mut VectorcraftApp, ui: &mut Ui) { Some(n) => crate::i18n::fmt(tl!("Save changes to {name}"), &[("name", n)]), None => tl!("Save theme to My Themes").into(), }; - if widgets::icon_button(ui, "save", &tip, false, 24.0).clicked() { + // The tip is translated around the theme's name, not looked up whole. + if widgets::icon_button(ui, "save", "", false, 24.0).on_hover_text(tip).clicked() { save(app, &ctx); } let doc = app.session.active().is_some(); diff --git a/crates/ui-egui/src/panels/doc_info.rs b/crates/ui-egui/src/panels/doc_info.rs index 9559ae00..88febc71 100644 --- a/crates/ui-egui/src/panels/doc_info.rs +++ b/crates/ui-egui/src/panels/doc_info.rs @@ -18,10 +18,12 @@ struct Cache { info: Value, } +/// A `label: value` row. `label` is shown as given: translate an interface label first, a name +/// stays as it is. pub(crate) fn row(ui: &mut Ui, label: &str, value: String) { let t = Tokens::get(ui.ctx()); ui.horizontal(|ui| { - ui.label(egui::RichText::new(tl!(label)).size(12.0).color(t.text_dim)); + ui.label(egui::RichText::new(label).size(12.0).color(t.text_dim)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Long values (paths) are cut to the panel's width; hovering shows them whole. ui.add(egui::Label::new(egui::RichText::new(value).size(12.0).color(t.text)).truncate()); @@ -70,10 +72,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if rows.is_empty() { widgets::dim_label(ui, tl!("None")); } - for r in rows { + let named = named_rows(s["id"].as_str().unwrap_or_default(), rows); + for (k, r) in rows.iter().enumerate() { let (label, value) = (r[0].as_str().unwrap_or_default(), r[1].as_str().unwrap_or_default()); + let label = super::label_or_name(label, !named.contains(&k)); if value.is_empty() { - widgets::dim_label(ui, label); + widgets::dim_name(ui, label); } else { row(ui, label, value.into()); } @@ -82,6 +86,21 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); } +/// The rows of Document Info section `id` labelled by a name (an artboard, a style, a swatch, a +/// font, an image) rather than an interface label: every row of the lists, and in Document the +/// artboards, which follow the Artboards count. Names are shown as they are. +fn named_rows(id: &str, rows: &[Value]) -> std::ops::Range { + match id { + "objects" => 0..0, + "document" => { + let Some(at) = rows.iter().position(|r| r[0] == "Artboards") else { return 0..0 }; + let count = rows.get(at).and_then(|r| r[1].as_str()).and_then(|n| n.parse::().ok()).unwrap_or(0); + at + 1..(at + 1).saturating_add(count).min(rows.len()) + } + _ => 0..rows.len(), + } +} + /// `docInfo.save {path?, selectionOnly?}`: write the text report to `path`, else a picked file /// (the web downloads it) → `{path}`. pub(crate) fn save_report(app: &mut VectorcraftApp, p: &Value) -> Result { @@ -136,6 +155,26 @@ mod tests { out.textures_delta.clear(); } + /// Artboard, font and other names in the rows are shown as they are, the labels in the UI + /// language: an artboard named like a label ("Units") is still a name. + #[test] + fn names_in_rows_are_told_from_labels() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + app.session.execute("artboard.setProps", &json!({"index": 0, "name": "Units"})).unwrap(); + app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi"})).unwrap(); + let info = app.session.execute("document.info", &json!({})).unwrap(); + let rows = |id: &str| info["sections"].as_array().unwrap().iter().find(|s| s["id"] == id).unwrap()["rows"].as_array().unwrap().clone(); + let doc = rows("document"); + let named: Vec<&str> = named_rows("document", &doc).map(|k| doc[k][0].as_str().unwrap()).collect(); + assert_eq!(named, ["Units"], "{doc:?}"); + assert_eq!(doc.iter().filter(|r| r[0] == "Units").count(), 2, "the label and the artboard"); + assert_eq!(named_rows("objects", &rows("objects")), 0..0); + let fonts = rows("fonts"); + assert!(!fonts.is_empty()); + assert_eq!(named_rows("fonts", &fonts), 0..fonts.len()); + } + #[test] fn categories_filter_the_panel_and_save_writes_the_report() { let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index 81fc527c..a41e611c 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -205,7 +205,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } let styles = db.styles(&family); let snames: Vec<&str> = styles.iter().map(String::as_str).collect(); - if let Some(i) = widgets::dropdown(ui, "gl-style", &style, &snames, (w * 0.38 - 8.0).max(60.0)) { + if let Some(i) = widgets::dropdown_names(ui, "gl-style", &style, &snames, (w * 0.38 - 8.0).max(60.0)) { set_pstate(ui.ctx(), "gl-font", Some((family.clone(), snames[i].to_string()))); } }); diff --git a/crates/ui-egui/src/panels/gradient.rs b/crates/ui-egui/src/panels/gradient.rs index b644af0f..3c0cc95d 100644 --- a/crates/ui-egui/src/panels/gradient.rs +++ b/crates/ui-egui/src/panels/gradient.rs @@ -408,7 +408,7 @@ fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: boo Some(n) => format!("{n} {}%", vectorcraft_color::tint_percent(s.tint)), None => s.color.to_hex().to_uppercase(), }; - widgets::dim_label(ui, &label); + widgets::dim_name(ui, &label); if resp.on_hover_text(tl!("Edit the stop")).clicked() { open_popover(app, i, r.left_bottom() + vec2(0.0, 4.0)); } diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 2e792586..3a08447c 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -22,12 +22,13 @@ fn expanded_id() -> egui::Id { egui::Id::new("layers-expanded") } -/// The name painted for a row: a generated `` name is translated, anything else is user data. -fn painted_name(n: &Node, name: &str) -> String { - if n.name.is_none() - && let Some(inner) = name.strip_prefix('<').and_then(|s| s.strip_suffix('>')) - { - return format!("<{}>", crate::i18n::t(inner)); +/// The name painted for a row in `lang`: a generated `` name is translated, anything else is +/// user data (an unnamed text object shows its text, which can look like ``; only an empty +/// one is called ``). +fn painted_name(n: &Node, name: &str, lang: crate::i18n::Lang) -> String { + let generated = n.name.is_none() && !matches!(&n.kind, NodeKind::Text(t) if t.runs.iter().any(|r| !r.text.is_empty())); + if generated && let Some(inner) = name.strip_prefix('<').and_then(|s| s.strip_suffix('>')) { + return format!("<{}>", crate::i18n::tr(lang, inner)); } name.to_string() } @@ -244,7 +245,11 @@ fn row( _ => { let painter = ui.painter().with_clip_rect(name_rect); // Generated names ("", "") are translated where painted; the stored name stays English. - let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { tl!("").to_string() } else { painted_name(n, &name) }; + let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { + tl!("").to_string() + } else { + painted_name(n, &name, crate::i18n::current()) + }; let text = painter.text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, shown, font, t.text); // A clipping path's name is underlined, a masked object's with a dashed line. if clip_path { @@ -464,6 +469,28 @@ mod tests { out.shapes.iter().filter(|c| matches!(&c.shape, egui::Shape::Circle(cs) if cs.radius == 3.2 && cs.fill != Color32::TRANSPARENT)).count() } + /// A generated `` name is translated where painted; an unnamed text object's text never + /// is, even when it reads like one. + #[test] + fn only_generated_names_are_translated() { + let zh = crate::i18n::Lang::from_code("zh-hant").unwrap(); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let run = |app: &mut VectorcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap(); + run(&mut app, "file.new", json!({"width": 100, "height": 100})); + let rect = run(&mut app, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap(); + let text = run(&mut app, "text.create", json!({"x": 10, "y": 40, "text": ""}))["id"].as_u64().unwrap(); + let doc = &app.session.active().unwrap().doc; + let names = |id: u64| { + let n = doc.node(NodeId(id)).unwrap(); + (n.display_name(), painted_name(n, &n.display_name(), zh)) + }; + assert_eq!(names(text), ("".to_string(), "".to_string())); + let (stored, painted) = names(rect); + let inner = stored.trim_start_matches('<').trim_end_matches('>'); + assert_eq!(painted, format!("<{}>", crate::i18n::tr(zh, inner))); + assert_ne!(painted, stored, "translated"); + } + #[test] fn target_circle_fills_for_non_default_transparency() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/panels/library_panel.rs b/crates/ui-egui/src/panels/library_panel.rs index 0812a358..bd68193d 100644 --- a/crates/ui-egui/src/panels/library_panel.rs +++ b/crates/ui-egui/src/panels/library_panel.rs @@ -113,6 +113,17 @@ pub(crate) fn submenu(category: &str) -> Option<&'static str> { } } +/// Is library `id` built in? A library the user saved to the User Defined folder or loaded from a +/// file (`user/…`, `loaded/…` ids) is named by its file. +fn builtin_library(id: &str) -> bool { + !(id.starts_with("user/") || id.starts_with("loaded/")) +} + +/// Library `id`'s name as shown: a built-in library's in the UI language, any other's as it is. +pub(crate) fn library_name<'a>(id: &str, name: &'a str) -> &'a str { + super::label_or_name(name, builtin_library(id)) +} + /// The opener of library `K`'s libraries (`window.swatchLibrary {library}`): open library `library` /// (an id or a name; `list`: the command listing them) in the panel, `null` closing it. `found` /// looks a library up: its id, name and item count. → {open, name, count} @@ -174,8 +185,9 @@ fn window(app: &mut VectorcraftApp, ctx: &egui::Context, id: &st ui.set_width(256.0); let (strip, _) = ui.allocate_exact_size(vec2(256.0, 26.0), Sense::hover()); ui.painter().rect_filled(strip, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.panel_darker); - let label = egui::RichText::new(tl!(&name)).font(theme::semibold(12.0)); - let galley = ui.painter().layout_no_wrap(tl!(&name).to_string(), theme::semibold(12.0), t.text); + let name = library_name(id, &name); + let label = egui::RichText::new(name).font(theme::semibold(12.0)); + let galley = ui.painter().layout_no_wrap(name.to_string(), theme::semibold(12.0), t.text); let tab = Rect::from_min_size(strip.min, vec2((galley.size().x + 24.0).min(190.0), 26.0)); ui.painter().rect_filled(tab, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel); ui.put(tab.shrink2(vec2(12.0, 0.0)), egui::Label::new(label.color(t.text)).truncate().selectable(false)); @@ -359,7 +371,7 @@ pub(crate) fn library_menu(app: &mut VectorcraftApp, ui: &mut Ui pub(crate) fn library_items(ui: &mut Ui, libs: &[LibraryRef], current: Option<&str>) -> Option { let mut chosen = None; let mut item = |ui: &mut Ui, l: &LibraryRef| { - if menu_item(ui, &l.name, true, current == Some(l.id.as_str())) { + if widgets::menu_item_name(ui, library_name(&l.id, &l.name), true, current == Some(l.id.as_str())) { chosen = Some(l.id.clone()); } }; @@ -598,5 +610,10 @@ mod tests { assert_eq!(app.session.documents().len(), 1, "not opened as a document"); let other = SwatchLibraries::list(&app).into_iter().find(|l| l.id == open.id).unwrap(); assert_eq!(other.submenu, Some("Other Libraries")); + // Built-in libraries' names are interface labels; a file's is shown as it is. + for l in SwatchLibraries::list(&app) { + assert_eq!(builtin_library(&l.id), !matches!(l.submenu, Some("User Defined" | "Other Libraries")), "{}", l.id); + } + assert!(!builtin_library(&open.id) && builtin_library(vectorcraft_engine::cmd::swatchlib::DOCUMENT_SWATCHES)); } } diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 1dcef79d..cc0c909b 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -372,6 +372,14 @@ pub(crate) fn set_pstate(ctx: &egui::Context, ctx.data_mut(|d| d.insert_temp(egui::Id::new(("panel-state", key)), v)); } +/// An entry of a list that mixes interface labels with names (user, file or system data) as +/// shown: a built-in entry in the UI language, a name exactly as it is (a profile or library the +/// user calls "Layers" stays "Layers"). Pass the result to the non-translating widgets +/// (`dropdown_names`, `menu_item_name`, `dim_name`). +pub(crate) fn label_or_name(s: &str, built_in: bool) -> &str { + if built_in { tl!(s) } else { s } +} + /// "Recent Colors" header + a row of chips (the Session's recent colours, which every paint /// command feeds); clicking one applies it to the active proxy (Alt: the inactive one). pub(crate) fn recent_colors_row(app: &mut VectorcraftApp, ui: &mut Ui) { diff --git a/crates/ui-egui/src/panels/pattern_options.rs b/crates/ui-egui/src/panels/pattern_options.rs index cf33bc7a..6a9c348d 100644 --- a/crates/ui-egui/src/panels/pattern_options.rs +++ b/crates/ui-egui/src/panels/pattern_options.rs @@ -169,7 +169,7 @@ fn idle(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.horizontal(|ui| { let labels: Vec<&str> = names.iter().map(String::as_str).collect(); - if let Some(i) = widgets::dropdown(ui, "po-pick", &chosen, &labels, 150.0) { + if let Some(i) = widgets::dropdown_names(ui, "po-pick", &chosen, &labels, 150.0) { set_pstate(ui.ctx(), "po-chosen", names[i].clone()); } if widgets::flat_button(ui, tl!("Edit Pattern"), 90.0).clicked() diff --git a/crates/ui-egui/src/panels/separations.rs b/crates/ui-egui/src/panels/separations.rs index 39c80ea2..c6bc2dc3 100644 --- a/crates/ui-egui/src/panels/separations.rs +++ b/crates/ui-egui/src/panels/separations.rs @@ -73,11 +73,13 @@ fn plates_section(app: &mut VectorcraftApp, ui: &mut Ui) { }); } +/// A dropdown of the `kind` profiles after `extra` (a built-in entry, shown in the UI language); +/// profile names are shown as they are. Returns the chosen name. fn profile_dropdown(ui: &mut Ui, id: &str, current: &str, kind: ProfileKind, extra: Option<&str>) -> Option { let names: Vec = extra.map(str::to_string).into_iter().chain(cms::profiles().into_iter().filter(|p| p.kind == kind).map(|p| p.name)).collect(); - let refs: Vec<&str> = names.iter().map(String::as_str).collect(); - widgets::dropdown(ui, id, current, &refs, 210.0).map(|i| names[i].clone()) + let refs: Vec<&str> = names.iter().map(|n| super::label_or_name(n, extra == Some(n.as_str()))).collect(); + widgets::dropdown_names(ui, id, super::label_or_name(current, extra == Some(current)), &refs, 210.0).and_then(|i| names.get(i).cloned()) } fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -143,9 +145,10 @@ fn proof_section(app: &mut VectorcraftApp, ui: &mut Ui) { let v = proof::view(); widgets::section_header(ui, tl!("Proof Setup")); let cur = v.setup.target.id(); - let label = PROOF_TARGETS.iter().find(|(id, _)| *id == cur).map_or(cur.as_str(), |(_, l)| l); - let labels: Vec<&str> = PROOF_TARGETS.iter().map(|(_, l)| *l).collect(); - if let Some(i) = widgets::dropdown(ui, "proof-target", label, &labels, 230.0) { + // A target that isn't one of ours (a loaded profile) shows its name as it is. + let label = PROOF_TARGETS.iter().find(|(id, _)| *id == cur).map_or(cur.as_str(), |(_, l)| tl!(*l)); + let labels: Vec<&str> = PROOF_TARGETS.iter().map(|(_, l)| tl!(*l)).collect(); + if let Some(i) = widgets::dropdown_names(ui, "proof-target", label, &labels, 230.0) { run(app, "view.proofSetup", json!({"target": PROOF_TARGETS[i].0})); } ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index 63e887fb..7fd3c3b2 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -405,7 +405,8 @@ pub(crate) fn profile_dropdown(app: &VectorcraftApp, ui: &mut Ui, cur: Option<&W ui.painter().text( row.left_center() + vec2(84.0, 0.0), egui::Align2::LEFT_CENTER, - tl!(e.label), + // A saved profile's name is the user's. + super::label_or_name(e.label, e.built_in), egui::FontId::proportional(11.5), t.text, ); diff --git a/crates/ui-egui/src/panels/text_styles.rs b/crates/ui-egui/src/panels/text_styles.rs index 5f34153a..4df4fb55 100644 --- a/crates/ui-egui/src/panels/text_styles.rs +++ b/crates/ui-egui/src/panels/text_styles.rs @@ -98,8 +98,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { } else if resp.hovered() { ui.painter().rect_filled(r, 0.0, t.hover); } - // The built-in "[Normal … Style]" names are translated where painted; other names are user data. - let shown = if name.starts_with('[') && name.ends_with(']') { tl!(name) } else { name.as_str() }; + // The built-in "[Normal … Style]" (listed first) is translated where painted; other names + // are user data, brackets or not. + let shown = super::label_or_name(name, *name == normal); let label = if overridden { format!("{shown}+") } else { shown.to_string() }; ui.painter().text(r.left_center() + vec2(8.0, 0.0), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(12.5), t.text); if resp.clicked() { diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index 17bb0409..472f4074 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -239,8 +239,8 @@ pub fn popup(app: &mut VectorcraftApp, ui: &mut egui::Ui) { ui.separator(); } crate::menus::Item::Cmd(label, id, p) => { - let label = crate::menus::dynamic_label(app, id, label); - let label = tl!(&label); + // As the Window menu draws it: custom workspace names (and Reset ) as they are. + let label = crate::menus::display_label(app, id, label); let text = match crate::menus::checked(app, id, &p) { Some(true) => format!("✓ {label}"), Some(false) => format!(" {label}"), @@ -338,7 +338,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.set_width(ui.available_width()); for n in names(&app.ui) { let builtin = is_builtin(&n); - let shown = if builtin { tl!(&n) } else { n.as_str() }; + let shown = crate::panels::label_or_name(&n, builtin); let text = if builtin { egui::RichText::new(shown).color(t.text_dim) } else { egui::RichText::new(shown).color(t.text) }; if ui.selectable_label(d.str("selected") == n, text).clicked() && !builtin { d.fields.insert("selected".into(), json!(n));