- release.yml: new linux-riscv64 job (cross-compiled on ubuntu-24.04 against
ports.ubuntu.com riscv64 libs, tar.gz only, CLI smoke-tested under QEMU),
ported from GridCraft; the draft release job needs it, so SHA256SUMS covers it.
- packaging/linux/package.sh: CROSS_ARCH/CROSS_TARGET/CROSS_COMPILE/EMULATOR
support, as in GridCraft.
- cargo xtask version set 0.6.0; README, hardware parity and ROADMAP log note
the new platform.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Consecutive paragraphs with the same Split Columns settings form a split
block: their text fills the sub-columns of the column one after the
other from the block's top, inset by the outside gutter and separated by
the inside gutter, then moves on to the next column. When text follows
the block, its last part is balanced across the sub-columns and the text
continues below the deepest one at the full column width; a block that
ends the story fills its sub-columns in turn. Keep options don't apply
between a block's lines.
IDML: SplitColumnInsideGutter and SplitColumnOutsideGutter import and
export (paragraph attributes split_inside_gutter, split_outside_gutter).
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* PDF export: Export Adobe PDF options dialog with remembered settings
File > Export PDF... now opens an options dialog (General / Compression /
Marks and Bleeds / Advanced) that exposes only the parameters the engine's
file.exportPdf already supports: PDF standard, image compression, flattener
preset, spreads, document bleed, crop/bleed/page-info marks with weight and
offset, tagged PDF, page range, and the title. Choosing a preset fills the
dependent fields; editing any field switches the preset to Custom.
The last-used settings persist in ui.json (UiState.pdfExport) and are reused
across exports and sessions. Programmatic callers that pass explicit params
(path, options) still export directly without the dialog; cancelling the save
picker writes nothing.
Adds English and ar/pt-br strings, unit tests for the settings serde, the
dialog defaults and preset mapping, the open/confirm flow (stub writer), and
the menu dispatch, plus a ROADMAP note.
* PDF export: rename dialog title to "Export PDF"
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Text: a line that exactly fills the measure fits
A line whose natural width overruns its measure by less than 0.01 pt
(rounded frame widths and advances) fits, in the paragraph and the
single-line composer. A one-word line that exactly filled its frame was
hyphenated and lost its last letters.
* Text: a word longer than the line breaks at the column edge
The paragraph composer gets emergency break points inside any word (or
part between hyphenation points) that is wider than the narrowest
measure even fully shrunk. They cost more than a hyphen, so a legal
hyphenation point wins where one fits; justified pieces are rated by the
space they leave, so each line takes as many letters as fit. Tracking is
part of the width.
Justified lines shrink no further than the paragraph's minimum word and
letter spacing and glyph scaling: an overfull line stays overfull
instead of overlapping its glyphs, and an overfull single word shrinks
within the letter and glyph limits.
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* 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 <[email protected]>
* Merge origin/main; on Windows OpenGL isn't added to the DX12 or Vulkan instance (#167)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Lists: numbering formats from IDML (A, B, C; i, ii, iii; 01, 02 …)
A paragraph's numbering format, number expression, bullet character and
text after the bullet import from IDML and export back. Numbered labels
come from the expression ("^#.^t", "Tabel ^#^t") with its
metacharacters expanded, in place of a fixed "n." label. Adds 001 and
0001 number styles, keeps letter and roman labels short for huge
numbers, and counts saturate instead of overflowing.
* Lists: an empty paragraph gets no bullet or number
A list paragraph with no characters before its return draws no label,
and numbering carries on past it: in a story's own list, in named lists
(also across stories) and in cross-reference paragraph numbers.
---------
Co-authored-by: Brandon Thomas <[email protected]>
A linked image that wasn't found on open has no data; the export warning said it could not be decoded. It now says it is missing and names the link.
Co-authored-by: Brandon Thomas <[email protected]>
* ci: simplify testkit dependency match for Clippy
* UI: keep menus and dialogs reachable at larger scales
* UI: share viewport-aware scrolling across menus
Incorporate @aiongg's shared menu-button, anchor-aware sizing, and
reopen-reset work from #279 (717ddb6), retaining #281's clipped-submenu
and scrollbar interaction fixes.
Use the wrapper for workspace, zoom, and panel menus as well as the
menu bar. Preserve checkbox dismissal and cover anchor placement,
scroll reset, and very short viewports with regression tests.
---------
Co-authored-by: skylartaylor <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* ci: simplify testkit dependency match for Clippy
* IDML: resolve table styles and shared cell edges
* Compose: size table cells by baseline and physical bounds
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Set warichu as smaller lines inside the parent line.
Type › Warichu stacks the selected run after the line is broken, closes up the rest of the line, and round-trips the IDML warichu properties. The line break itself still sees the full-size run.
* Keep clicks, the caret, and Up/Down on each warichu row.
The glyphs were already stacked inside the parent line, but hit testing, the caret, and vertical movement still used that parent line. Break minimums, row widths, spacing, and tab leaders now follow the small lines, and underlines sit on the small baseline.
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
A break character is stored as the last character of the paragraph it
ends, followed by the usual '\n' separator. The story model, paragraph
ranges, styles, editing and every exporter keep splitting paragraphs on
'\n' only, so the change stays in the places that create, lay out or
write break characters:
- IDML import writes the break and ends the paragraph, so the range that
follows keeps its own paragraph style. NextOddPage and NextEvenPage map
to new odd and even page break characters; IDML export writes a
paragraph that ends in a break as that one Br with its
ParagraphBreakType, so export and re-import give the same story.
- Word and RTF import, Type > Insert Break Character (with odd and even
page breaks added) and typing end the paragraph after a break. Typing
between a break and its return goes to the next paragraph; deleting
either deletes both.
- Layout: a break that ends a paragraph leaves no empty line; the next
paragraph starts in the next column, frame or page, as a paragraph start
option would. Page breaks and Next Page start options move to the next
frame on a later page; odd and even ones to the next page with that
page number parity (no such frame: overset). A break inside a paragraph
from an older document still moves the rest of that paragraph on.
Storing the break as a paragraph separator of its own would have changed
every place that counts or splits paragraphs on '\n'.
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* test(datamerge): pin facing-page record placement (#198)
#198 reported that with `facingPages: true` the merged records after the
first landed at identical coordinates on one spread, leaving a later page
empty. That no longer reproduces: `data.merge` now builds a new document
rather than duplicating pages inside the template, and the three records
land on absolute pages 0, 1 and 2 with no overlap.
Nothing in the engine changed here. These two tests pin the behaviour the
issue asked for, both at the one-record-per-page plan and at the short
block that tiles, so a future refactor of `tile_records` can't quietly
bring the stacking back.
Refs #198🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
* Merge origin/main; the facing-pages merge tests find each frame's page with page_of_item
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Codebuff <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* i18n: add Italian (it) UI translation
Adds an Italian interface catalog (crates/ui-egui/src/i18n/it.rs) covering every
key of the shared table and of the Arabic table (1,430 strings), written from the
English source using ordinary Italian desktop-publishing vocabulary.
Registers "Italiano" in Edit > Interface Language, wires it into tr(), and extends
the i18n and menu tests (lookup, no duplicates, full key coverage, translated menu
titles, language switch frames).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* contributors: add emanuelebonaventura (display name insanityxoff)
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
File ▸ Close, its shortcut (Ctrl/Cmd+W) and the tab's × closed a document
with unsaved changes at once. Closing also discards the document's
recovery data, so the edits were gone for good.
The three user paths now go through menus::close_document. A document
with unsaved changes opens a dialog with Save, Don't Save and Cancel:
- Save runs the same Save the menu does, then closes. If the document
is still unsaved afterwards (the file picker was cancelled), it stays
open.
- Don't Save closes it.
- Cancel (or Escape) does nothing.
A document with nothing to save closes as before. The engine command
file.close is unchanged and never asks, so scripts, MCP and the control
channel's engine.execute keep working unattended. ui.menu.invoke without
params acts like the menu item, as it does for other dialogs.
Quitting the app is not covered here. Recovery data survives a quit, so
unsaved documents come back on the next start.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Footnotes: the first line wraps at the column edge after the number
Line breaking gave tabs no width: a tab only reached its stop when the
line was laid out, so a line holding a tab ran past the measure by the
tab's advance. A footnote whose separator is a tab overflowed its first
line by the gap between the number and the tab stop, and body lines
with tabs did the same.
The greedy breaker now asks for each tab's advance at its position on
the line, computed by the same tab_advance helper line layout uses.
* Lists: the first line after a bullet or number wraps at the column edge
In a hanging indent (a negative first-line indent), the tab after a
bullet or number went to the next default tab stop, past the left
indent: the first line's text started late and pushed the line past the
column edge.
The left indent is now a left tab stop of its own. A tab before it
reaches it unless an explicit stop comes first, so the first line's
text lines up with the lines below.
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Text frames: column rules draw between columns on screen and in PDF
A frame with Column Rule on gets a bar centred in each gutter, inside the
insets, broken around paragraphs that span columns; right-to-left and
vertical frames included. object.textFrameOptions takes columnRuleWeight
and columnRuleColor (unknown swatches are an error) and an inset array
whose null sides stay; document.inspect reports the rule.
* Text Frame Options: Column Rules fields; OK changes only edited fields
The dialog shows the selected frame's options, with an inset field per
side, and sends only what the user changed, so unequal insets survive.
The rule weight is read in points.
* Column rules: tint, horizontal offset and top/bottom insets; IDML import and export
Text Frame Options gains Tint %, Top Inset, Bottom Inset and Horizontal
Offset for the rule, and object.textFrameOptions and document.inspect
take and report them. IDML reads the ColumnRule attributes of
TextFramePreference on frames and object styles and writes them when a
rule is set.
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Paragraph rules: a partial ruleAbove/ruleBelow edit keeps the other rule fields; Rule Above honours Text width
* Paragraph Rules: dialog for the selected paragraphs and a Paragraph Style Options section
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
- An RGB image with no ICC profile of its own was written as DeviceRGB
under the CMYK output intent, and the export only warned. In PDF/X-4
it now carries the sRGB profile (assets/icc/sRGB-v2-magic.icc, ICC
v2.1, CC0). JPEGs stay passed through. Untagged PNG, GIF and WebP,
and the decoded TIFF, BMP and PSD fallback, go in as RGB with the
profile. A re-encoded JPEG keeps the source's RGB profile or gets
sRGB. Grey images and images with their own profile are unchanged.
- Every image constructor asked for interpolation. PDF/X-4 and PDF/A
exports now write images without it, which also removes the
ImageInterpolation error that stopped PDF/A exports with a placed
image. Plain exports keep it.
- make_pdfx4 writes pdf:Trapped False into the XMP, matching
/Trapped/False in the document information.
- check_pdfx4 reports an image that asks for interpolation.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Text set in Spanish was not hyphenated at all. It now uses Javier
Bezos's Liang patterns, hyph-es.tex 5.0 from hyph-utf8 under MIT,
through the hypher crate 0.1.8 with only its alloc and spanish
features. The full feature, which embeds 48 languages under several
licences, stays off.
- hyphen::Lang names the hyphenator for a run: English, or a hypher
language. Lang::for_language maps a language name or locale code.
- word_breaks_in and hyphen_points_in take the language. word_breaks
and hyphen_points stay as the English entry points.
- Composition hyphenates a non-English run whose language has
patterns and skips the rest. The word cache key includes the
language.
- NOTICE carries the Bezos copyright line and the MIT text.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Add one shared swatch_menu_row (full-width clickable chip + name, current
highlight, hover) and use it in the Properties Appearance/text menus, the
shared swatch_picker (Control bar, Stroke, Character, Paragraph Border/
Shading) and both Contextual Task Bar fill/stroke menus. The Task Bar popups
gain a colour chip per swatch, and every swatch menu now applies on a click
anywhere on the row, including the chip.
Cap the popups at 280 pt so a full-width row keeps the menus InDesign-sized.
Label the chip controls and rows for accessibility.
Test: clicking a swatch chip in the Appearance Fill menu applies it (the Task
Bar uses the same swatch_menu_row helper).
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Styles: editing a character style renames it everywhere it is used, changes its Based On and unsets attributes; [None] can't be edited
* UI: Character Style Options edits a character style's name, Based On and character formats, opened by double-clicking the style, Edit in its menu, or New Character Style…
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Untranslated strings fall back to English, as in every other language;
only stale Ukrainian entries fail. Otherwise every PR adding UI text
breaks the build until someone translates it.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* #89: preserve browser import context and PDF options
* #91: reject PDF pages before narrowing on Wasm
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* UI: every font field opens the same searchable font menu, with the search ready for typing and hidden system fonts left out
* UI: the Contextual Task Bar moves by its grip and stays where it is dropped, with Pin Bar Position and Reset Bar Position in its menu
* Merge origin/main; one test_window harness with wheel, panel_rect and click_at
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Japanese covered menu titles, about 140 menu items and panel names. Of the
1,391 rows of the shared table, about 1,240 carried the English text in
their Japanese column, so every dialog, panel field, tooltip and status
line was still English.
Add i18n/ja.rs, a full catalog in the shape of ar.rs and pt_br.rs. It has a
row for every key of the shared table and of the Arabic and Brazilian
Portuguese tables (1,430 strings), keeps the existing Japanese menu words
unchanged, and adds 213 rows for labels the code passes to tr() but no table
had: command labels without their trailing ellipsis (the shortcut list), menu
paths (the menu list), special characters, colour-profile names and a few
field labels.
tr("ja", ..) looks in ja::TABLE first and falls back to the shared table's
Japanese column. The shared table is left as it is.
Co-authored-by: Claude Sonnet 5.5 <[email protected]>
A line ended by a column, frame or page break takes the paragraph's
last-line alignment, so a Left Justify paragraph sets it flush left.
A line ended by a forced line break stays justified.
The break that moves the following text to the next column or frame
is the line's last glyph, so a space after the break no longer keeps
the text in the same column.
A spanning paragraph closes the band of columns above it. Those columns
are balanced first and the paragraph sits below the deepest of them; the
columns after it start below the paragraph, so text that moves to the
next column no longer restarts at the frame top over the heading. A
spanning paragraph that doesn't fit moves to the next frame. Span counts
below the column count set the paragraph across the first columns only.
* docs: register the MCP server from an installed release
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* docs: drop the third-party link; the Windows path is the default install folder
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Reuse packaged-link candidates for embedded IDML images and native files.
Resolve missing original paths beside the opened document, retaining the
embedded image bytes and any original link that still exists. Bound path
lookup allocations and cover actual package relocation through commands.
Make existing data-merge test path assertions portable to Windows.
Co-authored-by: Codex <[email protected]>
The rows share one fixed height that preserves their composed total. Merged rows and rows that are not laid out are refused, and the table stays unchanged.
* 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 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
A placed PNG, GIF or WebP that is cut off (an interrupted download) or
has a bad checksum made the whole PDF export fail with
"PDF writer error: Image(Image(ImageRepr {..}), None, "unexpected end of
file")", which names no file. A damaged TIFF or BMP in the same place is
skipped with the warning "image `name` could not be decoded and was
skipped", and the rest of the document is exported.
The exporter decides whether an image can be embedded by asking krilla
to open it. krilla opens PNG, GIF and WebP lazily: it reads the header,
accepts the file, and decodes the pixels only when the PDF is written,
where an error ends the export. TIFF and BMP are decoded by us first, so
they took the skip path.
lossless() now decodes the image first for every format. A file that
does not decode returns None and takes the existing skip-with-warning
path. This costs one extra decode per PNG, GIF or WebP asset: the sample
magazine (two PNGs, 820 KB) exports in 0.19 s instead of 0.17 s in a
debug build.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
In every workspace that shows the Control panel (Advanced, Typography,
Printing and Proofing, Book), the second row of fields was cut off at the
panel's lower edge: Y and H in object mode, leading, tracking and the
indents in text mode.
The panel lays its groups out in a row that centres each child
vertically. The two-row grids were placed as if they were one row tall
and then grew downward, so they started 13.5 pt below the panel's top
instead of at it and ended 5.5 pt past the 59 pt panel.
The row is now top-aligned, which is where the font menus, the
separators and the reference-point proxy already sat.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Keyboard Shortcuts: the recorder waits for the key pressed with the modifiers
* Workspaces: Interactive for PDF and Digital Publishing open their own panels
* UI: the Quick Apply shortcut doesn't also run its first entry
Cmd+Return opened Quick Apply, and in the same frame the list saw the
same press as Enter and ran its first entry (New Document). A key press
that fires a command shortcut is now consumed, so nothing else handles
it that frame. This also stops Cmd+Return from inserting a paragraph
break while typing.
* UI: Quick Apply rows are left-aligned
Rows without a shortcut or style kind were drawn with their label
centred, because add_sized lays a button out centred and only the rows
with a shortcut have an element that fills the remaining width. Each
row is now laid out left to right in a fixed 460×22 area.
* UI: Quick Apply runs its first entry only on a fresh Return
Pressing Cmd+Return again while Quick Apply is open, or holding it until
it repeats, reached the palette as Enter and ran the first entry. The
palette now counts only a Return press that isn't a repeat and has no
Cmd.