mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
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:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<_>>());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user