diff --git a/ROADMAP.md b/ROADMAP.md index 0acba67..ace5456 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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) | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c57e74d..17d1d78 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -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>, pub want_focus: bool, pub context_issue: Option, diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3377f02..9e5744f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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()) } diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 6f35630..86d9f89 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -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]) { 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 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], @@ -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] diff --git a/docs/ui-parity.md b/docs/ui-parity.md index cbc968c..1a2c2f7 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -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 |