Find box and Results list borrow a document font for Georgian etc. (#3248)

The Find box and the Navigation pane's Results list draw the typed
query and document excerpts around each match with the interface
faces (Inter, Source Sans 3), which cover Latin plus the embedded CJK
faces — nothing else. Georgian (and Armenian, Thai, …) text typed into
the search box, or found in the document and shown in a result's
context snippet, was drawn as boxes.

Added ensure_find_fonts(), following the exact pattern already used
for the window caption (#565) and comment balloons/the Comments pane:
scan the query and the visible results' surrounding text for
characters the interface faces can't draw, and borrow a document
font's fallback face for them. Cached by (query, result count) so it
only rescans when either changes.

Verified live: built the app, inserted Georgian text, searched for a
Georgian word, and confirmed both the search box (typed query) and
the Results list (match context) now render the glyphs instead of
boxes — matching the issue's screenshot, now fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Michael
2026-10-20 17:12:49 +08:00
co-authored by Claude Sonnet 5
parent 9b4b948c6e
commit c3ead8d2c2
+77
View File
@@ -550,6 +550,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 Find query and result count [`WordApp::ensure_find_fonts`] last looked at.
find_fonts_for: Option<(String, usize)>,
fonts_frames: u32,
/// The effective interface theme last installed in egui ([`theme::apply`]).
applied_theme: Option<theme::Appearance>,
@@ -679,6 +681,7 @@ impl WordApp {
fonts_system_cjk: false,
want_system_cjk: false,
comment_fonts_for: None,
find_fonts_for: None,
fonts_frames: 0,
applied_theme: None,
native_scale: 1.0,
@@ -1909,6 +1912,38 @@ impl WordApp {
}
}
/// The Find box and the Navigation pane's Results list draw the typed query and document
/// excerpts around each match with the interface faces, which have no Georgian, Armenian,
/// Thai … (#3248): each such letter was drawn as a box. Borrow the document fonts' fallback
/// faces for them, as comments and the caption do. Looked at once per query/result-count
/// change; CJK text uses the CJK UI font instead.
fn ensure_find_fonts(&mut self) {
let key = (self.session.find.query.clone(), self.session.find.results.len());
if self.find_fonts_for.as_ref() == Some(&key) {
return;
}
self.find_fonts_for = Some(key);
let mut text = self.session.find.query.clone();
for (a, b) in self.session.find.results.iter().take(500) {
let Some(p) = self.session.doc.para_at(a) else { continue };
let from = p.clamp(a.off.saturating_sub(30));
let to = p.clamp((b.off + 40).min(p.len()));
if let Some(s) = p.text.get(from..to) {
text.push_str(s);
}
}
if text.chars().any(wordcraft_fonts::is_cjk) {
self.want_system_cjk = true;
}
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;
}
}
/// 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 {
@@ -1956,6 +1991,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_find_fonts();
self.install_ui_fonts(ctx, lang);
if let Some(answer) = self.desktop_theme.take_change() {
self.desktop_dark = Some(answer);
@@ -2066,6 +2102,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_find_fonts();
self.install_ui_fonts(ctx, lang);
self.report_dirty();
}
@@ -2805,6 +2842,46 @@ mod tests {
assert_eq!(arabic.canvas.ime_faces.len(), n, "the face is added once");
}
/// #3248: the Find box and the Navigation pane's Results list draw the typed query and
/// document excerpts with the interface faces, which have no Georgian (or Armenian, Thai,
/// …), so a Georgian search was all boxes. Running a search that finds Georgian text borrows
/// a document fallback face that covers it, as the caption and comments already do.
#[test]
fn georgian_find_text_is_drawn_in_the_navigation_pane() {
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.run("text.insert", json!({"text": "This is some text"})).unwrap();
latin.run("edit.find", json!({"text": "some text"})).unwrap();
frame(&mut latin);
assert!(latin.canvas.ime_faces.is_empty(), "a Latin query and result need no extra face");
let mut georgian = app();
georgian.run("text.insert", json!({"text": "ეს არის ქართული ტექსტი"})).unwrap();
georgian.run("edit.find", json!({"text": "ტექსტი"})).unwrap();
assert!(!georgian.session.find.results.is_empty(), "the search actually found the Georgian word");
frame(&mut georgian);
let letter = 'ტ';
if wordcraft_fonts::FontDb::global().fallback_for(letter, u32::MAX).is_some_and(|f| f.data().len() <= 16 << 20) {
assert!(georgian.canvas.ime_faces.iter().any(|f| f.covers(letter)), "the Results list borrows a face that covers the match");
}
let n = georgian.canvas.ime_faces.len();
frame(&mut georgian);
assert_eq!(georgian.canvas.ime_faces.len(), n, "the face is added once, cached by query and result count");
// The query box needs a fallback face even before any match is found (#3248's screenshot
// shows the typed query itself as boxes, not just the results).
let mut typing = app();
typing.run("text.insert", json!({"text": "unrelated"})).unwrap();
typing.session.find.query = "ტექსტი".into();
frame(&mut typing);
if wordcraft_fonts::FontDb::global().fallback_for(letter, u32::MAX).is_some_and(|f| f.data().len() <= 16 << 20) {
assert!(typing.canvas.ime_faces.iter().any(|f| f.covers(letter)), "the typed query alone (no match yet) still gets a fallback face");
}
}
fn app() -> WordApp {
WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default())
}