mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 05:36:46 -06:00
Merge #394 update: UI: CJK interface text from installed fonts, on the Latin baseline
# Conflicts: # crates/ui-egui/src/theme.rs
This commit is contained in:
@@ -1010,6 +1010,12 @@ impl FontDb {
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether the installed fonts are cataloged: a lookup by family name won't wait for the
|
||||
/// scan (always true on wasm, which has none). See [`FontDb::scan_in_background`].
|
||||
pub fn system_fonts_cataloged(&self) -> bool {
|
||||
cfg!(target_arch = "wasm32") || self.cataloged.get().is_some()
|
||||
}
|
||||
|
||||
/// Catalog the installed fonts on a background thread, so the first lookup by family name
|
||||
/// (opening a file, the font menus) doesn't wait for the scan. A no-op once they are
|
||||
/// cataloged, and on wasm.
|
||||
|
||||
@@ -492,6 +492,9 @@ pub struct DesignApp {
|
||||
styled: bool,
|
||||
/// The interface language the UI fonts were installed for (it orders the CJK fallbacks).
|
||||
fonts_lang: String,
|
||||
/// The installed CJK faces are among the UI fonts (false while the system fonts are still
|
||||
/// being cataloged: the fonts are installed again when they are).
|
||||
fonts_system: bool,
|
||||
pub restyle: bool,
|
||||
fonts_ready: bool,
|
||||
/// Files the operating system asked to open (macOS Finder, Dock, Open With), opened once the
|
||||
@@ -551,6 +554,7 @@ impl DesignApp {
|
||||
shot_token: 0,
|
||||
styled: false,
|
||||
fonts_lang: String::new(),
|
||||
fonts_system: false,
|
||||
restyle: false,
|
||||
fonts_ready: false,
|
||||
os_documents: Vec::new(),
|
||||
@@ -888,17 +892,21 @@ impl DesignApp {
|
||||
ctx.set_zoom_factor(scale);
|
||||
}
|
||||
if !self.styled {
|
||||
theme::install_fonts(ctx, &self.ui.language);
|
||||
self.fonts_system = theme::install_fonts(ctx, &self.ui.language);
|
||||
self.fonts_lang = self.ui.language.clone();
|
||||
self.styled = true;
|
||||
self.restyle = true;
|
||||
} else {
|
||||
if self.fonts_lang != self.ui.language {
|
||||
theme::install_fonts(ctx, &self.ui.language);
|
||||
if self.fonts_lang != self.ui.language || (!self.fonts_system && theme::system_fonts_ready()) {
|
||||
self.fonts_system = theme::install_fonts(ctx, &self.ui.language);
|
||||
self.fonts_lang = self.ui.language.clone();
|
||||
}
|
||||
self.fonts_ready = true;
|
||||
}
|
||||
if !self.fonts_system {
|
||||
// The font scan runs in the background: look again shortly.
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(250));
|
||||
}
|
||||
if self.restyle {
|
||||
theme::apply(ctx, &theme::Tokens::for_brightness(self.ui.brightness));
|
||||
self.restyle = false;
|
||||
|
||||
+56
-45
@@ -1,7 +1,6 @@
|
||||
//! Design tokens (InDesign's four interface brightness levels), fonts and egui style.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Color32, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Stroke, TextStyle, Visuals};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -255,21 +254,23 @@ impl Tokens {
|
||||
}
|
||||
|
||||
/// Install the UI fonts for the interface language `lang` (it orders the CJK fallbacks).
|
||||
pub fn install_fonts(ctx: &egui::Context, lang: &str) {
|
||||
// Installed faces are read once per process, so a language change only reorders them. Their
|
||||
// bytes are leaked once and borrowed by every `FontData` (`set_fonts` takes the definitions
|
||||
// by value on each call).
|
||||
static SYSTEM: OnceLock<Vec<SystemFont>> = OnceLock::new();
|
||||
/// Returns false while the installed fonts are still being cataloged: the built-in faces are
|
||||
/// installed without waiting, and the caller installs again once they are (see
|
||||
/// [`system_fonts_ready`]) to add the installed CJK faces.
|
||||
pub fn install_fonts(ctx: &egui::Context, lang: &str) -> bool {
|
||||
let craft = designcraft_fonts::CRAFT_FONTS;
|
||||
let system = SYSTEM.get_or_init(|| {
|
||||
system_ui_fonts(craft)
|
||||
.into_iter()
|
||||
.map(|f| SystemFont { bytes: Cow::Borrowed(Box::leak(f.bytes.into_owned().into_boxed_slice())), ..f })
|
||||
.collect()
|
||||
});
|
||||
let mut defs = font_definitions(craft, lang);
|
||||
add_system_fallbacks(&mut defs, system, lang);
|
||||
let system = system_ui_fonts(craft);
|
||||
if let Some(system) = system {
|
||||
add_system_fallbacks(&mut defs, system, lang);
|
||||
}
|
||||
ctx.set_fonts(defs);
|
||||
system.is_some()
|
||||
}
|
||||
|
||||
/// Whether [`install_fonts`] would find the installed fonts cataloged.
|
||||
pub fn system_fonts_ready() -> bool {
|
||||
designcraft_fonts::FontDb::global().system_fonts_cataloged()
|
||||
}
|
||||
|
||||
/// An installed font the UI falls back to for a script the built-in faces lack.
|
||||
@@ -277,8 +278,16 @@ pub(crate) struct SystemFont {
|
||||
/// "Jpan", "Hans", "Hant" or "Kore".
|
||||
script: &'static str,
|
||||
family: String,
|
||||
bytes: Cow<'static, [u8]>,
|
||||
index: u32,
|
||||
/// The face as egui takes it, on the UI face's baseline. Shared: installing the fonts again
|
||||
/// (another interface language) doesn't copy the file.
|
||||
data: Arc<FontData>,
|
||||
}
|
||||
|
||||
impl SystemFont {
|
||||
pub(crate) fn new(script: &'static str, family: String, bytes: Vec<u8>, index: u32) -> Self {
|
||||
let tweak = baseline_tweak(&bytes, index);
|
||||
SystemFont { script, family, data: Arc::new(FontData { index, tweak, ..FontData::from_owned(bytes) }) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Installed UI faces (sans serif) per script, the first one found of each list: macOS, then
|
||||
@@ -310,23 +319,29 @@ const SYSTEM_UI_FONTS: &[(&str, &[&str])] = &[
|
||||
/// the interface shows Japanese and Chinese without the craft-fonts build input, and Korean and
|
||||
/// Traditional Chinese (font names in the font menus) in any build. egui can't
|
||||
/// find system fonts itself; the document font scan (not on the web) does.
|
||||
fn system_ui_fonts(craft: &[designcraft_fonts::CraftFont]) -> Vec<SystemFont> {
|
||||
fn system_ui_fonts(craft: &[designcraft_fonts::CraftFont]) -> Option<&'static [SystemFont]> {
|
||||
static FACES: std::sync::OnceLock<Vec<SystemFont>> = std::sync::OnceLock::new();
|
||||
let db = designcraft_fonts::FontDb::global();
|
||||
SYSTEM_UI_FONTS
|
||||
.iter()
|
||||
.filter(|(script, _)| !craft.iter().any(|f| f.scripts.contains(script)))
|
||||
.filter_map(|(script, families)| {
|
||||
let family = families.iter().find(|f| db.has_family(f))?;
|
||||
let face = db.face(family, "Regular");
|
||||
// `face` falls back to another family when this one can't be read: keep only the real one.
|
||||
(face.family.eq_ignore_ascii_case(family) && !face.data().is_empty()).then(|| SystemFont {
|
||||
script,
|
||||
family: (*family).to_string(),
|
||||
bytes: Cow::Owned(face.data().to_vec()),
|
||||
index: face.index(),
|
||||
// Never wait for the font scan here (the first frame): it runs in the background, and the
|
||||
// faces are read once, when it is done.
|
||||
// (Tests wait instead, so that the fonts don't change in the middle of one.)
|
||||
if !db.system_fonts_cataloged() && !cfg!(test) {
|
||||
db.scan_in_background();
|
||||
return None;
|
||||
}
|
||||
Some(FACES.get_or_init(|| {
|
||||
SYSTEM_UI_FONTS
|
||||
.iter()
|
||||
.filter(|(script, _)| !craft.iter().any(|f| f.scripts.contains(script)))
|
||||
.filter_map(|(script, families)| {
|
||||
let family = families.iter().find(|f| db.has_family(f))?;
|
||||
let face = db.face(family, "Regular");
|
||||
// `face` falls back to another family when this one can't be read: keep only the real one.
|
||||
(face.family.eq_ignore_ascii_case(family) && !face.data().is_empty())
|
||||
.then(|| SystemFont::new(script, (*family).to_string(), face.data().to_vec(), face.index()))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.collect()
|
||||
}))
|
||||
}
|
||||
|
||||
/// The UI face every family starts with (Source Sans 3; the semibold shares its line metrics).
|
||||
@@ -365,7 +380,7 @@ fn cjk_script(lang: &str) -> &'static str {
|
||||
pub(crate) fn add_system_fallbacks(fonts: &mut FontDefinitions, system: &[SystemFont], lang: &str) {
|
||||
let name = |f: &SystemFont| format!("system-{}", f.family);
|
||||
for f in system {
|
||||
fonts.font_data.insert(name(f), Arc::new(FontData { font: f.bytes.clone(), index: f.index, tweak: baseline_tweak(&f.bytes, f.index) }));
|
||||
fonts.font_data.insert(name(f), f.data.clone());
|
||||
}
|
||||
let first = cjk_script(lang);
|
||||
let preferred: Vec<String> = system.iter().filter(|f| f.script == first).map(name).collect();
|
||||
@@ -541,7 +556,7 @@ mod japanese_font_tests {
|
||||
#[test]
|
||||
fn installed_fonts_give_japanese_glyphs_without_craft_fonts() {
|
||||
let bytes = designcraft_fonts::testing::font_with("DC UI Japanese", &['日', '本', '語']).unwrap();
|
||||
let system = [super::SystemFont { script: "Jpan", family: "DC UI Japanese".into(), bytes: bytes.into(), index: 0 }];
|
||||
let system = [super::SystemFont::new("Jpan", "DC UI Japanese".into(), bytes, 0)];
|
||||
let mut defs = super::font_definitions(&[], "ja");
|
||||
super::add_system_fallbacks(&mut defs, &system, "ja");
|
||||
for family in families() {
|
||||
@@ -583,11 +598,8 @@ mod japanese_font_tests {
|
||||
/// from an installed Korean face, after the Japanese and Chinese ones.
|
||||
#[test]
|
||||
fn installed_korean_faces_give_hangul_glyphs() {
|
||||
let face = |family: &str, chars: &[char], script| super::SystemFont {
|
||||
script,
|
||||
family: family.into(),
|
||||
bytes: designcraft_fonts::testing::font_with(family, chars).unwrap().into(),
|
||||
index: 0,
|
||||
let face = |family: &str, chars: &[char], script| {
|
||||
super::SystemFont::new(script, family.into(), designcraft_fonts::testing::font_with(family, chars).unwrap(), 0)
|
||||
};
|
||||
let system = [face("DC UI Korean", &['한', '글'], "Kore"), face("DC UI Japanese", &['日'], "Jpan")];
|
||||
let mut defs = super::font_definitions(&[], "ja");
|
||||
@@ -609,12 +621,8 @@ mod japanese_font_tests {
|
||||
/// before the built-in CJK faces and the other scripts' faces.
|
||||
#[test]
|
||||
fn interface_language_orders_the_cjk_faces() {
|
||||
let face = |family: &str, script| super::SystemFont {
|
||||
script,
|
||||
family: family.into(),
|
||||
bytes: designcraft_fonts::testing::font_with(family, &['日']).unwrap().into(),
|
||||
index: 0,
|
||||
};
|
||||
let face =
|
||||
|family: &str, script| super::SystemFont::new(script, family.into(), designcraft_fonts::testing::font_with(family, &['日']).unwrap(), 0);
|
||||
let system = [face("DC Ja", "Jpan"), face("DC Hans", "Hans"), face("DC Hant", "Hant"), face("DC Ko", "Kore")];
|
||||
for (lang, first) in [("ja", "DC Ja"), ("zh", "DC Hans"), ("zh-hant", "DC Hant"), ("ko", "DC Ko"), ("", "DC Ja")] {
|
||||
let mut defs = super::font_definitions(&[], lang);
|
||||
@@ -626,7 +634,10 @@ mod japanese_font_tests {
|
||||
let first_at = at(&format!("system-{first}"));
|
||||
assert!(first_at < at("craft-Stand-in"), "{lang}: {stack:?}");
|
||||
for f in &system {
|
||||
assert!(first_at <= at(&format!("system-{}", f.family)), "{lang}: {stack:?}");
|
||||
let name = format!("system-{}", f.family);
|
||||
assert!(first_at <= at(&name), "{lang}: {stack:?}");
|
||||
// Another language shares the face's data instead of copying the font file.
|
||||
assert!(std::sync::Arc::ptr_eq(&defs.font_data[&name], &f.data), "{lang}: {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user