The layout-parity files (ROADMAP, compose lib/tests, engine text, fonts fontdb, idml import/tests, canvas) resolve as in the merge of main into feat/layout-parity.
Conflict: ROADMAP.md: as feat/layout-parity's merge
Conflict: crates/compose/src/lib.rs: as feat/layout-parity's merge; this branch's trimmed trailing spaces follow main's warichu::place and come before the drop cap splice
Conflict: crates/compose/src/tests.rs: both sides' tests
Conflict: crates/engine/src/cmd/text.rs: as feat/layout-parity's merge; this branch's TypingFormat/typing_format and main's warichu_cmd
Conflict: crates/fonts/src/fontdb.rs: this branch's styles/traits_of/linked_style over style_traits; style_traits falls back to the family without main's format suffix (exact_style_traits)
Conflict: crates/idml/src/import.rs, crates/idml/src/tests.rs, crates/ui-egui/src/canvas.rs: as feat/layout-parity's merge
Conflict: crates/ui-egui/src/control.rs: main's page_index and automation_contract_tests, then this branch's tests module
Conflict: crates/ui-egui/src/lib.rs: this branch's ClipboardFn and main's Inbox of ImportedFile
Conflict: crates/ui-egui/src/menus.rs: main's Quick Apply tests, then this branch's copy and paste keys test
Conflict: ROADMAP.md: next's Right-to-left and Interface language lines (IDML Kashidas, German and Japanese cover every catalog, Ukrainian covers every key) plus the PR's Show CJK Features line
Conflict: crates/ui-egui/src/lib.rs: the PR's cjk_features() before next's opens_as_document (with .indd/.indt and its doc comment)
Preferences > Type > Show CJK Features (cjkFeatures; unset follows the
interface language: on for Japanese, Chinese and Korean) hides the
CJK-only Type menu items (Tate-Chu-Yoko, Ruby, Kenten, Warichu) and the
Ruby Quick Apply entry. The commands, layout and IDML work either way.
Upstream main moved 9e4f694 -> 736acff by 10 squashes: #258, #285, #153, #280, #288, #281,
#176, #299, #298, #297. New to next: #258 (datamerge test), #153 (temporary files and release
inputs), #280 (warichu), #176 (PDF image conversion cache), #297 (graphics backend before the
window). Already on next: #285, #288, #281, #299, #298.
Conflict: ROADMAP.md: next's lines; CJK1 takes main's wording (ruby unfinished, warichu set after the line is broken)
Conflict: crates/compose/src/lib.rs: list labels (#298): next's list_label structure; paragraph start / break moves: next's `go` (with #295's sub-columns) under the maintainer's #285 comment; Cursor: next's start/next_page/next_baseline with tops; line placement: #280's warichu::place right after layout_line, before next's trimmed trailing spaces and the drop cap splice (so neither is stacked and the spaces follow the closed-up end_x); caret(): next's drop-cap branch first, then #280's stacked-row metrics
Conflict: crates/compose/src/tests.rs: next's tests kept; #280's tests auto-merged
Conflict: crates/engine/src/cmd/text.rs: both (next's TypingFormat/typing_format, #280's warichu_cmd)
Conflict: crates/idml/src/tests.rs: next's side; #280's warichu round-trip test auto-merged
Conflict: crates/pdf/src/export.rs: #176's content cache and test counter with next's RasterMode (no Exporter `interpolate` field on next, so the maintainer's `mode.interpolate &= self.interpolate` is not carried); test exporter gets next's drawing_items field
Conflict: crates/ui-egui/src/canvas.rs: #280's compose::highlight_quads (its RTL branch equals next's mixed-direction spans) followed by next's drop-cap highlight
Conflict: crates/ui-egui/src/menus.rs: next's formatting_affects_text test kept
Auto-merge duplicates removed: #288's table-style tests and #298's list test in idml tests.rs,
#288's tests in doc styles.rs and #285's docx test (both files equal next's), #281's dialog
tests in dialogs.rs (equals next's) and its menu_button/menu_max_height/clipped_submenu in
menus.rs. contributors/people.toml takes main's (next had a duplicated
[people.emanuelebonaventura] table from the 9e4f694 merge, which broke the ui-egui build
script's TOML parse).
Maintainer edits carried over: #285 break characters and Next Column through one closure
(next's `go`, comment added); #281 viewport-aware menu_button in the app bar and the Paragraph
menu, per-dialog widths clamped to the window (all already on next); #280 warichu after the
last-line rules (layout_line with ends_para/forced_mid) and rules at each warichu row's
baseline (rule_baseline in render and pdf text_rules); #176 CMYK fallback through
lossless(.., mode) and its updated contract tests; #258 test fixes; #153 and #297 as landed.
This reverts commit 6e8f33c386, reversing
changes made to de5d32c90f.
#168 is closed; upstream main has #297, which picks the backend before the window exists.
Conflict: apps/designcraft/src/main.rs: kept mod logging; dropped #168's graphics module and its call
* Desktop: pick the graphics backend before the window exists; DirectX 12 on Windows
On an AMD hybrid-graphics laptop (Radeon RX 6800M beside an integrated
Radeon, with two versions of amdvlk64.dll loaded into one process) the
app died a few seconds after launch with an access violation inside
AMD's Vulkan driver at the first swapchain present. Nothing reached the
log: a fault inside a driver is a native crash the engine's panic guard
never sees. wgpu's default backend set (PRIMARY | GL) enumerates Vulkan
first, so every start on that machine went down the same path, while
WGPU_BACKEND=dx12 and WGPU_BACKEND=gl ran fine and the headless CPU
renderer was never affected.
The desktop app now chooses the backend before creating the window
(apps/designcraft/src/gpu.rs): DirectX 12 first on Windows, then Vulkan,
then OpenGL; Vulkan then OpenGL on Linux; Metal on macOS. OpenGL stays
in the set as the in-process fallback when the chosen backend has no
adapter. WGPU_BACKEND still overrides everything and turns the fallback
off.
Start-up fallback: gpu.json in the settings directory records the
backend being tried until the third frame begins (the first has been
presented by then) or the app exits normally. A record left behind means
that backend never showed a frame, so the next start skips it, tries the
next candidate and says so in the status bar. The list starts over when
every candidate has failed or the app version changes;
DESIGNCRAFT_NO_PREFS records nothing. The adapter, backend and driver
version are logged at start-up for bug reports.
Verified on the affected machine: a default start runs on DirectX 12; a
simulated crashed DX12 start moved to Vulkan, which faulted for real,
and the start after that came up on OpenGL with the status line. Nine
unit tests cover the chooser and the record; cargo xtask ci passes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Merge origin/main; on Windows OpenGL isn't added to the DX12 or Vulkan instance (#167)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #89: preserve browser import context and PDF options
* #91: reject PDF pages before narrowing on Wasm
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Upstream squash-merged #142#147#254#122#224#229#230#244#247#269#249#200#276#283#268#236#272#143 after 4fbf893. next already
holds every one of them except #200 (PDF/X-4 validation command:
designcraft-cli validate-pdf) and #268 (GitHub Actions version bumps),
which this merge brings in unchanged. Apart from those two, the merged
tree equals next's.
Each squash commit S equals the clean 3-way merge of S^ with the PR's
current head (refs/pull/N/head). For #142#147#254#236#143 that head
is newer than the one on next: the maintainer merged upstream main into
the PR before squashing.
Maintainer edits (all already on next, so nothing to carry over):
- #142 (7dd948a): control bar keeps #112's horizontal ScrollArea with
#142's horizontal_top inside it. next has the same (plus #255's
object_control and integration 00cf932's tests).
- #147 (fa4c605): the full-decode check at the top of lossless() kept
with #128's interpolate flag. next has the check at the top of #223's
lossless() with RasterMode.
- #254 (80c0ada): USAGE keeps #127's export-options note. next has the
same USAGE line.
- #236 (504ea4a): uk entries for "Can't Open the File" and "Can't Place
the File". next has both with the same translations (integration
92b1a96); kept at next's position, not duplicated.
- #143 (eaa76a4, d6c9762): one test_window harness with open, wheel,
panel_rect and click_at. next has the same module.
Conflict: ROADMAP.md: next's lines (CJK-MOJI block, Text engine line, RTL and Interface language with Italian and Ukrainian)
Conflict: apps/designcraft/src/main.rs: next's `mod graphics` (#168)
Conflict: crates/compose/src/lib.rs: next's paragraph start via `go` (#285 with #276's next_column), ends_para via is_break_char, in-paragraph break through break_start, Cursor without the second resume(); git auto-merged #276's band-closing block a second time, kept once
Conflict: crates/compose/src/tests.rs: next's tests; #283's line_before_a_break_character_is_a_last_line, auto-merged a second time, kept once
Conflict: crates/engine/src/cmd/fonts.rs: tests only; next's composite_tests (#126) kept
Conflict: crates/engine/src/cmd/interchange.rs: tests only; next's INDD routing test kept
Conflict: crates/fonts/src/fontdb.rs: next's plausible/typo_ascent (#81), style_traits/traits_of with exact_style_traits and canonical_family (#81, #126, #247), face() through canonical_family and find_or_load
Conflict: crates/idml/src/import.rs: next's tint() helper (#83) for #230's colour tint
Conflict: crates/idml/src/tests.rs: tests only; next's drop cap and anchored frame tests kept
Conflict: crates/pdf/src/export.rs: next's RasterMode image path (#223 with #147's decode check)
Conflict: crates/ui-egui/src/dialogs.rs: next's paragraph style dialog (new style, #144's basic_character_formats and character_color sections), Find Font FALLBACK_FAMILY default (#126)
Conflict: crates/ui-egui/src/i18n.rs: next's Italian (#218) beside #236 and #272
Conflict: crates/ui-egui/src/i18n/uk.rs: next's table (see #236 above)
Conflict: crates/ui-egui/src/lib.rs: next's Control bar second-row test via the object_control group (00cf932), browser import tests (#119)
Conflict: crates/ui-egui/src/menus.rs: next's language lists with it; #236's ukrainian_language test, auto-merged a second time, kept once
* Desktop: write log records to standard error and a rotating log file
The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: the crash guard's panic report, a failed
crash-recovery save, font folders that can't be read. A launch from a
desktop menu has no terminal either, so there was nothing to attach to a
bug report.
apps/designcraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/designcraft.log (beside
ui.json and prefs.json); each launch rotates the previous log to
designcraft.1.log and .2.log, so the log of a run that crashed
survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
(bounded) and written once the file is attached
- default levels: info for designcraft*, warn for everything else;
RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
error as the only sink
The logger is installed first in main (before the engine's panic hook)
and the file is opened after argument parsing (--version leaves no
file). The settings directory moves out of prefs_path into config_dir.
A --control or DESIGNCRAFT_CONTROL_PORT value that is not a port is now
logged instead of silently leaving the control channel off. Documented
in README.md (Quick start > Logs).
* Desktop: no eprintln! on the log and panic paths
eprintln! panics when standard error is a broken pipe (a GUI launch on
Windows), and a panic inside the panic hook aborts. The "no log file"
message goes through log::warn! (standard error only by then); the panic
hook's fallback writes to standard error with write_fmt and ignores the
error. Same fix as storytold/vectorcraft#465.
Asking for help is not an error, but `designcraft-cli --help` shared the
catch-all arm with unknown commands, so it wrote the usage block to
stderr and exited 1 — unusable in a pipeline and wrong by convention
(`--version` already exits 0).
Split the arm: `--help`, `-h` and `help` now print the same usage block
to stdout and succeed, while an unknown command keeps it on stderr and
still fails. The usage text and links line are hoisted into a shared
`USAGE` const and `usage_footer()` so the two paths can't drift.
Fixes#243🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In the browser, pressing AltGr while editing text on the canvas inserted
the word "AltGraph" into the story.
eframe's web backend (0.36.2, web/input.rs text_from_keyboard_event)
turns a keydown into a text event unless the key is on a fixed list of
names. Browsers report many more named keys than that list holds:
AltGraph, Dead, Unidentified, the media keys. Each one arrived as text.
Panel text fields are not affected, because a focused egui text field
takes its text from the browser's input events instead of keydown.
The web shell now drops a text event that is a key name (ASCII letters
and digits, starting with a capital, longer than one character) unless
an egui text field has the keyboard. One key press never types such a
word; a text field's input events can, so they are left alone.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* CLI: run rejects an export option with no export after it (#67)
--page, --scale and --pdf-options apply to the exports that follow them; one
given after the last --export or --all-pages did nothing and said nothing.
The usage text and docs/agents.md say so.
* MCP: document that text offsets and story lengths are UTF-8 bytes (#68)
* MCP: docs name the UI-only commands precisely; app.links works headless (#62)
* README: PDF and EPUB export ship (#60)
The macOS menu bar takes Cmd+A, Cmd+C, Cmd+X, Cmd+V, Cmd+Z and Cmd+Shift+Z
before the window sees them, so in a panel, dialog or search field they ran
the document's commands: Select All selected every frame, Paste pasted into
the layout, Undo undid the last document change.
While a text field has keyboard focus, these Edit items now act on the field,
whether chosen by shortcut or from the menu: Select All, Undo and Redo reach
it as their keys, Copy and Cut as clipboard events, and Paste asks the
window for the system clipboard. The items stay enabled for the field even
when the document has nothing to copy or undo. With no field focused, and
for every other item, the menu works as before.
* Opening an InDesign document says to export IDML (#21)
.indd and .indt files (recognised by their published 16-byte signature)
failed to open or place with a confusing format error. They now explain
that the format is closed and how to export IDML from InDesign instead.
File › Open offers .indd and .indt files (desktop and web), and dropping
one on the window opens it like a .designcraft or .idml file. A file that
can't be opened or placed from File › Open, Place, a drop, the command
line or the control channel's app.open shows an alert with the reason, as
well as the status line.
* File open: an .indd file is an unreadable file like any other
Remove the INDD signature check and message, and .indd/.indt from the
Open filters and drop routing. The alert tests use a generic unreadable
file.
* Control channel: a failed app.open answers with the error and opens no alert
An alert waits for a click that a control client can't give. app.open
reports the error in its response, as engine.execute does; File › Open,
drops and the command line still show the alert.
Conflict: apps/designcraft-cli/src/main.rs: #254's USAGE const and usage_footer(); #127's '(--page, --scale and --pdf-options apply to the exports that follow them)' line moved into USAGE
InDesign documents and templates (.indd, .indt) open through File › Open,
drop, the web inbox, file.open, file.openBytes, file.openIdml and the CLI.
The engine converts them to IDML in memory with indd-utils
(github.com/aiongg/indd-utils, rev 9cf456d, MIT OR Apache-2.0, no
dependencies, builds for wasm) and imports the result through the existing
IDML path. The converter's warnings come first in the open result's
`warnings`. A file the converter can't read is an open error ("can't
convert the InDesign file: ..."), shown in the Can't Open the File alert.
Routing: a path or name ending in .indd/.indt, or bytes starting with the
INDD signature (the web inbox passes the name without its extension).
opens_as_document and the desktop and web Open dialogs list indd/indt.
Tests use synthetic bytes only: signature-bearing junk, a zero-filled
.indd and a short .indt all fail as open errors with no document added;
a dropped unreadable .indd raises the open alert.
Conflict: crates/ui-egui/src/lib.rs: kept #119's drain_inbox, import_file and ImportRequest (and its Services.inbox doc); import_file's and confirm_pdf_import's file.place/file.openBytes go through #80's open_file, so a failure shows the alert; import_file uses #80's opens_as_document for layout files
Conflict: crates/ui-egui/src/dialogs.rs: kept #119's pdfImport arm (async confirm, target activation); its native file.place goes through #80's open_file
Conflict: crates/ui-egui/src/dialogs.rs: kept both: #80's max-width match gains #146's closeDocument (380), and #146's closeDocument confirm arm stays before #80's importOptions through open_file
Asking for help is not an error, but `designcraft-cli --help` shared the
catch-all arm with unknown commands, so it wrote the usage block to
stderr and exited 1 — unusable in a pipeline and wrong by convention
(`--version` already exits 0).
Split the arm: `--help`, `-h` and `help` now print the same usage block
to stdout and succeed, while an unknown command keeps it on stderr and
still fails. The usage text and links line are hoisted into a shared
`USAGE` const and `usage_footer()` so the two paths can't drift.
Fixes#243🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
New designcraft-pptx crate (L3) and file.exportPptx command (File ›
Export PowerPoint…, CLI --export x.pptx). Each page becomes a slide of
native objects: shapes with fills, gradients, strokes and effects,
pictures cropped like their frames, native tables, and text boxes.
Text keeps DesignCraft's composition by default: composed lines end in
line breaks, justified spacing becomes character spacing, and leading
becomes percentage line spacing (PowerPoint rounds exact spacing to
whole points; its 100% line is 1.2 em for every font). Threaded
stories split at their frames and wrapped lines get their own boxes.
lineBreaks: reflow writes one box per frame in native columns instead.
TrueType fonts are embedded as Embedded OpenType when their licences
allow. Objects PowerPoint can't represent (placed PDF/EPS, type on a
path, some effects) are rasterised through the existing
rasterize_where path.
Checked in PowerPoint for Mac and LibreOffice: on the sample magazine,
word positions match DesignCraft's PDF to within 0.8 pt horizontally
and 1.6 pt vertically.
In the browser, pressing AltGr while editing text on the canvas inserted
the word "AltGraph" into the story.
eframe's web backend (0.36.2, web/input.rs text_from_keyboard_event)
turns a keydown into a text event unless the key is on a fixed list of
names. Browsers report many more named keys than that list holds:
AltGraph, Dead, Unidentified, the media keys. Each one arrived as text.
Panel text fields are not affected, because a focused egui text field
takes its text from the browser's input events instead of keydown.
The web shell now drops a text event that is a key name (ASCII letters
and digits, starting with a capital, longer than one character) unless
an egui text field has the keyboard. One key press never types such a
word; a text field's input events can, so they are left alone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--page, --scale and --pdf-options apply to the exports that follow them; one
given after the last --export or --all-pages did nothing and said nothing.
The usage text and docs/agents.md say so.
eprintln! panics when standard error is a broken pipe (a GUI launch on
Windows), and a panic inside the panic hook aborts. The "no log file"
message goes through log::warn! (standard error only by then); the panic
hook's fallback writes to standard error with write_fmt and ignores the
error. Same fix as storytold/vectorcraft#465.
The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: the crash guard's panic report, a failed
crash-recovery save, font folders that can't be read. A launch from a
desktop menu has no terminal either, so there was nothing to attach to a
bug report.
apps/designcraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/designcraft.log (beside
ui.json and prefs.json); each launch rotates the previous log to
designcraft.1.log and .2.log, so the log of a run that crashed
survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
(bounded) and written once the file is attached
- default levels: info for designcraft*, warn for everything else;
RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
error as the only sink
The logger is installed first in main (before the engine's panic hook)
and the file is opened after argument parsing (--version leaves no
file). The settings directory moves out of prefs_path into config_dir.
A --control or DESIGNCRAFT_CONTROL_PORT value that is not a port is now
logged instead of silently leaving the control channel off. Documented
in README.md (Quick start > Logs).
Resolve conflicts with the CJK/Arabic work: trailing spaces follow the line
only in left-to-right paragraphs without right-to-left glyphs (layout_line
now takes the bidi info); style linking moves to ScopedFonts so the
document's own fonts count (FontDb delegates to scope 0); threading keeps
the receiving story's direction and Vertical Type, and frames split off by
threading or removed from a thread keep theirs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts keeping both sides: doc gains both `cjk` and
`datamerge` modules; file.open resolves data-merge sources and loads the
document's fonts; DocState keeps preview_stash/preview_record and fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The macOS menu bar takes Cmd+A, Cmd+C, Cmd+X, Cmd+V, Cmd+Z and Cmd+Shift+Z
before the window sees them, so in a panel, dialog or search field they ran
the document's commands: Select All selected every frame, Paste pasted into
the layout, Undo undid the last document change.
While a text field has keyboard focus, these Edit items now act on the field,
whether chosen by shortcut or from the menu: Select All, Undo and Redo reach
it as their keys, Copy and Cut as clipboard events, and Paste asks the
window for the system clipboard. The items stay enabled for the field even
when the document has nothing to copy or undo. With no field focused, and
for every other item, the menu works as before.
Window, Utilities, Data Merge lists the linked fields and previews a
record without merging. Create Merged Document runs the merge. The
desktop open dialog offers CSV, TSV, tab, text, and Excel files. The
roadmap records this phase of the feature.
- Double-clicking a text frame with the Selection tool edits its text. The
canvas counts presses itself (egui reports double clicks only on release,
so the press never saw them): 2 clicks select the word or run of spaces
under the pointer, 3 the line, 4 the paragraph, 5 the story
(`text.selectAt`; pointer events carry `clicks`).
- The caret moves past spaces typed at the end of a line: trailing spaces
follow the line undrawn instead of being dropped.
- Bold and Italic (Shift+Cmd+B / Shift+Cmd+I, B and I in the Character
panel) toggle the family's own styles per run, keeping the other trait
(Italic + Bold = Bold Italic). Formatting chosen at a caret applies to
what is typed next.
- Copy, Cut and Paste reach the system clipboard however they are chosen,
including from the native macOS menu; without a native menu the copy and
paste keys work for objects too.
- Formatting Affects Text (J, Tools panel) sends swatches, colours, tints,
None, Swap and Default to the text of the selected frames; the proxy and
the Swatches and Color panels show the text's colours.
.indd and .indt files (recognised by their published 16-byte signature)
failed to open or place with a confusing format error. They now explain
that the format is closed and how to export IDML from InDesign instead.
File › Open offers .indd and .indt files (desktop and web), and dropping
one on the window opens it like a .designcraft or .idml file. A file that
can't be opened or placed from File › Open, Place, a drop, the command
line or the control channel's app.open shows an alert with the reason, as
well as the status line.
A line that is not a JSON object with a string `method` (an HTTP request
line, other non-JSON text, a JSON array, invalid UTF-8, a line over 4 MiB)
now gets one error reply and the connection is closed, so a browser's
cross-origin fetch can't smuggle a command in its body. Connections are
capped at 16. Same transport as LightCraft (storytold/lightcraft#129) and
FilmCraft (storytold/filmcraft#123).
`designcraft-cli commands | head -1` panicked and exited with status 101:
thread 'main' panicked at library/std/src/io/stdio.rs:1168:9:
failed printing to stdout: Broken pipe (os error 32)
println! panics when stdout is closed. Output now goes through outln!, which
ends the program with status 0 on a broken pipe (the reader has what it
wanted) and reports any other write error on stderr with status 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MSI Icon id had no .ico/.exe extension, which Windows Installer needs for
advertised shortcuts and ARPPRODUCTICON, so both showed a blank icon. Release
builds of the desktop app are now GUI-subsystem (no console window; the CLI stays
a console program), the shortcut/summary description names what DesignCraft does, and
packaging-lint rejects an Icon id without an extension. Found in printcraft#57.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A FreeBSD workflow builds and tests the workspace in a FreeBSD 14 VM
(vmactions/freebsd-vm) on pushes to main, dependency changes and on
demand. The X11 backend choice for file drag-and-drop covered only
Linux; FreeBSD desktops use the same X11/Wayland stack, so it now applies
to all(unix, not(macos)).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>