diff --git a/crates/fonts/build.rs b/crates/fonts/build.rs index 8f46dcc44..b807eb45c 100644 --- a/crates/fonts/build.rs +++ b/crates/fonts/build.rs @@ -3,10 +3,10 @@ //! empty, `CRAFT_FONTS` is empty and the build is unchanged. It only reads the local checkout //! (no network). A bad checkout is a warning, or an error with `CRAFT_FONTS_REQUIRED=1`. //! -//! On wasm32 it embeds only BIZ UDPGothic Regular, the `Arab` faces and the `Thai` Regular faces -//! (about 5.9 MB), to keep the web build within hosting limits (Cloudflare Pages: 25 MiB per -//! file). The web build has no system fonts, so these are its only fallback for Japanese, -//! Arabic-script and Thai text. +//! On wasm32 it embeds only BIZ UDPGothic Regular, the `Arab` and `Geor` faces and the `Thai` +//! Regular faces (about 6.1 MB), to keep the web build within hosting limits (Cloudflare Pages: +//! 25 MiB per file). The web build has no system fonts, so these are its only fallback for +//! Japanese, Arabic-script, Georgian and Thai text. use std::fmt::Write as _; use std::path::PathBuf; @@ -56,7 +56,7 @@ fn craft_fonts(dir: &std::path::Path) -> Result { continue; } let has = |script: &str| scripts.split(',').any(|s| s.trim() == script); - let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || has("Arab") || (has("Thai") && *style == "Regular"); + let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || has("Arab") || has("Geor") || (has("Thai") && *style == "Regular"); if wasm && !web_face { continue; } diff --git a/crates/fonts/src/craft_fonts.rs b/crates/fonts/src/craft_fonts.rs index 801b84027..3436d1d5f 100644 --- a/crates/fonts/src/craft_fonts.rs +++ b/crates/fonts/src/craft_fonts.rs @@ -1,8 +1,8 @@ //! Fonts from the optional craft-fonts build input (). //! //! Built with `CRAFT_FONTS_DIR=`, `build.rs` embeds the fonts listed in its -//! `fonts/manifest.txt` (web builds: only BIZ UDPGothic Regular, the Arabic faces and the Thai -//! Regular faces). Without it [`CRAFT_FONTS`] is empty and nothing changes. The font table ([`crate::ttf`]) +//! `fonts/manifest.txt` (web builds: only BIZ UDPGothic Regular, the Arabic and Georgian faces and +//! the Thai Regular faces). Without it [`CRAFT_FONTS`] is empty and nothing changes. The font table ([`crate::ttf`]) //! lists the embedded fonts with the installed ones, under [`CraftFont::stem`]. A text style //! can name one (`BIZ UDPGothic`), and they are fallbacks for characters a text's font lacks. //! They are copied out of the binary only when first used. @@ -85,6 +85,7 @@ mod tests { Some("Arab") => '\u{0628}', Some("Thai") => '\u{0E01}', Some("Hang" | "Kore") => '가', + Some("Geor") => 'ა', _ => 'A', }; assert!(face.is_some_and(|x| x.has_glyph(sample)), "{} lacks {sample}", f.family); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 438c06d97..ecd0c98f2 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -276,6 +276,7 @@ fn run_language_command(app: &mut CadApp, id: &str, params: &Value) -> Option "No Arabic font is installed, so the interface stays in English. Install one (Noto Sans Arabic, for example) to use Arabic.", + "ka" => "This build has no Georgian font, so the interface stays in English. Build with craft-fonts (its Geor face) to use Georgian.", _ => "No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese.", }); } diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 17409f3ed..48cd8ac67 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -314,7 +314,8 @@ pub fn mono() -> FontId { /// language: file, layout and layer names can be Japanese or Chinese whatever the interface /// shows (#513). In a Chinese interface (`language` "zh…") the Chinese font comes first, else a /// Japanese one, so names take their own glyph forms; in an Arabic interface an installed -/// Arabic font follows. A font embedded from craft-fonts comes +/// Arabic font follows. The embedded Georgian face loads in every language, so the language +/// picker can show ქართული. A font embedded from craft-fonts comes /// last, for systems without any (and the web build, which has no system fonts). A Korean /// interface adds a Hangul font only when the CJK font found doesn't cover Hangul /// ([`korean_font`]). @@ -482,7 +483,8 @@ pub fn shows_vietnamese(language: &str) -> bool { /// shows (#513), with the interface's own face leading. A sans-serif face reads best, and a /// Chinese interface takes the face with its own glyphs ahead of a Japanese one — a Japanese face /// shares most ideographs but lacks the simplified-only ones (图 园 测), which would be tofu. -/// العربية adds its Arabic face after those, egui's bundled faces carrying no Arabic glyphs. +/// The Georgian face follows in every language (egui's bundled faces carry no Georgian glyphs, and +/// the picker names the language ქართული); العربية adds its Arabic face after those. fn embedded_ui_faces<'a>(faces: &[&'a cadcraft_fonts::CraftFont], language: &str) -> Vec<&'a cadcraft_fonts::CraftFont> { let cjk = |f: &&cadcraft_fonts::CraftFont| f.style == "Regular" && f.scripts.iter().any(|s| matches!(*s, "Jpan" | "Hans" | "Hant")); let mut out: Vec<_> = faces.iter().copied().filter(cjk).collect(); @@ -490,6 +492,7 @@ fn embedded_ui_faces<'a>(faces: &[&'a cadcraft_fonts::CraftFont], language: &str if language.starts_with("zh") { out.sort_by_key(|f| !f.scripts.contains(&"Hans")); } + out.extend(faces.iter().copied().filter(|f| f.style == "Regular" && f.scripts.contains(&"Geor"))); if language == "ar" { out.extend(faces.iter().copied().filter(|f| f.style == "Regular" && f.scripts.contains(&"Arab"))); } @@ -728,12 +731,14 @@ pub fn shows_hangul(language: &str) -> bool { /// Whether the interface can show `language` without missing-glyph boxes: Chinese needs a font /// with its glyphs (installed, or the CJK face this build embedded from craft-fonts), Korean a /// font with Hangul, العربية an installed Arabic font or the embedded Arabic face, Tiếng Việt a -/// system interface font with its tone-marked letters ([`vietnamese_font`]). +/// system interface font with its tone-marked letters ([`vietnamese_font`]), ქართული the +/// embedded Georgian face. pub fn can_show(language: &str) -> bool { match language { "ko" => shows_hangul("ko"), "vi" => vietnamese_font().is_some(), "ar" => arabic_font().is_some() || embedded_script("Arab"), + "ka" => embedded_script("Geor"), l if l.starts_with("zh") => cjk_font().is_some() || embedded_script("Hans") || embedded_script("Hant"), _ => true, } @@ -843,13 +848,16 @@ mod tests { let japanese = face("BIZ UDPGothic", &["Jpan", "Latn"]); let chinese = face("Han Test Face", &["Hans", "Latn"]); let arabic = face("Noto Sans Arabic", &["Arab"]); - let faces = [&japanese, &chinese, &arabic]; + let georgian = face("Noto Sans Georgian", &["Geor"]); + let faces = [&japanese, &chinese, &arabic, &georgian]; // Every interface reads CJK names, so both CJK faces load, the sans-serif face first. - assert_eq!(families(&super::embedded_ui_faces(&faces, "en")), ["BIZ UDPGothic", "Han Test Face"]); + // The Georgian face loads in every interface too, after them: the picker shows ქართული. + assert_eq!(families(&super::embedded_ui_faces(&faces, "en")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian"]); // A Chinese interface takes the Han face first: a Japanese face lacks 图 园 测. - assert_eq!(families(&super::embedded_ui_faces(&faces, "zh")), ["Han Test Face", "BIZ UDPGothic"]); + assert_eq!(families(&super::embedded_ui_faces(&faces, "zh")), ["Han Test Face", "BIZ UDPGothic", "Noto Sans Georgian"]); // العربية adds the Arabic face, after the CJK ones egui's bundled faces have no glyphs for. - assert_eq!(families(&super::embedded_ui_faces(&faces, "ar")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Arabic"]); + assert_eq!(families(&super::embedded_ui_faces(&faces, "ar")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian", "Noto Sans Arabic"]); + assert_eq!(families(&super::embedded_ui_faces(&faces, "ka")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian"]); // Another weight is a style of a face, not an interface face of its own. let bold = CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Jpan"], bytes: &[] }; assert!(super::embedded_ui_faces(&[&bold], "en").is_empty()); @@ -860,13 +868,38 @@ mod tests { /// leaving the installed-font answer the gate had before. #[test] fn embedded_faces_answer_for_their_scripts() { - let faces = [face("BIZ UDPGothic", &["Jpan", "Latn"]), face("Noto Sans Arabic", &["Arab"])]; - assert!(super::any_script(&faces, "Jpan") && super::any_script(&faces, "Arab")); + let faces = [face("BIZ UDPGothic", &["Jpan", "Latn"]), face("Noto Sans Arabic", &["Arab"]), face("Noto Sans Georgian", &["Geor"])]; + assert!(super::any_script(&faces, "Jpan") && super::any_script(&faces, "Arab") && super::any_script(&faces, "Geor")); assert!(!super::any_script(&faces, "Hans") && !super::any_script(&faces, "Hant")); // A `Bold`-only face is not the interface's, and a script code is matched whole. let bold = CraftFont { family: "Some Han Bold", style: "Bold", scripts: &["Hans"], bytes: &[] }; assert!(!super::any_script(&[bold], "Hans") && !super::any_script(&faces, "Ara")); assert!(!super::embedded_script("Hans") || super::can_show("zh")); + // ქართული rides on the embedded Georgian face alone. + assert_eq!(super::can_show("ka"), super::embedded_script("Geor")); assert!(super::can_show("en") && super::can_show("uk")); } + + /// With the craft-fonts Georgian face (release and web builds), every Mkhedruli letter and the + /// picker's ქართული render in both families, in a Georgian and in an English interface. + #[test] + fn embedded_georgian_face_covers_the_alphabet_in_every_interface() { + if !super::embedded_script("Geor") { + return; + } + let letters: Vec = ('ა'..='ჰ').chain("ქართული".chars()).collect(); + for language in ["ka", "en"] { + let ctx = egui::Context::default(); + super::install_fonts(&ctx, language); + let mut output = ctx.run_ui(Default::default(), |_| {}); + output.textures_delta.clear(); + ctx.fonts_mut(|view| { + for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] { + for c in &letters { + assert!(view.has_glyph(&egui::FontId::new(12.0, family.clone()), *c), "{language}: missing Georgian glyph {c} in {family:?}"); + } + } + }); + } + } } diff --git a/docs/localization.md b/docs/localization.md index 998fbe0a0..4c9baef0c 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -101,6 +101,12 @@ catalog, the saved `ko` preference is kept but the interface falls back to Engli command reports that a font is needed, and the picker uses the readable label **Korean** wherever Hangul can't be shown. This change does not fix platform IME issues or localize engine output. +The bundled egui fonts have no Georgian letters. Georgian is drawn with the Noto Sans Georgian face +(`Geor`) of the craft-fonts build input, which release builds embed (`craft-fonts-ref` in the +workflows, and `web-craft-fonts` for the web build, whose embedded faces include it). The face loads +in every interface language, so Georgian text (file, layout and layer names, labels) draws. A build +without craft-fonts has no Georgian face. + ## Greek (`el`) interface The Greek translation covers all **1,330** distinct source keys collected from the existing