diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index 80d3f777b..41a1e85c0 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -257,7 +257,7 @@ fn open_list(app: &mut WordApp, ui: &mut Ui) { fn open_page(app: &mut WordApp, ui: &mut Ui) { heading(ui, "Open"); - if ui.button(egui::RichText::new(tl!("📂 Browse…")).font(medium(14.0))).clicked() { + if ui.button(egui::RichText::new(tl!("Browse…")).font(medium(14.0))).clicked() { let _ = app.run("ui.openFileDialog", json!({})); } ui.add_space(18.0); diff --git a/crates/ui-egui/src/equation_tab.rs b/crates/ui-egui/src/equation_tab.rs index 7b2d144a6..0720afeda 100644 --- a/crates/ui-egui/src/equation_tab.rs +++ b/crates/ui-egui/src/equation_tab.rs @@ -210,7 +210,7 @@ fn symbols(ui: &mut Ui, app: &mut WordApp) { if ui.add_enabled(start + page < all.len(), egui::Button::new("▼").min_size(vec2(16.0, 22.0))).clicked() { ui.data_mut(|d| d.insert_temp(start_id, start + per_row)); } - let resp = ui.add(egui::Button::new("⋯").min_size(vec2(16.0, 22.0))).on_hover_text(tl!("All symbol sets")); + let resp = ui.add(egui::Button::new("…").min_size(vec2(16.0, 22.0))).on_hover_text(tl!("All symbol sets")); egui::Popup::menu(&resp).show(|ui| { ui.set_width(420.0); ui.horizontal_wrapped(|ui| { diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 6aead4fe8..dde77cf80 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -819,8 +819,7 @@ {key}+Click to follow link {key}+clic para seguir el vínculo {selected} of {words} words {selected} de {words} palabras {words} words {words} palabras - ➕ New comment ➕ Nuevo comentario - 📂 Browse… 📂 Examinar… + Browse… Examinar… Equation Ecuación Review Revisar Track\nChanges Control de\ncambios diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index a835285b6..c085c60da 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -790,8 +790,7 @@ Zoom Out 縮小 {group} settings {group} の設定 {key}+Click to follow link {key} キーを押しながらクリックしてリンク先を表示 - ➕ New comment ➕ 新しいコメント - 📂 Browse… 📂 参照… + Browse… 参照… {words} words {words} 語 {selected} of {words} words {selected}/{words} 語 Direction: 方向: diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8aec1974d..0f73e1ee5 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -790,8 +790,7 @@ Zoom Out Reduzir {group} settings Configurações de {group} {key}+Click to follow link {key}+clique para seguir o link - ➕ New comment ➕ Novo comentário - 📂 Browse… 📂 Procurar… + Browse… Procurar… {words} words Palavras: {words} {selected} of {words} words Palavras: {words}; seleção: {selected} Dark page (white text on black) Página escura (texto branco sobre preto) diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index d0fb78548..4f5b7a509 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -798,8 +798,7 @@ Zoom Out Umanji {group} settings Podešavanja za {group} {key}+Click to follow link {key}+klik za otvaranje veze - ➕ New comment ➕ Novi komentar - 📂 Browse… 📂 Potraži… + Browse… Potraži… {words} words {words} reči {selected} of {words} words {selected} od {words} reči Direction: Smer: diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index 925c6b3c9..80ee08751 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -802,8 +802,7 @@ Zoom Out Умањи {group} settings Подешавања за {group} {key}+Click to follow link {key}+клик за отварање везе - ➕ New comment ➕ Нови коментар - 📂 Browse… 📂 Потражи… + Browse… Потражи… {words} words {words} речи {selected} of {words} words {selected} од {words} речи Direction: Смер: diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index cdf03b337..50bf777cd 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -817,8 +817,7 @@ Zoom Out Зменшити масштаб {group} settings Параметри групи «{group}» {key}+Click to follow link {key}+клацання, щоб перейти за посиланням - ➕ New comment ➕ Новий коментар - 📂 Browse… 📂 Огляд… + Browse… Огляд… {words} words Слів: {words} {selected} of {words} words Слів: {words}; виділено: {selected} Apply Styles Застосувати стилі diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2608695d6..1f8abfd53 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -801,8 +801,7 @@ Zoom Out 缩小 {group} settings {group}设置 {key}+Click to follow link 按住 {key} 并单击可访问链接 - ➕ New comment ➕ 新建批注 - 📂 Browse… 📂 浏览… + Browse… 浏览… {words} words {words} 个字 {selected} of {words} words {selected}/{words} 个字 Direction: 方向: diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index baf569b6f..ab809147e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -791,8 +791,7 @@ Zoom Out 縮小 {group} settings {group}設定 {key}+Click to follow link 按住 {key} 並按一下以追蹤連結 - ➕ New comment ➕ 新增註解 - 📂 Browse… 📂 瀏覽… + Browse… 瀏覽… {words} words {words} 個字 {selected} of {words} words {selected}/{words} 個字 Direction: 方向: diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index cd19b5834..70267b9bf 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -44,7 +44,14 @@ fn header(ui: &mut Ui, title: &str) -> bool { ui.horizontal(|ui| { ui.label(egui::RichText::new(tl!(title)).font(semibold(15.0))); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.small_button("✕").clicked() { + // Drawn, not the "✕" character: the interface fonts have no glyph for it. + let t = crate::theme::Tokens::get(ui.ctx()); + let (r, resp) = ui.allocate_exact_size(egui::vec2(22.0, 22.0), egui::Sense::click()); + if resp.hovered() { + ui.painter().rect_filled(r, 4.0, t.hover); + } + crate::icons::paint(ui.painter(), egui::Rect::from_center_size(r.center(), egui::vec2(14.0, 14.0)), "close", t.icon, t.accent); + if resp.on_hover_text(tl!("Close")).clicked() { close = true; } }); @@ -216,7 +223,7 @@ fn comments(app: &mut WordApp, ui: &mut Ui) { let _ = app.run("view.commentsPane", json!({"value": false})); return; } - if ui.button(tl!("➕ New comment")).clicked() { + if crate::widgets::icon_text_button(ui, "newComment", tl!("New Comment"), regular(12.5)).clicked() { let _ = app.run("review.newComment", json!({"text": ""})); } ui.separator(); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index fb3dda442..992f6246f 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -120,21 +120,24 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { } ui.add_space(6.0); let track = app.session.doc.settings.track_changes; - ui.menu_button( - egui::RichText::new(format!("✎ {} ▾", if track { tl!("Reviewing") } else { tl!("Editing") })).font(regular(12.0)), - |ui| { - if ui.selectable_label(!track, tl!(tl!("Editing — edit the document directly"))).clicked() { - let _ = app.run("review.trackChanges", json!({"value": false})); - ui.close(); - } - if ui.selectable_label(track, tl!(tl!("Reviewing — edits become suggestions"))).clicked() { - let _ = app.run("review.trackChanges", json!({"value": true})); - ui.close(); - } - }, + let mode = crate::widgets::icon_text_button( + ui, + "pencil", + &format!("{} ▾", if track { tl!("Reviewing") } else { tl!("Editing") }), + regular(12.0), ); + egui::Popup::menu(&mode).show(|ui| { + if ui.selectable_label(!track, tl!(tl!("Editing — edit the document directly"))).clicked() { + let _ = app.run("review.trackChanges", json!({"value": false})); + ui.close(); + } + if ui.selectable_label(track, tl!(tl!("Reviewing — edits become suggestions"))).clicked() { + let _ = app.run("review.trackChanges", json!({"value": true})); + ui.close(); + } + }); ui.add_space(4.0); - if ui.button(egui::RichText::new(format!("💬 {}", tl!("Comments"))).font(regular(12.0))).clicked() { + if crate::widgets::icon_text_button(ui, "comment", tl!("Comments"), regular(12.0)).clicked() { let _ = app.run("view.commentsPane", json!({})); } }); @@ -366,7 +369,16 @@ fn home(app: &mut WordApp, ui: &mut Ui) { ui.label(egui::RichText::new(tl!("Bullet Library")).small().weak()); ui.horizontal(|ui| { for c in ["•", "○", "▪", "◆", "➢", "✓", "–"] { - if ui.button(egui::RichText::new(c).size(16.0)).clicked() { + // The interface fonts have no ➢, so its tile draws the arrowhead. + let label = if c == "➢" { " " } else { c }; + let resp = ui.button(egui::RichText::new(label).size(16.0)); + if c == "➢" { + let m = resp.rect.center(); + let pts = [(-4.0, -5.0), (5.0, 0.0), (-4.0, 5.0), (-1.5, 0.0)].map(|(x, y)| m + vec2(x, y)); + ui.painter().add(egui::Shape::convex_polygon(vec![pts[0], pts[1], pts[3]], Tokens::get(ui.ctx()).text, Stroke::NONE)); + ui.painter().add(egui::Shape::convex_polygon(vec![pts[3], pts[1], pts[2]], Tokens::get(ui.ctx()).text, Stroke::NONE)); + } + if resp.clicked() { let _ = app.run("para.bullets", json!({"kind": c})); ui.close(); } diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index a30a090f8..68c87314b 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -418,3 +418,53 @@ pub fn apply(ctx: &egui::Context, t: &Tokens) { pub fn c32(c: wordcraft_doc::Rgb) -> Color32 { Color32::from_rgb(c.0, c.1, c.2) } + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + /// Symbols written as text in the interface code must exist in the interface fonts (Inter and + /// egui's emoji fallbacks); a missing glyph draws as an empty box, as the panes' "✕" close + /// button once did. Letters are the catalogs' business (see the i18n font tests). + #[test] + fn interface_symbols_have_glyphs() { + let ctx = egui::Context::default(); + ctx.set_fonts(super::font_definitions(false, false)); + ctx.run_ui(egui::RawInput::default(), |_| {}).drop_without_applying_deltas(); + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut symbols = BTreeSet::new(); + for entry in std::fs::read_dir(&dir).unwrap() { + let path = entry.unwrap().path(); + if path.extension().is_none_or(|e| e != "rs") { + continue; + } + let code = std::fs::read_to_string(&path).unwrap(); + // Characters inside string literals, line by line (good enough for this code base). + for line in code.lines().filter(|l| !l.trim_start().starts_with("//")) { + for (i, part) in line.split('"').enumerate() { + if i % 2 == 1 { + symbols.extend( + part.chars() + .filter(|c| !c.is_ascii() && !c.is_alphabetic() && !c.is_whitespace()) + .map(|c| (c, path.file_name().unwrap().to_string_lossy().to_string())), + ); + } + } + } + } + // Equation gallery templates are UnicodeMath input (`lim┬(n→∞)`, `≜`) that the math + // renderer draws with document fonts, not interface text. + // The ➢ bullet is a value for `para.bullets`; its menu tile draws the shape instead. + let exempt = |c: char, file: &str| match file { + "equation_tab.rs" => matches!(c, '≜' | '┬'), + "ribbon.rs" => c == '➢', + _ => false, + }; + symbols.retain(|(c, file)| !exempt(*c, file)); + let font = super::regular(12.5); + let missing: Vec = ctx.fonts_mut(|f| { + symbols.iter().filter(|(c, _)| !f.has_glyph(&font, *c)).map(|(c, file)| format!("{c} U+{:04X} in {file}", *c as u32)).collect() + }); + assert!(missing.is_empty(), "symbols without a glyph: {missing:#?}"); + } +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 558569129..5ae4fc3b5 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -390,6 +390,28 @@ pub fn combo_list(ui: &mut Ui, width: f32, current: &str, items: &[String], prev out } +/// A text button with a drawn icon in front. Symbols such as ✎ or 💬 aren't in the interface +/// fonts and would draw as empty boxes, so icons are painted (`icons.rs`) rather than typed. +pub fn icon_text_button(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId) -> Response { + let t = Tokens::get(ui.ctx()); + let galley = ui.painter().layout_no_wrap(text.to_string(), font, t.text); + let pad = ui.spacing().button_padding; + let icon_w = 16.0; + let size = vec2(pad.x + icon_w + 4.0 + galley.size().x + pad.x, galley.size().y.max(icon_w) + 2.0 * pad.y); + let (r, resp) = ui.allocate_exact_size(size, Sense::click()); + let v = ui.style().interact(&resp); + ui.painter().rect(r, v.corner_radius, v.weak_bg_fill, v.bg_stroke, egui::StrokeKind::Inside); + icons::paint( + ui.painter(), + Rect::from_center_size(pos2(r.min.x + pad.x + icon_w / 2.0, r.center().y), vec2(icon_w, icon_w)), + icon, + t.icon, + t.accent, + ); + ui.painter().galley(pos2(r.min.x + pad.x + icon_w + 4.0, r.center().y - galley.size().y / 2.0), galley, t.text); + resp +} + #[cfg(test)] mod tests { use super::*;