125 Commits
Author SHA1 Message Date
BFlat 3837b08a71 M8: a damaged image is refused with a reason instead of showing as an empty box (#1724)
* M8: a damaged image is refused with a reason instead of showing as an empty box (#1474)

* M8: replacing an image refuses a damaged file too
2026-10-31 07:22:44 +07:00
BFlat f0ceb46dcb M8: Show in folder works in the Flatpak build and selects the file on Linux (#1701) (#1711)
* M8: Show in folder works in the Flatpak build and selects the file on Linux (#1701)

* M8: Show in folder's fallback path is tested on Linux
2026-10-31 05:44:29 +07:00
BFlat 437d052396 M8: a control request can set its own timeout, up to 10 minutes (#1696) (#1706)
A request line can carry timeoutMs beside id and method, capped at 600,000 ms; without it the wait stays 60 s and older apps ignore the field. figcraft-cli mcp --attach PORT --timeout SECONDS sends it with every request and reads the answers that much longer. A timeout's message says how long the request waited.
2026-10-31 03:41:56 +07:00
BFlat b6499a295c M8: a control request that times out says whether it was applied (#1673) (#1692)
The UI starts a request through its gate (a new Running state) just before running it and never runs one the transport already cancelled. At the timeout the reply says it was dropped and not applied, or that it is still running and will take effect; a reply that just made it is delivered. Only screenshots and input blame the window.
2026-10-31 01:37:09 +07:00
Dylan M. Taylor 0b7528b76d Update prompt shows the app icon and accent color instead of a letter (#1612)
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#02da96, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
2026-10-30 01:28:14 -04:00
BFlat 019058ffd5 M1: a recovery copy made during a version preview holds the file, not the version shown (#1588)
Desktop recovery copies saved DocState::doc, which is the version shown while a version (or a live text preview) is previewed, so a crash during a preview brought the old version back as the unsaved file. What a recovery copy holds is now one shared helper, branches::recovery_copy: the document itself (document_itself(), the same document the unsaved-changes check compares), its history and its branches. The desktop copies and the browser's (#1580) both use it.
2026-10-30 08:21:47 +07:00
BFlat 974ab39503 M8: keep unsaved files when a panic stops the browser build (#1123) (#1580)
wasm32 can't catch a panic, so the command and frame guards do nothing in the browser and a panic ended the page with its unsaved changes. The page now follows its files with unsaved changes after every frame; its panic hook writes them to local storage as .figcraft copies (as many as fit) and says on the page that it stopped. The next page opens them again as unsaved files: file.recover also takes the copy as base64 data. The recovery-copy helpers move from the desktop app into figcraft_engine::branches so both share them.
2026-10-30 06:36:33 +07:00
BFlat aa727500a4 M1: keep settings next to the executable in portable mode (#1320) (#1558)
The settings folder comes from craft-settings' rules: FIGCRAFT_CONFIG_DIR,
a `portable` file next to the executable (config/ beside it), then the
platform folder every earlier version used. The Windows portable zip ships
the marker, and a portable folder without preferences starts with a copy
of the platform folder's.
2026-10-30 03:35:12 +07:00
BFlat 05f811105c M1: start on other graphics when the window's first ones fail (#965, #1438) (#1557)
Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-call
validation shaders, the two causes of "Parent device is lost" at start.
When the graphics still fail before the first frame, the app starts again
with the next of a fixed list of attempts (Windows: Vulkan, OpenGL; Linux
and FreeBSD: OpenGL; then lower requirements on the power-saving GPU), and
the last failure's message names what was tried.
2026-10-30 03:04:11 +07:00
bflatastic d8a4de1bd5 Merge main into #1360 2026-10-29 11:28:49 +07:00
bflatastic 6abddb0f68 Merge main into #1465 2026-10-29 09:55:55 +07:00
Christopher Linnett 73398cedf1 fix(app): ask before ⌘Q quits with unsaved changes (#857)
winit's default macOS app menu ends the process through AppKit's
`terminate:` at once, so ⌘Q and Quit from the menu bar never asked about
unsaved changes (the recovery copies were all that survived).

- Install craft-libs' app menu (craft-menu, same rev as the other
  craft-libs pins; macOS only). Its Quit sends the main window a close
  request, the same path as the window's close button, so
  windows::on_close_request cancels the close and the unsaved-changes
  question asks first. Failure to install keeps winit's menu and logs it.
- windows_tests::a_quit_with_unsaved_changes_waits_for_the_answer pins
  the contract the menu relies on: a close request with unsaved changes
  sends CancelClose and asks; Don't Save then closes.
- Like the close button it closes the main window only: with several
  windows open, each quit closes one (documented).
- docs/hardware-parity.md macOS row, status line and revision history.
- Cargo.lock: craft-menu, craft-quit, craft-keymap, muda and their deps;
  cargo's own minimal resolution also moves a few windows-sys/getrandom
  edges to versions already in the lock.
2026-10-29 08:11:13 +08:00
Denis Gomes 57dcff6d9a M7: #1360 review: cap the clipboard PNG's bytes before copying it
The pixel cap reads only the PNG's header, so a small image followed by a
large tail of data passed it and was copied and base64-encoded whole.
MAX_CLIPBOARD_PNG_BYTES (256 MB, the same cap as design data read from the
clipboard) is checked on the NSData length before the copy on macOS, and
again in the paste path so every Services implementation is held to it.
A PNG past the cap is treated like an unreadable one: paste falls back to
the decoded image.
2026-10-28 14:39:35 -04:00
phonedu 8e6b3bf449 M12: MCP comment feedback and live change events
Comments become the task queue for an attached or headless MCP client: comment.prompt returns a bounded Markdown brief, the Comments panel gains Copy for AI, and the MCP server exposes fix_comments, watch_comments, figcraft://comments and wait_for_changes. Mutating comment.* commands append to a bounded in-memory event log; the control channel's events.poll long-polls it without the UI thread, and a successful batch commits its events only if the whole batch lands. No model, network dependency, asset or file-format change.
2026-10-28 21:42:31 +08:00
Denis Gomes d94609d63f M7: #1360 review: narrower density match, decoded image when the PNG is unreadable
A density now counts only within 0.005 of a whole 2x-4x multiple of
72 dpi. That covers the rounding of pixels per metre (144 dpi is stored
as 5669 or 5670) and rejects nearby resolutions: 141, 143 and 145 dpi
were read as 2x under the old 0.05.

Paste falls back to the decoded clipboard image when the clipboard's PNG
can't be read, instead of pasting nothing.

Also the line on what the objc2 crates are for, and the ROADMAP row for
the new placing default.
2026-10-27 01:39:28 -04:00
Denis Gomes a497b99078 M7: place images at their own size, in points for 2x screenshots
A placed image was always fitted into 1000 px on its longer side, so a
screenshot of a 1440 px wide page came in at 1000 px and stopped lining
up with the design it was taken from. Figma places an image at its pixel
size and only scales down a side past 4096 px.

Placing now does the same, and reads the screen density a PNG records:
macOS screenshots carry 144 dpi (pHYs), so an 800 x 600 px capture of a
400 x 300 pt area comes in at 400 x 300, as in Figma. Only whole 2x-4x
multiples of 72 dpi count as a screen density; 300 dpi print scans and
other values keep their pixel size. The image data stays at full
resolution, so exports at 2x and up stay sharp.

On macOS, pasting reads the clipboard's public.png as is instead of the
decoded pixels arboard returns, which drop the density (and the colour
profile). Other platforms keep the decoded image.

Files dropped, Place image, paste and image.place all go through the
same path; an explicit width/height still wins.
2026-10-27 01:38:19 -04:00
BFlat f229d91ff2 Merge pull request #1243 from SusieeTheLinuxUser/fix/1239-wayland-clipboard
UI: Route text clipboard writes through egui (#1239)
2026-10-26 10:25:41 +07:00
BFlat b2ac04bdf8 Merge pull request #1357 from storytold/land/1017
M9: make graphics-startup assertions independent of UI language (#1017)
2026-10-26 10:24:00 +07:00
bflatastic 42ac4a50d2 Merge main into #623 2026-10-26 09:20:06 +07:00
bflatastic 6fe1618ed4 Merge main into #1017 2026-10-26 09:19:54 +07:00
bflatastic 3bd905fbdf Merge main into #1243
# Conflicts:
#	apps/figcraft/src/services.rs
2026-10-26 09:15:53 +07:00
BFlat 23b443978e Merge pull request #919 from YarilaFox/fix/windows-gnu-a11y-crash
M7: avoid Windows GNU UI Automation crash
2026-10-26 08:33:45 +07:00
Dylan M. Taylor 106ef69351 feat: startup update prompt modal and direct self-updating (#1328)
* 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
2026-10-25 16:23:00 -04:00
236f3185d6 Migrate FigCraft to egui 0.37 (#1312)
* 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>
2026-10-25 15:04:39 -04:00
Marco Arnulfo f4d3f796d6 Dialogs start in the folder of the last file picked (fixes #1290) 2026-10-24 23:00:08 +02:00
NetterSilverandClaude Opus 5.5 10702311f3 Require a per-client secret to resume a collab client id (#1193)
Fixes #1054

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 05:26:27 -04:00
Dylan M. Taylor 25d48530a9 fix(security): owner-only recovery permissions, linear cli export suffixing, and web noopener (#1256) 2026-10-24 04:37:38 -04:00
BFlat 3476f4b1e7 Merge pull request #1244 from storytold/batch/r2-1
Community batch r2-1: 63 reviewed PRs
2026-10-24 11:15:53 +07:00
a1a842af9a chore: resolve SonarCloud maintainability issues (#1241)
* chore: resolve SonarCloud maintainability issues

- Define SHA256_FMT constant in packaging script to remove duplicated string literal (shelldre:S1192)
- Remove redundant 'static lifetime annotations from const items (rust:S8863)
- Replace closures with method and function pointers (rust:S1612)
- Extract helper functions in synthetic document generation, bezier polyline flattening, and component restoration to reduce cognitive complexity (rust:S3776)

* fix(packaging): use verify_sha256 helper function to avoid shellcheck SC2059

* fix(engine): remove duplicate closing brace in large_document

* fix(engine): take &mut Document in restore_id_map; keep 'static on associated consts

restore_id_map allocates ids, so it needs a mutable document. Elided
lifetimes in associated consts trip elided_lifetimes_in_associated_constant,
so those annotations are restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Assign positional parameters to locals in sha256 verification

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:13 -04:00
bflatastic 839e9fa4bc #623: the colour scheme watcher wakes the window on a change, reads GNOME's 0 (no preference) as light, and falls back to Read on portals before 1.17 2026-10-24 09:59:26 +07:00
bflatastic 92db01a808 Merge main and the community batch into #1017
# Conflicts:
#	ROADMAP.md
2026-10-24 09:57:35 +07:00
bflatastic e596272d5a Merge main and the community batch into #623
# Conflicts:
#	docs/target-app-parity.md
2026-10-24 09:54:39 +07:00
Susiee The Linux UserandGPT-6 Luna c6dd115e78 UI: Route text clipboard writes through egui (#1239)
Co-Authored-By: GPT-6 Luna <noreply@openai.com>
2026-10-24 04:28:02 +02:00
oracola91 42502a81d7 Control server: wait after a failed accept instead of retrying at once
The accepting thread read listener.incoming().flatten(), which drops a
failed accept and asks again at once. While accepting keeps failing, for
example when the process has run out of file descriptors, that thread
kept a core busy. It now waits 100 ms after a failed accept. The loop goes
through a small accepted helper so the wait can be tested.
2026-10-23 11:59:36 +03:00
F3R9 6c0ffe6677 M9: make graphics-startup assertions independent of UI language 2026-10-22 00:42:48 +03:00
Dylan M. Taylor 35da7b7cff feat(window): adopt shared craft-window for Linux decorations (depends on craft-libs#217) (#971)
* 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.
2026-10-21 06:05:08 -04:00
YarilaFox a559638419 M7: avoid Windows GNU UIA crash 2026-10-20 17:57:10 +05:00
bflatastic bc35f02ecb Merge main into #874 2026-10-20 01:43:43 +07:00
bflatastic 21d5090db2 #874: --control starts in a new file whatever the preference (control callers expect a document), with a test 2026-10-20 00:13:47 +07:00
bflatastic 918a28a4f4 Merge main into #848 2026-10-19 22:07:51 +07:00
justinjohn0306andClaude Opus 5.5 82a50dad1b M7: a preference to start on the Home screen (#857)
Launching opens a new empty file, which then has to be closed to get
back to Home. Preferences ▸ "Start on the Home screen"
(`pref.startOnHome {on?}`, off by default) starts the desktop app on Home
instead. Files named on the command line and recovered files still open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:53:24 +05:30
skylar taylor-barrick a0d1162976 Desktop: refuse unknown command-line options instead of ignoring them (#836) (#844)
A mistyped or unsupported flag (e.g. --automation-write-roots) used to be
dropped silently, so the app started unconfined. It now exits 2 with
'unknown option', like a bad automation root. A bare -- ends the options.

Adds a desktop control-channel test that file.save / file.saveAs outside the
write root are refused and create nothing, while saves inside succeed, and
documents that the root flags arrived in 0.7.0 (0.6.0 ignores them).
2026-10-18 15:20:15 -06:00
NetterSilverandClaude Haiku 5.5 23d0d9e49e Accept --control=PORT like --control PORT
Fixes #837

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 21:14:08 +00:00
bflatastic d7a27c593e #795: wait once at startup for held recovery folders, only on Unix
Collect the folders whose lock is held, wait 100 ms once and retry only those,
instead of up to 100 ms per held folder. Recovery::start takes its folder without
waiting again (a held name already falls through to another). Windows doesn't
pass the lock to child processes, so the wait and its test are Unix-only; the
test holds the lock 10 ms against the 100 ms wait.
2026-10-18 16:13:34 +07:00
Ashsajal e35f8eb1b7 M8: wait for a recovery folder lock that frees itself (#334)
A child process forked before its exec inherits the run's open descriptors,
and a flock lasts while any descriptor for the file is open. A crashed run
whose folder lock was still held by such a child read as a run that was still
going, so recover_all skipped it and its unsaved documents were not opened at
the next start. The desktop app's tests failed intermittently for the same
reason whenever another test spawned a process in parallel (the reduced-motion
read spawns gsettings on Linux and the BSDs): recovery_tests failed in 23 of
60 runs of the whole binary and never on its own.

Taking a folder's lock now retries try_lock briefly, so a lock that frees
itself is no longer read as a live run while a run that really is running
still keeps its folder.

Regression test: a_lock_freed_a_moment_later_is_not_a_run_that_is_still_going
holds the crashed run's lock on a second descriptor, the way a forked child
does, and frees it shortly after; the copies come back. The recovery tests
that collide with a spawning test went from 8 failures in 10 runs to 0 in 10.
2026-10-18 14:15:56 +06:00
BFlat 99e5db399b M7: quitting on macOS ends the process after saving, avoiding the Touch Bar teardown crash (#781) (#791)
- **Quit (macOS):** `on_exit` now calls `services::shutdown` and then, on macOS only,
  `std::process::exit(0)`. This happens before eframe tears the windows down, which is where the
  crash in #781 is thrown (same approach as the sibling apps, tracked in craft-libs#3).
- **`services::shutdown(app, dir, recovery)`** replaces `save_state`. It runs on every platform and
  everything in it is finished before it returns:
  1. the preferences (`state.json`) are written atomically, as before;
  2. this run's recovery copies are settled by the new `Recovery::quit(session, asked)`:
     - if the unsaved-changes question was asked (`ui.quit`: last window closed through the
       question, or `app.quit`), or nothing is unsaved, it works as before: `finish()` removes
       them;
     - otherwise the unsaved documents are copied (waiting for any copy still being written)
       and the copies stay for the next start, as after a crash. This is the macOS Quit menu
       (Cmd+Q): winit's menu calls `terminate:`, which reaches `on_exit` without asking. Until
       now that path removed the recovery copies of files that had unsaved changes;
  3. live sessions end explicitly: a session this app hosts tells everyone and stops; a joined
     session is left. Before, this only happened in the app's thread-local destructor at
     process exit, if at all.
- If the app can't be reached in `on_exit` (it's busy), the recovery copies are left on disk for
  the next start rather than removed.

Quit paths checked:
- **Cmd+Q / the menu bar's Quit (macOS):** `terminate:` → winit `applicationWillTerminate` →
  eframe `exiting` → `on_exit`. Covered.
- **Last window closed** (close button, title bar Close, Dock/Finder quit Apple event → `Close`):
  `on_close_request` → `begin` sets `ui.quit` → root `Close` → `on_exit`. Covered.
- **`app.quit` (control channel / MCP):** sets `ui.quit` and closes the root window → `on_exit`.
  Covered. Its reply goes to the connection thread before the window closes. The process now ends
  sooner after that, so a client that reads very slowly could in theory see EOF instead of the
  reply (not seen).
- **The frame guard (panic):** isn't a quit. It writes recovery copies and the app keeps running,
  so it doesn't need this. A panic outside the guard aborts as before.
- **eframe error mid-run:** eframe also calls `on_exit` before `run_native` returns `Err`. On macOS
  that error now only reaches the log (eframe logs it), not the "window couldn't open" box. Startup
  failures still show the box: `on_exit` only runs once the app exists.

Part of #781.
2026-10-18 14:09:51 +07:00
RealtaNua cae69a7ae8 Merge remote-tracking branch 'upstream/main' into feat/752-report-missing-fonts
# Conflicts:
#	ROADMAP.md
#	docs/file-format-parity.md
2026-10-17 22:21:25 +07:00
RealtaNuaandClaude Opus 5.5 825cb1e7af M7: list the fonts a file uses that aren't installed (#752)
Text in a family that isn't installed was drawn in a fallback font
without a word. Now:

- `text.missingFonts` lists the families a document names that no
  installed or built-in font provides: text layers and their styled
  ranges, text styles, and string variables bound to a font family
  (every mode, through aliases), with the number of text layers.
- The desktop app shows a Missing fonts window after the user opens a
  file that has any: once installed fonts are indexed (before that every
  one of them would count as missing) and after any import report is
  closed. Files named on the command line count. Programmatic opens and
  the browser build never open it by themselves. Text > Missing fonts...
  shows it for the file in front; Escape closes it.
- `figcraft-cli info` prints `missing font` lines (`missingFonts` with
  --json).
- file-format-parity: the row claimed REST import warnings that no code
  produced.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 21:49:32 +07:00
bflatastic e39d24a32e Merge fix/749 2026-10-17 20:48:35 +07:00
bflatastic d254d5a6ca Merge main into #592
# Conflicts:
#	ROADMAP.md
2026-10-17 20:23:05 +07:00