Commit Graph
2691 Commits
Author SHA1 Message Date
BFlat bb93e5e433 Merge pull request #929 from Silvertree2010/fix/h881-local-from-link-accepts-a-sign-i
Reject non-hex characters in Local::from_link token
2026-10-21 15:44:45 +07:00
BFlat da96d54157 Merge pull request #961 from Silvertree2010/fix/h1145-reversesubpaths-reverse-subpaths
Keep open subpaths open in ReverseSubpaths::reverse_subpaths
2026-10-21 15:35:17 +07:00
BFlat a1758c163f Merge pull request #958 from Silvertree2010/fix/h1112-live-text-svg-export-capitalises
Fix Title Case restarting mid-word across styled runs in SVG export
2026-10-21 15:34:55 +07:00
BFlat b1d00f0802 Merge pull request #943 from Silvertree2010/fix/h932-trim-num-prints-any-whole-number
Fix trim_num saturating whole numbers beyond i64 range
2026-10-21 15:34:34 +07:00
BFlat efb95b29bf Merge pull request #938 from Silvertree2010/fix/h911-same-server-ignores-the-scheme-s
Strip default port in same_server when the address has a path
2026-10-21 15:34:08 +07:00
BFlat 9257f97d69 Merge pull request #911 from Silvertree2010/fix/h737-parse-css-accepts-a-malformed-al
Reject malformed alpha and extra args in Color::parse_css
2026-10-21 15:33:47 +07:00
AKY 9cbe31221a Record Korean translation coverage checks
Use only root-supplied Korean translations; overlapping unmerged PRs are not dependencies. Preserve existing main translations. Static catalog, placeholder and source-lookup checks passed. Rust and native/web UI tests were not run because Rust installation remains awaiting approval.
2026-10-21 13:56:38 +09:00
AKY 1f78ca6aba Add Korean support
Preserve existing Korean translations and exclude unused keys and keys already proposed in open PRs.

Static catalog, key, placeholder, ellipsis, shortcut and whitespace checks passed. Cargo fmt, clippy, tests, xtasks and native/web UI validation were not run: Rust tools are unavailable and installation awaits approval. See docs/korean-translation-validation.md.
2026-10-21 13:34:40 +09:00
AganzeFelicite d68a2c54c5 Merge remote-tracking branch 'upstream/main' into fix/adobe-fonts-macos-298
# Conflicts:
#	ROADMAP.md
#	crates/text/src/fontdb.rs
#	crates/text/src/fontdb_tests.rs
#	docs/roadmap.md
2026-10-21 05:36:34 +02:00
NetterSilverandClaude Haiku 5.5 2e12bdfd91 Keep open subpaths open in ReverseSubpaths::reverse_subpaths
Found while testing crates/geom/src/lib.rs:287

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 02:46:23 +02:00
Siade Lucas f53f37dfe0 ui: scope page tracker by document UID and update on render
Include document UID in pages_seen_page to correctly handle page selection and scroll tracking when switching between documents. Record page as seen only after the active row renders so that pages filtered out by search scroll into view when the filter is cleared.
2026-10-20 21:28:09 -03:00
NetterSilverandClaude Haiku 5.5 503b6d606d Refuse to save documents containing NaN or infinite numbers
Found while testing crates/format/src/lib.rs:256 (to_json/write_json/save_bytes) and :323 (parse_json)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 01:27:12 +02:00
NetterSilverandClaude Haiku 5.5 59f3b3b733 Fix Title Case restarting mid-word across styled runs in SVG export
Found while testing crates/svg/src/lib.rs:589 (also :604 and :611)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 00:31:03 +02:00
tylerjenningsw 20f7d4eb2c test(canvas): grow the cold-preview scene instead of assuming a 50 ms render (M8)
The threaded test asserted a fixed glass scene renders in over 50 ms,
which would flake on a fast machine or once the glass speedups land.
It now grows the scene until the full render takes at least twice the
threshold plus the preview's own time, so the watchdog's preview comes
first with a margin wherever it runs.

Refs #295
2026-10-20 16:38:27 -04:00
tylerjenningsw f80bdb634d docs: zoom previews for cold transitions (M8)
Refs #295
2026-10-20 16:38:27 -04:00
tylerjenningsw 79d436a469 perf(canvas): preview cold zoom and page transitions once they run long (M8)
Whether a zoom got a preview rested on the previous render from
scratch, so the first render, another page or file, and a zoom from a
quick view to a heavy one came without one: the page colour showed
until the full render was done.

