Merge pull request #1298 from vbasky/fix/1089-fedora-fonts

UI: Rescan Fonts in the font menu; assert the Linux system scan (#1089)
This commit is contained in:
BFlat
2026-10-30 07:03:10 +07:00
committed by GitHub
6 changed files with 109 additions and 1 deletions
+56 -1
View File
@@ -385,7 +385,8 @@ pub fn scan_system() -> usize {
}
/// Scan the system font folders again, adding fonts installed since the last scan (faces already
/// known are kept, so font ids stay valid). Returns the number of faces added.
/// known are kept, so font ids stay valid). Returns the number of faces added. This walks every
/// fontconfig directory, so the UI calls [`start_rescan`] instead of running it on the frame thread.
pub fn rescan_system() -> usize {
let _scan = SCAN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let n = if cfg!(target_arch = "wasm32") { 0 } else { add_source(&DirectorySource::system()) };
@@ -393,6 +394,60 @@ pub fn rescan_system() -> usize {
n
}
/// A rescan started by [`start_rescan`], if one is in flight or finished and not yet polled.
#[cfg(not(target_arch = "wasm32"))]
static RESCAN: std::sync::Mutex<Option<std::thread::JoinHandle<usize>>> = std::sync::Mutex::new(None);
/// Start [`rescan_system`] on a background thread. Returns false when a rescan is already
/// running, the thread could not be spawned, or this build has no system font folders (wasm).
pub fn start_rescan() -> bool {
#[cfg(target_arch = "wasm32")]
{
false
}
#[cfg(not(target_arch = "wasm32"))]
{
let mut slot = RESCAN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if slot.as_ref().is_some_and(|h| !h.is_finished()) {
return false;
}
match std::thread::Builder::new().name("font-rescan".into()).spawn(rescan_system) {
Ok(handle) => {
*slot = Some(handle);
true
}
Err(_) => false,
}
}
}
/// Whether a background rescan is still walking the font folders.
pub fn rescan_busy() -> bool {
#[cfg(target_arch = "wasm32")]
{
false
}
#[cfg(not(target_arch = "wasm32"))]
{
RESCAN.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref().is_some_and(|h| !h.is_finished())
}
}
/// Faces added by the latest background rescan, once that thread has finished. `None` while it
/// is still running or when nothing was started.
pub fn poll_rescan() -> Option<usize> {
#[cfg(target_arch = "wasm32")]
{
None
}
#[cfg(not(target_arch = "wasm32"))]
{
let mut slot = RESCAN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let finished = slot.as_ref().is_some_and(std::thread::JoinHandle::is_finished);
if finished { slot.take().and_then(|h| h.join().ok()) } else { None }
}
}
/// Start the system font scan on a background thread, so the font menus are complete by the
/// time someone opens them without the first open waiting for the scan. Native only; on wasm
/// (no threads, no font folders) this does nothing.
+2
View File
@@ -203,6 +203,8 @@ fn system_scan_finds_fonts() {
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");
// A minimum face count on Linux belongs with the native-fonts gate in #1591. A container
// that has fontconfig and only a handful of faces must still pass this portable test.
if cfg!(target_os = "macos") {
assert!(n > 20, "{n}");
let r = resolve("Helvetica", "Bold");
+3
View File
@@ -144,6 +144,9 @@ impl EffectcraftApp {
// commands handled here use the right catalog already.
let language = crate::i18n::language(self);
crate::i18n::set_current(language);
// #1089: a font-menu rescan runs off the UI thread. Poll it on the logic tick so the
// status and the menu update even when the window is not painting.
crate::panels::text_panels::poll_font_rescan(self, ctx);
if self.styled_language != language {
self.want_paint = true;
}
+7
View File
@@ -434,6 +434,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", "ネストされたコンポジションはありません"),
@@ -1167,6 +1168,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", "没有嵌套合成"),
@@ -1951,6 +1953,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", "沒有嵌套合成"),
@@ -2695,6 +2698,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", "Немає вкладених композицій"),
@@ -3230,6 +3234,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"),
@@ -3991,6 +3996,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í"),
@@ -4575,6 +4581,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"),
+20
View File
@@ -654,6 +654,17 @@ struct FontSearch {
}
/// Shared font menu for Character and Properties: search, recent fonts and previews.
/// Finish a font rescan started from the font menu and report how many faces it added.
/// While the scan is still walking font folders, ask for another frame so the menu updates.
pub(crate) fn poll_font_rescan(app: &mut EffectcraftApp, ctx: &egui::Context) {
if effectcraft_engine::text::fonts::rescan_busy() {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
if let Some(n) = effectcraft_engine::text::fonts::poll_rescan() {
app.ui.status = format!("{} +{n}", tr("Rescan Fonts"));
}
}
pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, rect: Rect, current: &str, auto: &str, enabled: bool) -> Option<String> {
let state_id = id.with("search-state");
app.auto.add(auto, rect, "Font family");
@@ -724,6 +735,15 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui:
let boxed = widgets::checkbox(ui, box_rect, only_variable, &t, id.with("variable-check"));
ui.painter().text(label_rect.left_center(), Align2::LEFT_CENTER, tr("Variable fonts only"), Tokens::ui(11.5), t.text);
app_ref.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only");
// #1089: fonts installed after launch appear without restarting. The button is its own
// row: the popup is only 180 px wide, so a control pinned to the filter row covers
// "Variable fonts only". The scan itself runs off the UI thread (`poll_font_rescan`).
let rescan_h = 22.0;
let (resc, _) = ui.allocate_exact_size(vec2(ui.available_width(), rescan_h), egui::Sense::hover());
if widgets::text_button(ui, resc, tr("Rescan Fonts"), false, &t, id.with("rescan")).clicked() && effectcraft_engine::text::fonts::start_rescan() {
app_ref.ui.status = format!("{}…", tr("Rescan Fonts"));
}
app_ref.auto.add(&format!("{auto}.rescan"), resc, "Rescan Fonts");
let toggled = row.clicked() || boxed.clicked();
if toggled {
only_variable = !only_variable;
+21
View File
@@ -256,3 +256,24 @@ fn font_menu_grows_back_after_an_empty_search() {
click(&mut h, "character.font.option.JetBrains Mono");
assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().font, "JetBrains Mono", "a click on a row picks the font");
}
/// #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<_>>());
}