| author | |
| committer | |
| log | 17a124c2584691c108abddd1570f750a0553b9a9 |
| tree | 27340471a0e339d66c3ebad36685337039a8b337 |
| parent | 931a2dbdbbff49d90d4b2cca30f1bd7488a2859b |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Snowbound took one fallback family per script from fontique, so a
character that neither the text's font nor that family has drew as
the missing glyph or a Last Resort placeholder: ⇐ and most of Arrows,
Mathematical Operators, Letterlike Symbols and Miscellaneous Technical
on macOS, and on Windows 7, whose DirectWrite has no fallback by
script, everything outside Segoe UI. The page and the interface now
fall back, after that family, through the platform's own: CoreText's
cascade for the system font and the system fonts past it on macOS,
the symbol fonts, Segoe UI's linked fonts and the scripts' fonts on
Windows. Linux needs none; fontconfig's sort already reaches every
installed font.
More Symbols offers what that fallback draws: 25,943 named characters
on macOS (was 21,215 drawn, plus 2,701 Last Resort placeholders that
it failed to recognise), 13,602 on Windows 7 (was 2,897; Character
Map reaches 13,664 across every font), 28,541 on Debian with Noto.
Assisted-by: claude-opus-5.57 files changed, 182 insertions(+), 12 deletions(-)
crates/draw/src/lib.rs+26| ... | @@ -9,6 +9,32 @@ mod render; | ... | @@ -9,6 +9,32 @@ mod render; |
| 9 | #[cfg(feature = "render")] | 9 | #[cfg(feature = "render")] |
| 10 | pub use render::*; | 10 | pub use render::*; |
| 11 | 11 | ||
| 12 | /// Appends the installed `families` to every script's fallback in `collection`, after the | ||
| 13 | /// system's, so a character that neither the text's fonts nor its script's fallback draws | ||
| 14 | /// comes from the first of them that has it, as the system's own text does. | ||
| 15 | pub fn fall_back_to(collection: &mut parley::fontique::Collection, families: &[String]) { | ||
| 16 | use parley::fontique::{Script, ScriptExt}; | ||
| 17 | let extra: Vec<_> = families | ||
| 18 | .iter() | ||
| 19 | .filter_map(|name| collection.family_id(name)) | ||
| 20 | .collect(); | ||
| 21 | // Common, inherited and unknown characters keep runs of their own where no script's | ||
| 22 | // text surrounds them. | ||
| 23 | let scripts = Script::all_samples() | ||
| 24 | .iter() | ||
| 25 | .map(|(script, _)| *script) | ||
| 26 | .chain([*b"Zyyy", *b"Zinh", *b"Zzzz"].map(Script::from_bytes)); | ||
| 27 | for script in scripts { | ||
| 28 | let mut chain: Vec<_> = collection.fallback_families(script).collect(); | ||
| 29 | for family in &extra { | ||
| 30 | if !chain.contains(family) { | ||
| 31 | chain.push(*family); | ||
| 32 | } | ||
| 33 | } | ||
| 34 | collection.set_fallbacks(script, chain.into_iter()); | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 12 | /// sRGB's encoding of a linear channel. | 38 | /// sRGB's encoding of a linear channel. |
| 13 | fn encode(linear: f32) -> f32 { | 39 | fn encode(linear: f32) -> f32 { |
| 14 | if linear <= 0.003_130_8 { | 40 | if linear <= 0.003_130_8 { |
crates/snowbound/src/linux.rs+5| ... | @@ -1161,6 +1161,11 @@ pub fn pick_file(title: &str, types: &[&str]) -> Option<PathBuf> { | ... | @@ -1161,6 +1161,11 @@ pub fn pick_file(title: &str, types: &[&str]) -> Option<PathBuf> { |
| 1161 | )) | 1161 | )) |
| 1162 | } | 1162 | } |
| 1163 | 1163 | ||
| 1164 | /// None: fontique falls back through fontconfig's coverage, which reaches every installed font. | ||
| 1165 | pub fn symbol_fonts() -> Vec<String> { | ||
| 1166 | Vec::new() | ||
| 1167 | } | ||
| 1168 | |||
| 1164 | /// No icon theme lookup; the page draws a blank page for the file. | 1169 | /// No icon theme lookup; the page draws a blank page for the file. |
| 1165 | pub fn file_icon(_: &std::path::Path) -> Option<Vec<u8>> { | 1170 | pub fn file_icon(_: &std::path::Path) -> Option<Vec<u8>> { |
| 1166 | None | 1171 | None |
crates/snowbound/src/macos.rs+74| ... | @@ -857,6 +857,80 @@ pub fn clip_screen(proxy: EventLoopProxy<crate::UserEvent>) { | ... | @@ -857,6 +857,80 @@ pub fn clip_screen(proxy: EventLoopProxy<crate::UserEvent>) { |
| 857 | }); | 857 | }); |
| 858 | } | 858 | } |
| 859 | 859 | ||
| 860 | /// The families CoreText's cascade falls back to from the interface font, as the system's | ||
| 861 | /// text and Character Viewer draw what a font lacks, then the system fonts that hold nearly | ||
| 862 | /// all CoreText finds past it by searching every font. 10.6 and 10.7, which can't list the | ||
| 863 | /// cascade, have their symbol fonts in its place. | ||
| 864 | pub fn symbol_fonts() -> Vec<String> { | ||
| 865 | use std::ffi::c_void; | ||
| 866 | type Cascade = unsafe extern "C" fn(*const c_void, *const c_void) -> *const c_void; | ||
| 867 | #[link(name = "CoreText", kind = "framework")] | ||
| 868 | unsafe extern "C" { | ||
| 869 | static kCTFontFamilyNameAttribute: *const c_void; | ||
| 870 | fn CTFontCreateUIFontForLanguage( | ||
| 871 | kind: u32, | ||
| 872 | size: f64, | ||
| 873 | language: *const c_void, | ||
| 874 | ) -> *const c_void; | ||
| 875 | fn CTFontDescriptorCopyAttribute( | ||
| 876 | descriptor: *const c_void, | ||
| 877 | attribute: *const c_void, | ||
| 878 | ) -> *const c_void; | ||
| 879 | } | ||
| 880 | unsafe extern "C" { | ||
| 881 | fn CFArrayGetCount(array: *const c_void) -> isize; | ||
| 882 | fn CFArrayGetValueAtIndex(array: *const c_void, index: isize) -> *const c_void; | ||
| 883 | fn CFRelease(object: *const c_void); | ||
| 884 | } | ||
| 885 | const SYSTEM: u32 = 2; | ||
| 886 | let cascade = || unsafe { | ||
| 887 | // 10.8's. | ||
| 888 | let copy = libc::dlsym( | ||
| 889 | libc::RTLD_DEFAULT, | ||
| 890 | c"CTFontCopyDefaultCascadeListForLanguages".as_ptr(), | ||
| 891 | ); | ||
| 892 | if copy.is_null() { | ||
| 893 | return None; | ||
| 894 | } | ||
| 895 | let copy = std::mem::transmute::<*mut c_void, Cascade>(copy); | ||
| 896 | let font = CTFontCreateUIFontForLanguage(SYSTEM, 13.0, std::ptr::null()); | ||
| 897 | if font.is_null() { | ||
| 898 | return None; | ||
| 899 | } | ||
| 900 | let list = copy(font, std::ptr::null()); | ||
| 901 | CFRelease(font); | ||
| 902 | if list.is_null() { | ||
| 903 | return None; | ||
| 904 | } | ||
| 905 | let mut families = Vec::new(); | ||
| 906 | for index in 0..CFArrayGetCount(list) { | ||
| 907 | let descriptor = CFArrayGetValueAtIndex(list, index); | ||
| 908 | let name = CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute); | ||
| 909 | if !name.is_null() { | ||
| 910 | families.push((*name.cast::<NSString>()).to_string()); | ||
| 911 | CFRelease(name); | ||
| 912 | } | ||
| 913 | } | ||
| 914 | CFRelease(list); | ||
| 915 | Some(families) | ||
| 916 | }; | ||
| 917 | let mut families = cascade().unwrap_or_else(|| { | ||
| 918 | ["Lucida Grande", "Apple Symbols", "STIXGeneral", "Menlo"] | ||
| 919 | .map(String::from) | ||
| 920 | .into() | ||
| 921 | }); | ||
| 922 | families.extend( | ||
| 923 | [ | ||
| 924 | "Arial Unicode MS", | ||
| 925 | "Geneva", | ||
| 926 | "Noto Sans Coptic", | ||
| 927 | "Hiragino Sans", | ||
| 928 | ] | ||
| 929 | .map(String::from), | ||
| 930 | ); | ||
| 931 | families | ||
| 932 | } | ||
| 933 | |||
| 860 | /// Tells the user something they asked for could not be done: `message`, then what to do. | 934 | /// Tells the user something they asked for could not be done: `message`, then what to do. |
| 861 | pub fn alert(message: &str, detail: &str) { | 935 | pub fn alert(message: &str, detail: &str) { |
| 862 | let mtm = MainThreadMarker::new().expect("Window events run on the main thread"); | 936 | let mtm = MainThreadMarker::new().expect("Window events run on the main thread"); |
crates/snowbound/src/main.rs+3| ... | @@ -886,6 +886,8 @@ impl State { | ... | @@ -886,6 +886,8 @@ impl State { |
| 886 | let (surface, renderer) = surface::Surface::new(window.clone(), backdrop).await?; | 886 | let (surface, renderer) = surface::Surface::new(window.clone(), backdrop).await?; |
| 887 | let size = window.inner_size(); | 887 | let size = window.inner_size(); |
| 888 | let mut engine = TextEngine::default(); | 888 | let mut engine = TextEngine::default(); |
| 889 | let fallbacks = platform::symbol_fonts(); | ||
| 890 | draw::fall_back_to(&mut engine.fonts.collection, &fallbacks); | ||
| 889 | for path in substitutes { | 891 | for path in substitutes { |
| 890 | let target = engine | 892 | let target = engine |
| 891 | .register_substitute(parley::fontique::Blob::new(Arc::new(std::fs::read(path)?)))?; | 893 | .register_substitute(parley::fontique::Blob::new(Arc::new(std::fs::read(path)?)))?; |
| ... | @@ -1019,6 +1021,7 @@ impl State { | ... | @@ -1019,6 +1021,7 @@ impl State { |
| 1019 | // A window shown but never focused hears no focus event; a hidden one draws as focused. | 1021 | // A window shown but never focused hears no focus event; a hidden one draws as focused. |
| 1020 | ui.window_focused = !visible || window.has_focus(); | 1022 | ui.window_focused = !visible || window.has_focus(); |
| 1021 | platform::system_interface(&mut ui); | 1023 | platform::system_interface(&mut ui); |
| 1024 | ui.fall_back_to(&fallbacks); | ||
| 1022 | ui.set_focus(Some(page())); | 1025 | ui.set_focus(Some(page())); |
| 1023 | for family in FONTS { | 1026 | for family in FONTS { |
| 1024 | for (face, _) in engine.substitute(family).map_or(&[][..], |s| &s.faces) { | 1027 | for (face, _) in engine.substitute(family).map_or(&[][..], |s| &s.faces) { |
crates/snowbound/src/windows.rs+41| ... | @@ -1159,6 +1159,47 @@ pub fn open_file(path: &std::path::Path) { | ... | @@ -1159,6 +1159,47 @@ pub fn open_file(path: &std::path::Path) { |
| 1159 | reveal(path); | 1159 | reveal(path); |
| 1160 | } | 1160 | } |
| 1161 | 1161 | ||
| 1162 | /// What text falls back to past its fonts and DirectWrite's fallback for its script: the | ||
| 1163 | /// symbol fonts, then Segoe UI's linked fonts and the scripts' fonts of Windows 7, whose | ||
| 1164 | /// DirectWrite has no fallback by script. | ||
| 1165 | pub fn symbol_fonts() -> Vec<String> { | ||
| 1166 | [ | ||
| 1167 | "Segoe UI Symbol", | ||
| 1168 | "Cambria Math", | ||
| 1169 | "Segoe UI Emoji", | ||
| 1170 | // Segoe UI's FontLink\SystemLink in Windows 7's registry. | ||
| 1171 | "Tahoma", | ||
| 1172 | "Meiryo", | ||
| 1173 | "MS UI Gothic", | ||
| 1174 | "Microsoft JhengHei", | ||
| 1175 | "Microsoft YaHei", | ||
| 1176 | "Malgun Gothic", | ||
| 1177 | "PMingLiU", | ||
| 1178 | "SimSun", | ||
| 1179 | "Gulim", | ||
| 1180 | // Windows 7's fonts of the scripts those leave out, then Office's catch-all. | ||
| 1181 | "Microsoft Yi Baiti", | ||
| 1182 | "Euphemia", | ||
| 1183 | "Nyala", | ||
| 1184 | "Ebrima", | ||
| 1185 | "Mongolian Baiti", | ||
| 1186 | "DaunPenh", | ||
| 1187 | "Plantagenet Cherokee", | ||
| 1188 | "Microsoft New Tai Lue", | ||
| 1189 | "Iskoola Pota", | ||
| 1190 | "Estrangelo Edessa", | ||
| 1191 | "Microsoft PhagsPa", | ||
| 1192 | "MV Boli", | ||
| 1193 | "Microsoft Tai Le", | ||
| 1194 | "Microsoft Himalaya", | ||
| 1195 | "Mangal", | ||
| 1196 | "Latha", | ||
| 1197 | "Arial Unicode MS", | ||
| 1198 | ] | ||
| 1199 | .map(String::from) | ||
| 1200 | .into() | ||
| 1201 | } | ||
| 1202 | |||
| 1162 | /// No shell icon lookup yet; the page draws a blank page for the file. | 1203 | /// No shell icon lookup yet; the page draws a blank page for the file. |
| 1163 | pub fn file_icon(_: &std::path::Path) -> Option<Vec<u8>> { | 1204 | pub fn file_icon(_: &std::path::Path) -> Option<Vec<u8>> { |
| 1164 | None | 1205 | None |
crates/ui/src/lib.rs+6| ... | @@ -1297,6 +1297,12 @@ impl Ui { | ... | @@ -1297,6 +1297,12 @@ impl Ui { |
| 1297 | self.texts.preview_font(data, family); | 1297 | self.texts.preview_font(data, family); |
| 1298 | } | 1298 | } |
| 1299 | 1299 | ||
| 1300 | /// Draws what the interface's fonts and their scripts' fallbacks lack with the first of | ||
| 1301 | /// `families` that has it; see `draw::fall_back_to`. | ||
| 1302 | pub fn fall_back_to(&mut self, families: &[String]) { | ||
| 1303 | self.texts.fall_back_to(families); | ||
| 1304 | } | ||
| 1305 | |||
| 1300 | /// Whether the interface's fonts draw every glyph of `text`. | 1306 | /// Whether the interface's fonts draw every glyph of `text`. |
| 1301 | pub fn shows(&mut self, text: &str) -> bool { | 1307 | pub fn shows(&mut self, text: &str) -> bool { |
| 1302 | self.texts.shows(text, self.frame) | 1308 | self.texts.shows(text, self.frame) |
crates/ui/src/text.rs+27-12| ... | @@ -21,8 +21,8 @@ pub(crate) struct Texts { | ... | @@ -21,8 +21,8 @@ pub(crate) struct Texts { |
| 21 | context: LayoutContext<()>, | 21 | context: LayoutContext<()>, |
| 22 | /// By text, size, weight and preferred family, with the frame each was last used. | 22 | /// By text, size, weight and preferred family, with the frame each was last used. |
| 23 | cache: HashMap<LabelKey, (Rc<Label>, u64)>, | 23 | cache: HashMap<LabelKey, (Rc<Label>, u64)>, |
| 24 | /// The Last Resort font's data, once looked for. | 24 | /// The Last Resort font, once looked for. |
| 25 | last_resort: Option<Option<u64>>, | 25 | last_resort: Option<Option<Blob<u8>>>, |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | impl Texts { | 28 | impl Texts { |
| ... | @@ -106,24 +106,34 @@ impl Texts { | ... | @@ -106,24 +106,34 @@ impl Texts { |
| 106 | self.styled(&cut, size, *bold, *italic, font, frame) | 106 | self.styled(&cut, size, *bold, *italic, font, frame) |
| 107 | } | 107 | } |
| 108 | 108 | ||
| 109 | /// Whether the interface's fonts or their fallbacks draw every glyph of `text`, not the | 109 | /// Whether the interface's fonts or their fallbacks draw `text`: some glyph, and none the |
| 110 | /// missing glyph nor the placeholders of macOS's Last Resort font. | 110 | /// missing glyph or a placeholder of macOS's Last Resort font. |
| 111 | pub fn shows(&mut self, text: &str, frame: u64) -> bool { | 111 | pub fn shows(&mut self, text: &str, frame: u64) -> bool { |
| 112 | let last_resort = *self.last_resort.get_or_insert_with(|| { | 112 | let last_resort = self |
| 113 | let family = self.fonts.collection.family_by_name("LastResort")?; | 113 | .last_resort |
| 114 | let font = family.fonts().first()?; | 114 | .get_or_insert_with(|| { |
| 115 | Some(font.load(Some(&mut self.fonts.source_cache))?.id()) | 115 | let family = self.fonts.collection.family_by_name(".LastResort")?; |
| 116 | }); | 116 | family |
| 117 | .fonts() | ||
| 118 | .first()? | ||
| 119 | .load(Some(&mut self.fonts.source_cache)) | ||
| 120 | }) | ||
| 121 | .clone(); | ||
| 117 | let label = self.styled(text, 16.0, false, false, None, frame); | 122 | let label = self.styled(text, 16.0, false, false, None, frame); |
| 118 | label.layout.lines().all(|line| { | 123 | let mut glyphs = 0; |
| 124 | let drawn = label.layout.lines().all(|line| { | ||
| 119 | line.items().all(|item| match item { | 125 | line.items().all(|item| match item { |
| 120 | PositionedLayoutItem::GlyphRun(run) => { | 126 | PositionedLayoutItem::GlyphRun(run) => { |
| 121 | Some(run.run().font().font.data.id()) != last_resort | 127 | glyphs += run.glyphs().count(); |
| 128 | // By contents: the source cache may load the font afresh. | ||
| 129 | let data = run.run().font().font.data.data(); | ||
| 130 | last_resort.as_ref().is_none_or(|font| font.data() != data) | ||
| 122 | && run.glyphs().all(|glyph| glyph.id != 0) | 131 | && run.glyphs().all(|glyph| glyph.id != 0) |
| 123 | } | 132 | } |
| 124 | PositionedLayoutItem::InlineBox(_) => true, | 133 | PositionedLayoutItem::InlineBox(_) => true, |
| 125 | }) | 134 | }) |
| 126 | }) | 135 | }); |
| 136 | drawn && glyphs > 0 | ||
| 127 | } | 137 | } |
| 128 | 138 | ||
| 129 | /// Makes `family` the interface's font, where fontique doesn't know the system's. | 139 | /// Makes `family` the interface's font, where fontique doesn't know the system's. |
| ... | @@ -135,6 +145,11 @@ impl Texts { | ... | @@ -135,6 +145,11 @@ impl Texts { |
| 135 | } | 145 | } |
| 136 | } | 146 | } |
| 137 | 147 | ||
| 148 | pub fn fall_back_to(&mut self, families: &[String]) { | ||
| 149 | draw::fall_back_to(&mut self.fonts.collection, families); | ||
| 150 | self.cache.clear(); | ||
| 151 | } | ||
| 152 | |||
| 138 | pub fn preview_font(&mut self, data: Blob<u8>, family: &str) { | 153 | pub fn preview_font(&mut self, data: Blob<u8>, family: &str) { |
| 139 | self.fonts.collection.register_fonts( | 154 | self.fonts.collection.register_fonts( |
| 140 | data, | 155 | data, |