UI: rescan system fonts from the font menu; Linux scan smoke test (#1089)

Fonts installed after EffectCraft launched (dnf, GNOME Font Viewer, a
drop into ~/.local/share/fonts) stayed invisible until restart: only the
startup background scan ran, and the report in #1089 could not tell
discovery gaps apart from a stale scan.

- The font menu's filter row gains a Rescan Fonts button (Character and
  Properties panels share the popup) that runs the existing
  text.fonts {rescan} command and reports the number added in the status
  line. The press doesn't flip the Variable-fonts filter (the button is
  above the row's click area and closes the popup on top of it).
- system_scan_finds_fonts now asserts, on a native fontconfig desktop
  (any machine with /etc/fonts, which includes the Fedora reporter's and
  the CI runner), that the scan lists installed families rather than
  silently returning only the bundled fonts: 870 faces in ~10 ms on an
  Arch box.

Tests: ui_font_search 4 passed (new rescan test drives the real popup);
effectcraft-text lib system scan green; fmt and clippy --all-targets -D
warnings clean. Translations for 'Rescan Fonts' in all six catalogs.
This commit is contained in:
Vikram Bhaskaran
2026-10-16 22:20:10 +11:00
parent 19d19a339e
commit 7b3d3455cb
4 changed files with 44 additions and 0 deletions
+5
View File
@@ -943,6 +943,11 @@ mod tests {
let n = all_faces().iter().filter(|f| f.info.origin == "system").count();
eprintln!("system fonts: {n} faces in {:?}", t.elapsed());
assert_eq!(scan_system(), 0, "scans once");
if cfg!(all(unix, not(target_os = "macos"), not(target_arch = "wasm32"))) && std::path::Path::new("/etc/fonts").exists() {
// #1089: a fontconfig desktop (Fedora, Arch, Ubuntu, the CI runner): the scan must
// list the installed system families, not just the bundled three.
assert!(n > 20, "fontconfig system fonts missing: {n}");
}
if cfg!(target_os = "macos") {
assert!(n > 20, "{n}");
let r = resolve("Helvetica", "Bold");
+7
View File
@@ -351,6 +351,7 @@ const JAPANESE: &[(&str, &str)] = &[
("No layer selected", "レイヤーが選択されていません"),
("No matching effects or presets", "一致するエフェクトまたはプリセットはありません"),
("No matching fonts", "一致するフォントはありません"),
("Rescan Fonts", "フォントを再スキャン"),
("Variable fonts only", "可変フォントのみ"),
("No model credits were built into this copy.", "このビルドにはモデルのクレジットが含まれていません。"),
("No nested compositions", "ネストされたコンポジションはありません"),
@@ -893,6 +894,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("No layer selected", "未选择图层"),
("No matching effects or presets", "没有匹配的效果或预设"),
("No matching fonts", "没有匹配的字体"),
("Rescan Fonts", "重新扫描字体"),
("Variable fonts only", "仅可变字体"),
("No model credits were built into this copy.", "此副本未内置模型致谢。"),
("No nested compositions", "没有嵌套合成"),
@@ -1626,6 +1628,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("No layer selected", "未選取圖層"),
("No matching effects or presets", "沒有匹配的效果或預設集"),
("No matching fonts", "沒有符合的字體"),
("Rescan Fonts", "重新掃描字型"),
("Variable fonts only", "僅可變字型"),
("No model credits were built into this copy.", "此副本未內置模型致謝。"),
("No nested compositions", "沒有嵌套合成"),
@@ -2368,6 +2371,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
("No layer selected", "Шар не виділено"),
("No matching effects or presets", "Не знайдено відповідних ефектів або наборів"),
("No matching fonts", "Не знайдено відповідних шрифтів"),
("Rescan Fonts", "Повторно сканувати шрифти"),
("Variable fonts only", "Лише змінні шрифти"),
("No model credits were built into this copy.", "Ця копія не містить відомостей про моделі."),
("No nested compositions", "Немає вкладених композицій"),
@@ -2895,6 +2899,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
("New", "Novo"),
("New Comp from Selection", "Nova composição a partir da seleção"),
("Replace with Precomposition", "Substituir por pré-composição"),
("Rescan Fonts", "Reanalisar fontes"),
("used once", "usado uma vez"),
("used {} times", "usado {} vezes"),
("Compositions that use this item", "Composições que usam este item"),
@@ -3349,6 +3354,7 @@ const CZECH: &[(&str, &str)] = &[
("New", "Nový"),
("New Comp from Selection", "Nová kompozice z výběru"),
("Replace with Precomposition", "Nahradit předkompozicí"),
("Rescan Fonts", "Znovu nalézt písma"),
("used once", "použito jednou"),
("used {} times", "použito {}×"),
("Compositions that use this item", "Kompozice, které tuto položku používají"),
@@ -3926,6 +3932,7 @@ const SPANISH: &[(&str, &str)] = &[
("No layer selected", "No hay ninguna capa seleccionada"),
("No matching effects or presets", "No hay efectos ni ajustes preestablecidos que coincidan"),
("No matching fonts", "No hay fuentes coincidentes"),
("Rescan Fonts", "Volver a analizar fuentes"),
("Variable fonts only", "Solo fuentes variables"),
("No model credits were built into this copy.", "No se incluyeron créditos de modelos en esta copia."),
("No nested compositions", "No hay composiciones anidadas"),
+11
View File
@@ -642,6 +642,17 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui:
// Typing goes on into the search field.
ui.memory_mut(|m| m.request_focus(search_id));
}
// #1089: fonts installed after launch (dnf, GNOME Fonts, a drop into ~/.fonts) appear
// without restarting EffectCraft: the system scan also runs in the background at
// startup, this re-runs it on demand.
let resc = Rect::from_min_size(pos2(row.rect.max.x - 124.0, row.rect.min.y + 2.0), vec2(120.0, filter_h - 4.0));
if widgets::text_button(ui, resc, tr("Rescan Fonts"), false, &t, id.with("rescan")).clicked() {
match app.session.execute("text.fonts", json!({"rescan": true})) {
Ok(v) => app.ui.status = format!("{} +{}", tr("Rescan Fonts"), v.get("added").and_then(serde_json::Value::as_u64).unwrap_or(0)),
Err(e) => app.ui.status = e.to_string(),
}
}
app.auto.add(&format!("{auto}.rescan"), resc, "Rescan Fonts");
let query = state.query.trim().to_lowercase();
let mut seen = std::collections::HashSet::new();
let filtered: Vec<_> = rows
+21
View File
@@ -176,3 +176,24 @@ fn font_menu_filters_variable_fonts() {
key(&mut h, Key::Escape);
assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().font, "Inter", "filtering changes nothing");
}
/// #1089: the font menu can re-run the system scan without restarting the app, and the press
/// does not also flip the "Variable fonts only" filter.
#[test]
fn font_menu_rescan_button_adds_fonts_installed_later() {
let mut s = Session::default();
s.execute("comp.new", json!({"width": 320, "height": 180})).unwrap();
s.execute("layer.newText", json!({"text": "Keep this text", "font": "Inter"})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(move |_| EffectcraftApp::new(s));
h.state_mut().show_panel(PanelKind::Character);
h.run_steps(3);
click(&mut h, "character.font");
h.run_steps(3);
assert!(h.state().auto.find("character.font.rescan").is_some(), "the filter row offers a rescan");
click(&mut h, "character.font.rescan");
assert!(h.state().ui.status.contains("Rescan Fonts"), "the status reports the rescan: {}", h.state().ui.status);
// The popup stays open, the variable filter did not flip, and the list still has the bundled families.
assert!(h.state().auto.find("character.font.search").is_some(), "the popup stays open");
let rows = h.state().auto.query("character.font.option.");
assert!(rows.iter().any(|r| r.label == "Inter"), "{:?}", rows.iter().map(|r| &r.label).collect::<Vec<_>>());
}