fix(ui): draw non-Latin file names in the window caption (#840)

The painted title uses a Latin-only face, so names in other scripts were shown as question marks. A covering fallback face is now loaded for the caption.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 09:23:47 -04:00
1 parent 90515abd1e
commit fccc09e4c9
5 files changed
+117 -20

No files matched your search

+2 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** trivial (32-bit Windows starts on OpenGL so launch no longer exits; previously trivial: every measurement follows the Options unit, #755; previously trivial: Insert › SmartArt landed, #497: catalog 429/451 → 437/458 (95.4%); previously trivial: Turkish interface landed; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** trivial (non-Latin file names in the window caption; previously trivial: every measurement follows the Options unit, #755; previously trivial: Insert › SmartArt landed, #497: catalog 429/451 → 437/458 (95.4%); previously trivial: Turkish interface landed; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
WordCraft aims for complete parity with Microsoft Word, then goes further on speed, openness and
agent control. This page is the summary; the assessment is
@@ -132,6 +132,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
## Progress log
| Date | What landed |
| 2026-10-12 | Window caption: a non-Latin file name is drawn with a document fallback face (or the CJK UI font), instead of `?` |
| 2026-10-12 | 32-bit Windows starts on OpenGL. DirectX 12 graphics pipeline creation returns "The parameter is incorrect" and the app exited before the window appeared |
| 2026-10-12 | New documents follow the system region (#838): A4 and centimetres outside the US (Letter in the CLDR Letter territories, inches in the US), lengths written with the region's decimal separator (`2,54 cm`) and typed with either; an explicit Measurement units choice wins; `file.options {"region"}` for agents |
| 2026-10-12 | Paste from other programs keeps their formatting (#687): Ctrl+V and Home › Paste read the system clipboard's HTML (and RTF on Windows and Wayland); Merge Formatting takes the destination's font and paragraph and keeps bold, italic, underline and strikethrough; Paste Special lists what the system clipboard holds (HTML, RTF, picture, text). Linux: pictures copied in a browser or Spectacle paste on KDE and other Wayland desktops (data-control clipboard), and a paste that finds nothing usable names what the clipboard holds instead of "empty" (#833) |
+2 -1
View File
@@ -66,7 +66,8 @@ pub struct CanvasState {
pub focused: bool,
pub render_ms: f64,
pub ime_preedit: String,
/// Document faces added to the interface fonts to draw the input method's composition (#565).
/// Document faces added to the interface fonts so the input method's composition (#565) and a
/// file name in the window caption can be drawn.
pub(crate) ime_faces: Vec<std::sync::Arc<wordcraft_fonts::FontFace>>,
pub want_focus: bool,
pub context_issue: Option<serde_json::Value>,
+75 -12
View File
@@ -1230,6 +1230,25 @@ impl WordApp {
}
}
/// The window caption is painted with the semibold interface face (Inter). A file name in a
/// script those faces lack would otherwise be drawn as `?`. Borrow the document fonts'
/// fallback faces, as IME composition does (#565). A CJK name uses the CJK UI font instead:
/// those collections are too large to add here.
fn ensure_caption_fonts(&mut self) {
let stem = self.title_stem();
if stem.chars().any(wordcraft_fonts::is_cjk) {
self.want_system_cjk = true;
}
let new = theme::faces_for_preedit(&stem, &self.canvas.ime_faces, theme::caption_char_covered, |c| {
wordcraft_fonts::FontDb::global().fallback_for(c, u32::MAX)
});
if !new.is_empty() {
self.canvas.ime_faces.extend(new);
// Fonts are already installed for this frame's language; load the new faces too.
self.styled = false;
}
}
/// Document title for the title bar (and the suggested file name). An untitled document is
/// named in the interface language, as `Document1` is in English.
pub fn title_stem(&self) -> String {
@@ -1257,18 +1276,11 @@ impl WordApp {
}
options::sync(self);
// Chinese text wants the Chinese face before the Japanese one (one glyph style per line).
// An installed CJK font is read only once CJK text is shown (#241): never for an English
// interface that doesn't open the language list.
let system_cjk = self.fonts_system_cjk || self.want_system_cjk || lang.uses_cjk();
if !self.styled || lang.prefers_hans() != self.fonts_hans || system_cjk != self.fonts_system_cjk {
theme::install_fonts_extra(ctx, lang.prefers_hans(), system_cjk, &self.canvas.ime_faces);
self.fonts_hans = lang.prefers_hans();
self.fonts_system_cjk = system_cjk;
// Mod with -, = and 0 are Word shortcuts (optional hyphen, subscript, paragraph spacing);
// egui's keyboard zoom would also scale the whole window on them. Zoom is View › Zoom.
ctx.options_mut(|o| o.zoom_with_keyboard = false);
self.styled = true;
}
// An installed CJK font is read only once CJK text is shown (#241): a CJK interface, the
// language names in Options, or a file name the caption is drawing. A file opened later
// in this frame (dialog, drop, control channel) is picked up again at the end.
self.ensure_caption_fonts();
self.install_ui_fonts(ctx, lang);
if let Some(answer) = self.desktop_theme.take_change() {
self.desktop_dark = Some(answer);
}
@@ -1341,9 +1353,27 @@ impl WordApp {
};
}
}
// The open above may have changed the file name the caption draws.
self.ensure_caption_fonts();
self.install_ui_fonts(ctx, lang);
self.report_dirty();
}
/// Install the interface fonts when the language, the CJK fallback, or a caption fallback
/// face has changed since the last install.
fn install_ui_fonts(&mut self, ctx: &egui::Context, lang: i18n::Lang) {
let system_cjk = self.fonts_system_cjk || self.want_system_cjk || lang.uses_cjk();
if !self.styled || lang.prefers_hans() != self.fonts_hans || system_cjk != self.fonts_system_cjk {
theme::install_fonts_extra(ctx, lang.prefers_hans(), system_cjk, &self.canvas.ime_faces);
self.fonts_hans = lang.prefers_hans();
self.fonts_system_cjk = system_cjk;
// Mod with -, = and 0 are Word shortcuts (optional hyphen, subscript, paragraph spacing);
// egui's keyboard zoom would also scale the whole window on them. Zoom is View › Zoom.
ctx.options_mut(|o| o.zoom_with_keyboard = false);
self.styled = true;
}
}
/// Tell the host whether the document has unsaved changes ([`Services::on_dirty`]).
fn report_dirty(&self) {
if let Some(f) = &self.services.on_dirty {
@@ -1772,6 +1802,39 @@ mod tests {
assert_eq!(titles(&mut app), 1);
}
/// The caption is Inter semibold, which has no Hebrew (or Arabic, Thai, …), so egui would
/// paint each of those letters as `?`. Opening such a file borrows one document fallback
/// face. A CJK name uses the CJK UI font instead of that list.
#[test]
fn non_latin_file_name_is_drawn_in_the_caption() {
let ctx = egui::Context::default();
let frame = |app: &mut WordApp| {
ctx.run_ui(egui::RawInput::default(), |ui| app.logic(ui.ctx())).drop_without_applying_deltas();
};
let mut latin = app();
latin.session.path = Some("report.docx".into());
frame(&mut latin);
assert!(!latin.want_system_cjk);
assert!(latin.canvas.ime_faces.is_empty(), "a Latin name needs no extra face");
let mut hebrew = app();
hebrew.session.path = Some("מסמך.docx".into());
frame(&mut hebrew);
let letter = 'מ';
if wordcraft_fonts::FontDb::global().fallback_for(letter, u32::MAX).is_some_and(|f| f.data().len() <= 16 << 20) {
assert!(hebrew.canvas.ime_faces.iter().any(|f| f.covers(letter)), "the caption borrows a face that covers the file name");
}
let n = hebrew.canvas.ime_faces.len();
frame(&mut hebrew);
assert_eq!(hebrew.canvas.ime_faces.len(), n, "the face is added once");
let mut cjk = app();
cjk.session.path = Some("文档.docx".into());
frame(&mut cjk);
assert!(cjk.want_system_cjk, "a CJK file name loads the CJK UI font");
assert!(cjk.canvas.ime_faces.is_empty(), "CJK collections are not copied into the fallback list");
}
fn app() -> WordApp {
WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default())
}
+36 -5
View File
@@ -346,7 +346,8 @@ pub fn install_fonts_with(ctx: &egui::Context, prefer_hans: bool, system_cjk: bo
}
/// [`install_fonts_with`], also adding document-font `faces` as last-resort fallbacks (#565): the
/// faces that cover the text an input method is composing.
/// faces that cover text the interface fonts cannot draw (an input method's composition, or a
/// file name in the window caption).
pub fn install_fonts_extra(ctx: &egui::Context, prefer_hans: bool, system_cjk: bool, faces: &[Arc<wordcraft_fonts::FontFace>]) {
let mut fonts = font_definitions(prefer_hans, system_cjk);
add_fallback_faces(&mut fonts, faces);
@@ -368,10 +369,17 @@ pub fn add_fallback_faces(fonts: &mut FontDefinitions, faces: &[Arc<wordcraft_fo
}
}
/// Document faces needed to draw `text` (the input method's composition), one for each character
/// the interface fonts cannot draw, skipping faces in `have` (#565). Characters of the embedded
/// interface fonts are not looked up; very large faces (system CJK collections) are left to the CJK
/// fallback.
/// A character the window caption can already draw without a document fallback: the Latin range
/// the interface faces cover, or CJK, which uses the CJK UI font ([`install_fonts_with`]) rather
/// than a document face. System CJK collections are larger than [`faces_for_preedit`] will load.
pub fn caption_char_covered(c: char) -> bool {
(c as u32) < 0x250 || wordcraft_fonts::is_cjk(c)
}
/// Document faces needed to draw `text` (the input method's composition, or a file name in the
/// window caption), one for each character the interface fonts cannot draw, skipping faces in
/// `have` (#565). Characters of the embedded interface fonts are not looked up; very large faces
/// (system CJK collections) are left to the CJK fallback.
pub fn faces_for_preedit(
text: &str,
have: &[Arc<wordcraft_fonts::FontFace>],
@@ -689,6 +697,29 @@ mod tests {
assert!(defs.families[&egui::FontFamily::Proportional].last().is_some_and(|n| n.starts_with("fallback ")));
}
/// A file name in a script Inter lacks (Hebrew, Arabic, …) is drawn in the window caption,
/// which uses the semibold face. egui paints a missing glyph as `?`, so the caption borrows
/// one document fallback face. CJK stays with the CJK UI font: those files are too large to
/// add here.
#[test]
fn caption_borrows_one_fallback_face_for_hebrew() {
let face = wordcraft_fonts::FontDb::global().face("Source Sans 3", "Regular");
let lookup = |c: char| {
assert!(!super::caption_char_covered(c), "{c} is already drawn by the interface fonts");
Some(face.clone())
};
let hebrew = "מסמך";
let got = super::faces_for_preedit(hebrew, &[], super::caption_char_covered, lookup);
assert_eq!(got.len(), 1, "one face for the whole file name");
assert!(super::faces_for_preedit(hebrew, &got, super::caption_char_covered, lookup).is_empty(), "already added");
assert!(super::faces_for_preedit("文档", &[], super::caption_char_covered, lookup).is_empty(), "CJK uses the CJK UI font");
assert!(super::faces_for_preedit("report", &[], super::caption_char_covered, lookup).is_empty(), "Latin needs nothing");
let mut defs = super::font_definitions(false, false);
super::add_fallback_faces(&mut defs, &got);
let semi = &defs.families[&egui::FontFamily::Name("semibold".into())];
assert!(semi.iter().any(|n| n.starts_with("fallback ")), "the caption is painted with semibold");
}
/// #656: dialog primary buttons, the Discord pill, the table eraser and the Navigation search
/// highlight take their colours from the theme, not fixed light-theme values.
#[test]
+2 -1
View File
@@ -1,6 +1,6 @@
# UI parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (Navigation headings truncate in the resizable pane, #759; previously trivial: Find and Replace special characters and wildcards, #543; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (a non-Latin file name in the window caption is drawn with a fallback face instead of `?`; previously trivial: every measurement follows the Options unit, #755; previously trivial: Find and Replace special characters and wildcards, #543; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
@@ -44,6 +44,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | trivial | Window caption: a file name in a script the interface fonts lack (Hebrew, Arabic, and the rest; CJK via the CJK UI font) is drawn with a fallback face instead of `?` |
| 2026-10-12 | trivial | Navigation headings truncate with an ellipsis within the pane width, preserving resizing and full-text hover (#759) |
| 2026-10-11 | minor | Reading Mode: viewport-height horizontal columns, animated paging, transient display controls, heading navigation and responsive anchored resizing |
| 2026-10-11 | trivial | Alt activation handles native key/modifier events in either order, including key-up with the old Alt flag; taps toggle once, chords and focus loss do not activate KeyTips |