Rendering every such preview up front would cost 8-56% of a full render
(bench pages of 1k-50k layers at 2880x1800; per-layer costs dominate
large pages). Instead the render thread arms a watchdog thread as it
starts a render from scratch without a preview; once that render has
run past 12 ms the watchdog renders the preview beside it, on one core
while the full render keeps the others, and sends it unless the full
image is done. Quick renders cost nothing and never flash a preview.
Warm slow zooms keep the preview first, which keeps up with a gesture.

With two threads sending, finished images carry their request's
number: a poll takes the newest request's, and of one request the
refined image; the canvas never shows a preview over the refined image
of its own or a newer request.

Refs #295
2026-10-20 16:38:14 -04:00
Hamza 1aa1d61bd4 MCP: get_node keeps to the limit too, and windows a wide node
inspect_document sends a shallower tree when the document's is too large
and points at get_node for the rest, so get_node was the way out of that
limit -- and it had none of its own. Measured against a document with a
frame of 15,000 children, through figcraft-cli mcp:

    inspect_document depth 64, allPages   14.29 MiB   (8.00 MiB cap not
                                                       applied before #915)
    get_node depth 64                     10.51 MiB

Both are over the 10 MiB buffer the report is about, and for get_node no
depth makes it smaller: the width is at depth 1, so depth 1 is already
9.12 MiB of text and depth 0 has no children at all.

get_node now takes the same maxBytes, cuts the subtree by depth as the
document's tree is cut, and then takes a window over the children when
even depth 1 is too large. The marker says how many children were shown
of how many, and the offset to ask for next; `offset` is a new argument
for reading the rest. After this:

    get_node depth 64                     8.00 MiB, children 11427 of
                                          15000 from 0, next 11427

The hint in inspect_document's marker no longer promises that get_node
has no size limit, since that was the hole: it now says a wide node
answers with a window and an offset.

The test pages through a windowed node and asserts every child is
reported exactly once, in order, with every page inside the limit.
Unbounded, the first page is 23,003 bytes against a 7,667-byte limit.
2026-10-21 00:29:14 +05:00
Mex df5ffec87b M0.9: allow literal dash-prefixed CLI filenames 2026-10-20 21:25:49 +02:00
Hamza f3e5d2abc4 .fig import: a note says how much of the file it covers
A .fig import's notes were deduplicated and left uncounted, so each kind
was named once with no sign of how much of the file it applied to: a
file whose every text layer lost a setting read exactly like one where a
single layer did. That is what made the loss in #467 invisible -- the
reporter saw one line while three kinds of data went, one of them on all
83 text layers.

The mapper now collects its notes through the same `Warnings` the REST
importer already uses, which keeps the first-seen order and the cap and
appends `(×n)` where a note was met more than once. Nothing else
changes: the note text stays the catalog key it was, and the import
report already reads a `(×n)` suffix off before translating the base
(`ui-egui`'s `note_text`), so a counted note is still translated.
`figcraft-cli info` prints these notes, so it now names each kind with
its count.

With the count dropped again, the test reports the two notes plain:

    ["connector layers import as static frame/vector snapshots ...",
     "connector label was skipped: depth limit prevents nested ..."]

Part of #467.
2026-10-21 00:06:57 +05:00
NetterSilverandClaude Haiku 5.5 b788b55286 Keep a solid paint's own opacity when binding a colour variable
Fixes #923

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:43:51 +02:00
NetterSilverandClaude Haiku 5.5 570144ee90 Reject multiple leading '#' in Color::from_hex
Found while testing crates/color/src/lib.rs:125

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:49:45 +00:00
NetterSilverandClaude Haiku 5.5 77f4271061 Fix trim_num saturating whole numbers beyond i64 range
Found while testing crates/doc/src/text.rs:64

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:03:45 +00:00
YarilaFox 8f3afbf6a1 M7: clear comment drafts for control-channel tool changes 2026-10-20 21:04:04 +05:00
Hamza 3df5673627 MCP: maxBytes is the whole message, and depth 0 says when it is over
Two things the limit got wrong, both reported by CodeRabbit on #915.

maxBytes was compared with the escaped text block alone, but what goes
on the wire is that block inside a content object, inside the JSON-RPC
result, with the request id and the newline the transport writes: a
block measured at exactly maxBytes left as a larger message than the
caller said it could read. A kilobyte of the caller's figure is now kept
for that wrapping. About ninety bytes of it is fixed and the rest is the
request id, so the figure holds for any id up to roughly 900 bytes --
every id in practice, a number or a UUID. Sizing the real envelope here
would mean handing the transport's request id to every tool, and the
assumption is written down instead, in the schema and at the constant.

A depth: 0 request over the limit returned early, so the one response
that cannot be made any smaller was the one sent without the truncated
marker -- while a bisection that walked down to depth 0 sent the same
tree with it. Depth 0 now takes the same path as the bisection: the tree
goes out, over the limit, with the marker saying so.

Without the first fix the test reports a limit at the exact size of the
tree as no truncation; without the second the depth-0 marker is absent
(left: (None, None), right: (Some(0), Some(0))).
2026-10-20 20:56:08 +05:00
Siade Lucas 1c79c1646f ui: scroll active page into view when selection changes 2026-10-20 12:53:21 -03:00
YarilaFox bf3fc3cc6e M7: dismiss comment draft when switching tools 2026-10-20 20:47:16 +05:00
NetterSilverandClaude Haiku 5.5 8b198ea364 Strip default port in same_server when the address has a path
Found while testing crates/engine/src/online.rs:118

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:41:47 +00:00
Brandon ThomasandClaude Opus 5.5 d9c924c3ba Skip CodeRabbit reviews on Dependabot PRs (#937)
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:39:30 -04:00
Dylan M. Taylor 92290dd208 docs: add SonarCloud and CodeRabbit badges to README (#820)
* docs: add SonarCloud and CodeRabbit badges to README

* docs: put SonarCloud and CodeRabbit badges on their own line
2026-10-20 11:32:08 -04:00
YarilaFox 6165d2ec85 M7: preserve bindings and design layers when pasting images 2026-10-20 20:12:47 +05:00
Lucas 728f5c48ef i18n(pl): add Polish labels for remaining 7 engine commands
- Add Polish command labels for arrange.reorder, file.moveTab, pref.startOnHome,
  pref.uiScale, text.loadFont, text.refit, and text.rescanFonts.
- Ensures 100% command coverage with zero fallback to English in the Actions palette.
2026-10-20 16:46:58 +02:00
NetterSilverandClaude Haiku 5.5 7c68ebf183 Reject non-hex characters in Local::from_link token
Found while testing crates/collab/src/code.rs:~150 (Local::from_link, u64::from_str_radix on the token)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:27:09 +00:00
Lucas cabe2e3cfb i18n(pl): native review, catch-up and terminology fixes
- Mark Polish catalog as reviewed and approved by native speaker (@lukasz-sklad)
- Replace "Lekki" with "Jasny" for Light mode
- Translate disambiguated context keys (Fill|sizing, Join|stroke, Light|glass, Open|comments)
- Catch up with 15 UI strings added in recent main commits
- Update docs/translating.md to list Polish as reviewed
2026-10-20 15:58:55 +02:00
AganzeFelicite fc37c808aa M7: a document dropped on the canvas opens as a new file (#916)
A .figcraft, .fig or file JSON dropped on the canvas ran file.import,
which added its pages to the file in front. It now opens as a file of
its own, as on the home screen, in the browser too. Images and SVG
still import at the drop point; File > Import still adds pages.

- pending_open::open_dropped queues a dropped file as file.open; the
  home screen and the canvas share it.
- Dragging only documents over the canvas says "Drop to open" (not in
  the browser, which names no file until the drop).
- The tab-switch import test drops an SVG: it pinned where an import
  lands, using a .figcraft only as a file to import.
2026-10-20 15:27:27 +02:00
YarilaFox a559638419 M7: avoid Windows GNU UIA crash 2026-10-20 17:57:10 +05:00
YarilaFox 23463f719b M7: keep saved document titles in sync with filenames 2026-10-20 17:56:30 +05:00
YarilaFox 31da698dd5 Fill selected layer when pasting an image 2026-10-20 17:55:08 +05:00
Hamza b1f0eafbf7 MCP: inspect_document answers within what a client can read
MCP stdio is newline-delimited JSON, so one tool result is one message
and has to fit the client's read buffer whole; there is no streaming of
part of one. The TypeScript SDK's stdio transport defaults to 10 MiB and
hosts rarely raise it, so a document whose tree went past that killed
the server: the client stopped reading, the pipe broke, and every later
call in the session failed.

The tree is now reported at the deepest depth that fits, found by
bisection, with a `truncated` object giving the depth asked for, the
depth used, both sizes, the limit and a pointer to get_node for reading
one subtree deeply. `maxBytes` lets a caller that knows its own buffer
say so; the default is 8 MiB, leaving the envelope 2 MiB of the usual
10.

The size is measured as the transport sends it -- pretty-printed and
then escaped inside the text block, which adds about a tenth (342,136
bytes of tree became 379,954 on the wire in the fixture). A kilobyte is
reserved for the marker itself, which is added after the measurement.

A limit below even the shallowest tree still answers, at depth 0, with
the marker saying so: there is nothing smaller to send, and an error
would leave the caller with nothing at all.

The tool description now says that each level of depth multiplies the
response and points at get_node, which the report notes would have been
enough on its own to avoid the crash.

Fixes #912.
2026-10-20 16:30:03 +05:00
NetterSilverandClaude Haiku 5.5 49586b1c47 Reject malformed alpha and extra args in Color::parse_css
Found while testing crates/color/src/lib.rs:165 (also :173)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 09:30:01 +00:00
RiceDee b5a315a748 #903: name a new component set from its variants' path, not their first property
A component set took its name from the first variant's name up to the first '/' or '=',
so variants named "State=Default, Size=Small" made a set called "State". The set now
takes the path before the last '/' ("Button/State=Hover" -> "Button",
"Icons/Arrow/Dir=Up" -> "Icons/Arrow"), keeps a plain name as is ("Button"), and
otherwise gets the next numbered default ("Component 1"), as a new component does.
2026-10-20 16:32:16 +11:00
RiceDee bcd5b6e632 #903: Combine as variants in the context menu and Design panel; Add variant on a lone component 2026-10-20 16:18:13 +11:00
叶林橦 3cbf3aec48 Merge remote-tracking branch 'origin/main' into feat/popover 2026-10-20 13:06:58 +08:00
叶林橦 1472d4f03f Merge origin/main into feat/panel-grid: resolve conflicts
- right.rs: drop the unused ACCENT import (main removed it); keep the grid
  constants CAPTION_PT/TEXT_PT/TITLE_PT our branch added.
- typography.rs: keep our TEXT_PT size, adopt main's accent_text token rename.
- widgets.rs: keep our TEXT_PT size for the button label; the disabled colour is
  now main's text_tertiary from the match, so the old dim() closure is dropped.
- ui-parity.md: keep our row and main's two (#890, #894) changelog rows.
2026-10-20 13:04:45 +08:00
RiceDee bbe1a12eeb test(ui-egui): idle harness scans system fonts before its first frame 2026-10-20 15:29:27 +11:00
叶林橦 50b978b8f9 M4: the colour picker closes like every other detached panel
The picker draws on the shared popover, so it closes the way the popover does: the X,
Escape, and a press outside it.

- A press on the canvas closes it; presses in the panel beside it keep it, since a
  swatch, a style chip or a field there switches the colour it edits.
- The control that opened it is held out of that closing and its rect is refreshed every
  frame, so pressing the same swatch again reaches its own toggle — after the panel
  scrolls too (the bug #730 had, in the picker's hands).
- Escape closes it before the binding deselects the layer, and not while one of its own
  dropdowns is open.

Tests: a press on the canvas closes it, the same swatch closes it again (panel_remove_tests).
2026-10-20 12:24:09 +08:00
叶林橦 a18413b623 M4: the Design panel's sections on one 240 pt grid
Every section lays its rows out on the same grid: the panel is 240 pt, its content
16 pt in from the left and 8 pt from the right, two 88 pt columns at that width with
8 pt gutters and a 24 pt column for the buttons at a row's end (the columns are flex,
so they share a wider panel); rows are 24 pt in a 32 pt track, captions 10 pt under
them, labels and values 12 pt, the selection's type above them 14 pt. A row's two
buttons (eye, minus) are one group on the row's right edge, 24 pt each and 4 pt apart.

Two things put the right margins out before: block() inset every row by PAD on both
sides (16/16 instead of 16/8), and the panel's frame carried a 1 pt stroke (egui counts
a frame's stroke as its margin), which took a point off each side of the 240 pt panel.
The edge is drawn inside the panel now.

The paint, effect and layout grid rows reserve the whole button group before their
field (8 + 24 + 4 + 24 = 60), so the group lands on the panel's 8 pt margin; the export
preview frame takes the same 16 / 8. Every row that had its own width now asks the
grid: the stroke's align and weight fields split what the row leaves (74 + 8 + 74 with
two buttons, 88 + 8 + 88 with one), the type panel's size field and the full-width
dropdowns take the row's own width, and the Inspect tab's rows end at the 8 pt margin.

The fields' label inset is 8 (was 12) so "Intensity" fits its column whole beside its
value; the glass field's width in field_tests follows the wider column.

Tests: cargo test -p figcraft-ui-egui (549 passed).
2026-10-20 12:19:27 +08:00
叶林橦 3862a2db88 M4: the colour swatches toggle the picker; the pickers' review fixes
Clicking a colour swatch opens the picker; clicking the same one again closes
it (another swatch switches the picker to its paint). The page background, the
text range, the paint rows, the effect rows and the selection colours all
toggle.

The pickers' review fixes on top: the colour picker keeps its popover only
while it is open (a body that closed it left a stale dragged place for the next
picker to inherit); the style picker resets its retained popover when a new
picker opens and on its early-close paths; and Escape belongs to the nested
colour picker first, so closing it doesn't take the style picker down with it.

Opening the style picker, the layout settings or the stroke settings closes the
colour picker instead of leaving it lingering beside them.
2026-10-20 11:57:49 +08:00
叶林橦 345a07fb8f M4: the popover every detached panel shares
`Popover` is the one card detached panels draw on: a header 40 points high
with the title 16 in from its left edge and a 24-point X 8 in from its right,
a hairline under it, the theme's popup shadow, and one slot. Its measurements
are our own — the colour picker's header, which it now uses — not another
product's.

The colour picker and the style picker move onto it in this commit; each keeps
only its own body. The style picker keeps its dark surface through
`Surface::Menu`, and its tools row (view, detach, create) now starts the slot,
under the shared title.

Closing is the widget's now: the X, Escape, and a press outside it — except in
the rects the caller holds out (the button that opened it, a popover it opened
in turn), so that button's own click can toggle it shut instead of the press
closing it and the release opening it again (the bug #730 has). The colour
picker keeps its old habit of staying open while the panel beside it is used.

Tests: press-and-release for the X, for a press on the opener (open) and for a
press elsewhere (closed), plus Escape, the opening rule, the lift and the drag.
2026-10-20 11:57:49 +08:00
叶林橦 e9a0c437e2 M4: a generic popover — a 240 pt card with a 40 pt header
The chrome detached panels share, in one place: a 240 pt card with a 40 pt header —
the title 16 in from its left edge, a 24 pt X 8 in from its right — a hairline under
the header and the theme's popup shadow, around one slot:

    let mut p = Popover::new("my_popover", tr("Title"));
    let closed = p.show(ctx, Open::Anchor(pos), Hold::default(), |ui| { /* the slot */ });

The measurements are the ones the colour picker's header already used, which this widget
now draws; the card is as wide as the picker was. The shadow is the theme's
(`visuals.popup_shadow`), so the dark theme gets its own.

Closing is the widget's: the X, Escape, and a press outside it — except in the rects the
caller holds out (`Hold { rects, escape }`: the button that opened it, a popover it opened
in turn), so that button's own click can toggle it shut instead of the press closing it
and the release opening it again (the bug the fill-rule panel has). Press-and-release
tests pin all of it.

It opens against an anchor (its right edge on the anchor, top on it, lifted when that
would run off the screen's bottom) or at a point; the header drags it.

Six headless tests: the header's geometry, the opening rule, the lift, the drag, the X,
Escape, and the press-and-release of a press on the opener (open) and elsewhere (closed).
2026-10-20 11:57:48 +08:00
xiaou61 beb87d8696 M1: describe the fallback the export actually takes (#465)
The warning still said fixed and content-sized tracks were written as equal
tracks, which the export no longer does. It now reports only the tracks whose
size the format can't hold, and only for the tracks an exported grid has.

Co-authored-by: xiaou61 <113765024+xiaou61@users.noreply.github.com>
2026-10-20 11:14:02 +08:00