Without the craft-fonts build input the interface had no CJK glyphs:
egui can't find system fonts, so Japanese and Chinese menus and labels
drew as boxes, and Korean and Traditional Chinese font names in the
font menus did in every build.
- The first installed UI face of each script (Japanese, Simplified
Chinese, Traditional Chinese, Korean; macOS, Windows and Linux
families) is added as a fallback for the scripts craft-fonts doesn't
cover.
- A fallback face sits on the UI face's baseline: egui centres a
fallback's line height in the primary's, so a CJK face with a taller
ascent or a line gap sat above or below the Latin text beside it.
The tweak is computed from the two faces' line metrics.
- The interface language's script comes first, before the built-in CJK
faces, so Han characters take that language's forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2
Conflict resolutions:
- crates/engine/src/cmd/layout.rs: next's start_number, chapter_numbering and
layer.move validation kept; the branch's set_layer added after them;
spread_rotation_input_tests and layer_options_tests both kept.
- crates/engine/src/cmd/style.rs: next's delete_tests and the branch's
swatch_options_tests kept as separate modules.
- crates/ui-egui/src/dialogs.rs: both module declarations (style_cjk,
options) and both confirm arms (deleteCharacterStyle, swatchOptions /
colorGroupOptions / layerSettings) kept.
- crates/ui-egui/src/i18n.rs, i18n/{ar,it,ja,pt_br,uk}.rs: both sides' rows
appended; no key was duplicated.
- crates/ui-egui/src/panels/layers.rs: #378's drag-and-drop row
(Sense::click_and_drag, deferred cmds, payloads) kept; the branch's
accessibility name, double-click -> app.layerOptionsDialog (after the
eye/lock/triangle/selection-square branches, before activate) and the
Layer Options for "<name>"... context entry added through cmds.
- crates/ui-egui/src/panels/swatches.rs: next's apply_swatch kept, with the
branch's single-click-only guard so a double-click opens Swatch Options.
Integration:
- Document::rename_swatch_refs also renames table cell stroke gap colours
(CellStroke and CellStrokeAttrs gap_color), kenten and ruby fill/stroke
colours (CharAttrs kenten_fill, kenten_stroke, ruby_fill, ruby_stroke)
and paragraph border gap colours (ParaAttrs border_gap_color).
- The swatch rename test covers a table's cell gap colour, kenten fill,
ruby stroke and border gap colour.
- The Layer Options double-click test checks that the double-click does
not reorder layers.
- German rows for the 29 new shared keys in i18n/de.rs.
The Glyphs panel drew its whole grid as one texture: up to 1200
characters of a large CJK font in a narrow panel (7 columns x 60 px)
made it 10320 px tall and wgpu panicked past the 8192 px limit. The
grid now stops at the rows the largest texture holds, and every UI
texture goes through widgets::fit_texture / load_texture, which
sample an oversized image down instead of panicking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2
- file.save / file.saveAs / file.saveACopy add the .designcraft extension to a
given path that lacks it and report the path written; saving back to the
document's own file keeps its name.
- file.open tells DesignCraft and IDML packages apart by content, so files
without an extension open; anything else is "not a DesignCraft or IDML file".
- Dropped or web-picked files without an extension open as documents when their
content is one.
- Desktop save dialogs filter on the suggested extension and add it to a name
typed without it, asking before replacing an existing file; the open dialog
gains "All files".
- macOS: open-documents Apple events (Finder, Dock, Open With) through
fmv-macos-events, queued until the window is ready; Info.plist declares the
.designcraft type and an imported .idml type with CFBundleDocumentTypes.
- Windows MSI: .designcraft ProgId with an Open verb, IDML under Open with.
- Linux: shared-mime-info for both types and MimeType in the .desktop file.
Fixes#158, #211, #375
- swatch.options: rename, Name with Color Value, process/spot, colour mode
and values, tint, in one undo step; special swatches keep their names,
and a taken or empty name changes nothing.
- Document::rename_swatch_refs points every use at a renamed swatch:
tints, colour groups, inks, paragraph/character/object/cell/table
styles, item fills, strokes, gaps, effects and column rules, text
colours, rules, shading, borders, table cells and the footnote rule.
- layer.set takes every Layer field (show guides, suppress wrap when
hidden), a layer colour name, and rejects empty or duplicate names and
unknown colours.
- Swatches panel: double-click or Swatch Options… opens the dialog;
colour groups get Color Group Options…. Layers panel: double-click or
Layer Options for "name"… opens Layer Options (app.layerOptionsDialog).
Direct Selection selects and drags a frame's content, the Selection tool
drags it by the content grabber, and double-clicking a frame with content
toggles between frame and content, alongside #84's text threading and
canvas click counting.
Conflicts:
- crates/tools/src/select.rs: module doc keeps both paragraphs (ports,
content). The frame/content toggle runs on the second press of a double
click (Down with clicks 2, or a DoubleClick from the control channel),
Selection tool only, after threading ports and the text-frame
double-click, so those keep priority; the DoubleClick arm stays empty.
Overlays keep the Drag::Place marquee and add the content ghost.
- crates/tools/src/tests.rs: both sets of tests and fixtures kept; the
graphic fixture sets Graphic::wrap; the double-click toggle test drives
the click-count sequence and the DoubleClick form.
- crates/engine/src/tests.rs: both sets of tests kept.
- crates/engine/src/cmd/object.rs: both test modules kept.
ROADMAP.md was restructured in #355 and its feature list moved to
docs/target-app-parity.md. The smart guides line this branch reworded
is changed there instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Direct Selection selects and drags a frame's content; the Selection tool
drags it by the content grabber (hand cursor, shown on hover too) and
double-clicking a frame toggles between frame and content. Arrow keys
nudge a content selection. transform.move takes content?: bool (default:
the selection's content flag) and moves the placed graphic or pasted-in
items in the frame's inner space; nested items move in their container's
space. A content drag shows the whole graphic faintly with its bounds.
The Pencil tool's stroke only appeared once the mouse button was
released. The tool reported the stroke so far as a path overlay, but the
canvas skipped path overlays (they were never drawn), so nothing showed
while drawing. The canvas now draws them: curves flattened to about a
quarter of a screen pixel, dashed when asked. That also brings back the
Smooth and Erase tools' dashed drag previews.
The Pencil's preview was black, lost on a dark pasteboard; it is drawn in
the colour of the layer the path goes on, as InDesign does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased on the progress-docs restructure (#355): the ROADMAP bullets this
PR added moved to the inventory in docs/target-app-parity.md, and the
ROADMAP progress log gets a line for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up. Enter in the Save Document Preset popup created the
document and Escape closed the whole dialog: the dialog footer took
both keys while a popup was open. The dialog now leaves Enter and
Escape to an open popup (read before it draws, as egui closes a popup
on Escape that frame): Enter in the preset name saves the preset,
Escape closes only the popup. Clicking the name field no longer closes
the popup either (menus close on any click; this one only outside), and
the field takes focus when the popup opens.
Saved presets lived only in the UI state, so `file.new {preset: name}`
refused them. They are now engine preferences (saved with prefs.json):
`file.savePreset {name, …the file.new settings}` replaces a preset of
that name and refuses built-in names, `file.deletePreset {name}` removes
one, `file.presets` lists them after the built-in ones (`saved: true`),
and `file.new {preset}` makes documents from them by name. The Saved tab
reads and writes them through those commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File > New > Document now shows Recent, Saved, Print, Web and Mobile
tabs of page cards (drawn to the preset's proportions; double-click
creates) beside the preset's details: document name, width and height
with arrows, units, orientation, pages, start page #, facing pages,
primary text frame, columns and gutter, and collapsible margins and
bleed and slug per edge with link toggles. Save Document Preset keeps
presets by name; documents made are remembered for the Recent tab.
Small windows stack the details above the presets and scroll; the
layout mirrors for right-to-left languages. New strings go through the
translation tables (Italian, German, French, Spanish, Japanese, Chinese).
file.new takes intent, units, startPage and per-edge bleed and slug.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- table.setCell starts each chosen edge from the stroke rendering draws there (the winning
copy of a shared edge, the row/column pattern, or the table border) and writes the result
to both copies; table.getCellStroke reports drawn edges, counting each shared edge once.
compose::table::cell_side_strokes shares the resolution with the renderer.
- Commands accept dash patterns of 1-64 lengths, each 0 or 0.1-10000 pt. Screen and PDF
draw a dash or dot pattern that would repeat more than 20000 times along a path as solid.
- Stripes, wavy, hash and gap colour extend by half the weight at table corners.
- Cell Options: an edge change reseeds every stroke field and tolerates a non-object seed.
- Edge proxy: a triple-click decides from the choice before the first click.
Combines contour-wrap (wrap side, contour type, multi-segment lines) with
the placed-graphic and nested wraps already on next.
- crates/compose/src/lib.rs: exclusion() and the new graphic_exclusion()
share wrap_exclusion() (offsets, spine sides resolved per page, contour).
wrap_obstacles() builds every obstacle through them: the item's own wrap,
the graphic's wrap around its shown bounds, and children of groups and
pasted-into frames with their own transform (depth <= 64). Contour type
Bounding Box follows the shown part of the graphic, other types the frame
path. wrap_slot() returns all free segments (free_slots) and steps 1 pt
at a time beside a contour; Jump Object stays in wrap_slot only. The line
loop keeps grid gyoudori, Jump Object leading steps and grid snapping for
a line's first segment, and carries the other segments (with the line's
grid end) to the next lines; estimate_slots() lists every segment through
wrap_slot(). hit_line() breaks ties between segments by x, used by hit()
next to the drop-cap lookup.
- crates/compose/src/tests.rs: both sides' tests; the hostile-geometry
test builds Exclusion with side and contour.
- crates/engine/src/cmd/object.rs: object.textWrap is text_wrap() with
mode/side/contour (unknown values are errors) and content?, applied to
the frame's or the graphic's wrap.
- crates/idml/src/import.rs, export.rs: ContourOption/@ContourType is read
in the shared text_wrap() and written by text_wrap_pref() as a child of
TextWrapPreference after Properties, so graphic wraps carry it too.
- crates/idml/src/tests.rs: both sides' tests. tests/wrap.rs gains a test
of a contour, right-side wrap on a placed graphic shown in part.
- crates/ui-egui/src/panels/mod.rs: the Text Wrap panel's side and contour
combos read the targeted wrap (tw), the graphic's when content is selected.
Lines beside an object that lets text through on both sides are set in
segments, left then right, and the paragraph runs on from one to the
next. One-sided wraps (left, right, spine sides, largest area) block the
other side. A contour wrap follows the object's outline band by band,
with the offset around it; graphic frames follow their contour type
(bounding box of the graphic, frame path, same as clipping; detected
edges, alpha channel and image paths follow the frame path). The Text
Wrap panel sets the wrap side and contour type through object.textWrap,
and IDML import and export keep ContourOption.
- Recovery: file.recovery.open moves each entry to the reopened document's uid
(written before the old one is removed), so recovered documents stay on disk
until saved or closed, also when the app restarts on another adapter or
stops before its first recovery save.
- First-frame watch: a late first frame is logged and noted in the status bar.
The adapter counts as hanging only when the UI thread has been stuck outside
the app's code (logic/ui mark entry and exit) with no progress for 60 s while
the window isn't known to be minimized, occluded or unfocused. The watch asks
for a repaint on each look, so an idle UI ends it. VectorCraft restarts on
the late frame alone, which a held-back compositor or a long compose can set
off.
- Software renderers (WARP, llvmpipe) are passed over while another backend is
left; once no hardware adapter of any backend could show the window, the app
starts again with them allowed, once.
- A restart clears gpu.json's backend entry just before exec or spawn and puts
it back when starting again fails. Backends a restart left out are no longer
recorded as failed.
- The watch ends a hung process with TerminateProcess on Windows.
- The skip list travels as the restarted app's --gpu-skip= argument instead of
an environment variable, so processes the app starts don't inherit it.
Conflicts: compose lib.rs imports (both; Composer unused), idml tests.rs (both test modules), ui-egui
panels/mod.rs (next's ruler origin with the branch's graphic wrap for Direct Selection).
A right-to-left bound spread lists its right page first, parent spreads
included. The parent page shown behind a document page was picked by list
index (first = left), so every page of an imported right-to-left document
showed the other side's parent items. Pick it by position instead: the
leftmost parent page for a left page, the rightmost for a right page.
New parent spreads in a right-to-left document list their pages right
first, and changing the binding reorders existing parent spreads, so IDML
export writes them in binding order. Spread bleed on the canvas and in
spread PDF export takes the outer edges by position as well.
Kanji numbering style (一, 二, 三, …, written digit by digit: 10 is 一〇)
for sections, footnotes, endnotes and lists. IDML PageNumberStyle and
FootnoteNumberingStyle read and write Kanji and the leading-zero styles;
the list format "一, 二, 三, 四..." maps to it.
A placed graphic carries its own text wrap (Graphic.wrap). IDML import
reads TextWrapPreference on the Image/PDF element and export writes it
back there. Layout treats it as an obstacle around the part of the
graphic its frame shows, plus the offsets, and also collects wraps of
items inside groups. object.textWrap acts on the graphic when the
selection is the frame's content, and the Text Wrap panel shows its wrap.
- object.setLayer moves a group member's top-level group (and its members)
and rejects an unknown or missing layer; object.reorder sets the layer on
moved groups' members too.
- object.reorder returns without editing when the order and layers would
not change, so no undo step is recorded.
- Layers panel: locked and hidden layers take object and selection drops
only with Cmd/Ctrl held; the selection square drops the selected
members' top-level groups.
Conflicts: fonts fontdb.rs (the branch's em box with its from-BASE flag beside next's ICF cache); idml
tests.rs (the branch's copies of tests already on next, and the mojikumi test #129 replaced, dropped;
the Em Box Height test kept). German entry for Em Box Height.
The first baseline sits the em box top below the frame's top inset: the
font's BASE em box (idtp, else ideo + 1 em), else 0.88 em, for the
largest size on the first line. IDML FirstBaselineOffset="EmboxHeight"
imports and exports as itself; the footnote options list it and the
frame and footnote option commands take emboxHeight.
Conflicts: ui-egui dialogs.rs (next's numberingSection arm and the branch's cellOptions arm; the branch's
copies of the dialog test helpers already on next dropped, its cell options test kept); idml tests.rs and
the i18n tables appended at the end on both sides (both kept; the branch's Gap Tint: and Tint: rows that
next already had dropped).
Ported from VectorCraft's start-up fixes (#502, #545, #651, the display-GPU
choice, #806, #964) and reconciled with the backend fallback of #297:
- The app picks the window's adapter itself (eframe's native_adapter_selector,
apps/designcraft/src/gpu.rs): adapters that can't present never, then
WGPU_ADAPTER_NAME, hardware before software, native backends before OpenGL,
the GPU that drives the primary display (Windows EnumDisplayDevices through
winsafe, Linux /sys/class/drm), the power preference (WGPU_POWER_PREF;
otherwise power saving on Windows and macOS, the system's order elsewhere),
then DX12 before Vulkan. Every adapter and the choice are logged.
- wgpu's indirect-call validation is off (WGPU_VALIDATION_INDIRECT_CALL=1 turns
it back on): its compute shader failed to compile on an Intel Mac and lost
the Metal device (#334, first error).
- run_native runs under catch_unwind. A wgpu error or a panic in wgpu,
egui-wgpu or naga during the first frames, or a first frame that doesn't
reach the screen in 20 s, starts the app again without that adapter
(DESIGNCRAFT_GPU_SKIP; exec on Unix, a new process on Windows). When no
adapter of the start's backends is left, it starts again with the next
backend. Each restart leaves out one more adapter or backend, so they end.
Adapters without PCI ids (Metal) are keyed by name.
- The backend fallback (gpu/backend.rs, gpu.json) stays for driver faults that
take the process down. Windows tries DirectX 12, then OpenGL, then Vulkan,
each in its own instance: Vulkan last because creating its instance loads
every installed Vulkan driver, and OpenGL kept out of the DX12 instance
because creating it crashed in AMD's atio6axx.dll (#167).
- main returns an ExitCode and logs why the app stopped to standard error and
the log file, which is all a Windows start without a console leaves (#273).
README, architecture, gaps, hardware and target-app parity docs and ROADMAP
describe the choice, the restarts and the variables.
style.cell.create/edit stroke and style.table.create/edit border use the
table.setCell stroke parser: type, gapColor (alias gapSwatch) and gapTint, with
the same validation. A bad value is an error and changes nothing.
With cells selected, or the text cursor in a cell, the Stroke panel edits the
cell edges chosen on a drawn edge proxy: weight, type, colour, tint, gap colour
and gap tint. A click toggles a line, a double-click chooses all outer or all
inner lines, a triple-click chooses or clears all. Table › Cell Options ›
Strokes and Fills… has the same proxy and fields plus the cell fill colour and
tint, and applies the changed values in one undoable step. Translations for
the new strings in every interface language.
DesignCraft matched an SVG font-family case-insensitively when importing
the installed faces, but the final fontdb query compares names exactly,
so "DeJaVu SaNs" loaded DejaVu Sans and then set the text in the serif
fallback (Source Serif 4) without a word. Text frames already accept any
case. The SVG font selector now runs usvg default query with each named
family spelled the way the font database spells it; family order,
generics, weight, stretch and style are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSB7fMQUccqFQekksDGWpo
- object.reorder {ids, above|below: id | index, layer?}: restacks sibling
objects in front of or behind a target, or at an index counted from the
top of a layer, moving them to that layer. Group members stay in their
group. Bad input is an error and leaves the document unchanged.
- layer.move rejects a missing id or an out-of-range index.
- Layers panel: drag a layer row to reorder layers, an object row to
restack it or move it to another layer, and a filled selection square
onto a layer to move the selection there; a line or box shows the drop.
Groups expand to list their members. Layers list top (frontmost) first,
matching the stacking order.
CellStroke and cell/table style edge attributes carry gap colour and gap tint
(IDML ...EdgeStrokeGapColor/GapTint, ...BorderStrokeGapColor/GapTint). The table
renderer and PDF export draw every stroke type on cell edges, with the gap colour
under dashes, dots and stripes.
table.setCell validates stroke values, takes gapColor/gapTint and an edges list
(top/left/bottom/right are the target range's outer sides; innerHorizontal and
innerVertical the lines between its rows and columns). table.getCellStroke reports
the shown values of the chosen edges, null where they differ.
- A right-click while the menu is open closes it as a click outside;
the request now stays until a frame ends with the menu open, so the
menu reopens at the new click with the new target selected.
- While a context menu is open in either split-view pane, neither pane
takes pointer presses, guide drags or keys. The open state is read
once per frame, so the Escape or click that closes pane 0's menu does
not reach pane 1.
- Submenus copied from the menu bar (Arrange, Select, Fitting, Type
submenus...) are keyed by their menu bar path, so rows hidden with
Edit > Menus are hidden in the context menu too, with Show All Menu
Items below.
Tests: reopening on another object, Escape in split view, Escape with a
non-selection tool and a selection, the Type tool's keep-or-place-caret
rule, and hidden rows in a context submenu.