crates/ui-egui/src/panels/properties.rs, properties/object.rs: main's split module kept; preview_row and paint_corner_shape exported for the shading corner menus
crates/ui-egui/src/panels/properties/tests.rs: both sides' tests kept
crates/ui-egui/src/dialogs/tests.rs: main's tests kept; the border test uses the new corner labels; the shading layout test added
crates/ui-egui/src/i18n/*.rs: union of keys without duplicates
crates/ui-egui/src/dialogs.rs: the corner shape face uses corner_shape_button (no hard-coded colour in the dialog)
crates/ui-egui/src/dialogs.rs: both dialog tests kept
crates/ui-egui/src/i18n/*.rs: union of keys
docs/target-app-parity.md: main's change line plus the Text Frame Options entry
fit() counted every break character in the overset text and every
Start Paragraph option. A break followed by a paragraph whose option it
already meets moved the text once but asked for two frames, and breaks
that tracked changes delete or a hidden condition hides asked for
frames that composing never uses. Autoflow added those pages and
removed them again: 200 chapters that end in page breaks and start on
the next page took 61095 ids and 430 ms, now 798 ids and 16 ms.
Each paragraph composing places now counts once, when a visible break
ends the paragraph before it or its start option sends it on; after a
break it is at the top of a frame, where the option only fixes odd or
even page parity. A visible break inside a paragraph counts once more.
previous_visible_break and the new visible_break in designcraft-compose
are public, so the estimate and the composer use one rule.
Reserve column and spanning footnotes when fitting inline table row groups. Continue rows in the next column or frame when the remaining space is too small, and include cell-local footnotes in measured row height.
Fixes#1901
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The start option lookup now reads the next paragraph composing places,
past tracked-deleted ones. Frames that start options and breaks force
are counted and asked for in one round, so a story with a paragraph per
page no longer gets one frame per round.
* refactor(engine): split convert_all's per-story plan into convert_plan
Resolves SonarCloud rust:S3776 on convert_all (cmd/variables.rs).
* refactor(pdf): split graphic_unclipped into per-kind helpers
Resolves SonarCloud rust:S3776 on graphic_unclipped (pdf/src/export.rs).
* refactor(compose): extract wrap exclusions and frame text area from frame_specs_placed
Resolves SonarCloud rust:S3776 on frame_specs_placed (compose/src/lib.rs).
* refactor(ui): extract command shortcut lookup from shortcut_key
Resolves SonarCloud rust:S3776 on shortcut_key (ui-egui/src/menus.rs).
A column rule ran from the top of the columns to their bottom, so a frame
that the text only half fills drew its rules down to the empty bottom of
the frame. InDesign draws them from the top line's ascent to the bottom
line's descent. column_rule_rects now takes that extent from the frame's
lines, and the top and bottom insets shorten the rule from there. A frame
without lines draws no rules. Existing documents draw their column rules
at the new length, on screen, in PDF and in PowerPoint export.
layout_line set a line with a tab or a right indent tab without the optical
margin or CJK edge hangs, while the greedy breaker and the paragraph composer
counted them. A line that fit only because its period hangs was then squeezed
into the measure or set past it.
layout_line now hangs the edge glyphs of every line. Tabs are measured from the
hung start, so tab stops stay where they are, and a right indent tab fills to
the hung end. A tab at a line edge hangs nothing. Lines without tabs lay out as
before.
* Type: story Optical Margin Alignment with a size, and Ignore Optical Margin (engine, IDML)
A story's Optical Margin Alignment applies to every paragraph except those
with Ignore Optical Margin; hang amounts scale with the story's size.
story.options sets it, type.para and paragraph styles take
ignoreOpticalMargin. IDML imports and exports StoryPreference
OpticalMarginAlignment/OpticalMarginSize and IgnoreEdgeAlignment. The
per-paragraph opticalMargin switch keeps hanging at the text's own size.
Part of #947.
* Type: the Story panel, and Ignore Optical Margin in the Paragraph panel menu and Paragraph Style Options
Window > Type & Tables > Story sets Optical Margin Alignment and its size
for the stories of the text selection or the selected text frames through
story.options, and is disabled without either. type.selectionAttrs
reports the stories' values as storyOptions, null where they differ.
The Paragraph panel menu toggles ignoreOpticalMargin, and Paragraph Style
Options > Indents and Spacing has an Ignore Optical Margin checkbox.
Part of #947.
* ROADMAP: the story Optical Margin Alignment entry names its default and scope
* Anchored text frames compose against the grid and page where they are placed
An anchored text frame is not a spread item, so frame_specs composed it at
its stored transform: no page, no page side, no page rect, and a baseline
grid phase taken from that transform. A note set to Only Align First Line
to Grid snapped to the wrong line, and page-number markers and
spine-relative alignment inside it saw no page.
The composer now looks up the host story's composition first, finds where
it placed the frame (in its frames' anchored objects, footnotes or table
cells) and composes the frame's story against that placement
(ComposeOptions::placed). The cache does the same and adds the placement
to the entry's signature, so a note recomposes when its host moves it.
Host lookups stop after eight levels, which bounds stories anchored in
each other.
* Anchored text frames: the cache keys on the placed spread's pages
* Anchored objects: Relative to Spine mirrors custom positions on right-hand pages
* Anchored objects: page side from the frame's spread position; footnote cache keyed by page side
A group's transform can move a frame onto another page; the page lookup
used the frame's bounds without its parents' transforms, so the grid took
the wrong page's top margin.
The setting was imported and editable but never read: the composer, the
canvas grid and baseline snapping measured the grid from the page top.
BaselineGrid::origin gives the first line on a page (start, plus the
page's top margin for Top Margin), and all three use it. The composition
cache tracks the setting and the pages' top margins.
Also adds a left-to-right facing-pages guard test for mirrored margins
and the right parent page's guides and running head.
The first-line indent was added to the left indent whatever the paragraph's
direction, so a right-to-left paragraph's first line moved the wrong way. With
the hanging indent a numbered list uses — right indent 10 mm, first line
−10 mm — the first line went 10 mm outside the frame on the left while the
wrapped lines sat where it should have been.
It is the paragraph's start-side indent, which is the right one in a
right-to-left paragraph; the drop-cap case beside it already made that
distinction.
Measured on a 200 pt column with that hanging indent:
first line right edge 200 (the frame's edge, as InDesign sets it)
wrapped right edge 171.65 (10 mm in)
every line left edge >= 0 (nothing outside the frame)
and the same paragraph left to right is unchanged, first line at 0 and wrapped
lines 10 mm in.
The test checks both directions; the right-to-left half fails without the
change. 359 composition tests pass unchanged.
Paragraph Style Options › Paragraph Shading and Paragraph Border are headed
with the page's name; Tint is a spinner with InDesign's presets and has
Overprint beside it (shading, border stroke and gap: new paragraph
attributes, read and written in IDML as ParagraphShadingOverprint,
ParagraphBorderOverprint and ParagraphBorderGapOverprint; PDF export warns
that it doesn't overprint yet, as for objects). Corner Size and Shape is
InDesign's two-by-two grid of corner icons, size spinners and shape menus
showing each shape's glyph, with the link in the middle; the offsets have
spinners, with the edges and width beside them; the weights have spinners.
Found in the Learn Skills Daily course, chapter 41.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The points left out are recorded as story bytes, so the nested line styles
prepass (which indexes glyphs differently) leaves them out too, and a
style still covers exactly the lines it is set on. Their re-lays have a
bound of their own: each leaves out one more point of the paragraph, and
the keep options' budget no longer stops them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Hyphenate Across Column off, a paragraph whose line ending at an
automatic hyphenation point would end a column or frame is re-laid
without that point, so the word moves on whole, as InDesign does. The
points left out are kept per paragraph across re-lays and cleared when an
earlier paragraph is re-laid. On by default, as in InDesign.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep In Frame holds a rule above inside the frame (its top), not below the
top inset. A rule's gap colour in Text Color (`Text Color` or
`$ID/Text Color`) imports as Text Color, as the rule's colour already did,
and exports as a string property rather than as no swatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>