Review on #1309 pointed out that core-text 20.1's get_descriptors retains
the array it gets under the create rule a second time, so every scan kept
one array of font descriptors alive. Checking that turned up a larger leak:
Core Text autoreleases objects while it builds the collection, and the scan
runs where no autorelease pool is active (the app's background scan
thread, the CLI, the MCP server), so nothing ever drained them.
Measured with `figcraft-cli run` of N `text.rescanFonts` steps on macOS 15
(peak memory footprint):
| | 10 rescans | 200 rescans |
|---|---|---|
| main, without this PR | 221 MB | 220 MB |
| this PR before the fix | 228 MB | 796 MB |
| with the fix | 226 MB | 225 MB |
A standalone Swift loop over the same Core Text calls grows from 9 MB to
759 MB over 200 scans without a pool and stays at 20 MB inside one, so the
pool is what matters; the core-text bump removes the over-retain on top.
- The query runs inside objc2::rc::autoreleasepool. objc2 0.6 is already
in the lockfile (winit's), and the function is safe, so the workspace's
unsafe_code = "forbid" still holds.
- core-text goes to =22.0.0, the first release that takes ownership of the
array correctly. It brings core-foundation 0.10, core-graphics 0.25 and
core-graphics-types 0.2 beside the older versions other crates use. 22.0
moved framework linking behind a `link` feature that is on by default,
so with default features off it is named explicitly.
A rescan takes about 0.2 s instead of 0.08 s on that Mac (266 more font
files are indexed); it only runs when asked for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7f6a3a4e46)
Font managers such as RightFont, FontBase and Typeface keep fonts in their
own library folders and activate them through Core Text, without copying
them into /Library/Fonts or ~/Library/Fonts. Every other Mac app lists
these fonts; FigCraft walks only the font folders, so it didn't, and a
file using one of them drew in a fallback font.
On macOS the scan now also asks Core Text for the file of every available
font and indexes the files that lie outside the walked folders. All three
scans (the first, the on-demand one and text.rescanFonts) share this, so a
font activated while the app runs is found by a rescan like a newly
installed one.
Core Text needs unsafe FFI, which the workspace forbids, so the query goes
through the safe wrappers of the core-text crate, pinned at =20.1.0. That
version builds on the core-foundation 0.9, core-graphics 0.23 and
foreign-types 0.5 the lockfile already has: core-text is the only new
package. The call sits inside catch_unwind, so anything unexpected from
Core Text leaves the scan with the font folders alone. Other platforms are
unchanged.
On a Mac with RightFont, 15 families from its library are now listed
(Geist, JetBrains Mono, Clash Display and others) that weren't before. A
full font listing went from 0.06 s to 0.20 s on that machine.
Tests: the filter that keeps files outside the font folders once, indexing
a font file found outside them, and (macOS only) that Core Text lists the
system's font files. cargo test --workspace, cargo clippy -p figcraft-text
--all-targets -D warnings and cargo fmt --check pass on macOS 15. CI
builds on Linux only, so the macOS path is checked locally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5699e7abe6)
class_name runs layer names through deunicode (new dependency: pure Rust
tables, zero deps, BSD-3-Clause, the slug crate's base) before its ASCII
filter, so Korean, Cyrillic, Latin-diacritic and CJK names become
readable ASCII classes in HTML/CSS/React/Leptos output, component roots
and data-* variant attributes: 로그인 화면 → rogeuin-hwamyeon, кнопка →
knopka, über → uber, 按钮 → an-niu (before, every one came out layer-,
layer--2, ...). Symbols-only names take the node's kind (.text, .frame)
instead of layer-, the 40-character cut lands on a word boundary, and a
leading digit keeps the layer- prefix. SwiftUI/Compose type names are
unchanged (their own sanitizer); ASCII-only names are byte-identical.
html_codegen_tests updated and extended.
Two gaps CodeRabbit found in the guard:
- `same_file` compared paths, plain and resolved. A second hard link to
the file is a different path to the same data, so `convert`,
`run --out` and Save As to the link passed the guard and truncated
the file. It now asks the file system (same-file: device and inode on
Unix, volume and file index on Windows). same-file was already in the
lock through walkdir.
- `run --in X --out X` checked the tab the script left active. A script
that ends on `file.duplicate` or `file.new` leaves a tab with no count
of images left in X, so X was written over. The count is now taken
when X is opened and checked against `--out` whatever tab is active;
the active tab's own file is still checked as before.
The script check is a separate function so it can be tested without a
file past the 4 GiB budget.
Picks up the update prompt fix that draws its close mark and version pill
without glyphs a font may lack, so the prompt no longer takes the app down
when a bundled face has no ✕ or →.
Part of tracking issue storytold/craft-libs#319🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
* feat: startup update prompt modal and direct self-updating
The update flow (check at start or on request, the startup prompt, the self-update worker) is craft-update's Updater; the app only supplies the suite, its close path and its status window. The check at start is opt-in for the desktop app, an install holds back while documents are unsaved, and the Windows update helper runs before argument parsing.
Ref: storytold/craft-libs#319
* fix(updates): re-add the update hooks to the 0.37 frame loop
* fix(updates): address review feedback
Wake the UI when a manual check is queued from a menu callback; the update prompt owns the keyboard while it is open (LightCraft); Download opens through the desktop URL hook (FilmCraft); the Help menu dispatches through menus::invoke (FigCraft); print jobs and comment drafts hold an install back (WordCraft); Check for Updates is a registered command (GridCraft); an install holds back while documents are unsaved.
* fix(updates): show the prompt on Home and presenter frames and close the application, not one window
* fix(updates): add the update strings to the English catalog
* E21: require release font coverage and stabilize capture deadline tests
* E21: separate capture, IME and field-save regression phases
* M0: correct the migration qualification reference
* M-egui037: document input lifecycle and UI regression contracts
* M0: correct egui 0.37 status docs and stabilize join-code regression
---------
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Conflicts resolved keeping both sides: `.sketch` stays in `is_document`
(now via `OsStr::to_str`); ROADMAP and the parity, roadmap and
file-format docs keep main's headers and rows with the `.sketch` notes
and history rows added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked by importing synthetic files into the incumbent (Drafts > Import,
black-box) and against a "Save local copy" of a synthetic document.
- Images are named and referenced by the SHA-1 of their bytes. With
SHA-256 names, any file with an image failed to import ("Internal error
during import").
- Component sets hold their property definitions, variant properties
included (stateGroupPropertyValueOrders); each variant has its own copy
of each definition (parentPropDefId) and its variantPropSpecs, and its
layers and instances name that copy. Definitions and values also travel
as varValue; text values carry their lines; layers get componentPropRefs
next to the parameter consumption map. Before, sets came in without
variant properties and with their other properties unbound.
- An axis without a maximum size is written as infinite: 0 collapsed
auto layout frames with only a maximum width.
- Compound weights are spelt the way font families name their faces
("SemiBold"; "Semi Bold" in Inter): text in Libre Franklin Semi Bold was
a missing font there and wasn't drawn.
- The kiwi encoder writes infinite floats.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the two hand-rolled Howard Hinnant days-from-civil copies with chrono
as a workspace dependency:
- crates/collab/src/message.rs: utc_date (YYYY-MM-DD HH:MM UTC)
- crates/ui-egui/src/home_recents.rs: date (Mon D, YYYY, UTC; keeps the
translated month names and trf formatting)
Output is unchanged. Neither function had a test, so each gains one pinning the
exact strings the old code produced (leap days included). chrono uses only std,
so wasm stays safe. AGENTS.md/CLAUDE.md now say time goes through chrono.
Follow-up to storytold/cadcraft#1785
Texts an auto layout places keep the layout's position (the next layout
pass would take the baseline move back). A faded layer's group takes the
layer's id, so stacks, links and overrides still find it; faded masks
stay masks. figcraft-format no longer depends on figcraft-layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symbol masters become components, also those copied from libraries (on a
page of their own); instances carry their master's layers with their
overrides applied (text, nested symbol swaps and hiding, images, layer
and text styles, nested paths) and resize them by their constraints, so
editing a master still updates every instance. A symbol inside itself
stops with a note; instances of missing symbols are empty frames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(window): adopt shared craft-window for Linux decorations
Replace FigCraft's bespoke Wayland xdg-decoration probe
(apps/figcraft/src/linux_window.rs) with the shared craft-window crate
from craft-libs, so every Craft app agrees on who draws the window frame
per desktop.
The decision is now made once, before the viewport is built, by
craft_window::Choice::probe(Prefix::new("FIGCRAFT")) and mapped onto
FigCraft's existing TitleBar enum:
Frame::Desktop => TitleBar::Native (TitleBar::Inset on macOS, unchanged)
Frame::App(_) => TitleBar::Custom
This drops the old runtime send_viewport_cmd(Decorations(..)) switch and
the linux_window probe. macOS keeps its inset traffic lights and Windows
keeps the system frame exactly as before. The only behaviour change: on
KDE and other Wayland compositors with server-side decorations (Sway,
Hyprland, COSMIC) FigCraft now keeps the native frame instead of always
drawing its own custom title bar, matching the rest of the suite.
NOT MERGEABLE YET: craft-window is pinned to the unmerged feature branch
from storytold/craft-libs#217 and must be repointed to a merged rev
before this can land.
* fix(window): preserve Windows titlebar; pin craft-window to merged rev
- Keep FigCraft's own custom title bar on Windows. craft-window returns
Frame::Desktop on Windows, and the mapper was turning that into
TitleBar::Native, flipping Windows from its existing undecorated custom
bar to the system frame. Map Frame::Desktop to Custom on Windows (as
macOS maps to Inset); only Linux/BSD Frame::Desktop becomes Native. This
keeps macOS and Windows unchanged; the sole change stays KDE/Wayland.
- Repoint craft-window from the (now-merged) feature branch to the squash
commit on storytold/craft-libs (craft-libs#217), and commit Cargo.lock.
Verified: cargo build -p figcraft and cargo test -p figcraft (53 tests)
pass against the merged dependency; rustfmt clean.
ExportFormat::VectorDrawable ("XML", .xml) and ExportSetting.vd_compat
(bestEffort / exact / ignore, stored only when not the default) wire the
figcraft-vectordrawable writer into file.export, file.exportLayers and
export.add/update: a vdCompat param (junk refused), svgIdAttribute writes
android:name, and the results carry the writer's warnings (per file for
export layers). The Export section lists XML with an "Android Vector
Drawable" tooltip and its id attribute and compatibility options, File >
Export... gets an XML preset, and the toasts say when content was changed
or left out. CLI: --format xml / .xml outputs, --vd-compat, warnings as
note: lines. MCP: format xml returns the XML as text plus its warnings.
A .fig export writes XML settings as SVG with a note.
Docs: file-format-parity (row and revision), ui-parity and roadmap
revisions, architecture (the crate), cli and mcp.
A new L4 crate that writes layers as an Android Vector Drawable (XML):
shapes, booleans and text as baked path data, solid and gradient fills
(linear, radial and sweep, exact for any handle frame via a group and a
2x2 SVD), native strokes where Android draws them the same and filled
stroke outlines otherwise, clip frames and masks as <clip-path> groups.
What a drawable can't hold is governed by Compat (bestEffort / exact /
ignore): layer opacity over overlapping shapes, even-odd fills and clips
(rewritten to non-zero via pathops), diamond gradients, alpha masks;
effects, image and pattern fills and blend modes are left out with a
warning. Output is pretty-printed, numbers are finite and short, names
(android:name) are optional, escaped and unique.
The same JSON methods as the desktop control server (engine.execute, ui.inspect, ui.screenshot, …),
as promises on window.figcraft, like FilmCraft's window.filmcraft: an agent driving the editor in a
browser tab or an <iframe> uses the same commands as the CLI and the MCP server. openFile opens
bytes the way a dropped file opens; captureDownloads keeps saves in the page for readFile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A document carries things the app acts on for whoever opens it. Three of them were
taken as given.
Prototype Open link and Dev Mode dev resources both reached the platform's opener
with any scheme. In the web build the opener is window.open, so a saved document
could run script in the app's own origin; on the desktop any scheme went to the
system's handler. Both now pass through the new engine module `doc_urls`, which
allows http, https and mailto and nothing else. It is an allow-list on purpose: a
list of known-bad schemes is outgrown by every handler a platform adds.
A figcraft:// invite link joined the session at once. The system hands the app
whatever such link was followed, which may come from a page or a message, so the
Join dialog now opens with the link filled in and the person confirms.
A library link stored in a document was loaded from wherever it pointed. Only a
path on this computer is followed now, so opening someone else's file cannot make
the app contact their server (which on Windows would also send credentials).
Tests: each change has one that fails without it, verified by reverting. The
prototype test fires through an afterDelay trigger, since no click is needed, and
keeps a positive case so a refusal cannot pass vacuously.
egui-winit swallows the Cmd/Ctrl+V press and sends a paste event only
when the clipboard holds text, so with an image on the clipboard only
the key's release arrives and the image fallback, which waited for the
press, never ran. Paste from a V release whose press never came and
brought no paste event; the press and paste event are tracked from the
raw events at the start of each frame.
On Wayland arboard read the X11 clipboard, which the compositor does
not fill for a native Wayland window, so every system paste found it
empty. Enable arboard's wayland-data-control: it uses the Wayland
clipboard when WAYLAND_DISPLAY is set and falls back to X11 otherwise.
Right-click > Paste here now follows the same newest-wins rule, so a
newer image beats copied layers there too, and lands at the point.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
winit 0.30 reports no system theme there (EventLoop::system_theme is
None), so the "system" preference resolved to light every time. When
the windowing stack gives no theme, the interface now follows the
freedesktop colour scheme instead: read once and watched live through
the settings portal (org.freedesktop.appearance / color-scheme) in the
desktop services (Services::system_dark), so GNOME's and KDE Plasma 6's
appearance setting switches the app without a restart. Where the OS
theme is reported (macOS, Windows, web) it still wins, and without any
answer the light fallback stands.