diff --git a/Cargo.lock b/Cargo.lock index ed6728c..0221e7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4398,6 +4398,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", + "skrifa", "souvlaki", "tauri-winrt-notification", "tokio", diff --git a/Cargo.toml b/Cargo.toml index cd3914a..d859102 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,9 @@ egui = "0.35" # The icons on buttons. Glyphs like ⏭ and 🔊 come out of whichever fallback # font has them, at its own size and baseline; an icon font is one set. egui-phosphor = { version = "0.13", default-features = false, features = ["regular"] } +# Tests only: reads the bundled fonts' character maps. The version epaint +# already builds, so it costs no extra compile. +skrifa = { version = "0.42", default-features = false, features = ["std"] } # Poster thumbnails only: Proton renders them as JPEG, and a decoder that also # pulls in every exotic format is weight this app has no use for. image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } diff --git a/crates/pstr-app/Cargo.toml b/crates/pstr-app/Cargo.toml index 06dd9b6..b40587e 100644 --- a/crates/pstr-app/Cargo.toml +++ b/crates/pstr-app/Cargo.toml @@ -34,6 +34,9 @@ directories.workspace = true chrono.workspace = true souvlaki.workspace = true +[dev-dependencies] +skrifa.workspace = true + [target.'cfg(target_os = "linux")'.dependencies] dbus.workspace = true diff --git a/crates/pstr-app/assets/fonts/Inter-Regular.ttf b/crates/pstr-app/assets/fonts/Inter-Regular.ttf index b7aaca8..d2cad37 100644 Binary files a/crates/pstr-app/assets/fonts/Inter-Regular.ttf and b/crates/pstr-app/assets/fonts/Inter-Regular.ttf differ diff --git a/crates/pstr-app/assets/fonts/Inter-SemiBold.ttf b/crates/pstr-app/assets/fonts/Inter-SemiBold.ttf index 47f8ab1..2187923 100644 Binary files a/crates/pstr-app/assets/fonts/Inter-SemiBold.ttf and b/crates/pstr-app/assets/fonts/Inter-SemiBold.ttf differ diff --git a/crates/pstr-app/src/theme.rs b/crates/pstr-app/src/theme.rs index 4813f82..b908d9e 100644 --- a/crates/pstr-app/src/theme.rs +++ b/crates/pstr-app/src/theme.rs @@ -163,6 +163,14 @@ pub mod motion { /// The UI typeface, bundled rather than taken from the system so a title /// sets the same on every desktop. Inter, under the OFL — the licence is /// beside the files. +/// +/// Both files have their private use area mappings removed, because the +/// Phosphor icons live there. Inter maps 745 codepoints in that area to its +/// own alternate glyphs, and as the first font in the family it won: the +/// refresh arrow drew as an accented letter. The glyphs themselves are still +/// in the files; only the character map entries are gone. Strip them again +/// after any Inter update (fontTools: drop every `cmap` subtable entry in +/// U+E000..=U+F8FF). const INTER: &[u8] = include_bytes!("../assets/fonts/Inter-Regular.ttf"); const INTER_SEMIBOLD: &[u8] = include_bytes!("../assets/fonts/Inter-SemiBold.ttf"); @@ -190,8 +198,8 @@ fn font_definitions(cjk: Option) -> egui::FontDefinitions { .entry(Proportional) .or_default() .insert(0, "inter".into()); - // Right behind Inter. The icons are in the private use area, so nothing - // Inter has is shadowed by them. + // Right behind Inter. The icons are in the private use area, which the + // bundled Inter no longer maps, so neither font shadows the other. egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular); if let Some(cjk) = cjk { let name = "system-cjk-fallback".to_owned(); @@ -841,6 +849,27 @@ const RAMP_STOPS: usize = 64; mod tests { use super::*; + #[test] + fn the_bundled_inter_leaves_the_icon_codepoints_to_phosphor() { + use skrifa::MetadataProvider as _; + + for (name, bytes) in [("regular", INTER), ("semibold", INTER_SEMIBOLD)] { + let font = skrifa::FontRef::new(bytes).expect("bundled Inter parses"); + let shadowed: Vec = font + .charmap() + .mappings() + .map(|(codepoint, _)| codepoint) + .filter(|codepoint| (0xE000..=0xF8FF).contains(codepoint)) + .collect(); + assert!( + shadowed.is_empty(), + "Inter {name} maps {} private use codepoints, starting U+{:04X}", + shadowed.len(), + shadowed[0] + ); + } + } + #[test] fn the_shipped_default_is_the_palette_that_was_here_before() { let resolved = Palette::resolve(Appearance::default());