mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 10:56:48 -06:00
M7: missing fonts window only after a file opens; wait for the font scan (#752)
From review: - The check ran after any file.open/openRecent/import result, so a pasted or dropped image, reopening a file that was already open, or Clear recent brought the window back. It now runs only for results with a `summary` (a file opened, or pages imported), and never in the browser. - Text > Missing fonts... before the startup scan finished listed installed fonts as missing. It now says it is checking the installed fonts and lists them once that is done (`figcraft_text::installed_fonts_known`, also true when system fonts are opted out, where checks used to wait forever). - ROADMAP progress log and file-format-parity status line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cf6bd4eeb2
commit
db43dc5a4a
@@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-17** · Missing fonts (M7, #752): fonts a file names that aren't installed (text layers, styled ranges, text styles, font family variables) are listed in a Missing fonts window after the user opens the file (once installed fonts are indexed and any import report is closed), from Text ▸ Missing fonts…, by `text.missingFonts` and by `figcraft-cli info`. Their text still draws in a fallback font. Numbers unchanged.
|
||||
- **2026-10-17** · Control channel while the window is minimised (M8, #720): a minimised or covered window answers every request that acts on the app's state (edits, `ui.inspect`, `ui.pointer`, `ui.key`, `ui.type`, `ui.set`, `app.quit`…) without drawing, and what needs a drawn frame (`ui.screenshot`, `ui.input`, `window.new`) is an error at once; before, those waited up to 60 s and held up every later request on the connection (an attached agent's `inspect_ui` or window screenshot stalled until the window came back). Measured on Windows 11, minimised: requests ~100 ms each (eframe wakes a minimised window at most ten times a second; ~2 ms on screen), a window screenshot fails in ~100 ms instead of 60 s, idle CPU 0. Numbers unchanged.
|
||||
- **2026-10-17** · Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged.
|
||||
- **2026-10-17** · Frames and sections convert into each other (M1, #719): `object.convertToSection` turns a frame on the page or in a section into a section in place (same box, name, fill and layers, which stay where they are; what only a frame has goes: auto layout, clipping, layout grids, guides, scrolling), and `object.convertToFrame` now also turns a section into a frame that keeps its fill and doesn't clip, so the look stays. Nested frames, instances, main components and sections holding sections are refused with the reason; one undo step each. Both are in the canvas context menu (Convert to section / Convert to frame) and in Spanish. Numbers unchanged.
|
||||
@@ -317,6 +318,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-17 | minor | Community batch #753 (10 PRs): Selection colors for mixed selections (#403); smart-selection spacing fields and canvas handles (#402); a Present tab with device and scale controls (#537); adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys to frames only and warns about font styles a family lacks (#603); canvas images on whole device pixels (#359); Adwaita window controls on Wayland without server decorations (#592, not run on GNOME or KDE by the maintainers); the keyboard shortcuts panel closes on the home screen (#548); macOS paste modifiers in tests (#750); credits column tests (#347). `cargo xtask parity` re-run: 212 / 212 menu items (100%; the catalog gained View ▸ Full screen with #227 on 2026-10-14), 561 commands. Language table re-measured from the catalogs (2,272 entries): the drafts read 95–99% (strings added since 2026-10-12 show English), Indonesian has a draft (99%; it read none), and Russian, Swedish (its command labels from #599), Turkish and Czech are listed with their counts. Per-area parity rows for the day's landings in `docs/gaps.md`, `target-app-parity.md` and the area docs. Numbers unchanged |
|
||||
| 2026-10-17 | minor | Missing fonts (M7, #752): fonts a file names that aren't installed are listed after the user opens it, from Text ▸ Missing fonts…, by `text.missingFonts` and by `figcraft-cli info`. Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Control channel while the window is minimised (M8, #720): a minimised or covered window answers every request that acts on the app's state (edits, `ui.inspect`, `ui.pointer`, `ui.key`, `ui.type`, `ui.set`, `app.quit`…) without drawing, and what needs a drawn frame (`ui.screenshot`, `ui.input`, `window.new`) is an error at once; before, those waited up to 60 s and held up every later request on the connection (an attached agent's `inspect_ui` or window screenshot stalled until the window came back). Measured on Windows 11, minimised: requests ~100 ms each (eframe wakes a minimised window at most ten times a second; ~2 ms on screen), a window screenshot fails in ~100 ms instead of 60 s, idle CPU 0. Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Community fixes from #524 up (batches #727, #728, #733, #735, #736 and #729; 55 PRs) and a crash guard. `.fig` import: explicit instance identity, overrides by key, text style precedence, grid track sizes, slot content, connector labels, skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments and embedded images; large files open within budgets from the file's own size; an import report after opening; imported bytes stay out of the command journal. Files and paste: save a copy as `.fig`; Cmd/Ctrl+V pastes images copied in other apps and layers copied from another design app (`edit.pasteDesignLayers`); paste over and paste properties; clipboard reads on Linux and the BSDs time out after 2 s. Editor: live font preview on hover; Shift+I and Resources open Assets; Shift-drag moves the selection; thin strokes take a nearby double-click; dropped points merge; Flatten keeps open paths; the Actions palette gets tabs, ranked search, a keyboard highlight and recents; the colour picker opens on a new fill or stroke; text strokes follow their alignment; selection highlights after line breaks; a draggable pages/layers divider; variant properties reorder by drag; submenus keep their hover with a safe triangle; menu actions that need a value ask for it; File ▸ Rename edits the name in place. Engine: bad paint indexes, non-finite geometry and pages in copy/cut are refused; a param naming a layer says so when it isn't one; a CLI script is one undo step and resolves `$step`. Server and collaboration: message limits follow `max_file_mb` (client capped at 1 GiB); a damaged generation field no longer drops flushed edits; the web build joins a session from a `#join=` link. Also Swedish command labels, Nix CI through the shared workflow, paint bounds once per render, and a guard that turns a panic inside the boolean-op library into bad input (the stroke draws unclipped). Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged. |
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct Missing {
|
||||
|
||||
/// The families `doc` names that the font database doesn't have, by name. On the desktop the
|
||||
/// installed fonts are indexed in the background: ask once that's done
|
||||
/// (`figcraft_text::system_fonts_scanned`), or every installed family counts as missing.
|
||||
/// (`figcraft_text::installed_fonts_known`), or every installed family counts as missing.
|
||||
pub fn missing(doc: &Document) -> Vec<Missing> {
|
||||
// Lower-case family → (spelling, layers).
|
||||
let mut used: BTreeMap<String, (String, usize)> = BTreeMap::new();
|
||||
|
||||
@@ -112,7 +112,10 @@ fn blank_families_and_aliases_are_handled() {
|
||||
doc.get_mut(t).unwrap().bound.insert("fontFamily".into(), "V:alias".into());
|
||||
let other = text(&mut s, "FigCraft Missing G");
|
||||
s.doc_mut().unwrap().get_mut(other).unwrap().bound.insert("fontFamily".into(), "V:cycle".into());
|
||||
// The bound layer takes the value its alias resolves to; the cycle resolves to nothing.
|
||||
// The bound layer takes the value its alias resolves to (bindings are applied after the next
|
||||
// command); the cycle resolves to nothing.
|
||||
let NodeKind::Text(bound) = &s.doc().unwrap().node(t).unwrap().kind else { panic!() };
|
||||
assert_eq!(bound.style.font_family, "FigCraft Missing F");
|
||||
assert_eq!(families(&mut s), vec![("FigCraft Missing F".to_string(), 1), ("FigCraft Missing G".to_string(), 1)]);
|
||||
}
|
||||
|
||||
|
||||
@@ -893,6 +893,16 @@ fn system_fonts_opted_out() -> bool {
|
||||
std::env::var_os("FIGCRAFT_NO_SYSTEM_FONTS").is_some_and(|v| !v.is_empty() && v != "0")
|
||||
}
|
||||
|
||||
/// Every font text can use is known: the system fonts were indexed, or there are none to index
|
||||
/// (opted out, or the web). Until then a family the scan would find counts as missing.
|
||||
pub fn installed_fonts_known() -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if !system_fonts_opted_out() {
|
||||
return system_fonts_scanned();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Index the system fonts the first time the font database is used (headless apps: a run that
|
||||
/// lays out no text never pays for the scan). The desktop app scans in the background instead.
|
||||
pub fn scan_system_fonts_on_demand() {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub use breaks::grapheme_step;
|
||||
pub use color::{ColorBitmap, ColorInk, ColorLayer};
|
||||
pub use fontdb::{
|
||||
Axis, CRAFT_FONTS, Coords, Face, FontDb, NamedInstance, axis_label, db, face_with_char, family_names, family_styles, find_face, has_family,
|
||||
no_coords, scan_system_fonts, scan_system_fonts_on_demand, system_fonts_scanned,
|
||||
installed_fonts_known, no_coords, scan_system_fonts, scan_system_fonts_on_demand, system_fonts_scanned,
|
||||
};
|
||||
pub use layout::{Decoration, Glyph, Line, TextLayout, layout};
|
||||
pub use onpath::OnPath;
|
||||
|
||||
@@ -198,6 +198,7 @@ name = "English"
|
||||
"C or ⇧C" = "C or ⇧C"
|
||||
"CMYK colour" = "CMYK colour"
|
||||
"Can view" = "Can view"
|
||||
"Checking the fonts installed on this computer…" = "Checking the fonts installed on this computer…"
|
||||
"Checking the server…" = "Checking the server…"
|
||||
"Clear from this list" = "Clear from this list"
|
||||
"Code or invite link" = "Code or invite link"
|
||||
|
||||
@@ -201,6 +201,7 @@ status = "draft — needs native review"
|
||||
"C or ⇧C" = "C o ⇧C"
|
||||
"CMYK colour" = "Color CMYK"
|
||||
"Can view" = "Puede ver"
|
||||
"Checking the fonts installed on this computer…" = "Comprobando las fuentes instaladas en este equipo…"
|
||||
"Checking the server…" = "Comprobando el servidor…"
|
||||
"Clear from this list" = "Borrar de esta lista"
|
||||
"Code or invite link" = "Código o enlace de invitación"
|
||||
|
||||
@@ -15,19 +15,26 @@ pub struct Shown {
|
||||
/// The document it lists: the window goes when that document closes.
|
||||
pub uid: u64,
|
||||
pub families: Vec<Missing>,
|
||||
/// Asked for before the installed fonts were indexed: the list comes once they are.
|
||||
pub indexing: bool,
|
||||
}
|
||||
|
||||
impl Shown {
|
||||
pub fn inspect(&self) -> Value {
|
||||
json!({"families": self.families.iter().map(|m| json!({"family": m.family, "layers": m.layers})).collect::<Vec<_>>()})
|
||||
json!({
|
||||
"families": self.families.iter().map(|m| json!({"family": m.family, "layers": m.layers})).collect::<Vec<_>>(),
|
||||
"indexing": self.indexing,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// After a file the user opened (or imported into) is in front: look for missing fonts in it
|
||||
/// once that can be told ([`show`]).
|
||||
/// After the user opened a file, or imported pages into one (results with a `summary`; not a
|
||||
/// placed image or SVG, a file that was open already, or Clear recent): look for missing fonts in
|
||||
/// it once that can be told ([`show`]). Not in the browser, which has only its built-in fonts.
|
||||
pub fn check_later(app: &mut FigApp, command: &str, result: Option<&Value>) {
|
||||
if result.is_some()
|
||||
if !cfg!(target_arch = "wasm32")
|
||||
&& matches!(command, "file.open" | "file.openRecent" | "file.import")
|
||||
&& result.is_some_and(|r| r.get("summary").is_some())
|
||||
&& let Ok(ds) = app.session.ds()
|
||||
{
|
||||
app.ui.font_checks.push(ds.uid);
|
||||
@@ -38,25 +45,26 @@ pub fn check_later(app: &mut FigApp, command: &str, result: Option<&Value>) {
|
||||
fn list(app: &mut FigApp, uid: u64) -> Option<Vec<Missing>> {
|
||||
let ds = app.session.docs.iter().find(|d| d.uid == uid)?;
|
||||
let families = missing(&ds.doc);
|
||||
app.ui.missing_fonts = Some(Shown { uid, families: families.clone() });
|
||||
app.ui.missing_fonts = Some(Shown { uid, families: families.clone(), indexing: false });
|
||||
Some(families)
|
||||
}
|
||||
|
||||
/// Text ▸ Missing fonts (`dialog.missingFonts`): the window for the file in front.
|
||||
pub fn request(app: &mut FigApp) {
|
||||
if let Ok(uid) = app.session.ds().map(|d| d.uid) {
|
||||
let Ok(uid) = app.session.ds().map(|d| d.uid) else { return };
|
||||
if figcraft_text::installed_fonts_known() {
|
||||
list(app, uid);
|
||||
} else {
|
||||
app.ui.missing_fonts = Some(Shown { uid, families: Vec::new(), indexing: true });
|
||||
}
|
||||
}
|
||||
|
||||
/// Installed fonts are indexed: before that, every one of them would count as missing. The
|
||||
/// browser has only its built-in fonts, so files there don't open the window by themselves.
|
||||
fn fonts_known() -> bool {
|
||||
!cfg!(target_arch = "wasm32") && figcraft_text::system_fonts_scanned()
|
||||
}
|
||||
|
||||
pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
if !app.ui.font_checks.is_empty() && app.ui.import_report.is_none() && app.ui.missing_fonts.is_none() && fonts_known() {
|
||||
let known = figcraft_text::installed_fonts_known();
|
||||
if let Some(uid) = app.ui.missing_fonts.as_ref().filter(|m| m.indexing && known).map(|m| m.uid) {
|
||||
list(app, uid);
|
||||
}
|
||||
if !app.ui.font_checks.is_empty() && app.ui.import_report.is_none() && app.ui.missing_fonts.is_none() && known {
|
||||
let uid = app.ui.font_checks.remove(0);
|
||||
if list(app, uid).is_some_and(|f| f.is_empty()) {
|
||||
app.ui.missing_fonts = None;
|
||||
@@ -71,7 +79,9 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
let mut close_clicked = false;
|
||||
crate::dialogs::window(tr("Missing fonts"), ctx).open(&mut open).show(ctx, |ui| {
|
||||
ui.set_width(380.0);
|
||||
if shown.families.is_empty() {
|
||||
if shown.indexing {
|
||||
ui.label(tr("Checking the fonts installed on this computer…"));
|
||||
} else if shown.families.is_empty() {
|
||||
ui.label(tr("All the fonts this file uses are installed"));
|
||||
} else {
|
||||
ui.label(
|
||||
|
||||
@@ -16,11 +16,10 @@ fn saved(name: &str, families: &[&str]) -> String {
|
||||
path
|
||||
}
|
||||
|
||||
/// The installed fonts are indexed, as the desktop app does at startup; false when this machine
|
||||
/// opted out (nothing would ever count as known).
|
||||
/// The installed fonts are indexed, as the desktop app does at startup.
|
||||
fn fonts_indexed() -> bool {
|
||||
figcraft_text::scan_system_fonts();
|
||||
figcraft_text::system_fonts_scanned()
|
||||
figcraft_text::installed_fonts_known()
|
||||
}
|
||||
|
||||
fn listed(u: &Ui) -> Value {
|
||||
@@ -42,11 +41,12 @@ fn opening_a_file_lists_its_missing_fonts_and_not_the_installed_ones() {
|
||||
u.app.run("file.open", json!({"path": path}));
|
||||
u.frame(vec![]);
|
||||
assert_eq!(listed(&u), Value::Null);
|
||||
u.app.run("file.close", json!({}));
|
||||
|
||||
u.app.run_user("file.open", json!({"path": path}));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
u.frame(vec![]);
|
||||
assert_eq!(listed(&u), json!({"families": [{"family": "FigCraft Missing UI", "layers": 2}]}));
|
||||
assert_eq!(listed(&u), json!({"families": [{"family": "FigCraft Missing UI", "layers": 2}], "indexing": false}));
|
||||
u.frame(vec![]);
|
||||
let shown = u.frame_output(vec![]).shapes;
|
||||
assert!(crate::import_report_tests::shown_texts(shown).iter().any(|t| t == "2 text layers"));
|
||||
@@ -99,9 +99,10 @@ fn it_waits_for_the_import_report_to_close() {
|
||||
|
||||
#[test]
|
||||
fn text_menu_shows_the_window_for_the_file_in_front_even_with_nothing_missing() {
|
||||
assert!(fonts_indexed());
|
||||
let mut u = Ui::new(Session::new());
|
||||
assert!(u.app.run("text.missingFonts", json!({})).is_some());
|
||||
assert_eq!(listed(&u), json!({"families": []}));
|
||||
assert_eq!(listed(&u), json!({"families": [], "indexing": false}));
|
||||
u.frame(vec![]);
|
||||
let shown = u.frame_output(vec![]).shapes;
|
||||
assert!(crate::import_report_tests::shown_texts(shown).iter().any(|t| t == "All the fonts this file uses are installed"));
|
||||
@@ -110,3 +111,39 @@ fn text_menu_shows_the_window_for_the_file_in_front_even_with_nothing_missing()
|
||||
u.frame(escape);
|
||||
assert_eq!(listed(&u), Value::Null, "Escape closes it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asked_for_before_the_fonts_are_indexed_it_lists_them_once_they_are() {
|
||||
assert!(fonts_indexed());
|
||||
let path = saved("indexing", &["FigCraft Missing Indexing"]);
|
||||
let mut u = Ui::new(Session::new());
|
||||
u.app.run("file.open", json!({"path": path}));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
// As Text ▸ Missing fonts leaves it while the startup scan is still running.
|
||||
let uid = u.app.session.ds().unwrap().uid;
|
||||
u.app.ui.missing_fonts = Some(crate::missing_fonts::Shown { uid, families: Vec::new(), indexing: true });
|
||||
u.frame(vec![]);
|
||||
assert_eq!(listed(&u), json!({"families": [{"family": "FigCraft Missing Indexing", "layers": 2}], "indexing": false}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placing_images_or_clearing_recents_does_not_bring_it_back() {
|
||||
assert!(fonts_indexed());
|
||||
let path = saved("place", &["FigCraft Missing Place"]);
|
||||
let svg = std::env::temp_dir().join(format!("figcraft-missing-fonts-place-{}.svg", std::process::id()));
|
||||
std::fs::write(&svg, r#"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10"/></svg>"#).unwrap();
|
||||
let mut u = Ui::new(Session::new());
|
||||
u.app.run_user("file.open", json!({"path": path}));
|
||||
u.frame(vec![]);
|
||||
assert_eq!(listed(&u)["families"][0]["family"], "FigCraft Missing Place");
|
||||
u.app.ui.missing_fonts = None;
|
||||
// A placed SVG, the same file opened again (it only comes to the front), Clear recent.
|
||||
assert!(u.app.run_user("file.import", json!({"path": svg.to_string_lossy()})).is_some());
|
||||
assert!(u.app.run_user("file.open", json!({"path": path})).is_some());
|
||||
u.app.run_user("file.openRecent", json!({"clear": true}));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
std::fs::remove_file(&svg).unwrap();
|
||||
u.frame(vec![]);
|
||||
assert!(u.app.ui.font_checks.is_empty());
|
||||
assert_eq!(listed(&u), Value::Null);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (REST import reads grid track sizes (#465); `.fig` import fidelity from the community batches, budgets for large files, the import report, a `.fig` copy from Save as, paste from another design app; instance layers' size and resizing and Hug on import (#731, #732); SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fig` export: written files checked by opening them in other software; experimental `.fig` export; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; embedded image colour profiles; `.fig` import, best effort; animated GIF fills and TIFF images; last major: created, formats from the installed Figma's Info.plist and help center, ours from source) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (missing fonts listed on open (#752); REST import reads grid track sizes (#465); `.fig` import fidelity from the community batches, budgets for large files, the import report, a `.fig` copy from Save as, paste from another design app; instance layers' size and resizing and Hug on import (#731, #732); SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fig` export: written files checked by opening them in other software; experimental `.fig` export; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; embedded image colour profiles; `.fig` import, best effort; animated GIF fills and TIFF images; last major: created, formats from the installed Figma's Info.plist and help center, ours from source) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Every format Figma reads or writes, with FigCraft's support. Sources: `Info.plist` of
|
||||
`/Applications/Figma.app` (document types only; nothing inside the bundle's code was read), Figma's
|
||||
|
||||
Reference in New Issue
Block a user