Commit Graph
1149 Commits
Author SHA1 Message Date
Edivan TeixeiraandClaude Opus 5.5 13691c1cc0 Let batch steps reference ids created by earlier steps
Inside command.batch, `{"$step": n}` anywhere in a step's params is
replaced by the `id` in step n's result before the step runs
(`{"$step": n, "field": "..."}` picks another field). A nested tree,
such as a frame with children, now takes one batch and one undo step,
not one round-trip per level with ids copied between calls by hand.

Malformed references (forward or self, negative or fractional index,
missing or non-string field, extra keys) fail the batch with BadParams
naming the step, and the existing group machinery rolls it back. The
params walk is depth-capped. Steps are journalled with resolved params.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:43:31 -03:00
NetterSilverandClaude Haiku 5.5 d1430e3054 Keep text selection highlight inside the box at the end of the text
Fixes #365

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:56:27 +02:00
NetterSilverandClaude Haiku 5.5 fc6f52e852 Keep corner resize cursors diagonal on wide and tall layers
Fixes #370

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:50:00 +02:00
NetterSilverandClaude Haiku 5.5 3c00ea6440 Outline stroke on every selected layer, not just the first
Fixes #378

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:41:17 +02:00
NetterSilverandClaude Haiku 5.5 3d90d8edfe Keep View toggles such as Rulers in the preferences
Fixes #379

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:33:28 +02:00
NetterSilverandClaude Haiku 5.5 87b9a3a9ce Reject negative or fractional index in effect.set, export.update and export.remove
Found while testing crates/engine/src/cmd/edit.rs:256 (effect.set), :305 (export.update), :317 (export.remove)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:08:36 +00:00
vxMacDev 12d4dbfbc9 fix(engine): new text is readable on the frame it is drawn in (#362)
The colour of new text in the app was chosen from the page alone, so
on a dark page text drawn in a white frame (a new frame's default) was
white and invisible. readable_text_fills now looks at what the text is
drawn on: the nearest layer from its parent out whose top visible fill
is a mostly opaque solid colour, else the page. The Text tool and
text.create / node.create pass the text's parent.

The two tests that asserted the page-only rule in a frame now expect
black text in a white frame; new cases cover dark, unfilled, nested,
faint and stacked fills, and the Text tool.
2026-10-14 18:35:21 -05:00
Mateus Villain 3edb15c417 M1: copy and paste through the system clipboard in the browser
The web build's Services never wrote to the clipboard, so Cmd+V after
Cmd+C got the paste event with whatever text the clipboard held before
and made a text layer of it instead of pasting the copied layers.
WebServices now writes copies (text and PNG) through egui's output,
which eframe hands to the browser clipboard during the copy event.
2026-10-14 20:26:18 -03:00
Ahmed5Emad 93a75a4720 M1: adding auto layout to an existing frame keeps its size instead of hugging 2026-10-15 02:02:00 +03:00
Mateus Villain 01a0f4f332 M3: variables export as a download in the browser
The web build has no save dialogs, so Export… ▸ Variables JSON / CSS
custom properties returned silently. Services gains downloads() and
download(name, bytes); the browser hands the file over through a Blob
link, and the variables panel uses it where the platform saves by
downloads.
2026-10-14 20:01:53 -03:00
vxMacDev b87d2e476f fix(engine): drag with the Text tool to make a fixed-width text box (#360)
The Text tool made its text on the press and ignored the drag, so a
drag gave auto-width text like a click. The new text now stays in a
TextBox gesture while pressed: past the drag slop it becomes a box as
wide as the drag, its top-left at the drag's, with a fixed width and a
height that follows its text. A click, or a drag within the slop, still
makes auto-width text.
2026-10-14 17:29:09 -05:00
NetterSilverandClaude Haiku 5.5 2aa3ca3c45 Make edit.findReplace replace case-insensitive matches
Found while testing crates/engine/src/cmd/edit.rs:962-977

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 22:26:37 +00:00
Skyfuller1303 7a8e5373b1 M7: keep Windows windowed UI and canvas text sharp 2026-10-15 00:02:41 +02:00
NetterSilverandClaude Haiku 5.5 c02e864873 Replace every occurrence in edit.findReplace when replacement contains find
Found while testing crates/engine/src/cmd/edit.rs:975-981

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:22:29 +00:00
Rob CoenenandClaude Opus 5.5 3c03f370f9 M1: Cmd+R rename test sends Ctrl+R only where Ctrl is the command key
On macOS egui never sends `ctrl` together with `command`; Ctrl comes alone there
and Ctrl+R isn't a shortcut (rename is Cmd+R or F2), so case 1 failed on every
macOS run. The keymap is right; the test now presses Ctrl+R only off macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:07:05 -04:00
vxMacDev 8efb66ba19 fix(ui): step through the font picker with Up and Down (#345)
Up and Down now move a highlight through the family list, starting
from the current family and scrolling to keep it in view. The text
panel previews the highlighted family on the canvas, as it does for a
hovered font style, and Enter applies it. Escape leaves the text as it
was.
2026-10-14 15:24:40 -05:00
midasmatterhorn 65e52aa7b9 engine: long digit strings no longer overflow object.opacityKeys
opacity_from_digits multiplied and added on every character of its input
string before the one-or-two-digit check, so "9999999999" overflowed u32 on
the way (panic in debug builds, wrap in release). The scan now steps n with
checked_mul/checked_add and stops (None) at the first character the number
cannot take, so a long digit string is refused like any other bad input.

Test: opacity_digits (engine keymap) covers the ten-digit string and the
command's BadParams refusal.
2026-10-14 23:22:41 +03:00
IanHollow 1ddc2c338b Fit contributor columns inside the About window
Assisted-by: OpenAI Codex (GPT-6)
Signed-off-by: IanHollow <72767437+IanHollow@users.noreply.github.com>
2026-10-14 13:12:53 -07:00
HaSh3003 f387d3f464 M9: translate Remove from session
The menu item and the collab.removePerson label in es, de, fr, ja, ko, pt-BR and zh-Hans (es must be complete).
2026-10-14 23:00:42 +03:00
midasmatterhorn 245837ff27 doc: name counters stop at u64::MAX instead of overflowing
Document::next_name summed max + 1 over numbers parsed from existing layer
names, and bump_name did the same for the duplicate-rename path: a document
(or peer, or node.set) holding a layer named "Frame 18446744073709551615"
overflowed both (panic in debug, wrap to a wrong name in release). next_name
now collects the numbers it sees and, when the counter is spent, picks the
lowest free number (mirrors new_style_id); bump_name only bumps when the
number can step, else falls to its existing " 2" branch.

Tests: next_name_never_overflows (doc) and
duplicating_a_layer_named_past_the_counter_does_not_overflow (engine panel
behaviour).
2026-10-14 22:52:15 +03:00
midasmatterhorn ca2f5dbcee collab: refuse sequence numbers a host cannot advance
The welcome's seq and processed.last_batch are the host's word and were used
unchecked: accepted computed self.seq + 1 and queue bumped next_batch, so a
host sending u64::MAX overflowed the client (panic in debug, wrap in release).
Both counters now step with checked_add/saturating_add, and a sequence that
cannot advance is refused as a protocol error (the established rejoin path).

Test: hostile_sequence_numbers_do_not_overflow_the_client.
2026-10-14 22:47:51 +03:00
Barzabel 5e14632207 M1: cursor and double-click tests scan the system fonts themselves (#333) 2026-10-14 22:39:17 +03:00
HaSh3003 fa0365470e M9: remove someone from a live session
Right-click a person's avatar ▸ "Remove <name> from session", or
`collab.removePerson {client}`, when you host the session or own the file
on a server. The hub closes their connection as the end of the session for
them (close 1001: they keep their copy and don't reconnect) and bars their
address from that file while it runs, so the code or link they hold no
longer gets them in; everyone else stays connected.

Protocol addition within version 2: `Admin::RemovePeer {file, client}` on
an open hosting connection with the file's owner token (or the server
key). Older servers say they can't.

Tests: loopback (removed, can't rejoin, others stay), engine (host can,
guest can't, bad ids refused), codec round trip; the collab junk sweep
covers the new command.
2026-10-14 22:32:56 +03:00
Barzabel b8043cc2ad M3: renaming a style folder no longer closes the app 2026-10-14 22:23:51 +03:00
Patrick Wozniak 30478894fd M0.9: a window on screen draws requests for it while the main window is hidden (#269)
Requests reach the UI through the main window's logic. A request for
another window is answered in that window's frame, and the main window's
frame (`drain`) asks that window to draw. While the main window is
hidden, only its logic runs and nothing asked: an idle window on screen
draws no frames, so the request waited until something else repainted
the window, or timed out after 60 s.

`drain_hidden` now asks a window on screen that a request waits for to
draw. A hidden window isn't asked: it can't draw, and the main window's
logic already answers what it can.
2026-10-14 21:17:42 +02:00
Filip Strajnar 6bd87bccc6 M7: the Small caps sample in Type settings draws without a box
The Small caps button in Type settings labelled itself "Aɢ", but the
small-capital G (U+0262) is missing from egui's default fonts, which the
UI uses whenever Inter isn't built in, so the button showed "A" and a box.
The sample is now a capital followed by smaller capitals laid out from
ordinary letters every UI font has. A test checks the UI fonts draw every
letter of the sample.
2026-10-14 21:12:39 +02:00
Patrick Wozniak 64eae8c8c5 M0.9: every hidden window answers control requests, not only the main one (#269)
`drain_hidden` answered only requests for the main window, and only
while that window was hidden. A request addressed to another window
(`window.new`, `window` param) that is minimised or covered still waited
for that window's frame and timed out after 60 s, whether the main
window was on screen or not. An agent that works in its own window lost
it as soon as the window went behind others or to another Space.

`drain_hidden` now looks at the target window of each request: if that
window is hidden (`ViewportInfo::visible() == Some(false)` for its
viewport) and the method needs no frame, it is answered from the main
window's logic with the target window's state swapped in, as while it
draws, so it acts on that window's file. `draw` and the new
`windows::as_window` share the swap (`enter`, `adopt`, `leave`). The
main window's logic runs on every request (the control server asks for
a repaint), so this works while the main window is on screen too. What
needs a frame keeps waiting, as before; a request for a window that
doesn't exist is an error at once instead of waiting for a frame.
2026-10-14 20:45:19 +02:00
vxMacDev b821b6f8f4 fix(recovery): keep the version history and branches in recovery copies (#185)
Recovery copies were written with plain `save_bytes`, so a file
recovered after a crash came back without its version history and
branches, even though `file.recover` reads both. Copies now go through
`save_bytes_with_history` like a normal save, with each open branch
tab's latest edits (`branches::current`), since `sync` only puts those
in before the next command.

A copy was also only redone when the main file's revision changed, and
a file with edits only in a branch tab didn't count as changed. Both
checks now include the file's branch tabs.
2026-10-14 13:41:17 -05:00
Barzabel a3048770b6 M1: deeply nested layers don't run the layout out of stack (#210) 2026-10-14 21:37:55 +03:00
Rob CoenenandClaude Opus 5.5 3e777d2485 M7: home screen test waits for the recent-file reader
The first-run test settled the home screen with a fixed 12 frames, but
the recent files are looked at on a thread of their own and a card shows
no name until that thread has answered. On a busy CI runner it hadn't
yet, so "ridgeline" was still a grey placeholder and the test failed
(seen on this PR and on codex/issue-250-async-dialog). settle() now waits
until every recent file has been looked at, as home_recents_tests does.
Checked with a reader slowed to 200 ms: the old test fails at the same
line as CI, the new one passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:30:10 -04:00
Marco Arnulfo ebf6045184 M1: SVG export keeps a nested frame's fill and stroke inside its parent's clip (fixes #325) 2026-10-14 19:40:08 +02:00
Barzabel 15ae2e9115 M1: memory budgets for decoded images and a file's images (#203, #189) 2026-10-14 20:36:47 +03:00
Patrick Wozniak 639c4d7db0 Home: test the file row layout on Open files
Recent files are cards since #249, so the row test now opens two
documents with synthetic paths and checks the Open files list: the row
that fits shows name and path in full; the long one shows the name cut at
its end, then the path cut at its start, at least 12 px apart, and both in
full on hover. Without the row fix the name shows as "Quar…" under the
path.
2026-10-14 19:05:12 +02:00
Patrick Wozniak f1e0a758d1 Home: file rows no longer draw the path over the name
The Open files and Recent files rows painted the detail (the file's path)
right-aligned at its full width and only then elided the name into
whatever was left, with a floor of 40 px. A path wider than the row ran
straight over the name, and the name itself shrank to "Mae…".

The row is now laid out name first: the name keeps its full width when
both fit, otherwise at least 60% of the row; the detail gets the room
left after a 16 px gap and is cut at its start ("…/02-technik/KNX/
file.figcraft"), which keeps the file and its nearest folders. A detail
with less than 48 px of room is left out. Rows of Recents' own lists show
name and path in full on hover when either was cut.

The new widgets::elide_start_font mirrors elide_font, keeping the end.
2026-10-14 18:59:47 +02:00
Rob CoenenandClaude Opus 5.5 9404226235 M4: vector edit mode keeps the canvas still under the pointer
With no points picked, the Design panel's hint ("Select points to edit
their position, radius and mirroring") didn't wrap and made the canvas
49 points wider, so entering vector edit mode moved the view by about 25
points and picking a region moved it back. The jump came in the middle
of the click that picked the region: with the smallest pointer movement
the release landed elsewhere on the page and the region was dragged by
that distance (about 47 units at 70%). Both edit mode hints now wrap.
Regression test: the canvas rectangle stays the same entering edit mode,
picking a region and switching to the width tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:35:06 -04:00
Rob CoenenandClaude Opus 5.5 d5fee489f5 M7: recent files notice files deleted while the home screen is open
The recent files' dates were only read again when the home screen came
back into view, so a file deleted or moved in the meantime kept its card
until then. They are now also read again every 2 seconds while in view,
and at once when the window gets the focus back; a file that is gone
shows "File not found" without leaving the home screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:07:53 -04:00
tylerjenningsw 72390d7b51 fix(canvas): preview in the browser and judge previews by coverage (M8)
Review of the zoom previews found three problems:

- The browser worker never previewed: wasm builds timed every render
  as 0 ms, below the 12 ms threshold. Renders are now timed with
  web-time's monotonic clock on every target (already in the tree via
  the engine), and the inline worker is built everywhere so a test runs
  it with the production threshold: preview one frame, refine the next,
  a newer request skips the old refinement.
- A preview was refused whenever the texture on screen was refined at
  the same zoom, even when that texture showed another part of the page
  (back at 100% about another point after 800%: 1/64 of the view). It
  now has to show the whole view asked for, not just its zoom.
- The latencies read the frame's start time before the inline render
  and before the texture was set. They now run on the same clock from
  the request to the image handed to egui as the canvas texture.

Refs #295
2026-10-14 11:57:58 -04:00
Barzabel 698e249fbf M1: drop shadows under a background blur (#255)
A layer with a background blur (or glass) drew its blurred backdrop into
its own pixels before casting its drop shadows from them, so a translucent
fill cast a shadow as dark as an opaque one, and with "show behind
transparent areas" the shadow was cast from the backdrop's pixels. The
backdrop was also read before the shadows were drawn, so a shadow showing
behind was never seen through the blur.

When a layer has both, it now casts its shadows from what it paints (with
the layer blur, noise and texture it will get), draws them, and only then
reads the backdrop, which goes under its content; the content still sees
the first backdrop under it while it's drawn. Layers without both draw as
before.
2026-10-14 18:45:15 +03:00
Marco Arnulfo 1aeb5abb74 M1: a click on the toolbar no longer reaches the canvas under it (#307) 2026-10-14 17:43:32 +02:00
AganzeFelicite f9b7aa03ca M0.9: answer control requests while the window is hidden
A minimised or covered main window draws no frames, and control requests
were only answered inside a frame, so every control-channel and attached
MCP call (save_file among them) timed out after 60 s while the app was out
of sight. eframe still calls the app's logic then whenever a repaint is
requested, which the control server already does for each request.

The desktop app's logic now answers, while the window is hidden, the
requests that need no frame: engine.execute, engine.commands,
document.inspect, node.get, window.list and ui.render. Screenshots, input
and the interface's own state still wait for the window, and their
timeout now says that the window has to be on screen.

Closes #269
2026-10-14 17:27:01 +02:00
Barzabel 4f321fb68d M1: creating a variable collection no longer closes the app (#224)
Creating a collection (and adding a mode or a variable, double-clicking
a row, or Rename in its menu) asked egui to focus the new name's field in
the same frame, but that field is only drawn from the next frame on. For
one frame the AccessKit update named a focused node that wasn't in the
tree, and the platform adapter aborts on that, outside the app's frame
guard; Windows keeps AccessKit on through UI Automation far more often,
hence the silent close there. A rename now leaves a flag and its field
takes the focus in the frame it's drawn, through widgets::rename_field,
as the layer rows do.
2026-10-14 18:21:08 +03:00
Marco Arnulfo ed0e7a9b2d M1: leaving Design ends the pen path and goes back to Move (fixes #307) 2026-10-14 17:11:20 +02:00
tylerjenningsw 4c23cb71c0 perf(canvas): render coarse zoom previews before refinement (M8)
A new zoom can't reuse the retained canvas image, and the previous one,
re-projected, covers little of the view after a zoom out (1/64 of it
from 800% to 100%), so the page background showed until the full render
finished.

When renders from scratch have been slow (a frame or more), the worker
first renders the whole requested view with a quarter of the pixels
each way, in a Retained of its own so pans and edits still build on the
full image, then refines it unless a newer request is already waiting
(that one gets its own preview). The web build renders the preview and
the refined image in consecutive frames. Pixel preview gets none.

Finished images carry the file, page and whether they are coarse: the
canvas never shows an image of another file or page, and a late preview
never replaces a refined image already at the zoom asked for. ui.inspect
reports the canvas quality and the latency from request to the frame
showing the preview and the refined image.

Refs #295
2026-10-14 11:08:55 -04:00
midasmatterhorn f1f7da1f40 engine: refuse page.move for node ids that are not pages 2026-10-14 18:01:41 +03:00
tylerjenningsw a3a273cfd3 fix(pages): inherit the current page background when adding pages (M1)
page.add gave every added page the interface theme's colour. Figma's
documentation says added pages inherit the current page's background;
the theme only decides a new file's page. Read the active page's colour
before creating the new page, in the command, so the app, CLI, control
channel and MCP agree.

Refs #295
2026-10-14 10:58:17 -04:00
fede9234 3917c26802 Text: colour-glyph comments say only what the fonts are documented to carry
"Placeholder rectangles" was a claim about Apple Color Emoji's glyf
outlines that no source I can show supports: the font's sbix chapter
documents that most emoji are bitmaps while the flags are drawn from
outlines, and it is Noto Color Emoji that has no outlines at all. The
comments, the ROADMAP row and the parity row now say that instead.

No behaviour change: comments and docs only.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-14 16:55:07 +02:00
AganzeFelicite 00ac44f6ca M1: save files automatically
Preferences > Save files automatically (pref.autoSave, off by default;
hidden in the browser, which has no files to save to). With it on, each
open file that has a path is saved 3 s after it changes, never during a
gesture, a live text preview or while a version is shown. A failed save
says why in a toast and tries again a minute later.

Version history checkpoints (version.autosave, every 30 minutes) only ran
for files with unsaved changes, so a file that saves itself would stop
getting them; with auto-save on, edits made since the file was opened
count.

Closes #296
2026-10-14 16:48:56 +02:00
fede9234 de5ba60bc4 Text: colour-glyph comments cite the right source and size
The placement formula is Skia's own fontations port
(src/ports/fontations/src/bitmap.rs) reading the sbix spec's
lower-left-corner rule; the earlier comment named CoreText, which that
source does not mention. Apple Color Emoji measures ~180 MB on current
macOS (the 190 MB figure is a hosted copy of the file), so the comments
say ~180 MB.

No behaviour change: comments only.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-14 16:44:20 +02:00
fede9234 54ba84eddc Text: colour emoji draw from their own colour tables (#268)
Colour emoji came out as the placeholder rectangles their outlines carry
(Apple Color Emoji) or as .notdef when the face has no outline at all:
nothing read sbix, CBDT/EBDT or COLR. A glyph now asks its face for
colour ink before its outline: an embedded bitmap strike decoded to
premultiplied RGBA and placed by the strike's bearings the way Skia (and
CoreText) places it, scaled to the size the run asks for, or a COLR v0
glyph's layers filled from the font's palette.

Faces that carry only colour tables are indexed again: Apple Color Emoji
is no longer skipped for being 190 MB (the scan still reads headers and
name tables only), and "Segoe UI Emoji" and "Noto Color Emoji" join
"Apple Color Emoji" as colour fallbacks, read only for a text that
nothing else covers. The png crate decodes the strikes; skrifa keeps
shaping, metrics and outlines.

Left for later: COLR v1 paint graphs, alpha-only strikes and the SVG and
PDF exports (text is outlines there).

Tests: 8 cases in the text crate (placement, PNG and raw-BGRA decoding,
scaling, COLR layers and bitmaps of the machine's own colour font) and a
render case gated on that font; the font-free ones run everywhere.
Verified on Windows with Segoe UI Emoji (COLR) and with Noto Color Emoji
(CBDT) read from a downloaded file; the sbix path runs through the same
code and needs a macOS run to be seen.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-14 16:19:46 +02:00
MarekSpodBiedry 6eeac4fa04 Keep the redo steps when a failed command batch rolls back (#196) 2026-10-14 16:14:35 +02:00