Commit Graph
1442 Commits
Author SHA1 Message Date
sushi-killer 1b4db71957 fix(collab-net): keep the client's message cap in range on 32-bit targets 2026-10-17 00:37:55 +03:00
Ahmed5Emad 3b5c5d563b Palette follows the bar: toggle, dismiss, no invented icons
The Actions button toggles (re-click and Cmd+K close), reads on in
accent blue with a white icon while open, and a press anywhere outside
closes the panel in one click. The opening press is spared once (the
spare expires on press-free frames); presses on the button itself never
dismiss, so the release toggles cleanly shut instead of reopening.

Rows drop the per-family icons (labels + right-side hints only);
plugins and widgets keep their letter tiles. The panel anchors to the
canvas centre at the toolbar pill's exact width (toolbar::main_width),
at a fixed size, with inset hover pills and larger section titles.

Tests: one-click dismiss pinned (palette_tests).
2026-10-16 23:27:45 +03:00
sushi-killer b0f56ed169 fix(server): let message limits follow max_file_mb so files above 64 MiB publish and open 2026-10-16 23:19:32 +03:00
Ahmed5Emad dc97118c6f Actions palette rework: Figma-shaped panel over the toolbar
Tabs (All, Assets, Plugins & widgets), Recents kept in the preferences
(newest first, capped at 5), commands grouped by top-level menu, this
file's components (placed at the view centre) and styles (applied to
the selection), installed plugins and widgets with letter tiles. Rows
carry right-side shortcut/kind hints; Enter runs the first visible row.

The panel floats 8 px above the toolbar pill with its exact width and
centre (toolbar::main_width, canvas-centred), at a fixed size so tab
switches never resize it. Tabs keep their frames when idle so the
active pill never moves its neighbours.

