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:
con3code
2026-10-11 08:30:39 +09:00
co-authored by Claude Opus 5.5
parent a62188aa9b
commit 70cfd2d4fa
4 changed files with 73 additions and 40 deletions
+6
View File
@@ -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.
+1 -1
View File
@@ -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),
+11 -3
View File
@@ -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
View File
@@ -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}");
}
}
}