Backstage: non-Latin file names in Recent and Recover draw, not boxes (#3545)

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 <noreply@anthropic.com>
This commit is contained in:
Michael Mu
2026-10-23 12:17:35 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 05839461af
commit 6cab843f00
+73
View File
@@ -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<u64>,
/// The list of recent document paths [`WordApp::ensure_recent_fonts`] last looked at.
recent_fonts_for: Vec<String>,
/// 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<char> = 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.