upstream/main c3859c82 (#614e198eb5d, #6159509b2bc, #6168e151a2f, #617c3859c82).
Conflict: ROADMAP.md: header keeps next's change list plus orderly native exit recovery; progress log both
Conflict: apps/designcraft-cli/src/main.rs: next's case-insensitive .jpg/.jpeg check with main's write_atomic
Conflict: apps/designcraft/src/main.rs: on_exit keeps next's startup.presented() (not an Option) and adds preserve_documents_on_exit; next's discard_marked_text and main's preserve_documents_on_exit both; Services write uses write_atomic and keeps next's clipboard_text; tests both
Conflict: crates/engine/src/cmd/book.rs: main's ChapterBatch paginate with next's chapter-number resolution and {pages, chapters} result and doc
Conflict: crates/engine/src/cmd/file.rs: revert takes main's discard_document ordering without the revision + 1 (next's DocState::new hands out a fresh revision)
Conflict: crates/engine/src/recovery.rs: main's save_tracked, write_entry and open (st.bump_revision() for next's global revisions); next's reopen-twice test assertions kept
Conflict: docs/gaps.md, docs/target-app-parity.md: headers combined; next's Language, Books and Numbering lines plus main's book update safety line
Conflict: crates/engine/src/cmd/object.rs: object.fit's doc keeps next's text and adds the `axis` clause; test modules from both sides
Conflict: crates/ui-egui/src/canvas.rs: kept next's input code (it sends the second press as Down with clicks: 2) and dropped the PR's release-time DoubleClick; the PR's UI test stays
Conflict: crates/tools/src/select.rs: the handle double-click joins next's second-press preamble, after the port checks and before editing text and the frame/content toggle (Down or DoubleClick with click_count 2 on a handle runs object.fit along fitting_axis); the DoubleClick arm stays empty; module doc from both
Conflict: crates/tools/src/tests.rs, ROADMAP.md: both sides
Conflict: crates/engine/src/cmd/file.rs: the spec keeps next's pdfCrop boxes and loadCursor with the PR's width bound and placement precedence; the PR validates `width` earlier, so next's later want_w line goes; next's loadCursor branch stays
color.sample takes `matchFrom`: with no opaque image pixel under the point (an empty part of an image frame, a transparent pixel), it applies that object's attributes as object.matchAttributes does, in the same command; the tool passes the object under the pointer. sample_item and the tool walk shown_children(), so only a multi-state object's active state is sampled, and sample_item skips points outside the graphic's clipping path. object.matchAttributes's body is a function both commands share.
Conflict: crates/engine/src/cmd/style.rs: object.color keeps next's apply_color, spec and enabled check (#84); takes the PR's color.sample (with next's enabled check), image_color_at and sample_item; drops the PR's color_swatch, color_items and has_text_or_selection; explicit `id`/`ids` win over a text selection in apply_color; the PR's tests follow next's caret behaviour (a caret takes the sampled colour for the next typing)
Conflict: crates/tools/src/tests.rs: both sides; the eyedropper test's Asset and Graphic literals gain next's pdf_crop, pdf_box, clip and wrap fields
Conflict: crates/ui-egui/src/i18n.rs, i18n/{ar,it,ja,pt_br,uk}.rs, ROADMAP.md: both sides
Conflict: crates/engine/src/tooling.rs: every with_ctx caller (IME preedit, commit, end, caret) takes the PR's `|s, cx| s.tool.…` form; pointer keeps next's end_composition before dispatch and tool_key its composing() early return
Conflict: crates/ui-egui/src/canvas.rs: the cursor block takes `space` from navigation_space, then the zoom branch, then `(space && !cmd) || alt_text`; the spring-zoom pointer events carry next's `clicks`; the PR's tests name their button helper `press` (next's tests have a `primary`) and compare cursors with what cursors::show sets for the engine cursor (next paints or rasterizes cursors per platform)
Conflict: ROADMAP.md, docs/target-app-parity.md, docs/ui-parity.md: status lines list both sides; progress and change-log rows keep both
Conflict: crates/engine/src/cmd/layout.rs: Document Setup keeps next's refusal of an out-of-range start page; layout.section's spec keeps next's styles (kanji) with the PR's 1–99999 bound; the section body takes the PR's SectionEdit, whose start number goes through next's start_number check; tests keep next's section_start_numbers_are_bounded with the PR's one-undo-step test (the PR's start_numbers_are_bounded expects Document Setup to clamp, which next refuses, so it is left out)
Conflict: crates/doc/src/lib.rs: kept next's iterative saturating page_number; dropped the PR's second MAX_PAGE_NUMBER (next has it in page.rs)
Conflict: crates/ui-egui/src/dialogs.rs: next already leaves Enter and Escape to an open popup; kept next's side and added the PR's Style-menu key test
Conflict: crates/engine/src/cmd/mod.rs, crates/ui-egui/src/{i18n.rs,i18n/it.rs}: rebased duplicates; kept next's side
Conflict: crates/engine/src/cmd/text.rs: kept next's selection_attrs (typing format, `mixed`) and took the PR's selected_text_preview for the selected text
Conflict: crates/engine/src/cmd/file.rs: spec strings keep next's (extension, return value, .indd); Save a Copy adds the extension then rebases data-source paths on a copy; file_save takes main's prepare-then-commit order with next's bump_revision
Conflict: ROADMAP.md: status line lists both changes; progress log keeps both sides
Keep recovered snapshots available through a second crash before autosave. Track checkpoint ownership across folders and restarted IDs; Save, Close and Revert clean only checkpoints owned by that document.
Follow-up to #506 and the retention concern in #380.
PR #616.
Prepare and stage every chapter before a book update writes files. Preserve imported IDML sources, reject open native chapters and report completed paths after a later write failure.
Follow-up to #485.
PR #615.
Preserve document paths, revisions and collection contents when persistence fails. Rebase Save a Copy paths without changing the open document.
Follow-up to #485.
PR #613.
Conflict: crates/engine/src/atomic.rs: main's side (the reviewed #485 code supersedes the PR head on next)
Conflict: crates/engine/src/recovery.rs: main's #506 code, with next's write_entry helper kept
Conflict: apps/designcraft/src/profile.rs: main's own_run returning Option (and_then), with next's portable root_with argument
Conflict: crates/ui-egui/src/lib.rs: main's QuitState quit flow (d020b4e8) over the #545 head; next's Find Font alert guard; one import_warning_tests module (next's, with the Find Font test)
Conflict: apps/designcraft/src/native_menu.rs: next's MAC_APP_MENU layout, which already has the dc-quit item running app.quit
Conflict: ROADMAP.md: next's header with the quit change added; next's log entries then main's three
Conflict: docs/target-app-parity.md: main's atomic save, recovery and quit lines
Review follow-up.
- OK ran layout.section and then layout.chapterNumbering: two undo steps,
and a chapter start the second command refused (over 99 999) left the
section applied. `layout.numberingOptions {page, …section, chapter}`
checks both parts before the document changes and applies them in one
edit; the window's OK runs it. layout.section and
layout.chapterNumbering share its parsing.
- Enter or Escape with a Style menu open confirmed or closed the whole
dialog. Dialogs now leave both keys to an open popup (read before the
dialog draws, since egui closes a popup on Escape that frame).
- A start page of 4294967295 overflowed page_number (a panic in debug
builds, page 0 in release). Page numbers saturate; layout.section
refuses start numbers outside 1-99 999 (MAX_PAGE_NUMBER, as chapter
numbers) and Document Setup clamps its start page instead of wrapping
a 64-bit number into 32 bits.
Also rebased on main: the restructured ROADMAP keeps main's text, and the
inventory in docs/target-app-parity.md lists chapter numbering and the
window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layout > Numbering & Section Options now opens its own window instead of
the generated command form, on the section of the page in view: Start
Section (always on for page 1), Automatic Page Numbering or Start Page
Numbering at, and a Page Numbering group with Section Prefix, Style,
Section Marker and Include Prefix when Numbering Pages; then Document
Chapter Numbering with its style, Automatic Chapter Numbering, Start
Chapter Numbering at, Same as Previous Document in the Book (only in a
book) and the book's name. OK and Cancel sit beside the options, out of
the scrolling body.
OK runs layout.section and layout.chapterNumbering; a refused value
(a prefix over eight characters or with + or a comma, a start number
below 1) keeps the window open and changes nothing. Opening the window
reads the chapter numbering without running a command, so it doesn't
stop a Data Merge preview. New strings are translated (German, French,
Spanish, Japanese, Chinese, Italian).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A section prefix is at most eight characters and contains no plus or
comma (page references and ranges use them); `layout.section` refuses
others and leaves the section unchanged. Symbols, a footnote style,
no longer number pages. The parameter documentation lists the 01, 001
and 0001 styles, and a test pins the fields the generated dialogs read
from both numbering commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A document's chapter number now has a style (1, I, i, A, a, 01, 001,
0001) and a source, as in Numbering & Section Options > Document
Chapter Numbering: Automatic Chapter Numbering (one more than the
previous document in the book; 1 first), Start Chapter Numbering at,
or Same as Previous Document in the Book.
`layout.chapterNumbering {style?, start?, source?}` sets them (undoable)
and, with no parameters, reports them with the open book that holds the
document. The book's Update Numbering (`book.paginate`) resolves chapter
numbers in book order and reports them. Chapter Number variables and
cross-reference chapter numbers use the style.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
type.selectionAttrs copied the whole selection to a string before taking
the first 60 characters, so selecting a long story copied all of it on
every query. It now skips leading blanks and stops after 60 characters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
type.selectionAttrs reports the start of the selected text on one line
(`selectedText`, up to 60 characters), for previews such as the font
menu's Selected Text sample. The preferences keep `recentFonts`, the
fonts last picked in the font menus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validate caller-supplied character fills before merging selection attributes.
Deleted swatch names already retained in text no longer block New Style from
Selection; explicit unknown fills still fail without changing state.
Cover swatch creation, text formatting, deletion and style capture, including
non-fill overrides and explicit-fill rejection with fromSelection.
The DestIn mask now covers the whole render target (and the PDF's
exported area), with the gradient kept in item space through the paint
transform, since past the gradient's ends the end opacity carries on: a
child's long drop shadow or thick stroke is faded, not cut off at a box.
feather_bounds counts only shown children and active states, walking
nested groups without recursion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gradient feather on a group (Gradient Feather tool over grouped text, as
in LYH Studio's poster tutorial) did nothing on screen: groups never went
through the effects path. In PDF the group's mask was the group's own path
bounds, and a group has no path, so the mask covered a box of zero size and
hid the group.
- Item::feather_bounds: the box the feather spans, the objects' bounds for
a group.
- Render: a feathered group draws its objects in one layer, then fades it
with the same mask as other objects (fx::gradient_feather_mask).
- PDF: the group's mask spans its objects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A placed PDF, EPS or SVG under the Eyedropper matches attributes again,
as before, instead of failing with "no placed image under the point"
(Asset::is_raster, shared by the tool and color.sample). color.sample
hands its colour to object.color, which now colours selected text, the
text of selected frames ({text: true}), or the typing colour of an empty
story; a caret in existing text is refused before a swatch or undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Eyedropper on a placed image now does what InDesign does: with text
selected, clicking the photo colours the text with the image's colour under
the pointer; with objects selected, it fills them. The colour is an unnamed
colour (reused when the same colour is sampled again). Images on locked
layers can be sampled, and unfilled frames over the image let it show through.
- color.sample {at, spread?, target?: fill|stroke, ids?} samples the frontmost
visible raster image at a spread point; one undo step; a caret or a point
with no image is refused without an edit.
- object.color shares its swatch lookup with it.
- The Eyedropper tool sends color.sample over an image, matchAttributes
elsewhere as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canvas reports a double-click on release (after the second Up), so the
handle double-click now fires from the mouse. Side handles map to the frame's
local axis through spread and frame rotation. Graphic frames honour the axis.
Locked frames and frames on locked layers are left alone. Text frames fit by
searching the frame height with its own columns and vertical justification,
so two columns stay two columns and Bottom/Center/Justify don't inflate it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In InDesign double-clicking a selection handle fits the frame to its
content: a corner fits both sides, a top or bottom handle the height, a
side handle the width. LYH Studio's tutorial uses it at 9:30 to fit the
INDESIGN text frame before moving it to the corner. In DesignCraft the
double-click switched to the Type tool.
- Selection tool: a double-click on a handle runs object.fit
{mode: fitFrameToContent, axis: both|height|width}.
- object.fit fitFrameToContent fits text frames to their composed text:
the bottom to the last line's descent, the right side to the longest
line (one column), plus the insets, keeping the top-left. Overset text
grows the frame first. Before, it fitted graphic frames only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep each app's recovery data in an owned run folder. Validate current folder and lock ownership and disable recovery when isolation fails instead of using the shared root.
Fixes#496.
Co-authored-by: noah1400 <48360809+noah1400@users.noreply.github.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Replace native engine saves and engine/MCP exports atomically, preserving private permissions during staging and cleaning up failed writes. Add regressions for long filenames and caught writer unwinding.
Fixes#436.
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Conflict: crates/compose/src/lib.rs: one re-export line with next's hit_cell_with and the branch's BorderHit/hit_border
Conflict: crates/tools/src/text.rs: next's IME/preedit, guides and unit_selected kept alongside the border drag; border press checked after the Down resets; drag keeps next's `cell_drag || unit_selected` guard after the border branch; Esc cancels a border drag before next's preedit guard; BorderHit via next's `compose` alias
Conflict: crates/tools/src/tests.rs: both sides' appended tests kept
Conflict: docs/target-app-parity.md: next's IDML line, branch's Tables line
Conflict: ROADMAP.md: branch entry on top of next's entries
Hovering a table's inner column or row border, or its end or bottom
edge, shows a double-arrow cursor; dragging resizes instead of selecting
text. A column drag changes the column before the border and moves later
columns; Shift keeps the table width (the next column compensates), and
Shift on the end edge scales every column. Rows work the same way, and a
dragged row becomes At Least. Sizes clamp at 3 pt. One undo step per
drag, Esc cancels, locked frames refuse, and the pointer is mapped
through the frame's transform so rotated frames and turned spreads work.
New command table.resizeBorder {story?, table?, frame?, axis, index,
delta | size, keepTableSize?}; the Type tool emits it through
Begin/Preview/Commit. compose::hit_border finds the border under a point
in frame inner space.
Closes#595
Conflict: ROADMAP.md: both progress lines kept
Conflict: docs/target-app-parity.md: next's UI and tools lines kept, with the Fill/Stroke proxy keys added to the UI line
Conflict: crates/ui-egui/src/lib.rs: UiState keeps next's tabs_panel and the branch's stroke_front
Conflict: crates/ui-egui/src/menus.rs: UI_COMMANDS keep next's togglePresentation and formattingAffectsText entries and add the color.* entries
Conflict: crates/ui-egui/src/toolbar.rs: next's text-aware proxy buttons (color_target, colors_affect_text, apply_swatch) kept; the proxy reads stroke_front from UiState instead of egui temp data
Conflict: crates/engine/src/cmd/file.rs: spec keeps next's pdfCrop list and adds loadCursor; file_place loads next's asset (pdf_crop/pdf_box already set) with the box size (bw, bh), so LoadedGraphic carries no separate crop and place_drop keeps next's xf with clip: None; load_cursor takes the branch's body, place_load builds the Asset with ..Default::default()
Conflict: crates/engine/src/lib.rs: LoadedGraphic is { asset, size }; on next the crop box lives in the asset
Conflict: crates/ui-egui/src/lib.rs: next's open_documents_later/open_os_documents and the branch's place_file both kept
File › Place (native picker, browser import, Place with Import Options
and the PDF page dialog) passes loadCursor to file.place. With no empty
or graphic frame selected and no x/y given, an image is added to the
document and loaded into the place cursor instead of landing in a new
frame on the first spread; a click places it at its natural size, a drag
draws its frame. The loaded graphic keeps the chosen PDF page and crop
box. Escape or choosing another tool unloads the cursor. Dropped files
and file.place without loadCursor behave as before.
object.swapFillStroke and object.defaultFillStroke change every unlocked
target in one edit, so one undo restores them all. Swap works per object
and keeps each object's tints; a stroke that gets a colour at zero weight
gets the default 1 pt. Objects that are locked, or on a locked layer,
stay as they are. color.swapFillStroke and color.defaultFillStroke run
these commands.
#550 places a selected graphic at x/y from its page's left edge; next measures
transform.set x/y from the rulers' zero point (view.zeroPoint, ruler origin), and the
Transform fields show a graphic's bounds from that point. transform_content now targets
x/y from the same origin as the frame path, so a typed value lands where the fields
say it is.
Conflict: crates/engine/src/cmd/object.rs: transform.set doc string joins next's "measured from the rulers' zero point" with the PR's `content?`; test modules: kept next's copy_ids_tests/tint_alone_tests and the PR's content_transform_tests
Conflict: crates/ui-egui/src/panels/mod.rs: kept next's swatch_list_tests and the PR's content_selection_tests
Conflict: ROADMAP.md: kept both progress-log lines
Conflict: crates/ui-egui/src/lib.rs: import_file takes the PR's `styles` early return first, then next's content-sniffing `layout` line
Conflict: ROADMAP.md: kept both progress-log lines
Styles could not be taken from one document into another; a client's
styles had to be rebuilt in every new file. The Paragraph Styles and
Character Styles panels now have Load Paragraph Styles, Load Character
Styles and Load All Text Styles: pick a .designcraft or .idml file,
tick the styles to load and choose what happens to one the document
defines differently (redefine, keep, or load under a new name). The
styles, swatches and lists a loaded style names come along.
`style.load` does it in one undo step.
With a placed graphic selected, transform.set acted on the graphic but
the Control bar and Properties still read the frame's values: after
setting the image of a -45° frame to 0°, a spinner step sent -44, and a
200 pt frame holding a 400 pt image showed W 200, so 201 halved the
image. transform.info now reports the graphic's values at the top level
in that case, and sel_info gives the graphic's bounds.
Also: `id` names the target explicitly, like `ids`, so it isn't taken
as a content selection; and a content selection of locked frames
records no empty undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>