i18n: panels show user, file and system names as they are

Panels passed names to the translating widgets, so a name that happened to
be a catalog key was translated (a font style "Regular", a workspace called
"Layers"). Font styles, pattern names, recent URLs, ICC profiles, saved
width profiles, user and loaded libraries, plug-in effects and their
parameters, Document Info's artboard/style/font/image rows, text-object
contents in Layers, user text styles in brackets, the Artboard tool's
canvas label and the app-bar workspace switcher now show names verbatim;
built-in entries (Working CMYK, (closest), built-in libraries and effects,
[Normal ... Style]) still translate. Formatted tips around a name are
translated as templates only (panels::label_or_name, library_name).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-07 08:34:39 +07:00
parent cf7a43d786
commit 6572636d62
19 changed files with 190 additions and 44 deletions

View File

@ -1201,6 +1201,12 @@ fn ime_output(app: &mut VectorcraftApp, ctx: &egui::Context, xf: &Xf) {
});
}
/// Is overlay label `text` the Artboard tool's "01 - <artboard name>"? 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() }
}

View File

@ -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() }));
}
});

View File

@ -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 色印刷作業…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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>, 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<usize>, 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<usize>, 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<usize>, k:
}
for (key, v) in &params {
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]

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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

View File

@ -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();

View File

@ -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<usize> {
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::<usize>().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<Value, String> {
@ -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());

View File

@ -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())));
}
});

View File

@ -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));
}

View File

@ -22,12 +22,13 @@ fn expanded_id() -> egui::Id {
egui::Id::new("layers-expanded")
}
/// The name painted for a row: a generated `<Kind>` 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 `<Kind>` name is translated, anything else is
/// user data (an unnamed text object shows its text, which can look like `<Path>`; only an empty
/// one is called `<Text>`).
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 ("<Path>", "<Opacity Mask>") are translated where painted; the stored name stays English.
let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { tl!("<Opacity Mask>").to_string() } else { painted_name(n, &name) };
let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) {
tl!("<Opacity Mask>").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 `<Kind>` 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": "<Path>"}))["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), ("<Path>".to_string(), "<Path>".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());

View File

@ -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<K: LibraryKind>(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<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui
pub(crate) fn library_items(ui: &mut Ui, libs: &[LibraryRef], current: Option<&str>) -> Option<String> {
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));
}
}

View File

@ -372,6 +372,14 @@ pub(crate) fn set_pstate<T: Clone + Send + Sync + 'static>(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) {

View File

@ -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()

View File

@ -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<String> {
let names: Vec<String> =
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| {

View File

@ -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,
);

View File

@ -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() {

View File

@ -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 <name>) 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));