mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 19:37:03 -06:00
fix(ui): borrow fallback fonts and preserve visual order for recent files (#2067)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user