Commit Graph
176 Commits
Author SHA1 Message Date
dotCooCoo f55cbf313c Fix #2249: macOS converts the canvas from sRGB to the display's profile
The window's CAMetalLayer had no color space: wgpu 30.0.1 sets it to
nil for an sRGB surface each time it configures the surface, and Core
Animation does not color-match a layer without one. On a wide-gamut
display #FF0000 showed as the display's own red, (255, 0, 0) in a
capture where Preview shows the same sRGB red as (234, 51, 35).

A paint callback that draws nothing runs after egui configures the
surface and before the frame is presented. It sets sRGB on each
window's Metal layer that has no color space and leaves any other color
space as it is. wgpu's own fix (gfx-rs/wgpu#10286) is in no release yet.

Fixes #3161.
2026-10-29 14:36:37 +07:00
dotCooCoo 59711d61f3 macOS: the app builds and passes clippy again after main.rs was split
`cargo build -p vectorcraft` on macOS stops with nine E0433 errors:
app.rs and host_services.rs use `mac_menu`, `mac_window`,
`open_documents`, `gpu`, `mac_save_format`, `mac_tablet` and
`mac_screen_color` without importing them since those items moved out
of main.rs. The two files now import them under the same cfg as the
code that uses them.

`cargo clippy -p vectorcraft --all-targets -- -D warnings` on macOS then
stops at the wildcard import in clipboard.rs's macOS module, which now
names the two items it uses.

Other platforms compile the same code as before.
2026-10-29 14:31:10 +07:00
Don Omondi e94b74eea4 test(app): keep a child's start from holding the GPU start file's lock
The GPU start tests release the start file's lock and take it again.
portal_save's mock_portal starts dbus-daemon meanwhile, and until a child
runs its program it holds a copy of every open descriptor, so the lock
outlived the test's own handle: the next start found the file locked, took
it as another copy of the app running and left nothing out. Parallel runs
of the app's tests failed 12 times in 20.

A test-only RwLock: mock_portal holds it exclusively while it spawns, and
each GPU start test holds it shared for the whole test. 0 failures in 70
parallel runs.
2026-10-29 13:00:41 +07:00
dotCooCoo c0ad2f23c4 Save As: on Linux an older portal's answer to the save dialog is no longer lost
A desktop portal older than 0.9 ignores handle_token and answers on a
request path of its own. save_file subscribed to that path only after
SaveFile returned it. zbus makes three bus round trips to set up that
subscription. A Response that arrived during them was dropped, and the
save waited forever. On CI the test
the_answer_is_awaited_on_the_path_the_portal_returns waited until the
job limit.

save_file now makes one stream for every Response from the portal
before it calls SaveFile, and takes the first Response whose path is
the returned handle and whose sender is the connection that sent the
reply. A Response that comes before the reply stays queued in the
stream.

The mock portal now creates its message stream before it starts its
thread and before the client connects, so a SaveFile call that arrives
before the thread runs is no longer dropped. A new mode sends the
Response before the reply, and the D-Bus tests fail after 30 seconds
instead of waiting without end.
2026-10-29 13:00:39 +07:00
dotCooCoo 6d97ffa700 Tests: tests that depend on wall-clock time or font loads pass on a loaded machine
On a machine busy with other builds, four tests each failed once and
passed when run again alone.

The two render tests that count the rasters a drag moves draw content
painted with a pattern or showing a symbol, whose raster key holds the
font generation. The fonts change even while a test holds
fonts_steady(): the first lookup by family name catalogs the installed
fonts, and resolving a cataloged family or a fallback character loads a
face. When that happened between the two frames of a drag, the count
was 0. The new with_fonts_unchanged runs a step up to 100 times, until
the generation is the same after a run as before it, and the two tests
take their frames from it. The second render test,
a_moved_group_draws_the_shadow_of_the_instance_art_in_it_while_interacting,
was not seen failing but has the same fault.

The screenshot test's allow_due moved the capture's due time but not
its 8 s wall-clock deadline, so on a slow run the reply was the timeout
error. allow_due now also sets the deadline to f64::MAX; the two timeout
tests set it in the past themselves.

The persistent control server test gave the server 2 s to wait for
each request, 120 ms to read a line and 150 ms to write a reply. It now
gives the line and the reply 1 s each and the wait 30 s. The handler
sleeps 1.25 s, longer than both budgets, so a deadline kept from the
first request still runs out.

