From 88bb5677a86053f352e1cc2082cef0795a7dab01 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 11 Oct 2026 21:42:24 +0200 Subject: [PATCH] UI fonts: interface text falls back to the cell font (#349) On macOS the UI font (SF) has no Hebrew and no Hebrew fallback face was found, while cells in Arial drew it: the formula bar showed boxes. The interface families now carry the cell face ahead of the script fallbacks, and the macOS Hebrew candidates include ArialHB.ttc / SFHebrew.ttf / LucidaGrande.ttc. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/theme.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 03830fb..01fe9a3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-11 | Formula bar, name box and dialogs fall back to the cell font: Hebrew (and any script cells can show) no longer turns into boxes there (#349) | | 2026-10-11 | Legacy Excel 97-2003 `.xls` (BIFF8) import with formulas, number formats, merges, sizes and names (#121) | | 2026-10-11 | XLSX pass-through: custom XML, external links, connections, query tables, printer settings, form/ActiveX controls and OLE objects are kept on save (#330, #324) | | 2026-10-11 | Data › Forecast › Forecast Sheet: a new sheet with the history, FORECAST.ETS forecast, confidence bounds, statistics and a chart (#322) | diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 9218929..485b9a2 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -416,6 +416,20 @@ pub fn font_definitions_with(language: crate::i18n::Language, used: &[String]) - for family in crate::fonts::installed_among(used) { bound.extend(crate::fonts::bind_family(&mut fonts, &family)); } + // Interface text (formula bar, name box, dialogs) falls back to the cell font before the + // script faces: whatever a cell can draw, the formula bar can draw too, even where no script + // fallback face was found (#349: macOS UI font without Hebrew, cells in Arial with it). + let cell_key = format!("sys-{CELL}"); + if fonts.font_data.contains_key(&cell_key) { + for role in [UI, UI_BOLD, SERIF, MONO] { + if let Some(chain) = fonts.families.get_mut(&FontFamily::Name(role.into())) + && !chain.contains(&cell_key) + { + let at = chain.iter().position(|k| fallback.contains(k)).unwrap_or(chain.len()); + chain.insert(at, cell_key.clone()); + } + } + } // Default proportional text in widgets uses the UI font. if let Some(ui) = fonts.families.get(&FontFamily::Name(UI.into())).cloned() { fonts.families.insert(FontFamily::Proportional, ui); @@ -594,8 +608,11 @@ fn hebrew() -> Vec<(String, u32)> { v.push((format!("{win}\\david.ttf"), 0)); // David v.push((format!("{win}\\frank.ttf"), 0)); // FrankRuehl v.push((format!("{win}\\tahoma.ttf"), 0)); // Tahoma + v.push((format!("{mac_sys}/ArialHB.ttc"), 0)); // Arial Hebrew + v.push((format!("{mac_sys}/SFHebrew.ttf"), 0)); v.push((format!("{mac_sup}/Arial Hebrew.ttf"), 0)); v.push((format!("{mac_sup}/David.ttf"), 0)); + v.push((format!("{mac_sys}/LucidaGrande.ttc"), 0)); v.push((format!("{mac_sys}/Lucida Grande.ttc"), 0)); for d in lin { v.push((format!("{d}/NotoSansHebrew-Regular.ttf"), 0)); @@ -774,6 +791,25 @@ mod tests { } } + #[test] + fn interface_families_fall_back_to_the_cell_font() { + // Structural (#349): the formula bar, name box and dialogs draw every script the cells can, + // so each interface chain carries the cell face, ahead of the script fallback faces. + let fonts = font_definitions_for_language(Language::En); + let cell = format!("sys-{CELL}"); + if !fonts.font_data.contains_key(&cell) { + return; // no system cell font on this machine + } + let fb: Vec = fallback_faces(HanOrder::Chinese).into_iter().map(|(k, _)| k.to_string()).collect(); + for family in [FontFamily::Proportional, FontFamily::Name(UI.into()), FontFamily::Name(UI_BOLD.into()), FontFamily::Name(MONO.into())] { + let chain = fonts.families.get(&family).cloned().unwrap_or_default(); + let at = chain.iter().position(|k| *k == cell); + assert!(at.is_some(), "{family:?} lacks the cell face: {chain:?}"); + let first_fb = chain.iter().position(|k| fb.contains(k)).unwrap_or(chain.len()); + assert!(at.unwrap_or(usize::MAX) < first_fb, "{family:?}: cell face must precede the script faces"); + } + } + #[test] fn language_reorders_only_the_han_faces() { let keys = |han| fallback_faces(han).into_iter().map(|(k, _)| k).collect::>();