diff --git a/crates/draw/src/lib.rs b/crates/draw/src/lib.rs index 2cc498a09b6799d0ce85faff1185301d778a3dba..6159cce35631f97f9795089a3d480bd9ad8cb24f 100644 --- a/crates/draw/src/lib.rs +++ b/crates/draw/src/lib.rs @@ -9,6 +9,32 @@ mod render; #[cfg(feature = "render")] pub use render::*; +/// Appends the installed `families` to every script's fallback in `collection`, after the +/// system's, so a character that neither the text's fonts nor its script's fallback draws +/// comes from the first of them that has it, as the system's own text does. +pub fn fall_back_to(collection: &mut parley::fontique::Collection, families: &[String]) { + use parley::fontique::{Script, ScriptExt}; + let extra: Vec<_> = families + .iter() + .filter_map(|name| collection.family_id(name)) + .collect(); + // Common, inherited and unknown characters keep runs of their own where no script's + // text surrounds them. + let scripts = Script::all_samples() + .iter() + .map(|(script, _)| *script) + .chain([*b"Zyyy", *b"Zinh", *b"Zzzz"].map(Script::from_bytes)); + for script in scripts { + let mut chain: Vec<_> = collection.fallback_families(script).collect(); + for family in &extra { + if !chain.contains(family) { + chain.push(*family); + } + } + collection.set_fallbacks(script, chain.into_iter()); + } +} + /// sRGB's encoding of a linear channel. fn encode(linear: f32) -> f32 { if linear <= 0.003_130_8 { diff --git a/crates/snowbound/src/linux.rs b/crates/snowbound/src/linux.rs index d0d9b2109a096e3df1840b32a099634a26ffce71..e7d2f730a6dc85fef9093bf87952e70ae63229ce 100644 --- a/crates/snowbound/src/linux.rs +++ b/crates/snowbound/src/linux.rs @@ -1161,6 +1161,11 @@ pub fn pick_file(title: &str, types: &[&str]) -> Option { )) } +/// None: fontique falls back through fontconfig's coverage, which reaches every installed font. +pub fn symbol_fonts() -> Vec { + Vec::new() +} + /// No icon theme lookup; the page draws a blank page for the file. pub fn file_icon(_: &std::path::Path) -> Option> { None diff --git a/crates/snowbound/src/macos.rs b/crates/snowbound/src/macos.rs index 99092e6b2f00c296b9e0e3d8e39c152435f70d6e..dbc5d172edc68060847a36279c237eb784860562 100644 --- a/crates/snowbound/src/macos.rs +++ b/crates/snowbound/src/macos.rs @@ -857,6 +857,80 @@ pub fn clip_screen(proxy: EventLoopProxy) { }); } +/// The families CoreText's cascade falls back to from the interface font, as the system's +/// text and Character Viewer draw what a font lacks, then the system fonts that hold nearly +/// all CoreText finds past it by searching every font. 10.6 and 10.7, which can't list the +/// cascade, have their symbol fonts in its place. +pub fn symbol_fonts() -> Vec { + use std::ffi::c_void; + type Cascade = unsafe extern "C" fn(*const c_void, *const c_void) -> *const c_void; + #[link(name = "CoreText", kind = "framework")] + unsafe extern "C" { + static kCTFontFamilyNameAttribute: *const c_void; + fn CTFontCreateUIFontForLanguage( + kind: u32, + size: f64, + language: *const c_void, + ) -> *const c_void; + fn CTFontDescriptorCopyAttribute( + descriptor: *const c_void, + attribute: *const c_void, + ) -> *const c_void; + } + unsafe extern "C" { + fn CFArrayGetCount(array: *const c_void) -> isize; + fn CFArrayGetValueAtIndex(array: *const c_void, index: isize) -> *const c_void; + fn CFRelease(object: *const c_void); + } + const SYSTEM: u32 = 2; + let cascade = || unsafe { + // 10.8's. + let copy = libc::dlsym( + libc::RTLD_DEFAULT, + c"CTFontCopyDefaultCascadeListForLanguages".as_ptr(), + ); + if copy.is_null() { + return None; + } + let copy = std::mem::transmute::<*mut c_void, Cascade>(copy); + let font = CTFontCreateUIFontForLanguage(SYSTEM, 13.0, std::ptr::null()); + if font.is_null() { + return None; + } + let list = copy(font, std::ptr::null()); + CFRelease(font); + if list.is_null() { + return None; + } + let mut families = Vec::new(); + for index in 0..CFArrayGetCount(list) { + let descriptor = CFArrayGetValueAtIndex(list, index); + let name = CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute); + if !name.is_null() { + families.push((*name.cast::()).to_string()); + CFRelease(name); + } + } + CFRelease(list); + Some(families) + }; + let mut families = cascade().unwrap_or_else(|| { + ["Lucida Grande", "Apple Symbols", "STIXGeneral", "Menlo"] + .map(String::from) + .into() + }); + families.extend( + [ + "Arial Unicode MS", + "Geneva", + "Noto Sans Coptic", + "Hiragino Sans", + ] + .map(String::from), + ); + families +} + /// Tells the user something they asked for could not be done: `message`, then what to do. pub fn alert(message: &str, detail: &str) { let mtm = MainThreadMarker::new().expect("Window events run on the main thread"); diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index c1f51af01b0c1cb21d08b4922eae11f2769f61f2..7816868a9e5254c598f0fe818237b42781ebfc4a 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -886,6 +886,8 @@ impl State { let (surface, renderer) = surface::Surface::new(window.clone(), backdrop).await?; let size = window.inner_size(); let mut engine = TextEngine::default(); + let fallbacks = platform::symbol_fonts(); + draw::fall_back_to(&mut engine.fonts.collection, &fallbacks); for path in substitutes { let target = engine .register_substitute(parley::fontique::Blob::new(Arc::new(std::fs::read(path)?)))?; @@ -1019,6 +1021,7 @@ impl State { // A window shown but never focused hears no focus event; a hidden one draws as focused. ui.window_focused = !visible || window.has_focus(); platform::system_interface(&mut ui); + ui.fall_back_to(&fallbacks); ui.set_focus(Some(page())); for family in FONTS { for (face, _) in engine.substitute(family).map_or(&[][..], |s| &s.faces) { diff --git a/crates/snowbound/src/windows.rs b/crates/snowbound/src/windows.rs index e3085d1d67cf030a95f7b58fba226e61fd384a9a..9c0965e64a2476cd268070d42640568fdbafa7c2 100644 --- a/crates/snowbound/src/windows.rs +++ b/crates/snowbound/src/windows.rs @@ -1159,6 +1159,47 @@ pub fn open_file(path: &std::path::Path) { reveal(path); } +/// What text falls back to past its fonts and DirectWrite's fallback for its script: the +/// symbol fonts, then Segoe UI's linked fonts and the scripts' fonts of Windows 7, whose +/// DirectWrite has no fallback by script. +pub fn symbol_fonts() -> Vec { + [ + "Segoe UI Symbol", + "Cambria Math", + "Segoe UI Emoji", + // Segoe UI's FontLink\SystemLink in Windows 7's registry. + "Tahoma", + "Meiryo", + "MS UI Gothic", + "Microsoft JhengHei", + "Microsoft YaHei", + "Malgun Gothic", + "PMingLiU", + "SimSun", + "Gulim", + // Windows 7's fonts of the scripts those leave out, then Office's catch-all. + "Microsoft Yi Baiti", + "Euphemia", + "Nyala", + "Ebrima", + "Mongolian Baiti", + "DaunPenh", + "Plantagenet Cherokee", + "Microsoft New Tai Lue", + "Iskoola Pota", + "Estrangelo Edessa", + "Microsoft PhagsPa", + "MV Boli", + "Microsoft Tai Le", + "Microsoft Himalaya", + "Mangal", + "Latha", + "Arial Unicode MS", + ] + .map(String::from) + .into() +} + /// No shell icon lookup yet; the page draws a blank page for the file. pub fn file_icon(_: &std::path::Path) -> Option> { None diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 6e4ecb694e116ec4eb06c565b143a0cec246f9c8..c2f8d4c6969b13f8ab528be0d2e3cd605bda9c81 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1297,6 +1297,12 @@ impl Ui { self.texts.preview_font(data, family); } + /// Draws what the interface's fonts and their scripts' fallbacks lack with the first of + /// `families` that has it; see `draw::fall_back_to`. + pub fn fall_back_to(&mut self, families: &[String]) { + self.texts.fall_back_to(families); + } + /// Whether the interface's fonts draw every glyph of `text`. pub fn shows(&mut self, text: &str) -> bool { self.texts.shows(text, self.frame) diff --git a/crates/ui/src/text.rs b/crates/ui/src/text.rs index ee4a72ea06162fc6407f7bd89ef0e490a3f7505c..2c86375435a77acf3b4f241d07751afeefed3599 100644 --- a/crates/ui/src/text.rs +++ b/crates/ui/src/text.rs @@ -21,8 +21,8 @@ pub(crate) struct Texts { context: LayoutContext<()>, /// By text, size, weight and preferred family, with the frame each was last used. cache: HashMap, u64)>, - /// The Last Resort font's data, once looked for. - last_resort: Option>, + /// The Last Resort font, once looked for. + last_resort: Option>>, } impl Texts { @@ -106,24 +106,34 @@ impl Texts { self.styled(&cut, size, *bold, *italic, font, frame) } - /// Whether the interface's fonts or their fallbacks draw every glyph of `text`, not the - /// missing glyph nor the placeholders of macOS's Last Resort font. + /// Whether the interface's fonts or their fallbacks draw `text`: some glyph, and none the + /// missing glyph or a placeholder of macOS's Last Resort font. pub fn shows(&mut self, text: &str, frame: u64) -> bool { - let last_resort = *self.last_resort.get_or_insert_with(|| { - let family = self.fonts.collection.family_by_name("LastResort")?; - let font = family.fonts().first()?; - Some(font.load(Some(&mut self.fonts.source_cache))?.id()) - }); + let last_resort = self + .last_resort + .get_or_insert_with(|| { + let family = self.fonts.collection.family_by_name(".LastResort")?; + family + .fonts() + .first()? + .load(Some(&mut self.fonts.source_cache)) + }) + .clone(); let label = self.styled(text, 16.0, false, false, None, frame); - label.layout.lines().all(|line| { + let mut glyphs = 0; + let drawn = label.layout.lines().all(|line| { line.items().all(|item| match item { PositionedLayoutItem::GlyphRun(run) => { - Some(run.run().font().font.data.id()) != last_resort + glyphs += run.glyphs().count(); + // By contents: the source cache may load the font afresh. + let data = run.run().font().font.data.data(); + last_resort.as_ref().is_none_or(|font| font.data() != data) && run.glyphs().all(|glyph| glyph.id != 0) } PositionedLayoutItem::InlineBox(_) => true, }) - }) + }); + drawn && glyphs > 0 } /// Makes `family` the interface's font, where fontique doesn't know the system's. @@ -135,6 +145,11 @@ impl Texts { } } + pub fn fall_back_to(&mut self, families: &[String]) { + draw::fall_back_to(&mut self.fonts.collection, families); + self.cache.clear(); + } + pub fn preview_font(&mut self, data: Blob, family: &str) { self.fonts.collection.register_fonts( data,