diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index f930ba7..dd3e306 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -37,6 +37,8 @@ pub struct CanvasState { pub focused: bool, pub render_ms: f64, pub ime_preedit: String, + /// Document faces added to the interface fonts to draw the input method's composition (#565). + pub(crate) ime_faces: Vec>, pub want_focus: bool, pub context_issue: Option, pub context_synonyms: Option, @@ -109,6 +111,7 @@ impl Default for CanvasState { focused: false, render_ms: 0.0, ime_preedit: String::new(), + ime_faces: Vec::new(), want_focus: true, context_issue: None, context_synonyms: None, @@ -676,6 +679,19 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { }); } if !app.canvas.ime_preedit.is_empty() { + // The composition is interface text, but it may be in a script the interface fonts lack + // (Bangla, Thai ...): borrow the document fonts' fallback faces, or it draws as boxes (#565). + let new = crate::theme::faces_for_preedit( + &app.canvas.ime_preedit, + &app.canvas.ime_faces, + |c| (c as u32) < 0x250, + |c| wordcraft_fonts::FontDb::global().fallback_for(c, u32::MAX), + ); + if !new.is_empty() { + app.canvas.ime_faces.extend(new); + crate::theme::install_fonts_extra(ui.ctx(), app.fonts_hans, app.fonts_system_cjk, &app.canvas.ime_faces); + ui.ctx().request_repaint(); + } let g = painter.text(pos2(x, y1), egui::Align2::LEFT_BOTTOM, &app.canvas.ime_preedit, regular(c.height * scale * 0.8), caret_color); painter.line_segment([pos2(g.min.x, g.max.y), pos2(g.max.x, g.max.y)], Stroke::new(1.0, caret_color)); } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c458e35..3a37d05 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -945,7 +945,7 @@ impl WordApp { // interface that doesn't open the language list. let system_cjk = self.fonts_system_cjk || self.want_system_cjk || lang.uses_cjk(); if !self.styled || lang.prefers_hans() != self.fonts_hans || system_cjk != self.fonts_system_cjk { - theme::install_fonts_with(ctx, lang.prefers_hans(), system_cjk); + theme::install_fonts_extra(ctx, lang.prefers_hans(), system_cjk, &self.canvas.ime_faces); self.fonts_hans = lang.prefers_hans(); self.fonts_system_cjk = system_cjk; // Mod with -, = and 0 are Word shortcuts (optional hyphen, subscript, paragraph spacing); diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 2ac7bbe..a6d330d 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -275,7 +275,52 @@ pub fn install_fonts_for(ctx: &egui::Context, prefer_hans: bool) { /// a glyph (#241, #486). Reading that font is a large file read, so callers ask for it only when /// CJK text is on screen: a CJK interface language, or the language names in Options. pub fn install_fonts_with(ctx: &egui::Context, prefer_hans: bool, system_cjk: bool) { - ctx.set_fonts(font_definitions(prefer_hans, system_cjk)); + install_fonts_extra(ctx, prefer_hans, system_cjk, &[]); +} + +/// [`install_fonts_with`], also adding document-font `faces` as last-resort fallbacks (#565): the +/// faces that cover the text an input method is composing. +pub fn install_fonts_extra(ctx: &egui::Context, prefer_hans: bool, system_cjk: bool, faces: &[Arc]) { + let mut fonts = font_definitions(prefer_hans, system_cjk); + add_fallback_faces(&mut fonts, faces); + ctx.set_fonts(fonts); +} + +/// Add `faces` after every other font of the interface families. +pub fn add_fallback_faces(fonts: &mut FontDefinitions, faces: &[Arc]) { + for f in faces { + let name = format!("fallback {} {} {}", f.family, f.style, f.id()); + let mut data = FontData::from_owned(f.data().to_vec()); + data.index = f.index(); + fonts.font_data.insert(name.clone(), Arc::new(data)); + for fam in [FontFamily::Proportional, FontFamily::Name("medium".into()), FontFamily::Name("semibold".into())] { + if let Some(v) = fonts.families.get_mut(&fam) { + v.push(name.clone()); + } + } + } +} + +/// Document faces needed to draw `text` (the input method's composition), one for each character +/// the interface fonts cannot draw, skipping faces in `have` (#565). Characters of the embedded +/// interface fonts are not looked up; very large faces (system CJK collections) are left to the CJK +/// fallback. +pub fn faces_for_preedit( + text: &str, + have: &[Arc], + covered: impl Fn(char) -> bool, + lookup: impl Fn(char) -> Option>, +) -> Vec> { + let mut out: Vec> = Vec::new(); + for c in text.chars().filter(|c| !c.is_ascii() && !c.is_whitespace() && !covered(*c)) { + if have.iter().chain(&out).any(|f| f.covers(c)) { + continue; + } + if let Some(f) = lookup(c).filter(|f| f.data().len() <= 16 << 20 && !have.iter().chain(&out).any(|h| h.id() == f.id())) { + out.push(f); + } + } + out } /// The interface fonts; see [`install_fonts_with`]. @@ -510,6 +555,26 @@ pub fn c32(c: wordcraft_doc::Rgb) -> Color32 { mod tests { use std::collections::BTreeSet; + /// #565: the composition of an input method (Bangla through ibus) drew as boxes because only the + /// interface fonts were used. Faces found for the uncovered characters are added as fallbacks, + /// once, and not for characters the interface fonts draw. + #[test] + fn preedit_gets_document_fallback_faces() { + let face = wordcraft_fonts::FontDb::global().face("Source Sans 3", "Regular"); + let lookup = |_: char| Some(face.clone()); + let bangla = "\u{0986}\u{09AE}\u{09BF}"; + let got = super::faces_for_preedit(bangla, &[], |_| false, lookup); + assert_eq!(got.len(), 1, "one face, not one per character"); + assert!(super::faces_for_preedit(bangla, &got, |_| false, lookup).is_empty(), "already added"); + assert!(super::faces_for_preedit(bangla, &[], |_| true, lookup).is_empty(), "the interface fonts draw it"); + assert!(super::faces_for_preedit("abc", &[], |_| false, lookup).is_empty(), "ASCII never needs one"); + let mut defs = super::font_definitions(false, false); + let before = defs.font_data.len(); + super::add_fallback_faces(&mut defs, &got); + assert_eq!(defs.font_data.len(), before + 1); + assert!(defs.families[&egui::FontFamily::Proportional].last().is_some_and(|n| n.starts_with("fallback "))); + } + /// Symbols written as text in the interface code must exist in the interface fonts (Inter and /// egui's emoji fallbacks); a missing glyph draws as an empty box, as the panes' "✕" close /// button once did. Letters are the catalogs' business (see the i18n font tests).