Engine: Prefs.recent_actions (persisted, skipped when empty). UI:
invoke/menu/plugin picks record recents; gone entries drop out.
Locales: new keys in en + es, retired the old search hint everywhere.
Docs: ui-parity row describes the new surface (palette_tests).
2026-10-16 22:58:36 +03:00
Yarila 8c90ef962a M7: open inputs for parameterized menu actions 2026-10-16 23:36:23 +05:00
RealtaNuaandClaude Opus 5.5 d27f45e063 fix(format): write a list's start on every line; report it in get_node (#686)
The .fig export now writes a numbered list's start on each of its lines,
so a reader that takes it per line or per list counts the same. Bullets
no longer keep a start. The field is named start_offset (it holds the
offset: 1 shows "2."). get_node reports listStartOffsets. The parity doc
lists what's still not read from .fig lists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 00:58:20 +07:00
NetterSilverandClaude Haiku 5.5 9aeb9fb752 Cap stroke width in the outliner so huge weights don't panic
Found while testing crates/engine/src/cmd/props.rs:361 (strokeWeight is only clamped with .max(0.0)); the panic is raised when the renderer draws the stroke (render crate / tiny-skia stroker)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:58:00 +02:00
RealtaNuaandClaude Opus 5.5 4adbd6cee9 fix(ui): a V held while the window was away doesn't paste on release (#663)
winit sends no press for a key held across a focus change (X11's
synthetic presses are dropped by egui-winit), so letting go of V after
coming back looked like a swallowed Cmd+V. After a focus gain, a V
release only pastes with Cmd still held.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 00:35:46 +07:00
RealtaNuaandClaude Opus 5.5 60fa813eb2 fix(format): numbered lists from .fig files keep their start number (#686)
Figma stores where a numbered list starts counting on each text line
(listStartOffset; a list shown as "2." has offset 1). The import dropped
it, so every list counted from 1.

A paragraph's list settings now carry a start number (written only when
set, so older files open and save unchanged). Numbering counts on from
it where a list starts; items that go on counting ignore it. The .fig
import reads it, the .fig export writes it on a list's first line, and
plain paragraphs drop it. Toggling a list off ignores differing starts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 00:27:34 +07:00
RealtaNuaandClaude Opus 5.5 569a3b3669 fix(engine): a Shift-click on a selected widget doesn't also click it (#498)
With Shift let go before the button, the release took the layer out of
the selection and also ran the widget's click. The take-out now skips
the widget click, as a Shift-click always has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 23:11:00 +07:00
RealtaNuaandClaude Opus 5.5 40955aae7c fix(ui): Cmd/Ctrl+V pastes an image copied in another app (#663)
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>
2026-10-16 23:09:59 +07:00
Dylan M. Taylor d1d8c3f652 ci: standardize FreeBSD workflow on shared FreeBSD 15.1 action (#640)
* ci: standardize FreeBSD workflow on shared FreeBSD 15.1 action

* ui: widen About window so contributor table fits with Inter font
2026-10-16 10:08:58 -06:00
RealtaNuaandClaude Opus 5.5 8303cfa56f fix(engine): Shift-drag on a selected layer moves the selection (#498)
Shift on a selected layer took it out of the selection on press, so the
drag that followed had nothing to move, and with several layers selected
the grabbed one stayed behind. It now leaves the selection on release,
and only when the press didn't become a drag. Where no move can start
(a version being viewed), it still leaves on press.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:52:41 +07:00
Barzabel 554821afef M9: a damaged generation field no longer drops a log's flushed edits (#408)
A log whose generation isn't its snapshot's was skipped as a leftover of a
compaction cut short, but neither generation is under a checksum: one flipped
bit made the server serve the older document, report nothing and overwrite the
log. Replay such a log over the snapshot instead: replay skips what the
snapshot already holds, so a genuine leftover changes nothing, while edits the
snapshot lacks are recovered and the damage is reported.
2026-10-16 17:31:11 +03:00
NetterSilverandClaude Haiku 5.5 0e25cf4c26 Refuse pages in edit.copy and edit.cut
Found while testing crates/engine/src/cmd/edit.rs:499 (copy_to_clipboard, called from copy at :536 and cut at :542)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 13:47:07 +00:00
Marco Arnulfo 3065cd6da9 M3: drag variant properties to reorder them in the Properties section 2026-10-16 15:05:00 +02:00
Yarila ac1a192a3a M7: keep mobile presenter fixed and confirm window close 2026-10-16 17:25:13 +05:00
phonedu 9daf39e24c M1: a param that names a layer says so when it isn't one
`node.create` read its `parent` as `id(p, "parent").unwrap_or(page)`, so a `parent` that isn't a
layer id — a `{"$step": n}` nothing had resolved, a name, a negative number — quietly meant "no
parent" and the layer landed on the page, with the call reporting success. The same shape sat on
`image.place` and `instance.create`, on `index` (a string index appended where it was asked to
insert), and on the `ids` of every command that targets layers (an entry that wasn't an id was
dropped and the rest ran). A number that names nothing was reported; anything that isn't a number at
all was not.

`params::opt_id` and `params::opt_index` now say what the commands mean: missing and `null` are
"none", and anything else has to be a layer id (or a whole, non-negative number) and is named when it
isn't. `req_targets` refuses an `ids` entry that isn't an id, before falling back to the selection. A
number that names no layer still reports `no such node`, and a boolean is still a number to `f64_of`,
as it is for `x`, `width` and the rest.

This turns a silently wrong result into an error on purpose, so a caller that passed something that
isn't an id and relied on the fallback now hears about it.
2026-10-16 19:53:43 +08:00
Ahmed5Emad 1aa499c84f Merge remote-tracking branch 'origin/main' into feat/font-hover-preview
# Conflicts:
#	crates/ui-egui/src/style_picker.rs
#	crates/ui-egui/src/typography.rs
2026-10-16 14:34:02 +03:00
phonedu 0f4c79b629 M0.9: a script runs as one undo step and resolves $step, like command.batch
`figcraft-cli run` ran each step of a script on its own, so a script's `{"$step": n}` reached
`parent` unresolved and the layer landed on the page while every step reported success — the same
commands through `command.batch` (the MCP `batch` tool, the control channel, plugins) put it inside
the frame. One `edit.undo` took back one step of a script rather than the script, and a failing step
left the earlier ones applied.

`Session::batch_step` is now the one path both take — one step of a batch, its `$step` references
resolved against the results before it — and `run_script` wraps its steps in `Session::grouped`: the
whole script is one undo step, and a step that fails takes the script back. The result lines come out
once the script is through, so none of them reports work a later failure took back; a failure prints
one line with `rolledBack: true` and exits 1. `docs/cli.md` documents the reference and the output.
2026-10-16 19:33:59 +08:00
Hamza 805a121e15 Hold the duplicate step for a dragged and a nudged copy (#567)
Duplicate repeats the last duplicate's offset, but of the ways to move the copy
before the second Cmd+D only `node.set` and Alt-drag were covered. Dragging it
with the mouse is how almost everyone does it, and is what #567 describes; an
arrow-key nudge is how a small offset is usually made exact. Both work, because
the step is measured from where the copy ended up rather than recorded from the
gesture, but nothing held them there.

The drag test also asserts the drag moved the copy rather than making another
one, left it selected, and left the original in place: the ways the feature
could break without the step arithmetic breaking.

Both fail if the step lookup in `edit.duplicate` is stubbed out.
2026-10-16 16:30:58 +05:00
Ahmed5Emad e1d9f24312 Merge remote-tracking branch 'origin/main' into fix/layout-add-keeps-frame-size 2026-10-16 14:27:19 +03:00
Hamza c5a8420089 Make the control server's connection-limit test pass on macOS
`cargo xtask ci` was red on a clean main on macOS: the test's last shutdown
runs after the server has answered an invalid request and closed the
connection, and `shutdown(2)` on a socket whose peer has already closed is
ENOTCONN on macOS and the BSDs where Linux returns success.

The test is right about what it checks; it checked a return value that is not
portable. Closing the client is what the worker thread waits for, and dropping
the socket does that whether or not shutdown reported anything. The file's
other six shutdowns run while the peer is still open, which is well defined on
both platforms, so they stay checked.

With no CI running the tests on pull requests, the local gate is the only
signal a contributor gets, and it could not be green on macOS.
2026-10-16 16:27:05 +05:00
Marco Arnulfo 7d7a724764 Text: selection highlights after a line break start at the line's start, and the empty last line shows (fixes #665) 2026-10-16 13:27:01 +02:00
Andrii VolkovandClaude Opus 5.5 52eb736241 perf(render): look each layer up once per render
Node lookups in the document's B-tree were still a third of a render on a
900 000-layer file: a small subtree was walked for its bounds, again to
see whether it could be splatted, and again to splat it, and each child
of a drawn layer was looked up by paint_runs, by the mask check and by
draw_node.

- The render's table also keeps the layers it looked up, keyed with a
  multiply hash instead of SipHash (ids are counters), and so do the
  bounds it keeps.
- draw_children looks each child up once, works out the paint runs from
  those (Document::paint_runs_of) and draws the child it has.
- Whether a layer can be splatted is kept like its bounds: layers with
  children only, answers the nesting cap decided aren't kept, and kept
  answers are read only where their levels fit under the cap.

Ctx gets a second lifetime, for the document the kept layers come from.

Single-threaded cycles per 1440x900 render fitted to the page: 0.39 ->
0.34 G on a page of 246 204 layers, 0.26 -> 0.22 G on one of 118 358.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
2026-10-16 13:20:53 +03:00
Yarila be3912f986 M7: Reflow presenter device screens and simplify tablet and desktop previews 2026-10-16 15:05:13 +05:00
Andrii VolkovandClaude Opus 5.5 4484c2d1b0 perf(render): work out paint bounds once per render
Culling a layer asked for its paint bounds, which walk its whole subtree,
and then did the same for each child: every layer's subtree was walked
again at each level above it, and once more in each parallel band. The
bounds worked out during a render are now kept for it (layers with
children only; bounds the 64-level cap cut short are not kept, and kept
ones are read only where their levels still fit under the cap, so culling
is exactly as before).

Every render also looked through every layer of the document for the
largest background blur, to size the overlap of its bands: 900 000 layers
on one large file, on the main thread before the bands start. The
document now works it out when first asked after a change, like whether
it has masks.

A 1440x900 render on a 900 000-layer file, page of 118 358 layers: at
100% 13.0 -> 0.6 ms, at 25% 16.5 -> 3.4 ms, fitted to the page 56 -> 31 ms;
page of 246 204 layers: 12.6 -> 2.5 ms, 12.1 -> 2.0 ms, 69 -> 62 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
2026-10-16 12:31:55 +03:00
Ahmed5Emad 2cac36a4e2 Live font preview on hover in font picker (#653)
Hovering a family in the Typography font dropdown previews it on the
canvas via text.preview; closing without picking restores the original,
clicking applies it as one undo step.
2026-10-16 12:31:50 +03:00
Yarila 6d3e7aad20 M7: isolate presenter tab and device controls 2026-10-16 14:16:44 +05:00
AganzeFelicite b962603707 M4: points dropped on a point merge
In vector edit mode a point dragged onto another point of its path
snapped there but stayed a separate point, so a path couldn't be closed
or joined by dragging. A single dragged point dropped within the point
hit radius (6 px) of another point now merges into it, in the same undo
step as the move, and that point stays selected.

Closes #401
2026-10-16 10:46:36 +02:00
loyet 11455e0fe9 M1: .fig import reads images carried in the message (image.dataBlob)
Copies written by plugins and tools carry their image bytes in the
message: a paint's image.dataBlob indexes the message's blobs. A bare
canvas (what the clipboard holds, see #647) has no images/, so these
fills came in as grey placeholders. The importer now falls back to the
embedded bytes, keyed by image hash, after the archive's images/.

Blobs over the image size cap and indexes past the blobs are skipped and
stay placeholders. Tests: a bare canvas carrying its image in a blob, and
a dataBlob past the blobs.
2026-10-16 10:44:29 +02:00
NetterSilverandClaude Haiku 5.5 a911ab9614 Reject negative or fractional index in fill.set and stroke.set
Found while testing crates/engine/src/cmd/edit.rs:1180

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 08:29:55 +00:00
Andrii VolkovandClaude Opus 5.5 afdb21bad6 fix(ui): drag the line between pages and layers to size the pages list
The pages list took a fixed 40% of the left panel (#272). The line under
it now drags to make the list taller or shorter: at least three rows, and
always leaving the layers their header and three rows. A double-click puts
back the default. The main window keeps the height in the preferences, as
it does the panel widths, saved once the drag is over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
2026-10-16 11:24:39 +03:00
AganzeFelicite e8b181de39 M4: Flatten keeps open paths
Flatten (Cmd+E, Alt+Shift+F) and Join selection (Cmd+J) built the result
from each layer's fill outline, so open paths, which have none, were
dropped: two pen paths flattened to "nothing to flatten", and next to a
closed shape they vanished. Vectors now bring their own network (open
paths, unfilled loops and each region's winding stay), mapped into the
parent's space; other layers still flatten to their outline.

Closes #377
2026-10-16 10:23:57 +02:00
Andrii VolkovandClaude Opus 5.5 0efddc3894 fix(format): reopen large documents once saved
`document.json` was read under the 512 MB cap for any archive member, so a
large imported document saved but didn't open again: 900 000 layers save
as a 1.48 GB document.json, packed into 244 MB. It may now unpack to 16
times its packed size (counted up to the archive's own size), within
512 MB and 4 GB; images and other members keep 512 MB. A small archive
still can't unpack into gigabytes.

That saved file reopens in 13 s with a 5.9 GB peak.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
2026-10-16 10:46:08 +03:00
Andrii VolkovandClaude Opus 5.5 12d43b1f99 fix(format): open large .fig files
A real 705 MB .fig file (401 644 nodes, 894 004 layers once instances are
expanded) didn't open: "message: too many values". Lifting that cap alone
made it import only its first 200 000 layers, and lifting that one took
41 GB of memory.

The global caps stay as they are. The .fig import gets budgets from the
size of its compressed canvas instead (counted up to the file's own size),
not from the message it unpacks to, since chunks inflate up to 512 MB:
- the kiwi value budget is one value per 2 bytes of the canvas, at least
  the old 20 million (kiwi is only read for .fig files);
- the layer cap is one layer per 32 bytes of the canvas, at least the REST
  import's 200 000 and at most 2 000 000. A small or crafted file can't
  expand its instances into millions of layers, however far it unpacks; the
  REST JSON import keeps 200 000.

Memory:
- The mapper no longer passes the document through `json!`, which serialises
  its arguments: the whole tree was deep-copied twice.
- Vector networks are built once per distinct network and handed to the REST
  importer beside the JSON (`vectorNetworkRef`), not written into it as JSON
  objects for every instance that shows them.
- Decoded kiwi objects are trimmed, and each import stage's input is dropped
  as soon as the next stage is built.

On that file: peak footprint 41 GB -> 11.3 GB, 67 s -> 25 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
2026-10-16 10:46:08 +03:00
Skyfuller1303 41d465a678 M1: reword the vector segment style import notes 2026-10-16 09:40:06 +02:00
Skyfuller1303 d8e64a73bf M1: reword the vector segment style import notes 2026-10-16 09:40:04 +02:00
Skyfuller1303 771f7e869f M1: reword the unresolved point style import note 2026-10-16 09:40:04 +02:00
Skyfuller1303 a4156d2910 M3: re-read a library file whose stamp is as recent as the read
A library file is cached by its length and modification time. File systems keep the time in coarse ticks, so a same-length rewrite right after a read can keep the stamp, and library.updates then reports no change. A stamp now proves the file unchanged only once its modification time lies at least two seconds before the read; otherwise the file is read again.
2026-10-16 09:37:07 +02:00
Skyfuller1303 657a8f4df6 M1: translate the vector style import notes 2026-10-16 09:16:10 +02:00
JimKnoxxandClaude Opus 5.5 ab10e37d43 M1: paste layers copied in Figma
Figma puts copied layers in the clipboard's HTML as a base64 .fig canvas
between (figma) markers. edit.pasteFromFigma reads it with the .fig import,
turns the layers into the session clipboard and pastes them as edit.paste
does (selected frame, in place or view centre), with the import's warnings.
The desktop app reads the clipboard's HTML (arboard) and Paste prefers a
Figma copy; the browser's paste event carries plain text only.

Tested on copies written by our own .fig export; not yet on real copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:20:49 +02:00
Ivan Shishkanov 5be9a8caef M1: cache smart-selection geometry and skip unchanged drag edits 2026-10-16 08:06:26 +03:00
Ivan Shishkanov 1ac0e475c1 M3: adapt Selection colors to current main 2026-10-16 07:58:58 +03:00
Patrik LermonandClaude Opus 5.5 107aa27d0a docs: Swedish now covers the command labels and menu wordings
ROADMAP.md, docs/localization-parity.md and docs/translating.md said
Swedish had 120 of the 588 command labels and menu wordings; with the
previous commit it has all of them. Worded without counts, so the docs
stay right as commands are added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 06:20:40 +02:00
Zaxaerith 167e06f695 docs: correct example plugin count (#392) 2026-10-16 12:08:26 +08:00
Dylan M. Taylor cc26c29217 ci: switch to shared nix workflow from craft-common-actions 2026-10-15 23:57:45 -04:00
Patrik LermonandClaude Opus 5.5 a00b2e0583 i18n(sv): translate the command and menu labels
The [commands] and [menu] rows held their command id as the text
("align.bottom" = "align.bottom"), which the menus and the command
palette would show as is. They now translate the English labels the
code gives those ids (spec! and m! in crates/engine), e.g.
"align.bottom" = "Justera nederkant". Also the interface strings that
are new on upstream main (23bdebd).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:42:54 +02:00
Patrik Lermon ba3455cc6e Merge upstream/main into swedish-ui-2026-10-15 2026-10-16 05:41:29 +02:00
skylar taylor-barrick 23bdebd973 Merge pull request #176 from dylanmtaylor/use-craft-common-actions
ci: delegate release, freebsd, windows-arm64, packaging-lint to craft-common-actions and add nix check
2026-10-15 21:34:16 -06:00