fix(ui): borrow fallback fonts and preserve visual order for recent files (#2067)

This commit is contained in:
harshpsi9194
2026-10-16 11:22:02 +05:30
parent eae2421761
commit e5ffb15cb5
3 changed files with 83 additions and 3 deletions
+4 -2
View File
@@ -353,8 +353,10 @@ fn open_list(app: &mut WordApp, ui: &mut Ui) {
ui.painter().rect_filled(r, 4.0, t.hover);
}
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 18.0, r.center().y), vec2(22.0, 22.0)), "printLayout", t.icon, t.accent);
ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 13.0), Align2::LEFT_CENTER, &name, medium(13.0), t.text);
ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 29.0), Align2::LEFT_CENTER, &p, regular(11.0), t.text_dim);
let display_name = crate::canvas::bidi_display(&name);
let display_p = crate::canvas::bidi_display(&p);
ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 13.0), Align2::LEFT_CENTER, display_name.as_ref(), medium(13.0), t.text);
ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 29.0), Align2::LEFT_CENTER, display_p.as_ref(), regular(11.0), t.text_dim);
if resp.clicked() {
let _ = app.run("file.open", json!({"path": p}));
app.ui.backstage = false;
+22 -1
View File
@@ -1841,10 +1841,24 @@ fn card_shapes(painter: &egui::Painter, t: &Tokens, card: Rect, color: Color32,
}
/// Does `text` hold right-to-left letters (Arabic, Hebrew, Syriac, Thaana, N'Ko …)?
fn has_rtl(text: &str) -> bool {
pub(crate) fn has_rtl(text: &str) -> bool {
text.chars().any(|c| matches!(unicode_bidi::bidi_class(c), unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL))
}
/// Put `text` into visual display order for egui when it contains right-to-left characters.
/// Uses an LTR base paragraph direction so left-aligned UI elements (recent files, paths) preserve
/// trailing extensions and numbers in their proper visual order.
pub(crate) fn bidi_display(text: &str) -> std::borrow::Cow<'_, str> {
if !has_rtl(text) {
return std::borrow::Cow::Borrowed(text);
}
let info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
match info.paragraphs.first() {
Some(p) => std::borrow::Cow::Owned(visual_row(&info, p, text, 0..text.len())),
None => std::borrow::Cow::Borrowed(text),
}
}
/// The row `line` of paragraph `p` of `para` in display order, for egui: the bidi runs left to
/// right as they are shown, and inside a right-to-left run its words reversed. The letters of each
/// word stay in logical order, because egui's shaper already runs an Arabic word right to left.
@@ -3622,6 +3636,13 @@ mod tests {
assert_eq!(visual_row(&info, p, text, 0..text.len() + 5), "");
}
#[test]
fn bidi_display_preserves_extensions_and_numbers() {
assert_eq!(bidi_display("report.docx").as_ref(), "report.docx");
assert_eq!(bidi_display("نواقص 2026.docx").as_ref(), "2026 نواقص.docx");
assert_eq!(bidi_display("مذكرة داخلية.docx").as_ref(), "داخلية مذكرة.docx");
}
#[test]
fn a_balloon_edits_its_comment() {
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
+57
View File
@@ -472,6 +472,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 list of recent document paths [`WordApp::ensure_recent_fonts`] last looked at.
recent_fonts_for: Vec<String>,
fonts_frames: u32,
/// The effective interface theme last installed in egui ([`theme::apply`]).
applied_theme: Option<theme::Appearance>,
@@ -599,6 +601,7 @@ impl WordApp {
fonts_system_cjk: false,
want_system_cjk: false,
comment_fonts_for: None,
recent_fonts_for: Vec::new(),
fonts_frames: 0,
applied_theme: None,
native_scale: 1.0,
@@ -1711,6 +1714,32 @@ impl WordApp {
}
}
/// 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.
fn ensure_recent_fonts(&mut self) {
if self.recent_fonts_for.as_slice() == self.ui.recent.as_slice() {
return;
}
self.recent_fonts_for = self.ui.recent.clone();
for p in &self.ui.recent {
if p.chars().any(wordcraft_fonts::is_cjk) {
self.want_system_cjk = true;
}
let new = theme::faces_for_preedit(p, &self.canvas.ime_faces, theme::caption_char_covered, |c| {
wordcraft_fonts::FontDb::global().fallback_for(c, u32::MAX)
});
if !new.is_empty() {
for face in new {
if !self.canvas.ime_faces.iter().any(|f| f.id() == face.id()) {
self.canvas.ime_faces.push(face);
}
}
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 {
@@ -1758,6 +1787,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_recent_fonts();
self.install_ui_fonts(ctx, lang);
if let Some(answer) = self.desktop_theme.take_change() {
self.desktop_dark = Some(answer);
@@ -1868,6 +1898,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_recent_fonts();
self.install_ui_fonts(ctx, lang);
self.report_dirty();
}
@@ -2561,6 +2592,32 @@ mod tests {
assert_eq!(arabic.canvas.ime_faces.len(), n, "the face is added once");
}
/// Recent documents in Backstage and the Start screen draw file names and paths with the
/// interface faces, which have no Arabic, Hebrew, Thai …: a recent document borrows a
/// document fallback face that covers it (#2067).
#[test]
fn recent_documents_borrow_fallback_faces() {
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.ui.recent.push("/home/ice/report.docx".into());
frame(&mut latin);
assert!(latin.canvas.ime_faces.is_empty(), "a Latin file name needs no extra face");
let mut arabic = app();
arabic.ui.recent.push("/home/ice/نواقص 2026.docx".into());
frame(&mut arabic);
let letter = 'ن';
if wordcraft_fonts::FontDb::global().fallback_for(letter, u32::MAX).is_some_and(|f| f.data().len() <= 16 << 20) {
assert!(arabic.canvas.ime_faces.iter().any(|f| f.covers(letter)), "the recent list borrows a face that covers the file name");
}
let n = arabic.canvas.ime_faces.len();
frame(&mut arabic);
assert_eq!(arabic.canvas.ime_faces.len(), n, "the face is added once");
}
fn app() -> WordApp {
WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default())
}