From 6cab843f00ecd6ffb83c0171959c0dc52c3f55f2 Mon Sep 17 00:00:00 2001 From: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:17:35 +0800 Subject: [PATCH] Backstage: non-Latin file names in Recent and Recover draw, not boxes (#3545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File › Home's Recent list and File › Recover draw file names with the interface faces, which have no CJK, Arabic, Hebrew… glyphs; nothing asked for a covering face the way the window caption and comment balloons do, so a name like 小红书帖子-草稿.md showed as a row of boxes. A new ensure_backstage_fonts does what ensure_caption_fonts does for those names: CJK loads the CJK UI font, other scripts borrow the document fallback faces, re-checked only when the names change. Regression test fails without the call; checked live on the Recover page. Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/lib.rs | 73 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a726f5cfc..3a9a00fbf 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -578,6 +578,8 @@ pub struct WordApp { /// The document (id, revision) whose comment text [`WordApp::ensure_comment_fonts`] last /// looked at. comment_fonts_for: Option<(u64, u64)>, + /// The Recover list's document names [`WordApp::ensure_recover_fonts`] last looked at. + recover_fonts_for: Option, /// The list of recent document paths [`WordApp::ensure_recent_fonts`] last looked at. recent_fonts_for: Vec, /// The Find query and result count [`WordApp::ensure_find_fonts`] last looked at. @@ -716,6 +718,7 @@ impl WordApp { fonts_system_cjk: false, want_system_cjk: false, comment_fonts_for: None, + recover_fonts_for: None, recent_fonts_for: Vec::new(), find_fonts_for: None, fonts_frames: 0, @@ -2010,6 +2013,34 @@ impl WordApp { } } + /// File › Recover draws the recovered documents' names with the interface faces too: a + /// Chinese name was a row of boxes, an Arabic or Hebrew one likewise. The same treatment as + /// the caption (and as the Recent list, `ensure_recent_fonts`): CJK loads the CJK UI font, + /// other scripts borrow the document fallback faces. Looked at again only when the names change. + fn ensure_recover_fonts(&mut self) { + use std::hash::{Hash, Hasher}; + let docs = self.session.recovery_documents(); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + docs.iter().for_each(|d| d.name.hash(&mut hasher)); + let key = hasher.finish(); + if self.recover_fonts_for == Some(key) { + return; + } + self.recover_fonts_for = Some(key); + let chars: std::collections::BTreeSet = docs.iter().flat_map(|d| d.name.chars()).collect(); + if chars.iter().any(|c| wordcraft_fonts::is_cjk(*c)) { + self.want_system_cjk = true; + } + let text: String = chars.into_iter().collect(); + let new = theme::faces_for_preedit(&text, &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); + self.styled = false; + } + } + /// Recent documents in Backstage (and the Start screen) show file names and paths with the /// interface faces. Borrow document fallback faces for characters in non-Latin scripts (#2067), /// as the caption and comments do. @@ -2142,6 +2173,7 @@ impl WordApp { // in this frame (dialog, drop, control channel) is picked up again at the end. self.ensure_caption_fonts(); self.ensure_comment_fonts(); + self.ensure_recover_fonts(); self.ensure_recent_fonts(); self.ensure_find_fonts(); self.install_ui_fonts(ctx, lang); @@ -2254,6 +2286,7 @@ impl WordApp { // The open above may have changed the file name the caption draws. self.ensure_caption_fonts(); self.ensure_comment_fonts(); + self.ensure_recover_fonts(); self.ensure_recent_fonts(); self.ensure_find_fonts(); self.install_ui_fonts(ctx, lang); @@ -3167,6 +3200,46 @@ mod tests { assert!(cjk.canvas.ime_faces.is_empty(), "CJK collections are not copied into the fallback list"); } + /// File › Recover draws the recovered documents' names with the interface faces: a Chinese + /// name there was a row of boxes because nothing asked for the CJK UI font. (The Recent list + /// has its own, `ensure_recent_fonts`.) + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn non_latin_names_in_the_recover_list_get_a_face() { + let with_copy = |name: &str| { + let root = std::env::temp_dir().join(format!( + "wordcraft-recover-fonts-{}-{}", + std::process::id(), + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() + )); + let id = "0123456789abcdef0123456789abcdef"; + std::fs::create_dir_all(root.join(id)).unwrap(); + let snapshot = json!({ + "version": 1, "generation": 1, "info": {"id": id, "name": name, "savedAt": "2026-10-11T10:42:00Z"}, + "document": wordcraft_doc::Document::from_text("Recovered work") + }); + std::fs::write(root.join(id).join("snapshot-00000000000000000001.json"), serde_json::to_vec(&snapshot).unwrap()).unwrap(); + let mut a = app(); + a.session.enable_recovery(root.clone(), std::sync::Arc::new(|| {})).unwrap(); + let ctx = egui::Context::default(); + let end = std::time::Instant::now() + std::time::Duration::from_secs(10); + loop { + ctx.run_ui(egui::RawInput::default(), |ui| a.logic(ui.ctx())).drop_without_applying_deltas(); + if a.session.recovery_ready() && !a.session.recovery_documents().is_empty() { + // One more frame looks at the names it found. + ctx.run_ui(egui::RawInput::default(), |ui| a.logic(ui.ctx())).drop_without_applying_deltas(); + break; + } + assert!(std::time::Instant::now() < end, "the recovery copy was never found"); + std::thread::sleep(std::time::Duration::from_millis(5)); + } + let _ = std::fs::remove_dir_all(&root); + a + }; + assert!(!with_copy("Synthetic draft").want_system_cjk, "Latin names need no CJK font"); + assert!(with_copy("会议记录-草稿").want_system_cjk, "a CJK name in Recover loads the CJK UI font"); + } + /// Comment balloons draw comment text and author names with the interface faces, which have /// no Arabic, so an Arabic comment was all boxes. A document with one borrows a document /// fallback face that covers it, once per document revision.