Browse / github.com/storytold/craft-launcher / patch
Draw Georgian with the system's Georgian font
Draw Georgian with the system's Georgian font ## Summary None of the bundled faces (Inter, Archivo, Geist Mono) has Georgian letters. This lets Georgian use the launcher's existing installed-font mechanism for the CJK languages, so a Georgian interface (the language itself comes in #806) draws instead of showing boxes. - `i18n/lang.rs`: `Script::Georgian` (added to `Script::ALL`); doc comments say "system font (CJK, Georgian)". - `cjk_font.rs`: `georgian_candidates()`, best first: SF Georgian (`/System/Library/Fonts/SFGeorgian.ttf`, family `.SF Georgian`), then Arial Unicode MS on macOS; Sylfaen on Windows; Noto Sans Georgian (Debian/Ub
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- devius (imported from pull request #805, open)
- Base
4ab652afd834081ff76a47ffb752fbbf9424f9c1- Size
- +103 −10 in 5 files
- Review
- pending
- Shared
- 2026-10-28
- Id
65e31d1e747aa7c63a2ebe6d
How it went
No other operator has reported using it yet.
Diff
raw · AGENTS.md crates/ui-egui/src/cjk_font.rs crates/ui-egui/src/i18n/lang.rs crates/ui-egui/src/settings_dialog.rs crates/ui-egui/src/widgets.rs
@@ -95,7 +95,8 @@ means adding its row to every catalog marked `complete_menus` (the `*.tsv` files the English text, never through another catalog. Product and app names (ArtCraft Launcher, Craft App Suite, PhotoCraft…) stay in English. A CJK language is drawn with the system's own CJK font (`crates/ui-egui/src/cjk_font.rs`, desktop-only; English where there is none): never bundle a CJK font, they are 10 MB and more.+only; English where there is none): never bundle a CJK font, they are 10 MB and more. Georgian+borrows the system's Georgian font the same way. ### Other rules @@ -1,6 +1,8 @@ //! A CJK font from the operating system, for the CJK interface languages. The bundled faces have //! no CJK glyphs, and a CJK font is too large to ship (10 MB or more), so the desktop build //! borrows the one every system that can show them already has. Desktop only.+//!+//! Georgian, which the bundled faces don't draw either, borrows its system face the same way. use std::path::{Path, PathBuf}; @@ -40,6 +42,7 @@ fn script_name(script: Script) -> &'static str { match script { Script::Chinese => "Chinese", Script::Japanese => "Japanese",+ Script::Georgian => "Georgian", } } @@ -48,6 +51,7 @@ fn script_families(script: Script) -> &'static str { match script { Script::Chinese => "PingFang SC, Hiragino Sans GB, Heiti SC, Microsoft YaHei, SimSun, Noto Sans CJK SC, WenQuanYi and MiSans", Script::Japanese => "Hiragino Sans, Yu Gothic, Meiryo, MS Gothic, Noto Sans CJK JP, Noto Sans JP, IPAGothic and TakaoGothic",+ Script::Georgian => "SF Georgian, Arial Unicode MS, Sylfaen, Noto Sans Georgian and DejaVu Sans", } } @@ -76,6 +80,11 @@ pub(crate) fn load(script: Script) -> Option<FontData> { y_offset_factor: (body_ascent - face_ascent) + 0.5 * (face_height - body_height), ..Default::default() };+ if matches!(script, Script::Georgian) {+ // Upper-case labels use Mtavruli only where this face draws it.+ let caps = FontRef::from_index(&bytes, index).is_ok_and(|f| ('\u{1c90}'..='\u{1cba}').all(|c| f.charmap().map(c).is_some()));+ crate::widgets::set_georgian_caps(caps);+ } let mut font = FontData::from_owned(bytes).tweak(tweak); font.index = index; return Some(font);@@ -123,6 +132,7 @@ fn candidates(script: Script) -> Vec<Candidate> { match script { Script::Chinese => chinese_candidates(), Script::Japanese => japanese_candidates(),+ Script::Georgian => georgian_candidates(), } } @@ -305,6 +315,50 @@ fn japanese_candidates() -> Vec<Candidate> { out } +/// Where each system keeps a Georgian font, best first. These faces are small (about 0.1–1 MB),+/// except Arial Unicode MS, which macOS ships too.+fn georgian_candidates() -> Vec<Candidate> {+ let mut out = Vec::new();+ let mut add = |path: PathBuf, family: &'static str| out.push(Candidate { path, family });+ #[cfg(target_os = "macos")]+ {+ // SF Georgian is the system UI face for Georgian; its family name starts with a dot, as+ // macOS's hidden UI faces do.+ add("/System/Library/Fonts/SFGeorgian.ttf".into(), ".SF Georgian");+ add("/System/Library/Fonts/Supplemental/Arial Unicode.ttf".into(), "Arial Unicode MS");+ add("/Library/Fonts/Arial Unicode.ttf".into(), "Arial Unicode MS");+ }+ #[cfg(target_os = "windows")]+ {+ let root = std::env::var_os("SystemRoot").map_or_else(|| PathBuf::from(r"C:\Windows"), PathBuf::from);+ add(root.join("Fonts").join("sylfaen.ttf"), "Sylfaen");+ }+ #[cfg(all(unix, not(target_os = "macos")))]+ {+ // Debian/Ubuntu, Arch, Fedora (static and variable) and FreeBSD's ports.+ for path in [+ "/usr/share/fonts/truetype/noto/NotoSansGeorgian-Regular.ttf",+ "/usr/share/fonts/noto/NotoSansGeorgian-Regular.ttf",+ "/usr/share/fonts/google-noto/NotoSansGeorgian-Regular.ttf",+ "/usr/share/fonts/google-noto-vf/NotoSansGeorgian[wght].ttf",+ "/usr/local/share/fonts/noto/NotoSansGeorgian-Regular.ttf",+ ] {+ add(path.into(), "Noto Sans Georgian");+ }+ // DejaVu Sans draws Georgian too, and most desktops have it.+ for path in [+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",+ "/usr/share/fonts/TTF/DejaVuSans.ttf",+ "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf",+ "/usr/share/fonts/dejavu/DejaVuSans.ttf",+ "/usr/local/share/fonts/dejavu/DejaVuSans.ttf",+ ] {+ add(path.into(), "DejaVu Sans");+ }+ }+ out+}+ /// `file` in macOS's downloaded font assets: /// `/System/Library/AssetsV2/com_apple_MobileAsset_Font*/*.asset/AssetData/<file>`. #[cfg(target_os = "macos")]@@ -7,17 +7,18 @@ use super::detect::system_lang; /// The writing system a language is drawn in, and so the system font it needs. Languages that /// share one can share a face; Simplified and Traditional Chinese do not (they want different-/// faces), so each script is its own. The bundled faces have no CJK glyphs, so a language with a-/// script borrows a system font for it (`crate::theme::cjk_font`).+/// faces), so each script is its own. The bundled faces have no CJK or Georgian glyphs, so a+/// language with a script borrows a system font for it (`crate::theme::cjk_font`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Script { Chinese, Japanese,+ Georgian, } impl Script { /// Every script a language can be written in.- pub const ALL: [Script; 2] = [Script::Chinese, Script::Japanese];+ pub const ALL: [Script; 3] = [Script::Chinese, Script::Japanese, Script::Georgian]; } /// One supported UI language.@@ -29,7 +30,8 @@ pub struct LangInfo { pub name: &'static str, /// The language's name in English, shown instead where its own script can't be drawn. pub english_name: &'static str,- /// The writing system this language is drawn in, or `None` if it needs no system CJK font.+ /// The writing system this language is drawn in, or `None` if it needs no system font+ /// (CJK, Georgian). pub script: Option<Script>, /// Catalog file contents (empty for the built-in English). pub source: &'static str,@@ -295,12 +297,12 @@ impl Lang { self.0.english_name } - /// Does drawing this language need a CJK font from the system?+ /// Does drawing this language need a font from the system (CJK, Georgian)? pub fn needs_cjk_font(self) -> bool { self.0.script.is_some() } - /// The writing system this language is drawn in, if it needs a system CJK font.+ /// The writing system this language is drawn in, if it needs a system font. pub fn script(self) -> Option<Script> { self.0.script }@@ -159,7 +159,7 @@ impl LauncherUi { if pref.needs_cjk_font() && !theme::cjk_font_available(pref) { // In English on purpose: the language asked for can't be drawn. let text = "No font for this language was found on this system, so the interface stays in English. \- Installing one (for example Noto Sans CJK) and restarting fixes it.";+ Installing one (for example Noto Sans CJK or Noto Sans Georgian) and restarting fixes it."; ui.add(egui::Label::new(RichText::new(text).size(12.0).color(t.warn)).wrap()); ui.add_space(14.0); }@@ -4,6 +4,7 @@ //! Upper-case text is drawn upper case but announced to screen readers as written. use std::sync::Arc;+use std::sync::atomic::{AtomicBool, Ordering}; use egui::{Color32, CornerRadius, Galley, Rect, Response, RichText, Sense, Stroke, Ui, vec2}; @@ -20,10 +21,37 @@ pub fn tone_color(t: &Tokens, tone: Tone) -> Color32 { } } +/// Whether the Georgian face in use draws Mtavruli (U+1C90–U+1CBF), the capitals Unicode maps+/// Georgian letters to. Set when the face loads; until then, and without one, Georgian stays as+/// written, since a face without them (DejaVu Sans, older Sylfaen) would draw boxes.+static GEORGIAN_CAPS: AtomicBool = AtomicBool::new(false);++#[cfg_attr(target_arch = "wasm32", allow(dead_code))]+pub(crate) fn set_georgian_caps(on: bool) {+ GEORGIAN_CAPS.store(on, Ordering::Relaxed);+}++/// `text` in upper case. Georgian goes to Mtavruli only when the face draws it.+fn upper(text: &str) -> String {+ upper_with(text, GEORGIAN_CAPS.load(Ordering::Relaxed))+}++/// [`upper`] with the face's Georgian capitals given rather than read from [`GEORGIAN_CAPS`].+fn upper_with(text: &str, georgian_caps: bool) -> String {+ text.chars().fold(String::with_capacity(text.len()), |mut out, c| {+ if !georgian_caps && ('\u{10d0}'..='\u{10ff}').contains(&c) {+ out.push(c);+ } else {+ out.extend(c.to_uppercase());+ }+ out+ })+}+ /// `text` laid out upper case in `family`, `spacing` points between letters. pub fn caps(ui: &Ui, text: &str, size: f32, family: egui::FontFamily, spacing: f32, color: Color32) -> Arc<Galley> { let format = egui::TextFormat { font_id: egui::FontId::new(size, family), color, extra_letter_spacing: spacing, ..Default::default() };- ui.painter().layout_job(egui::text::LayoutJob::single_section(text.to_uppercase(), format))+ ui.painter().layout_job(egui::text::LayoutJob::single_section(upper(text), format)) } /// A `.hud-label`: 11 px Geist Mono, upper case, widely spaced.@@ -60,7 +88,7 @@ pub fn hud_elided(ui: &mut Ui, text: &str, color: Color32, max_width: f32) -> Re extra_letter_spacing: theme::HUD_SPACING, ..Default::default() };- let mut job = egui::text::LayoutJob::single_section(text.to_uppercase(), format);+ let mut job = egui::text::LayoutJob::single_section(upper(text), format); job.wrap = egui::text::TextWrapping::truncate_at_width(max_width.min(ui.available_width()).max(0.0)); let galley = ui.painter().layout_job(job); let elided = galley.elided;@@ -764,4 +792,12 @@ mod tests { } } }++ #[test]+ fn upper_keeps_georgian_unless_the_face_has_capitals() {+ // The capability is passed in: `GEORGIAN_CAPS` is shared with tests that load a face.+ assert_eq!(super::upper_with("Update all (2)", false), "UPDATE ALL (2)");+ assert_eq!(super::upper_with("ყველას განახლება (2) · ArtCraft", false), "ყველას განახლება (2) · ARTCRAFT");+ assert_eq!(super::upper_with("ყველას განახლება · ArtCraft", true), "ᲧᲕᲔᲚᲐᲡ ᲒᲐᲜᲐᲮᲚᲔᲑᲐ · ARTCRAFT");+ } }