UI: draw icons instead of symbols the interface fonts lack (✕ ✎ 💬 ➕ 📂 ⋯ ➢) (#305)

Several buttons put symbol characters in their text that neither Inter nor
egui's emoji fallbacks contain, so they drew as empty boxes: the panes' close
button (✕), the title-row Editing/Reviewing (✎) and Comments (💬) buttons,
the Comments pane's New comment (➕), File › Open's Browse… (📂), the
Equation tab's symbol-set button (⋯) and the ➢ tile in the Bullet Library.

- Pane close buttons paint the existing `close` icon, with a Close tooltip.
- New `widgets::icon_text_button` paints an icon from icons.rs next to text;
  Editing/Reviewing (pencil), Comments and New Comment use it. The
  Editing/Reviewing menu is now an egui Popup on that button.
- Browse… drops the emoji; ⋯ becomes …; the ➢ bullet stays a bullet choice
  and only its menu tile draws the arrowhead.
- Catalogs: `📂  Browse…` is renamed `Browse…` (translation without the
  emoji) and `➕ New comment` is removed in favour of the existing
  `New Comment`, in every language.

theme::tests::interface_symbols_have_glyphs scans the UI sources' string
literals for non-letter, non-ASCII characters and checks each against the
real interface font setup, with a short commented exception list (equation
templates drawn by the math renderer, the ➢ bullet value).


Claude-Session: https://claude.ai/code/session_01WtHKQ7wrZm3uBuaG1Ye8po

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dmitrii Khlapov
2026-10-10 15:36:13 -04:00
committed by GitHub
co-authored by Dmitrii Khlapov Claude Opus 5.5
parent fc0c11a440
commit 82b31efd67
14 changed files with 117 additions and 34 deletions
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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| {
+1 -2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 25.
+1 -2
View File
@@ -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: 方向:
Can't render this file because it contains an unexpected character in line 416 and column 13.
+1 -2
View File
@@ -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)
Can't render this file because it contains an unexpected character in line 6 and column 25.
+1 -2
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+1 -2
View File
@@ -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: Смер:
Can't render this file because it contains an unexpected character in line 11 and column 25.
+1 -2
View File
@@ -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 Застосувати стилі
Can't render this file because it contains an unexpected character in line 6 and column 31.
+1 -2
View File
@@ -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: 方向:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+1 -2
View File
@@ -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: 方向:
Can't render this file because it contains an unexpected character in line 417 and column 13.
+9 -2
View File
@@ -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();
+26 -14
View File
@@ -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();
}
+50
View File
@@ -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<String> = 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:#?}");
}
}
+22
View File
@@ -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::*;