InDesign's Hyphenation Zone ("the white space allowed at the end of a
line of unjustified text before hyphenation begins") applies only with the
Single-line Composer. The Paragraph Composer, the default, weighs every
break of the paragraph instead. DesignCraft applied the default 3p0 zone
to both, so ragged paragraphs under the Paragraph Composer refused
hyphens InDesign makes. In LYH Studio's poster tutorial the placeholder
text breaks "aper-natem" and "mosticsia-tur"; ours ran a line longer.
The breaker now gets the zone only for unjustified Single-line Composer
paragraphs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
note.edit was the one story-text command still reading `text` without
text_param. The progress-log line held real CR and CRLF characters where
it meant to show the escapes; it reads `\r` and `\r\n` now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frame.create and text.insert turned \r and \r\n into paragraph returns,
but story.replaceRange, story.setText, note.new, footnote and endnote
text, type on a path and table cells kept the CR in the text, where it
drew as a missing glyph and joined the paragraphs. They all use
text_param now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Previews were kept in the context's data for each sample text, size
and colour, and never let go: every new text selection loaded textures
for every visible row. They now live in a cache of at most 256 that
drops the least recently used.
- A family the background read didn't list (installed since) was read
from disk on the UI thread every frame and never kept; it is now read
once and stored with the others.
- Return and Escape in a dialog's font menu are covered by #447's test
on main; this branch's duplicate is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The class, Recently Added and Show Similar Fonts filters read every
family's kind through face(), which loads the family and keeps its bytes
for the session (with no size cap). They use family_kind now, which
reads each file and lets it go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Return, Escape and the arrows are consumed by the menu (Escape closes
it), so a dialog under the menu never confirms or cancels (#447).
- The class and Recently Added filters and Show Similar Fonts read every
family's kind on a background thread, once per document font scope;
the menu says Loading fonts... meanwhile. The web reads its few bundled
fonts inline.
- The colour font badge uses theme tokens instead of fixed colours.
- Every font field, the hand-drawn ones included, reports as a combo box
with the current family as its value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With few fonts installed the font's own row in the main list is on screen
too, so the label matched twice (FreeBSD CI).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The font menu (Control bar, Character panel, Properties, task bar, style
dialogs, Find Font, Glyphs) now has InDesign's layout:
- a search field with a clear button, and a Filters row: classification
(All Classes, serif, slab serif, sans serif, script, monospaced,
decorative, symbol), Show Favorites Only, Show Recently Added (fonts
added in the last 30 days);
- the sample text (Selected Text, Sample, Aa Bb Cc, a pangram, digits)
in three sizes, remembered with the UI state;
- Recently Used fonts at the top, then every family;
- a family of several styles shows how many and opens to list them, each
previewed in its own face; picking a style applies it as it is;
- each family's format after its sample, and Show Similar Fonts (≈) to
list the families of the same class nearest by PANOSE.
Find More and the activated-fonts filter are Adobe Fonts services and
are left out. The font combo is a field and a plain popup now: egui's
combo box kept its menu at the size it last had, so a family opening or
a filter clearing didn't show. The ≈ button stays while the pointer is
on it (the row under it no longer counts as hovered there). New strings
are translated.
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>
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>
ScopedFonts::family_kind reads a family's format, class, PANOSE and file
date from its Regular face. A loaded face is asked; an installed family
that isn't loaded is read from its file, which is let go afterwards,
instead of loading every family the font menu's filters look at for the
rest of the session. Files over MAX_DOCUMENT_FONT_BYTES are skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each face reports what InDesign's font menu shows and filters by, read
once from its tables: its format (TrueType, OpenType/CFF, variable,
colour), its class (serif, slab serif, sans serif, script, monospaced,
decorative, symbol) from PANOSE, else the OS/2 IBM family class, else
the family name, else the shape of its "l" (serifs widen the foot), its
PANOSE numbers (with a distance for similar fonts) and when its file
was added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paste Into gives a frame nested content, but it isn't a group: the Effects
panel showed "Group:" and enabled Isolate Blending and Knockout Group for
it. It now asks Item::is_group, so such a frame shows "Object:" with the
group options disabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Effects panel had the blend mode, opacity and Drop Shadow only. Like InDesign's it now names the level the settings apply to with its blend mode and opacity (Group: Color Dodge 100%, Object: Normal 100%), and has Isolate Blending and Knockout Group, enabled for a selected group, through object.transparencyGroup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Probe reused connections before writing so an app restart does not lose the next call. Report connection failures as not reachable with nothing sent, while retaining uncertain outcomes and no replay after sending begins.
Add same-port restart and unavailable-app loopback regressions, and retain the lost-reply guard.
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>
Prompt for unsaved documents on window close and application/menu quit. Defer discarding documents until all approvals are current, retaining every tab when a later decision cancels quitting.
Refs #544. Cancelable macOS Dock Quit remains separate.
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
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>
free_slot held a column to the minimum gap width (1.5 em) whenever a wrap
object overlapped the line's band vertically, even if the object sat beside
another column of the frame: in a frame of narrow columns, an object beside
column 2 dropped column 1's lines. The minimum now applies only when the
object reaches the column itself (aiongg's review on #589).
Test: wrap_object_beside_one_narrow_column_leaves_the_other_alone (two
18 pt columns in a 14 pt paragraph, an object beside column 2) fails before
(overset) and passes after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the release workflow moved to storytold/craft-common-actions, it has
passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads
values from the release environment, but only for names the caller lists, so
the AZURE_* names that Trusted Signing uses came through empty and Windows
builds would ship unsigned. Same fix as storytold/craft-launcher#174.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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
color.defaultFillStroke, color.swapFillStroke and color.applyNone act on
what the Tools panel proxy shows: with colours going to text
(panels::colors_affect_text) they set the text's fill and stroke through
type.char in one step (Default: black fill, no stroke; Swap keeps tints;
Apply None through panels::apply_swatch), otherwise they run
object.defaultFillStroke, object.swapFillStroke and object.fill/stroke.
The proxy's Default, Swap and Apply None buttons run these commands.
fill_stroke_proxy_keys covers J with a text frame selected.
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
Conflict: crates/ui-egui/src/canvas.rs: focus-lock filter placed after request_focus, before next's menu_open/keyboard_pane early return; the new test kept after next's added tests
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.
While the canvas has keyboard focus, arrow keys, Tab and Escape no longer
move egui's focus to a neighbouring widget. Before, Down or Tab while
typing focused e.g. the status bar's Next Spread button, and the next
Space or Return pressed it, so the view jumped to another spread.
A portable copy unzipped into a read-only folder such as Program Files
kept pointing settings and recovery at a DesignCraftData it could not
write, so both were lost without a word. The data folder is now probed
(created, then a file written and removed) and, when that fails, the
platform folders are used as if there were no portable.txt; the log and
the status bar say which folder could not be written.
DESIGNCRAFT_CONFIG_DIR still wins. Adds
a_portable_install_that_cannot_write_its_data_uses_the_platform_paths.
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.
The proxy's tooltips named keys that did nothing. color.defaultFillStroke
(D), color.swapFillStroke (Shift+X), color.toggleFillStroke (X) and
color.applyNone (/) are UI commands; the proxy's buttons run the same
commands. Swap keeps the fill and stroke tints.
Which of fill and stroke is in front lives in UiState, shared by the
Tools panel proxy, the Color panel and the commands. The single-key
shortcuts stay out of text being typed, like the tool shortcuts.
Conflict: crates/compose/src/lib.rs: next's wrap_obstacles and free_slots kept; frame_specs keeps only exclusions that reach the frame's text area (as wrap.rs does for anchored obstacles), and free_slots applies the 1.5 em minimum segment only when an exclusion reaches the column
Conflict: crates/compose/src/tests.rs: both sides kept; the PR's tombstone tests plus tests for a sliver beside a wrap object in a narrow frame and a narrow first column beside a wrap object in column 2