The idle expiry test read until the server closed the connection after
its 150 ms idle budget, and the client gave up after 3 s, so a pause of
about 3 s in the server's thread failed it with TimedOut. Every test in
control_server/tests.rs that waits for the server to reply, to close a
connection or to return its slot now waits up to WAIT (20 s), and
line_timeouts() gives the wait for a request and the reply 30 s. The
budgets under test stay 120 and 150 ms (1 s in the persistent test).
2026-10-29 13:00:25 +07:00
dotCooCoo f9fc20b3b9 Part of #4326: a drag whose mouse-up was lost ends once macOS reports no button down
egui ends a drag only on a button release or Escape. When the window
never receives the mouse-up, a drag such as a dock column's resize keeps
following the pointer with no button held.

On macOS, each frame's input now gets a release for every button egui
still holds when NSEvent.pressedMouseButtons reports no button down, at
the pointer's last position. Frames that carry a button event, and
control-channel input that holds a button between steps, are left as
they are.
2026-10-29 10:02:50 +07:00
Dylan M. Taylor 5fd2282b79 Merge pull request #4497 from dylanmtaylor/chore/remove-windows7
Remove the experimental Windows 7 build
2026-10-28 18:46:56 -04:00
Dylan M. Taylor cc3ca69d50 Remove the experimental Windows 7 build
Drop the unsupported Windows 7 x64 portable build and everything that existed only for it:
the windows7 workflow, packaging script and docs/windows7.md; the windows7 feature and the
win7-target cfgs (plain cfg(windows) now); the Windows 7 print transport
(printing/windows7.rs); the glow renderer path; and vendor/windows-link, the patched crate
only that build used. NOTICE, README and the living docs no longer describe it; dated
change-history rows are left as they were.
2026-10-28 17:57:17 -04:00
Dylan M. Taylor f07afc992b Scope platform-specific imports 2026-10-28 18:18:29 +00:00
Dylan M. Taylor e6da628e56 Simplify application integration tests 2026-10-28 16:44:19 +00:00
Dylan M. Taylor 90ef99cb2c Simplify file import and export checks 2026-10-28 16:44:05 +00:00
dotCooCooandHiggs32584 52c92cb419 Windows: a maximized window stops a pixel short of an auto-hidden taskbar
An auto-hidden taskbar reserves no work area, so the guard that keeps
the custom title bar's window borderless left a maximize covering the
whole monitor. The shell treats a caption-less window that covers its
monitor as a full-screen app, and in PhotoCraft, which has the same
guard, the hidden taskbar then did not show when the pointer reached
its edge (storytold/photocraft#4845).

While a maximized window covers its whole monitor, the guard now finds
the taskbars (the Shell_TrayWnd and Shell_SecondaryTrayWnd windows),
takes the monitor edge nearest each one's center, and stops the window
one pixel short of the monitor on each of those edges where the work
area reaches the monitor's edge. A taskbar that reserves a work area
is still cleared by the move into the work area. This ports
Higgs32584's fix in PhotoCraft (storytold/photocraft#6878).

Docs: docs/target-app-parity.md.

Co-authored-by: Higgs32584 <mhiggins217@gmail.com>
2026-10-27 14:36:12 +07:00
dotCooCooandFredApps a5f0e6ba36 Windows: a maximized window stays clear of a taskbar on the top or a side
With the taskbar on the top edge, a maximize, a restore from the
taskbar or a drag to the top of the screen showed a classic title bar
above the app bar (#3242, #3723, #2072). The guard that keeps the
custom title bar's window borderless trimmed only the bottom of a
parked maximize to the work area, so with the taskbar on the top or a
side the window still covered it, and the strip between the window's
edge and the client area, which winit keeps on the work area, was
drawn as an old-style caption.

The guard now moves a parked maximize inside the work area on every
edge, keeping the resize border it was parked with, which it reads
from an edge without a taskbar. A rect it already moved, a dragged or
snapped window and a monitor with no reserved taskbar are left alone.
The move uses SWP_FRAMECHANGED and SWP_NOCOPYBITS and invalidates the
window, so the whole window is drawn again, and the style strip uses
SWP_NOCOPYBITS too. This ports FredApps's fix in PhotoCraft
(storytold/photocraft#5237).

Docs: docs/target-app-parity.md.

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-10-27 14:36:12 +07:00
aiongg 7bbf803f7e refactor(vectorcraft): move main.rs items to {app,host_services,prefs,titlebar}.rs 2026-10-26 22:01:24 +00:00
aiongg f1d427762b style: cargo fmt on files moved without formatting 2026-10-26 19:32:10 +00:00
aiongg a983cf2d5f refactor(vectorcraft): move main.rs tests to tests.rs 2026-10-26 18:18:18 +00:00
aiongg a977a92bb4 refactor(vectorcraft): move logging.rs tests to logging/tests.rs 2026-10-26 18:18:18 +00:00
aiongg 212cef5787 refactor(vectorcraft): move control_server.rs tests to control_server/tests.rs 2026-10-26 18:18:17 +00:00
aiongg bef332f5b2 refactor(vectorcraft): move gpu.rs tests to gpu/tests.rs 2026-10-26 18:18:17 +00:00
dotCooCoo a44c9a2d0b Tests: the --help tests wait for the app to exit on a loaded machine
`apps/vectorcraft/tests/it/help.rs` gave the desktop binary 5 seconds
to exit and otherwise reported that the app had started. While
`cargo xtask ci` runs the workspace's tests in parallel, starting the
binary can take longer than that, and both help tests failed although
the binary printed its usage.

The test now reads standard output and standard error to their end on
threads and waits up to 120 seconds for them. It also sets
`VECTORCRAFT_CONTROL_PORT` to a value the app rejects, so a binary that
does not stop at `--help` exits with that error instead of opening a
window, and the usage checks fail.
2026-10-26 20:34:42 +07:00
Andrés Ortega 88060b199f Windows fonts: list the fonts only GDI has loaded, such as Creative Cloud's Adobe Fonts (#1258)
Squashed from #4130 onto main.
2026-10-26 17:51:42 +07:00
Taha-Mahmoodi 0a241a7fdd Brushes: Window › Brush Libraries opens brush libraries whose brushes click or drag into the document's Brushes
Squashed from #4020 onto main.
2026-10-26 15:17:30 +07:00
skylar taylor-barrick 581c79dcb1 M14: distinguish qualification from publication (#4173) 2026-10-25 23:47:58 -04:00
dotCooCoo dbc446329f Pen tablets: a pen's eraser end selects the Eraser on Windows
With Windows Ink, winit reports a pen as a touch and drops its pen
flags, so on Windows the eraser end of a pen draws with the current
tool. The message hook that reads the barrel button now also reads
each pen pointer message's POINTER_PEN_INFO and writes the pen's
pressure and end into the canvas's pen feed: PEN_FLAG_INVERTED or
PEN_FLAG_ERASER is the eraser end. The canvas then selects the Eraser
tool and the tip brings back the tool used before, as on Linux and
macOS. A pen leaving detection range or the window, another pointer,
or a mouse message that Windows did not make from a pen or a touch
clears the feed, so a mouse presses fully.
2026-10-26 09:09:47 +07:00
dotCooCoo 728ef19a42 Tests: two control server line-deadline tests pass on a loaded machine
buffered_partial_control_line_also_expires and
stalled_control_line_is_refused_with_the_reason used short_timeouts(),
which gives a connection 150 ms before its first byte and 150 ms to write
a reply. When a test thread pauses longer than that, the server closes
the connection without a reply, the client reads end of stream, and the
test fails parsing an empty line (CI run 40212833958 on #4152).

The two tests now use line_timeouts(): the same 120 ms line deadline,
with idle and write budgets of 10 s. The other short_timeouts() tests
are unchanged.
2026-10-26 08:18:20 +07:00
skylar taylor-barrick 4ddd0332cf chore(deps): refresh muda, webbrowser and weezl (#4189)
* chore(deps): bump muda to 0.21.2 and webbrowser to 1.2.5

* chore(deps): bump weezl to 0.2 in render and pdf
2026-10-25 19:31:19 -04:00
Dylan M. Taylor caef923806 feat: startup update prompt modal and direct self-updating (#4138)
* 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, and the Windows update helper runs before argument parsing.

Ref: storytold/craft-libs#319

* refactor(updates): hold the install back while documents are unsaved

* fix(updates): drop the manual window's custom id; pin craft-update to craft-libs main (db19c37)

* 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): localize the update status messages

* build: resolve the craft-update pin after rebasing
2026-10-25 16:22:52 -04:00
aiongg 714434527e Docking: panels on craft-ui's shared docking, floating panels in OS windows (#4135)
* Build: craft-libs at 2869b8cc

Pin craft-pdf, craft-print, craft-docpath, craft-ui, craft-keymap,
craft-window and the patched hayro/krilla backends to craft-libs
2869b8cc1077530bd07a7e192fb2b0e1304a1354, with the pdf-deps gate,
NOTICE, ASSETS.md and docs/pdf-backend.md at the same revision.

PDF: no modification date beyond the creation date, DeviceN images
are not JPEG-encoded, and clipping text draws nothing (its outlines
arrive as a clip path).

* Docking: compile against craft-ui at 2869b8cc

Floating windows are trees of groups: window_panels and window_active
read one as a flat group, and windows rebuilt from the legacy groups
keep their ids. Unknown dock actions are rejected, the new ones are
checked like the old, and every panel a close action removes keeps its
return location.

* Docking: panels and canvas on craft-ui's docking area

* Docking: tests for the dock area, flyouts, floating panels and the Tools panel

* Docking: catalogs without the old dock's strings

* Build: craft-libs at 8a503cfe

* Docking: floating panels in OS windows of their own on the desktop

* Docking: modal dialogs block panel windows; their keys and dropped files reach the main window

* Docking: art dragged onto a panel in its own window drops there

* Docking: module doc names the OS windows

* Build: craft-libs at a042cf26

* Docking: onto, above, below, collapsed, icon and column places on Float and Dock Panel; ui.docking

* Docking: command descriptions of Float, Dock, Move and Collapse Panel for the craft-ui layout

* Docking: control protocol describes the craft-ui layout, its commands and ui.docking

* Docking: rustfmt

* Docking: layouts saved before the canvas joined convert on load

Saved ui.json files and workspaces from before the shared layout held the
canvas (a dock-only tree plus floating_panels, docking_icons,
docking_collapsed, dock_collapsed and dock_width) convert into one layout:
the dock tree's groups in the main column, the icon column with the panels
dropped on it, floating groups and sets, sets docked as columns, collapsed
groups, the dock width and the places of closed panels. The legacy fields
are read, never written. A ui.json read only in part is copied to
ui.unreadable.json before anything saves over it.

* Docking: catalogs without the old collapse message

* Docking: converted floating sets start at most 720 points tall

* Docking: closed icon panels convert into the icon column; old floating rects, set heights and a copy of the file before conversion

* Build: craft-libs at 27e60765

* Docking: dock column places and icon count from the icon column; a docked column moves to a later place

* Build: craft-libs at d24e4bcc

* Build: craft-libs at 5bbe9f62

* Docs: floating panels in OS windows of their own close the other-monitor gap

* Docking: keys forwarded from a floating panel's OS window run shortcuts while the main window is unfocused
2026-10-25 15:25:35 -04:00
skylartaylor 340e3495d8 egui037: migrate VectorCraft UI and reconcile current main 2026-10-25 03:27:29 +01:00
aiongg 8bf090b253 Shortcuts: use the shared craft-keymap crate for the catalogue, overrides, dispatch and .kys import (#3119)
* Shortcuts: the keymap, overrides and dispatch come from craft-keymap

The editor's catalogue (commands, UI commands, panels, tools; one tool
group per tool; show/hide commands marked as toggles) builds a
craft-keymap Keymap. UiState::shortcut_overrides is a craft_keymap
Overrides stored through keymap::stored, which writes the same map of
entry key -> one string as before, with panel: keys and VectorCraft's
spellings, so earlier versions read the file; a value that is not text
drops that entry instead of every preference.

Key presses run through craft_keymap's Dispatcher with accept filters:
keys that type text go to the one-key text path, and while the Type
tool edits only command-key chords run. Held keys repeat. Conflicts
and assignment use the crate's conflicts and set_bindings. Extra Shift
no longer falls back to the chord without it.

* Shortcuts: .kys import matches and applies through craft-keymap

The review proposes targets with craft_keymap's namematch, and Apply
hands the ready rows to craft_keymap::plan as one set, each row's
target passed as the remembered answer for its entry. The later row
takes a chord, and commands outside the set lose the chords the set
gives. Rows with one name in one block keep their own targets.
The engine's own namematch module is removed.

* Shortcuts: Cmd+, beside other hits, web Mac platform, cached dialog view

- Pin craft-keymap at 362eb59f.
- Cmd+, opens Settings on a Mac after the frame's other shortcuts run, with the
  Mac read from egui's OS; drop settings_chord, which dispatch no longer used.
- The web build follows the browser's OS, so Ctrl stays Ctrl on a Mac.
- The Keyboard Shortcuts dialog keeps its keymap, row texts and conflict count
  between frames while the working set is unchanged.
- Every default shortcut text becomes its entry's binding (test).
- craft-keymap counts as a UI crate for the layer check.
- Translate "too many changed shortcuts"; the parse error reuses the existing
  "can't parse shortcut `{c}`" message.

* Shortcuts: the chords the operating system takes can't be assigned

The keymap reserves craft_keymap::reserved::system for its platform:
window switching and closing, and the virtual consoles off the Mac.
The Keyboard Shortcuts dialog and shortcuts.set refuse them with
"<chord> is reserved by the system", and a saved override on one keeps
the entry's default. No default shortcut uses a reserved chord.

* Shortcuts: craft-keymap at craft-libs 2eca9b3a

craft-keymap comes from storytold/craft-libs (craft-libs #164 and #186), at the rev craft-pdf
already uses. The web build keeps the browser's full platform, so Windows browsers get Windows'
reserved chords, and each platform caches its own default keymap.
2026-10-24 15:01:26 -04:00
dotCooCoo 4821ec92c2 Control: requests are answered at full speed while the window is hidden on macOS
Within about a minute of its window no longer being visible, macOS App
Nap lowers the app's threads to the lowest priority and coalesces its
timers. On a loaded machine, control requests then took seconds to
answer, and the 60 s timeout reply arrived up to 26 s late.

When the control channel is on (--control or VECTORCRAFT_CONTROL_PORT),
the app now holds an NSProcessInfo activity (user initiated, allowing
idle system sleep, latency critical) for as long as it runs, so App Nap
does not apply. The activity does not keep the Mac awake.
docs/control-protocol.md states this in the paragraph on the 60 s
timeout.
2026-10-24 20:58:05 +07:00
dotCooCoo d29790b34e Image Trace: a Preview checkbox chooses whether setting changes trace at once
Every change in the Image Trace panel traced the selected Image Trace
object again at once, and there was no way to adjust several settings
first and trace once.

The panel now has a Preview checkbox before Trace. It is on by default
and kept with the UI preferences; Reset to Default leaves it. With
Preview off, setting changes wait in the panel until Trace applies them
as one undo step, or until Preview is turned on again. When Preview is
turned on with no Image Trace object selected, the object is traced
with the waiting changes when it is selected again. A change of view
keeps the waiting settings, and selecting another Image Trace object
shows that object's own settings. The control channel's ui.set takes
imageTracePreview.
2026-10-24 17:46:12 +07:00
Chris MainandClaude Opus 5.5 61504c84fb Print: turn only landscape paper, and only for macOS queues
Review of #3925: the spooled job turned every sheet wider than tall, so
custom paper that is wide in Portrait (a 4 × 2 in label) went to the queue
tall, and a Portrait + Transverse page was turned back. Only the sheets
whose paper the layout turned to landscape (not transverse) are written
upright now.

The desktop print service asked for upright pages on every platform, but
craft-print already turns each sheet to its own orientation on Windows:
only macOS asks for them now; Linux keeps main's behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 17:29:32 +07:00
Chris MainandClaude Opus 5.5 41430cc171 Print: landscape pages print upright on macOS print queues
A landscape print job was a page wider than tall, and `lp` on macOS puts
each page on the paper as it is: on upright Letter it printed unturned,
cut off on the right (checked with cupsfilter through an AirPrint
driver). Saving the PDF and printing it from Preview worked because
Preview turns the page.

`file.print` takes `forPrinter`: a page wider than tall is written
upright, the art turned a quarter anticlockwise as a printer driver turns
a landscape job. Page by page, so auto-rotated jobs that mix wide and tall
artboards print right too. The desktop print service asks for it
(`PrintService::turns_landscape`); saved PDFs, PostScript, the preview and
the browser's print dialog keep landscape pages as they are.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 17:29:32 +07:00
bflatastic 8dbca35893 macOS pen pressure: a feed written on the UI thread never waits for a touch 2026-10-24 09:26:27 +07:00
bflatastic b1e36d69f3 macOS: a tablet pen's pressure reaches the tools (#852)
An AppKit local event monitor (apps/vectorcraft/src/mac_tablet.rs) reads each mouse and tablet event's pressure and the pen's end before winit's view handles it and writes them into the canvas's StylusFeed. The feed keeps the pressure of each pointer move, and the canvas pairs them with the frame's moves, so a fast stroke keeps its curve and each point its own pressure; mouse event coalescing is off. The eraser end selects the Eraser tool, as on Linux. Ported from PhotoCraft's crates/tablet (pressure and eraser end only).
2026-10-24 09:26:27 +07:00
aiongg 8cc69ed45c CI: one test binary per crate, nextest, line tables only (#3855)
* CI: line tables only for workspace crates, no debug info for dependencies

* CI: one integration-test binary per crate

Each crate's tests/*.rs files become modules of tests/it/main.rs. Files that change
process-wide state (Color Settings, the font database, installed plug-ins, the working
directory), the fuzz sweeps and the test targets the Affinity corpus workflow names stay
their own binaries.

* Docs: name the integration tests' new paths

* CI: xtask test with nextest and doctests, one wasm check, clippy --keep-going

cargo xtask ci runs the tests with cargo nextest (cargo test when it is not installed),
builds them with --lib --bins --tests, runs the doctests as their own step, and checks
the wasm crates in one cargo check --keep-going call.
2026-10-23 21:36:10 -04:00
Dylan M. Taylor 42f8b513a2 Fix clipboard read-policy bypass and lock-held file checks
- copied_file: check the read policy on the real path when it is
  valid UTF-8; skip non-UTF-8 paths while automation roots are
  active instead of checking a lossy name (path traversal via
  symlink, CodeRabbit #3872 thread 1).
- full_bytes_cached: collect matching cache candidates under the
  FULL lock, then run the read checks after releasing it, so
  filesystem walks no longer stall other FULL users (thread 2).
2026-10-24 05:16:20 +07:00
Dylan M. Taylor e95e8418ed Audit: automation roots follow file.showPackage, the Place queue, headless file methods, clipboard files, placed-document workers and caches, folder searches and the recovery folder 2026-10-24 05:16:20 +07:00
Taha-Mahmoodi 7da5e3062d Symbols: Window › Symbol Libraries opens symbol libraries whose symbols click into the document's Symbols
Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each symbol library
in a library panel of its own, on the shared library panel code the swatch and graphic style
libraries use (https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html,
.../symbols-panel-options.html, .../create-and-place-symbols.html).

- Basic Shapes: one original library generated in code (circle, squares, triangle, diamond,
  hexagon, star, burst, arrow); no other app's libraries.
- A click adds a library symbol to the document's Symbols as one undo step, with its Symbol
  Options and the resources it uses (nested symbols, patterns, swatches, brushes, graphic styles,
  images), and makes it the current symbol; one already there isn't added again, a clash of
  names numbers the library's. Shift/Cmd-clicks select for Add to Symbols. Dragging a library
  symbol onto the canvas adds it and places an instance there, in the same step.
- Save Symbols… (library menu, Window menu) and Save Symbol Library… (Symbols panel menu) write
  the document's symbols as a .vectorcraft document holding only them, into the user library
  folder (User Defined, window.userSymbolLibrary1-10) or a file; Other Library… reads the symbols
  of any document VectorCraft opens, and a file it can't read is an error.

Commands: symbol.libraries, symbol.library, symbol.addFromLibrary (place, x, y),
symbol.saveLibrary, symbol.loadLibrary; UI: window.symbolLibrary (.other), ui.saveSymbolLibrary
(dialog saveSymbolLibrary). i18n rows in every complete catalog; docs updated.
2026-10-24 03:09:10 +07:00
dotCooCoo 8fa918b30b Pen tablets: a pen's eraser end selects the Eraser on Linux under X11 and XWayland
On Linux under X11, and under XWayland, the X server lists a pen's
eraser end as a pointer device of its own, and winit 0.30 hands it to
the window as a mouse. Turning the pen over and dragging the eraser
end drew with the active tool, for example a Paintbrush stroke.

The tablet reader now marks a pen device whose name or XInput type
contains "eraser", and Services::stylus carries the pen's end with
its pressure. Once a frame while no press is held and the pointer is
in the window, the canvas selects the Eraser when the eraser end
moves, and it selects the tool used before when the tip moves again.
The return goes through Session::switch_tool, so a perspective grid
hidden meanwhile stays hidden. A tool chosen while the eraser end
stands in stays when the tip comes back. The feed starts afresh when
the pen turns over, so a press of one end does not take the pressure
the other end last touched with. Native Wayland is unchanged.

The switch follows PhotoCraft's sync_eraser_tool
(storytold/photocraft#255, and storytold/photocraft#5043 for the
cleared tool).

Docs: docs/development.md, docs/gaps.md, docs/hardware-parity.md,
docs/target-app-parity.md.
2026-10-24 03:09:06 +07:00
aiongg cb06c7b34e i18n: missing translations never fail; xtask ci prints one report (#3856)
* i18n tests: report missing translations instead of failing

The scans for untranslated menu strings, tl! literals, undo labels, menu labels and status
messages print what a complete language lacks. A missing translation shows the English
text until a translator adds the row, so it no longer fails the gate.

* i18n: one untranslated report in xtask ci; drop one-language wording tests

`i18n::tests::untranslated_report` lists, per language, the menu strings, tl! literals, undo
step names and templates, menu labels, render-site labels, status and error messages,
PostScript error meanings and default swatch names its catalog lacks; strings every language
lacks are listed once. It never fails on a gap, only on a scan that finds too few strings.
`cargo xtask ci` runs it in an `i18n-report` step, and `cargo xtask i18n-report` runs it alone.

The five scan tests, the render-site and swatch coverage tests and untranslated_messages.txt
fold into the report. Tests that pinned one language's wording are gone; where they also
checked code (lookups, plural rules, locale tags, menu and History step names, engine
parameter errors), that check stays and compares with the catalog lookup.
2026-10-23 16:01:11 -04:00
MarekSpodBiedry df2a350c41 Bring the window forward when Finder opens a file (#3769) 2026-10-24 02:05:18 +07:00
dotCooCoo 44b1310f49 Linux: a tablet pen's pressure reaches the tools under X11 and XWayland
On Linux under X11, and under XWayland, winit 0.30 hands a drawing
tablet's pen to the window as a mouse without its pressure. Every pen
stroke reached the tools at full pressure: a Calligraphic brush with
Size on Pressure drew one width, and the Liquify tools' Use Pressure
Pen applied full intensity.

When the window is on X11, the desktop app now reads XInput2 raw
events on a second X connection (x11rb's RustConnection, without
unsafe). A device with an "Abs Pressure" valuator is a pen, unless
its XInput type is TOUCHPAD or TOUCHSCREEN. Its pressure, scaled from
the valuator's range to 0..1, goes into Services::stylus, and another
pointer moving clears it. The canvas uses that pressure when a frame
has no touch force. A press takes the highest pressure of the pen's
latest touch since the canvas last read the feed, and waits up to
5 ms for the touch of a hovering pen. The moves and the release of a
held press keep the pressure the pen last touched with when its lift
has already arrived. Native Wayland is unchanged.

The valuator mapping and the reader come from PhotoCraft's tablet
crate (storytold/photocraft#255), with the pressure axis only.

Docs: README.md, docs/development.md, docs/hardware-parity.md,
docs/target-app-parity.md.
2026-10-23 11:31:17 +07:00
dotCooCoo 8064c77b1e Windows: a pen's barrel button is the right mouse button with Windows Ink
With Windows Ink on, winit reports a pen in contact as a touch and
drops its barrel button, and egui-winit presses the primary button for
that touch. A tap with the barrel button held selected art instead of
opening the canvas context menu, and a drag with it held moved art or
drew with the active tool.

A message hook (winit's with_msg_hook) now notes each WM_POINTERDOWN
and whether its wParam has POINTER_MESSAGE_FLAG_SECONDBUTTON, which
Windows sets for a pen in contact with its barrel button pressed.
Before egui reads a frame's input, the primary press and release that
egui-winit made for such a contact become the secondary button, as
#3659 maps the barrel button on Linux. A canceled contact, or one
whose end never arrives, gets a secondary release. A touch with
the tip alone and finger touches are unchanged. photocraft's 8fc62f55
by animo fixed the same fault there.

Docs: docs/target-app-parity.md.
2026-10-23 08:01:43 +07:00
Cursor Agentandnrq6993-a11y 06790288e1 Linux stylus: drop generic Other(10|11) right-click remap
Review: X11 codes 10/11 are not stylus-specific; Wacom X11 already maps barrel to middle/right.

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-23 02:04:47 +07:00
Cursor Agentandnrq6993-a11y b37522bbb3 fix(linux): map pen barrel buttons before egui-winit drops them
## Summary

On Linux (Wayland and X11), tablet stylus side buttons arrive as winit
MouseButton::Other (BTN_STYLUS and neighbours). egui-winit ignores those
codes, so barrel presses never reached the canvas. Run the desktop app
through a thin winit ApplicationHandler wrapper on Linux that remaps
barrel buttons to right and middle mouse buttons before eframe handles
the event.

## Tests

cargo test -p vectorcraft linux_stylus
cargo clippy -p vectorcraft --all-targets -- -D warnings

Closes storytold/vectorcraft#3632

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-23 02:04:47 +07:00
Cursor Agentandnrq6993-a11y 0490ca0788 fix(linux): restore and remember maximized window state
## Summary

On Linux, asking winit to maximize before the window is mapped often does
nothing, and egui sometimes omits the maximized viewport flag while the
window manager still has the window maximized. Defer maximize until the
first frames after startup, and read maximized state from winit when
saving geometry for the next launch.

## Tests

cargo test -p vectorcraft window::
cargo clippy -p vectorcraft --all-targets -- -D warnings

Closes storytold/vectorcraft#3635

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-22 23:56:01 +07:00
bflatastic 1b7cfc8bee Automation: --automation-read-root and --automation-write-root may each be given more than once (#3546)
Squashed from #3631 onto main.
2026-10-22 21:36:15 +07:00
Dylan M. Taylor f64db69b53 Use chrono instead of hand-rolled UTC conversion (#3387)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled Howard Hinnant civil/days-from-civil copies and the
leap-year/days-in-month tables with chrono as a workspace dependency:
- apps/vectorcraft/src/logging.rs: timestamp (log line time)
- crates/doc/src/metadata.rs: civil (Unix seconds -> [y,m,d,h,mi,s]) and iso8601
- crates/pdf/src/file_info.rs: unix (date -> seconds) and in_month, used by the
  PDF/XMP date parsers (pdf_date, xmp_date)
Output formats and parsing semantics are unchanged and pinned by the existing
tests (PDF D:... and XMP dates, offsets, leap-year rejection, year clamp 0-9999,
pre-epoch clamp). chrono uses only std, so wasm stays safe. AGENTS.md/CLAUDE.md
now say time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp now uses the fallible DateTime::from_timestamp
and reads such a clock (or a pre-epoch one) as the epoch instead. Years past
9999 are written with {:04} like before, because %Y would add a '+'. Tests
cover the out-of-range and 5-digit-year cases.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any instant
past chrono's year range to 1970-01-01. Restore the old conversion domain with
a cycle-based helper that routes the calendar through chrono (the calendar
repeats every 400 years = 146_097 days, so month/day/time come from chrono and
only the year is carried in i128, keeping every i64 exact).

- apps/vectorcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/doc/src/metadata.rs: `civil` cycle-based, keeping its clamp EXACTLY
  as the old code had it: the input is clamped to 0000-01-01..9999-12-31 first
  and the year is clamped to 0..=9999 afterwards. (With the input clamp the old
  `unwrap_or_default()` was already unreachable; this drops it and makes the
  conversion explicit, with identical output.)

Pinned old outputs in tests:
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
          10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  metadata civil(i64::MIN)[0]=0, civil(i64::MAX)[0]=9999;
          iso8601(i64::MIN)=0000-01-01T00:00:00Z,
          iso8601(i64::MAX)=9999-12-31T23:59:59Z (year clamp, as before)

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:32 -04:00