mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 19:26:46 -06:00
UI: installed CJK faces are read once, without waiting for the font scan
- install_fonts no longer waits for the system font catalog on the first frame: the built-in faces are installed at once, the scan runs in the background, and the installed CJK faces are added when it is done. - The installed faces are read once and shared: changing the interface language reorders them without copying the font files into egui again. - fonts: line_metrics no longer sits between feature's doc comment and feature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a62188aa9b
commit
70cfd2d4fa
@@ -881,6 +881,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.
|
||||
|
||||
@@ -80,7 +80,6 @@ pub struct ShapedGlyph {
|
||||
pub rtl: bool,
|
||||
}
|
||||
|
||||
/// An OpenType feature setting: `"liga"`, `"-kern"`, `"ss01"`.
|
||||
/// Line metrics of the font at `index` of `data`, in font units: (units per em, ascent, descent
|
||||
/// (negative below the baseline), line gap) — what text layouts such as egui's build lines from.
|
||||
/// `None` if the font can't be read.
|
||||
@@ -91,6 +90,7 @@ pub fn line_metrics(data: &[u8], index: u32) -> Option<(f32, f32, f32, f32)> {
|
||||
Some((f32::from(m.units_per_em.max(1)), m.ascent, m.descent, m.leading))
|
||||
}
|
||||
|
||||
/// An OpenType feature setting: `"liga"`, `"-kern"`, `"ss01"`.
|
||||
pub fn feature(tag: &str) -> Option<Feature> {
|
||||
let (on, t) = match tag.strip_prefix('-') {
|
||||
Some(r) => (false, r),
|
||||
|
||||
@@ -474,6 +474,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,
|
||||
pub integrated_titlebar: bool,
|
||||
@@ -514,6 +517,7 @@ impl DesignApp {
|
||||
shot_token: 0,
|
||||
styled: false,
|
||||
fonts_lang: String::new(),
|
||||
fonts_system: false,
|
||||
restyle: false,
|
||||
fonts_ready: false,
|
||||
integrated_titlebar: false,
|
||||
@@ -726,17 +730,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;
|
||||
|
||||
+55
-36
@@ -254,11 +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) {
|
||||
/// 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 mut defs = font_definitions(craft, lang);
|
||||
add_system_fallbacks(&mut defs, &system_ui_fonts(craft), 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.
|
||||
@@ -266,8 +278,16 @@ pub(crate) struct SystemFont {
|
||||
/// "Jpan", "Hans", "Hant" or "Kore".
|
||||
script: &'static str,
|
||||
family: String,
|
||||
bytes: Vec<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
|
||||
@@ -299,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: 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).
|
||||
@@ -354,10 +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 { index: f.index, tweak: baseline_tweak(&f.bytes, f.index), ..FontData::from_owned(f.bytes.clone()) }),
|
||||
);
|
||||
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();
|
||||
@@ -533,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, 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() {
|
||||
@@ -575,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(),
|
||||
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");
|
||||
@@ -601,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(),
|
||||
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);
|
||||
@@ -618,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