mirror of
https://github.com/aphrody-labs/wordcraft.git
synced 2026-10-14 03:29:04 +00:00
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:
5 files changed
+117
-20
No files matched your search
+2
-1
@@ -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) |
|
||||
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user