From 2ef3ebb45d225217d1031d32e424bb61f15721aa Mon Sep 17 00:00:00 2001 From: skylartaylor Date: Sun, 25 Oct 2026 05:17:15 +0100 Subject: [PATCH] E21: require release font coverage and stabilize capture deadline tests --- .github/workflows/ci.yml | 9 +- .github/workflows/freebsd.yml | 2 +- .github/workflows/release.yml | 9 +- ATTRIBUTION.md | 7 +- ROADMAP.md | 5 +- crates/text/build.rs | 173 +++++++++++++++--- crates/text/tests/build_input.rs | 4 + crates/text/tests/fixtures/latin-families.txt | 52 ++++++ crates/ui-egui/src/idle_tests.rs | 7 +- crates/ui-egui/src/windows_tests.rs | 4 +- docs/egui-037-local-followups.md | 51 ++++-- docs/egui-037-migration-plan.md | 4 + docs/releasing.md | 13 +- 13 files changed, 274 insertions(+), 66 deletions(-) create mode 100644 crates/text/tests/build_input.rs create mode 100644 crates/text/tests/fixtures/latin-families.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7abae66a..b79af502 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,10 @@ jobs: # Tracks @main so suite-wide CI fixes reach every app without a PR in each repository. uses: storytold/craft-common-actions/.github/workflows/gate.yml@main with: - # The craft-fonts commit release.yml builds with. Needs fonts/latin-manifest.txt (Inter): - # crates/text/build.rs fails a build that requires craft-fonts without it. Only craft-fonts' - # pr/latin-presentation-fonts has it; its main has Noto Sans Georgian but no Inter. Re-pin - # once one commit has both (storytold/craft-fonts#42). - craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83 + # Combined Latin presentation and core UI catalog. Required web builds enforce + # Inter Regular/SemiBold and Noto Sans Georgian Regular after asset-policy filtering. + craft-fonts-ref: 37827adc41fd165f9a9234ac40f94dbad353aa69 + require-craft-fonts: true packages: >- libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libgtk-3-dev diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index e030b167..80adf16a 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -32,4 +32,4 @@ jobs: uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main with: run-package-check: true - craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83 + craft-fonts-ref: 37827adc41fd165f9a9234ac40f94dbad353aa69 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b37ee39..f93219fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,13 +24,12 @@ jobs: with: app-name: figcraft app-title: FigCraft - # Needs fonts/latin-manifest.txt (Inter): crates/text/build.rs fails a release without it. Only - # craft-fonts' pr/latin-presentation-fonts has it; its main has Noto Sans Georgian but no Inter. - # Re-pin once one commit has both (storytold/craft-fonts#42). - craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83 + # Combined Latin presentation and core UI catalog. Required web builds enforce + # Inter Regular/SemiBold and Noto Sans Georgian Regular after asset-policy filtering. + craft-fonts-ref: 37827adc41fd165f9a9234ac40f94dbad353aa69 has-server: true # The browser has no system fonts: without craft-fonts the web build can't draw text (#885). - # crates/text/build.rs keeps the web build to Inter. + # crates/text/build.rs keeps the web allowlist small (Inter and Georgian). web-craft-fonts: true version: ${{ inputs.version }} secrets: diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index bf6fdece..46b789a8 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -26,9 +26,12 @@ Release builds embed fonts from the optional craft-fonts build input |---|---|---| | Inter | SIL Open Font License 1.1 | https://github.com/rsms/inter | | Archivo | SIL Open Font License 1.1 | [Google Fonts / Archivo](https://github.com/google/fonts/tree/6c70c829f09ea345d3590406693220ea35c6553f/ofl/archivo) | -| Roboto, Montserrat, Lato, Open Sans, Poppins, Nunito Sans, Merriweather, Playfair Display, Source Sans 3, Source Serif 4, Carlito, Caladea | SIL Open Font License 1.1 | Google Fonts / upstream repositories (see craft-fonts) | +| Roboto, Montserrat, Lato, Open Sans, Poppins, Nunito Sans, Merriweather, Playfair Display, Carlito, Caladea | SIL Open Font License 1.1 | Google Fonts / upstream repositories (see craft-fonts) | | Liberation Sans, Serif, Mono | SIL Open Font License 1.1 | https://github.com/liberationfonts | -| BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans Arabic | SIL Open Font License 1.1 | Google Fonts (see craft-fonts) | +| BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans Arabic, Noto Sans Georgian | SIL Open Font License 1.1 | Google Fonts (see craft-fonts) | + +The central catalog also contains Adobe-authored Source Sans 3 and Source Serif 4. +FigCraft filters both families from every build in accordance with its asset policy. egui's default fonts (compiled into the `egui`/`epaint` crates) keep their own licences (OFL / Apache-2.0 / Ubuntu Font Licence); see those crates. diff --git a/ROADMAP.md b/ROADMAP.md index a27cc4df..0af8cbac 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -132,6 +132,8 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-25** · egui 0.37 capture lifecycle: screenshots with hidden or unknown visibility queue, request a hidden paint, and expire after 60 seconds if no capture arrives (headless-tested contract). Logic-only edits, saves and inspection continue while hidden. Native visible GUI and forced hidden-window paint, browser and Wayland behavior remain unqualified; parity percentages are unchanged. + - **2026-10-24** · Dev Mode tail (M6, gap 16): Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Dev Mode 98% / 78%. - **2026-10-24** · Boards (M12): File ▸ New board makes a board, a file kind of its own saved in `.figcraft` and carried to people joining a live session. Sticky notes (8 colours, text that shrinks as the note grows, the author's name, Shift+Cmd/Ctrl+Enter for the next one), shapes with text (7 outlines), connectors that re-route when what they join moves (computed in the engine's normalize pass, so local edits, undo and others' edits agree), marker, highlighter and eraser, stamps with the placer's name on hover; a board toolbar, a contextual bar over the selection and the Design panel hidden until asked for; prototyping and Dev Mode refuse on boards. Design files and boards have their own marks (a pen nib, a note) on tabs, the home screen and the Open files list. Checked in the app, with two app instances in one live session, and through MCP; the web build compiles ([boards-parity](docs/boards-parity.md)). - **2026-10-15** · Auto-save (M1, #305): Preferences ▸ Save files automatically (`pref.autoSave`, off by default, desktop only) saves each open file that has a path 3 s after its last change, never during a gesture, a live text preview or while a version is shown; the file is encoded off the UI thread, a failed save says why and is tried again a minute later. Numbers unchanged. @@ -329,6 +331,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-25 | minor | egui 0.37 capture lifecycle: screenshots with hidden or unknown visibility queue, request a hidden paint, and expire after 60 seconds if no capture arrives (headless-tested contract). Logic-only edits, saves and inspection continue while hidden. Native visible GUI and forced hidden-window paint, browser and Wayland behavior remain unqualified; parity percentages are unchanged. | | 2026-10-24 | minor | Dev Mode tail (M6, gap 16): Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Dev Mode 97% → 98% breadth, 76% → 78% ready, 2–4 h → 1–2 h; headline numbers unchanged at this rounding | | 2026-10-24 | minor | Boards (M12): a file kind of its own (`file.newBoard`, saved in `.figcraft`, carried to live-session joiners) with sticky notes, shapes with text, connectors that re-route when what they join moves (an engine recompute, the same for local edits, undo and others' edits), marker, highlighter and eraser, stamps with the placer's name, a board toolbar and a contextual bar; prototyping and Dev Mode refuse on boards; design files and boards have their own marks on tabs and file rows. Boards row added outside the weighted numbers; headline numbers unchanged at this rounding | | 2026-10-15 | minor | Auto-save (#305, #296): Preferences ▸ Save files automatically (`pref.autoSave`, off by default; desktop only, the browser has no files to save to) saves each open file that has a path 3 s after its last change, so steady editing doesn't save every few seconds, and never during a gesture, a live text preview or while a version is shown. The file is encoded on a thread of its own and written as of that moment (edits made meanwhile stay unsaved); a failed save says why in a toast and is tried again a minute later, and version history checkpoints keep coming with it on. Numbers unchanged | diff --git a/crates/text/build.rs b/crates/text/build.rs index f46547cd..ecca6a45 100644 --- a/crates/text/build.rs +++ b/crates/text/build.rs @@ -1,6 +1,7 @@ //! Embeds the optional craft-fonts build input (`CRAFT_FONTS_DIR`): both `fonts/manifest.txt` and the //! opt-in Latin presentation fonts in `fonts/latin-manifest.txt` (Inter, Roboto, …). Unset or missing, -//! `CRAFT_FONTS` is empty and the app uses system fonts. Recipe: craft-fonts `docs/integration.md`. +//! `CRAFT_FONTS` is empty and native apps use system fonts. Required web releases enforce Inter +//! Regular/SemiBold and Noto Sans Georgian Regular after filtering. Recipe: craft-fonts `docs/integration.md`. //! Noto Sans CJK only with `FIGCRAFT_EMBED_CJK` set (the web build's only way to show CJK UI text). use std::fmt::Write as _; use std::path::PathBuf; @@ -10,30 +11,13 @@ fn main() { println!("cargo::rerun-if-env-changed=CRAFT_FONTS_REQUIRED"); println!("cargo::rerun-if-env-changed=FIGCRAFT_EMBED_CJK"); let mut src = String::from("pub static CRAFT_FONTS: &[CraftFont] = &[\n"); - if let Some(dir) = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from) { - let mut ok = false; - for manifest in ["fonts/latin-manifest.txt", "fonts/manifest.txt"] { - // Presentation faces are an optional extension of craft-fonts. Required mode - // validates the core manifest and every extension that is actually supplied. - let path = dir.join(manifest); - if manifest == "fonts/latin-manifest.txt" && !path.exists() { - watch(&path); - continue; - } - match craft_fonts(&dir, manifest) { - Ok(entries) => { - ok = true; - src.push_str(&entries); - } - Err(e) if std::env::var_os("CRAFT_FONTS_REQUIRED").is_some() => { - println!("cargo::error=CRAFT_FONTS_DIR={}: {e}", dir.display()); - } - Err(e) => println!("cargo::warning=craft-fonts {manifest} skipped: CRAFT_FONTS_DIR={}: {e}", dir.display()), - } - } - if !ok { - println!("cargo::warning=building without craft-fonts (once it is there, `cargo clean -p figcraft-text` makes the next build use it)"); - } + let dir = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from); + let required = std::env::var_os("CRAFT_FONTS_REQUIRED").is_some(); + let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32"); + let embed_cjk = std::env::var_os("FIGCRAFT_EMBED_CJK").is_some(); + match input_fonts(dir.as_deref(), wasm, required, embed_cjk) { + Ok(entries) => src.push_str(&entries), + Err(e) => println!("cargo::error={e}"), } src.push_str("];\n"); let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("craft_fonts.rs"); @@ -42,13 +26,45 @@ fn main() { } } +/// Collect the effective catalog and enforce the required web release faces after filtering. +fn input_fonts(dir: Option<&std::path::Path>, wasm: bool, required: bool, embed_cjk: bool) -> Result { + let Some(dir) = dir else { + return if required { Err("CRAFT_FONTS_REQUIRED is set but CRAFT_FONTS_DIR is missing".into()) } else { Ok(String::new()) }; + }; + let mut out = String::new(); + let mut faces = Vec::new(); + for manifest in ["fonts/latin-manifest.txt", "fonts/manifest.txt"] { + let path = dir.join(manifest); + if manifest == "fonts/latin-manifest.txt" && !path.exists() { + watch(&path); + continue; + } + match craft_fonts(dir, manifest, wasm, embed_cjk) { + Ok(catalog) => { + out.push_str(&catalog.entries); + faces.extend(catalog.faces); + } + Err(e) if required => return Err(format!("CRAFT_FONTS_DIR={}: {e}", dir.display())), + Err(e) => println!("cargo::warning=craft-fonts {manifest} skipped: CRAFT_FONTS_DIR={}: {e}", dir.display()), + } + } + if required && wasm { + for (family, style) in [("Inter", "Regular"), ("Inter", "SemiBold"), ("Noto Sans Georgian", "Regular")] { + if !faces.iter().any(|(f, s)| f == family && s == style) { + return Err(format!("required web font missing: {family} {style}; CRAFT_FONTS_DIR must include both Latin and core release faces")); + } + } + } + Ok(out) +} + /// Rebuild when `manifest` changes or, while it is missing, when it is added to its folder. /// /// Cargo takes a watched path that doesn't exist as changed on every build, and then rebuilds this /// crate and every crate that depends on it each time. So a missing manifest is never watched /// itself: its folder is, when there is one (a craft-fonts checkout without the Latin manifest). /// With no craft-fonts at all there is nothing to watch, so the build can't notice it arriving: -/// the warning in `main` says how to pick it up. +/// rebuild this crate after adding a previously absent checkout. fn watch(manifest: &std::path::Path) { let watched = if manifest.exists() { Some(manifest) } else { manifest.parent().filter(|folder| folder.is_dir()) }; if let Some(path) = watched { @@ -56,20 +72,29 @@ fn watch(manifest: &std::path::Path) { } } -fn craft_fonts(dir: &std::path::Path, manifest_rel: &str) -> Result { +struct Catalog { + entries: String, + faces: Vec<(String, String)>, +} + +fn craft_fonts(dir: &std::path::Path, manifest_rel: &str, wasm: bool, embed_cjk: bool) -> Result { let manifest = dir.join(manifest_rel); watch(&manifest); let text = std::fs::read_to_string(&manifest).map_err(|e| format!("{}: {e}", manifest.display()))?; - let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32"); let mut out = String::new(); + let mut faces = Vec::new(); for line in text.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')) { let f: Vec<&str> = line.split(" | ").map(str::trim).collect(); let [family, style, file, scripts, ..] = f.as_slice() else { return Err(format!("malformed manifest line: {line}")); }; + // FigCraft's asset policy excludes Adobe-authored visual assets on every target. + if matches!(*family, "Source Sans 3" | "Source Serif 4") { + continue; + } // CJK fonts are large; the desktop app finds system CJK fonts instead unless asked. let cjk = scripts.contains("Hans"); - if cjk && std::env::var_os("FIGCRAFT_EMBED_CJK").is_none() { + if cjk && !embed_cjk { continue; } // Web builds have a size budget: Inter, Noto Sans Georgian (small, for the Georgian UI), only the @@ -80,7 +105,95 @@ fn craft_fonts(dir: &std::path::Path, manifest_rel: &str) -> Result Self { + let dir = std::env::temp_dir().join(format!("fig-font-input-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed))); + std::fs::create_dir_all(dir.join("fonts")).unwrap(); + // The build script validates manifest/file presence; font bytes are checked centrally. + std::fs::write(dir.join("fonts/face.ttf"), b"synthetic build-input fixture").unwrap(); + let fixture = Self(dir); + fixture.manifest("manifest.txt", &[("Noto Sans Georgian", "Regular")]); + fixture + } + + fn manifest(&self, name: &str, faces: &[(&str, &str)]) { + let text = faces.iter().map(|(family, style)| format!("{family} | {style} | fonts/face.ttf | Latn\n")).collect::(); + std::fs::write(self.0.join("fonts").join(name), text).unwrap(); + } + } + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn restored_latin_catalog_preserves_permitted_native_faces_and_web_budget() { + let f = Fixture::new(); + std::fs::write(f.0.join("fonts/latin-manifest.txt"), include_str!("tests/fixtures/latin-families.txt")).unwrap(); + let native = craft_fonts(&f.0, "fonts/latin-manifest.txt", false, false).unwrap(); + assert_eq!(native.faces.len(), 43, "all prior permitted presentation faces remain available"); + let web = craft_fonts(&f.0, "fonts/latin-manifest.txt", true, false).unwrap(); + assert_eq!(web.faces.len(), 4, "the web catalog keeps only the four Inter faces"); + for catalog in [&native, &web] { + assert!(!catalog.entries.contains("Source Sans 3")); + assert!(!catalog.entries.contains("Source Serif 4")); + } + assert!(native.faces.iter().any(|(family, _)| family == "Carlito")); + assert!(native.faces.iter().any(|(family, _)| family == "Liberation Serif")); + assert!(input_fonts(Some(&f.0), true, true, false).is_ok()); + } + + #[test] + fn required_web_rejects_core_only_and_partial_latin_catalogs() { + let f = Fixture::new(); + assert!(input_fonts(Some(&f.0), true, true, false).unwrap_err().contains("Inter Regular")); + f.manifest("latin-manifest.txt", &[("Inter", "Regular"), ("Unrelated", "SemiBold")]); + assert!(input_fonts(Some(&f.0), true, true, false).unwrap_err().contains("Inter SemiBold")); + } + + #[test] + fn required_web_embeds_both_catalogs_and_requires_georgian() { + let f = Fixture::new(); + f.manifest("latin-manifest.txt", &[("Inter", "Regular"), ("Inter", "SemiBold")]); + let entries = input_fonts(Some(&f.0), true, true, false).unwrap(); + assert_eq!(entries.matches("CraftFont {").count(), 3); + assert!(entries.contains("family: \"Inter\"")); + assert!(entries.contains("family: \"Noto Sans Georgian\"")); + f.manifest("manifest.txt", &[("Other", "Regular")]); + assert!(input_fonts(Some(&f.0), true, true, false).unwrap_err().contains("Noto Sans Georgian Regular")); + } + + #[test] + fn optional_and_native_inputs_keep_their_existing_policy() { + let f = Fixture::new(); + assert!(input_fonts(None, true, false, false).unwrap().is_empty()); + assert!(input_fonts(None, true, true, false).unwrap_err().contains("CRAFT_FONTS_DIR is missing")); + assert!(input_fonts(Some(&f.0), true, false, false).unwrap().contains("Noto Sans Georgian")); + assert!(input_fonts(Some(&f.0), false, true, false).unwrap().contains("Noto Sans Georgian")); + } + + #[test] + fn required_web_rejects_missing_asset_even_with_all_face_rows() { + let f = Fixture::new(); + f.manifest("latin-manifest.txt", &[("Inter", "Regular"), ("Inter", "SemiBold")]); + std::fs::remove_file(f.0.join("fonts/face.ttf")).unwrap(); + assert!(input_fonts(Some(&f.0), true, true, false).unwrap_err().contains("fonts/face.ttf")); + } } diff --git a/crates/text/tests/build_input.rs b/crates/text/tests/build_input.rs new file mode 100644 index 00000000..6f87ac1f --- /dev/null +++ b/crates/text/tests/build_input.rs @@ -0,0 +1,4 @@ +//! Exercise the actual build-script catalog policy without changing process environment. +#[allow(dead_code)] +#[path = "../build.rs"] +mod build_script; diff --git a/crates/text/tests/fixtures/latin-families.txt b/crates/text/tests/fixtures/latin-families.txt new file mode 100644 index 00000000..e9db6f41 --- /dev/null +++ b/crates/text/tests/fixtures/latin-families.txt @@ -0,0 +1,52 @@ +# Synthetic paths; family/style inventory from craft-fonts 76ba5a0496503c0e2841abd626295038cbe07cde. +Inter | Regular | fonts/face.ttf | Latn +Inter | Italic | fonts/face.ttf | Latn +Inter | SemiBold | fonts/face.ttf | Latn +Inter | Bold | fonts/face.ttf | Latn +Carlito | Regular | fonts/face.ttf | Latn +Carlito | Bold | fonts/face.ttf | Latn +Carlito | Italic | fonts/face.ttf | Latn +Carlito | Bold Italic | fonts/face.ttf | Latn +Caladea | Regular | fonts/face.ttf | Latn +Caladea | Bold | fonts/face.ttf | Latn +Caladea | Italic | fonts/face.ttf | Latn +Caladea | Bold Italic | fonts/face.ttf | Latn +Liberation Sans | Regular | fonts/face.ttf | Latn +Liberation Sans | Bold | fonts/face.ttf | Latn +Liberation Sans | Italic | fonts/face.ttf | Latn +Liberation Sans | Bold Italic | fonts/face.ttf | Latn +Liberation Serif | Regular | fonts/face.ttf | Latn +Liberation Serif | Bold | fonts/face.ttf | Latn +Liberation Serif | Italic | fonts/face.ttf | Latn +Liberation Serif | Bold Italic | fonts/face.ttf | Latn +Liberation Mono | Regular | fonts/face.ttf | Latn +Liberation Mono | Bold | fonts/face.ttf | Latn +Liberation Mono | Italic | fonts/face.ttf | Latn +Liberation Mono | Bold Italic | fonts/face.ttf | Latn +Source Sans 3 | Regular | fonts/face.ttf | Latn +Source Sans 3 | Italic | fonts/face.ttf | Latn +Source Sans 3 | Semibold | fonts/face.ttf | Latn +Source Sans 3 | Bold | fonts/face.ttf | Latn +Source Serif 4 | Regular | fonts/face.ttf | Latn +Source Serif 4 | Italic | fonts/face.ttf | Latn +Source Serif 4 | Semibold | fonts/face.ttf | Latn +Source Serif 4 | Bold | fonts/face.ttf | Latn +Montserrat | Variable | fonts/face.ttf | Latn +Montserrat | Italic Variable | fonts/face.ttf | Latn +Lato | Regular | fonts/face.ttf | Latn +Lato | Bold | fonts/face.ttf | Latn +Lato | Italic | fonts/face.ttf | Latn +Open Sans | Variable | fonts/face.ttf | Latn +Open Sans | Italic Variable | fonts/face.ttf | Latn +Roboto | Variable | fonts/face.ttf | Latn +Roboto | Italic Variable | fonts/face.ttf | Latn +Merriweather | Regular | fonts/face.ttf | Latn +Merriweather | Bold | fonts/face.ttf | Latn +Merriweather | Italic | fonts/face.ttf | Latn +Playfair Display | Variable | fonts/face.ttf | Latn +Playfair Display | Italic Variable | fonts/face.ttf | Latn +Poppins | Regular | fonts/face.ttf | Latn +Poppins | Bold | fonts/face.ttf | Latn +Poppins | Italic | fonts/face.ttf | Latn +Nunito Sans | Variable | fonts/face.ttf | Latn +Nunito Sans | Italic Variable | fonts/face.ttf | Latn diff --git a/crates/ui-egui/src/idle_tests.rs b/crates/ui-egui/src/idle_tests.rs index 4c35ae40..c5da205c 100644 --- a/crates/ui-egui/src/idle_tests.rs +++ b/crates/ui-egui/src/idle_tests.rs @@ -345,12 +345,15 @@ fn cjk_names_are_looked_for_in_edits_not_on_every_pan() { fn a_hidden_window_answers_at_once() { // #269, #720: minimised or covered, the window draws no frames, but the app's logic still runs: // it answers edits, inspection, saves and the interface's state; what needs a frame (a capture - // of the window) is an error at once instead of waiting for the window to show. + // of the window) queues for a forced hidden paint and otherwise expires at its deadline. let dir = std::env::temp_dir().join(format!("figcraft-hidden-save-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("hidden.figcraft"); let (mut h, _) = document(); h.settle(); + // Exercise admission well after startup, independently of settling speed. + h.time += 120.0; + h.app.clock.set_test_now(Some(h.time)); let (tx, rx) = std::sync::mpsc::channel(); let request = |id: u64, method: &str, params: Value| ControlRequest { id: json!(id), @@ -376,7 +379,7 @@ fn a_hidden_window_answers_at_once() { assert_eq!(h.app.capture.shots.len(), 1, "one admitted capture"); assert!(h.app.control_queue.is_empty(), "nothing waits for the window"); // Its deadline expires it once, with the same error the transport used to get. - h.app.clock.set_test_now(Some(crate::control::SHOT_TIMEOUT + 1.0)); + h.app.clock.set_test_now(Some(h.time + crate::control::SHOT_TIMEOUT + 1.0)); let _ = ctx.run_logic(&hidden, |ctx| crate::control::background(&mut h.app, ctx)); let shot = rx.try_recv().expect("expired"); assert_eq!(shot.id, json!(3)); diff --git a/crates/ui-egui/src/windows_tests.rs b/crates/ui-egui/src/windows_tests.rs index 1537f426..6da3a011 100644 --- a/crates/ui-egui/src/windows_tests.rs +++ b/crates/ui-egui/src/windows_tests.rs @@ -137,6 +137,8 @@ fn another_hidden_window_answers_requests_that_need_no_frame_in_its_own_file() { // A capture that needs a frame is admitted and asks the hidden window to paint, rather than // failing at once; only its deadline expires it (once). + // Model a request admitted long after startup, without waiting in real time. + app.clock.set_test_now(Some(120.0)); let shot = request(&mut app, "ui.screenshot", json!({"window": w, "path": "unused.png"})); let unknown = request(&mut app, "window.list", json!({"window": 99})); logic(&ctx, &mut app, &[ViewportId::ROOT, other]); @@ -145,7 +147,7 @@ fn another_hidden_window_answers_requests_that_need_no_frame_in_its_own_file() { assert!(unknown.try_recv().expect("answered").result.unwrap_err().contains("no window 99")); assert!(app.control_queue.is_empty()); // The whole request lifetime is over: one timeout reply, and nothing retained. - app.clock.set_test_now(Some(crate::control::SHOT_TIMEOUT + 1.0)); + app.clock.set_test_now(Some(app.clock.now() + crate::control::SHOT_TIMEOUT + 1.0)); logic(&ctx, &mut app, &[ViewportId::ROOT, other]); assert!(shot.try_recv().expect("answered").result.unwrap_err().contains("didn't deliver a screenshot")); assert!(app.capture.shots.is_empty(), "released after its deadline"); diff --git a/docs/egui-037-local-followups.md b/docs/egui-037-local-followups.md index dde4e5c1..670fc30f 100644 --- a/docs/egui-037-local-followups.md +++ b/docs/egui-037-local-followups.md @@ -1,7 +1,24 @@ # egui 0.37 local implementation — stage 1 notes and follow-ups +Current candidate font pin: `37827adc41fd165f9a9234ac40f94dbad353aa69` restores the complete prior Latin catalog +alongside the migration fallbacks. Central font tests and license checks passed; +publication is pending approval. Managed run `egui037-figcraft-latin-ci-20261025-0711` +passed with required fonts: five targeted build-input tests plus full CI, including +WASM and plugin builds (3,257 passing executions, zero failures, nine ignored across +72 summaries; repeated tests are counted). This supersedes the optional-font-only +qualification below for the restored catalog. + ## Current local qualification — 2026-10-25 +The published refresh preserves main `9f788753` and passed managed full CI +`egui037-figcraft-main-refresh-ci-20261025-0618`: 3,247 passing executions, zero +failures and nine ignored. The 3,242 counts below describe the earlier `10702311` +qualification, before five additional main-branch tests. These are execution counts, +not distinct regressions. The release font input is under review separately; a +passing optional-font host build does not qualify the required web font catalog. + +### Earlier qualification at `10702311` + The migration preserves captured main `10702311f345393ee1d8868c3257679befcfa14d` (53 commits and 114 paths beyond the first integration). The final review commit records both the migration history and that upstream target as parents. The selected egui revision is @@ -147,19 +164,19 @@ an unhandled command for hidden tabs. Hidden-browser-tab `ui.screenshot`/`ui.inp unqualified even though the native path now queues and settles them. This is a forkfix prerequisite, not a passing claim. -## Local dependency and lock prerequisites (parent) +## Dependency and lock prerequisites -- Parent config `local-integration-config.toml` patches the egui family and winit, and `craft-ui`/ - `craft-print`. It does **not** patch `craft-window`. FigCraft now disables craft-window's optional - `egui` feature (it only needs the frame probe), but Cargo still resolves a package's optional - dependencies into the lock, and that rev declares `egui = "0.36"`. The managed graph must - therefore also patch/repoint `craft-window` to the reviewed craft-libs revision whose workspace - targets 0.37 (or otherwise prove the lock has exactly one egui family); otherwise the lock will - contain a second egui 0.36. -- No `[patch]` rows or path dependencies live in this repository's manifests; the parent generates - `Cargo.lock`. Do not publish this tree's resolved paths. -- Fonts: production keeps its central `CRAFT_FONTS_DIR`/system-font policy; no font binaries are - added here. Color-emoji claims still require a separate `epaint/color_fonts` feature job. +- The committed root `[patch.crates-io]` table pins the egui family and winit to + immutable published fork revisions. `craft-window` is pinned to the reviewed + craft-libs revision targeting egui 0.37; its optional `egui` feature stays disabled + because FigCraft needs only the frame probe. +- `Cargo.lock` is committed and resolves the canonical Git sources without local + path overrides. Use `--locked`; the earlier parent integration config is not + required to reproduce this graph. +- Fonts remain an optional central `CRAFT_FONTS_DIR` input with native system-font + fallback; no font binaries are added here. Required release-font qualification + is separate from optional-font host CI. Color emoji still needs a separate + `epaint/color_fonts` feature check. ## Qualification gaps and baseline backports/perf operations @@ -182,6 +199,12 @@ operations to measure once the parent can build: `#8731` text tessellation, `#87 startup, `#8512` font discovery, `#8763`/PR `#8764` TextEdit click-away, `#8748` hidden deferred children. None of these were applied, removed or claimed as fixed. -## Optional Latin manifest compatibility +## Required web font catalog -The pinned central font catalog has a required `fonts/manifest.txt` and no optional `fonts/latin-manifest.txt`. Required-font builds now validate the core and all supplied extension assets, while an absent opt-in Latin manifest is watched but not required. This follows the existing build script documentation and keeps malformed or incomplete supplied manifests fatal in required mode. No font binary was copied into FigCraft. Both required-core and no-font configurations remain validation gates. +Native optional-font builds retain the system-font policy. A required web build must +embed Inter Regular and SemiBold plus Noto Sans Georgian Regular after filtering; +missing required face/style coverage fails the build, regardless of which manifest supplies it. Required mode +also rejects an unset `CRAFT_FONTS_DIR`. The migration's original `b076417` catalog +lacks Inter and is not a qualified required-web input. A combined central font pin restores the prior Latin catalog while preserving the new +core fallbacks. Source Sans 3 and Source Serif 4 are excluded on every target; no font binary belongs in this repository. Required-web, required-native +and optional-font configurations remain separate validation gates. diff --git a/docs/egui-037-migration-plan.md b/docs/egui-037-migration-plan.md index fc7ecbc4..27ce7c5c 100644 --- a/docs/egui-037-migration-plan.md +++ b/docs/egui-037-migration-plan.md @@ -1188,6 +1188,10 @@ validation and generates the lockfile from its local dependency config. ## Current local qualification — 2026-10-25 +Historical qualification: the following counts apply to the earlier `10702311` +integration. The current `9f788753` refresh and its 3,247 passing executions are +recorded in the status update above. + The migration preserves captured main `10702311f345393ee1d8868c3257679befcfa14d` (53 commits and 114 paths beyond the first integration). The final review commit records both the migration history and that upstream target as parents. The selected egui revision is diff --git a/docs/releasing.md b/docs/releasing.md index 07492f6c..3dbc0fbf 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -101,11 +101,14 @@ builds with `CRAFT_FONTS_DIR` and `CRAFT_FONTS_REQUIRED=1`, so releases embed it rather than ship without them. The browser has no system fonts, so a web build without craft-fonts can't draw text. `crates/text/build.rs` reads both `fonts/manifest.txt` and `fonts/latin-manifest.txt`, so the pin -must be a commit that has the Latin manifest (today `4391305`, on craft-fonts' -`pr/latin-presentation-fonts` branch; re-pin to `main` once that lands). That commit has no -Noto Sans Georgian (craft-fonts' main has it but no Latin manifest), so desktop Georgian uses an -installed font; re-pin when one commit has both (craft-fonts#42). The web build embeds only Inter, -Noto Sans Georgian and the Archivo SemiBold wordmark, when the pin has them. Packages carry each embedded font's licence as `OFL-.txt` +must combine the Latin presentation catalog and the current core UI faces. Required web builds +validate the emitted Inter Regular/SemiBold and Noto Sans Georgian Regular faces after filtering; +a missing required face, style or asset fails the required web build. Excluded Adobe rows +are skipped before resolving their asset paths. +Native builds retain the permitted Latin presentation faces. Source Sans 3 and Source Serif 4 +are filtered on every target under FigCraft's asset policy. The web build keeps its smaller +allowlist: Inter, Noto Sans Georgian and Archivo SemiBold when supplied. Packages carry each +embedded font's licence as `OFL-.txt` (`copy_font_licences` in `packaging/env.sh`; `Contents/Resources/Licenses` in the macOS app). ### macOS