| 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 | 9 | #[cfg(feature = "render")] |
| 10 | 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 | 38 | /// sRGB's encoding of a linear channel. |
| 13 | 39 | fn encode(linear: f32) -> f32 { |
| 14 | 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 | 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 | 1169 | /// No icon theme lookup; the page draws a blank page for the file. |
| 1165 | 1170 | pub fn file_icon(_: &std::path::Path) -> Option<Vec<u8>> { |
| 1166 | 1171 | None |
crates/snowbound/src/macos.rs+74| ... | ... | @@ -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 | 934 | /// Tells the user something they asked for could not be done: `message`, then what to do. |
| 861 | 935 | pub fn alert(message: &str, detail: &str) { |
| 862 | 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 | 886 | let (surface, renderer) = surface::Surface::new(window.clone(), backdrop).await?; |
| 887 | 887 | let size = window.inner_size(); |
| 888 | 888 | let mut engine = TextEngine::default(); |
| 889 | let fallbacks = platform::symbol_fonts(); | |
| 890 | draw::fall_back_to(&mut engine.fonts.collection, &fallbacks); | |
| 889 | 891 | for path in substitutes { |
| 890 | 892 | let target = engine |
| 891 | 893 | .register_substitute(parley::fontique::Blob::new(Arc::new(std::fs::read(path)?)))?; |
| ... | ... | @@ -1019,6 +1021,7 @@ impl State { |
| 1019 | 1021 | // A window shown but never focused hears no focus event; a hidden one draws as focused. |
| 1020 | 1022 | ui.window_focused = !visible || window.has_focus(); |
| 1021 | 1023 | platform::system_interface(&mut ui); |
| 1024 | ui.fall_back_to(&fallbacks); | |
| 1022 | 1025 | ui.set_focus(Some(page())); |
| 1023 | 1026 | for family in FONTS { |
| 1024 | 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 | 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 | 1203 | /// No shell icon lookup yet; the page draws a blank page for the file. |
| 1163 | 1204 | pub fn file_icon(_: &std::path::Path) -> Option<Vec<u8>> { |
| 1164 | 1205 | None |
crates/ui/src/lib.rs+6| ... | ... | @@ -1297,6 +1297,12 @@ impl Ui { |
| 1297 | 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 | 1306 | /// Whether the interface's fonts draw every glyph of `text`. |
| 1301 | 1307 | pub fn shows(&mut self, text: &str) -> bool { |
| 1302 | 1308 | self.texts.shows(text, self.frame) |
crates/ui/src/text.rs+27-12| ... | ... | @@ -21,8 +21,8 @@ pub(crate) struct Texts { |
| 21 | 21 | context: LayoutContext<()>, |
| 22 | 22 | /// By text, size, weight and preferred family, with the frame each was last used. |
| 23 | 23 | cache: HashMap<LabelKey, (Rc<Label>, u64)>, |
| 24 | /// The Last Resort font's data, once looked for. | |
| 25 | last_resort: Option<Option<u64>>, | |
| 24 | /// The Last Resort font, once looked for. | |
| 25 | last_resort: Option<Option<Blob<u8>>>, | |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | 28 | impl Texts { |
| ... | ... | @@ -106,24 +106,34 @@ impl Texts { |
| 106 | 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 | |
| 110 | /// missing glyph nor the placeholders of macOS's Last Resort font. | |
| 109 | /// Whether the interface's fonts or their fallbacks draw `text`: some glyph, and none the | |
| 110 | /// missing glyph or a placeholder of macOS's Last Resort font. | |
| 111 | 111 | pub fn shows(&mut self, text: &str, frame: u64) -> bool { |
| 112 | let last_resort = *self.last_resort.get_or_insert_with(|| { | |
| 113 | let family = self.fonts.collection.family_by_name("LastResort")?; | |
| 114 | let font = family.fonts().first()?; | |
| 115 | Some(font.load(Some(&mut self.fonts.source_cache))?.id()) | |
| 116 | }); | |
| 112 | let last_resort = self | |
| 113 | .last_resort | |
| 114 | .get_or_insert_with(|| { | |
| 115 | let family = self.fonts.collection.family_by_name(".LastResort")?; | |
| 116 | family | |
| 117 | .fonts() | |
| 118 | .first()? | |
| 119 | .load(Some(&mut self.fonts.source_cache)) | |
| 120 | }) | |
| 121 | .clone(); | |
| 117 | 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 | 125 | line.items().all(|item| match item { |
| 120 | 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 | 131 | && run.glyphs().all(|glyph| glyph.id != 0) |
| 123 | 132 | } |
| 124 | 133 | PositionedLayoutItem::InlineBox(_) => true, |
| 125 | 134 | }) |
| 126 | }) | |
| 135 | }); | |
| 136 | drawn && glyphs > 0 | |
| 127 | 137 | } |
| 128 | 138 | |
| 129 | 139 | /// Makes `family` the interface's font, where fontique doesn't know the system's. |
| ... | ... | @@ -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 | 153 | pub fn preview_font(&mut self, data: Blob<u8>, family: &str) { |
| 139 | 154 | self.fonts.collection.register_fonts( |
| 140 | 155 | data, |