236 Commits
Author SHA1 Message Date
Martin BallandOPUZZLES ae0692ee66 Layout: avoid repeated text-box lookups during page drawing (#4837)
Lands #4837.

Co-Authored-By: OPUZZLES <109076338+OPUZZLES@users.noreply.github.com>
2026-11-06 02:33:02 +01:00
Martin Ballandpick2510 9a9246915c Layout: Times and Courier take Times New Roman's and Courier New's line spacing (#3241)
Lands #3241.

Co-Authored-By: pick2510 <4068086+pick2510@users.noreply.github.com>
2026-11-06 02:03:34 +01:00
Martin Ballandfinwarman 35e960c850 Align ordinary tabs with paragraph alignment (#3915)
Lands #3915.

Co-Authored-By: finwarman <1339254+finwarman@users.noreply.github.com>
2026-11-05 23:55:58 +01:00
c21d5af2a6 #4181: a click just outside a floating table skips its turned cell text (#4189)
Lands #4189.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: rabis7575 <340973947+rabis7575@users.noreply.github.com>
2026-11-05 23:35:58 +01:00
ro-legal-docxandClaude Opus 5.5 2f49c8ad2e Layout: keep a cell's floats laid out in the cell inside it (layoutInCell) (#4545)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 21:56:00 +01:00
db8d944c5c Fonts: reuse shared MATH construction records (#4660)
Co-Authored-By: OPUZZLES <109076338+OPUZZLES@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 18:50:25 +01:00
107996dd24 REF: preserve paragraph numbers when updating cross-references (#4609)
* M6: preserve numbered REF results during field updates

Adapt and validate the patch published in WordCraft #4533, retaining main's bounded numbering renderer and adding relative-context/cache guards.

Co-authored-by: Alyosha-dot-shop <218732667+Alyosha-dot-shop@users.noreply.github.com>

* M6: reuse bookmark lookup for numbered REF updates

* REF: use explicit imports for the workspace clippy gate

* #4609: adapt synthetic fixtures to shared document data

* #4609: checkpoint review fixes and isolated validation notes

* #4609: retain rendered REF labels and isolate relative context by list

---------

Co-authored-by: Alyosha-dot-shop <218732667+Alyosha-dot-shop@users.noreply.github.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-05 09:13:52 +01:00
Aleksander HelgakerandClaude Opus 5.5 650a97b623 ODT: remove the display settings; one choice for each open case (#5609)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:08:09 +01:00
FredApps 69945e0ccb Merge branch 'main' of https://github.com/storytold/wordcraft into pr-5559
# Conflicts:
#	ROADMAP.md
#	docs/layout-parity.md
2026-11-04 16:08:39 +01:00
pakiatandClaude Opus 5.5 2df2bfc30e Show fresh SEQ, STYLEREF and REF results in PDF export and printing (#5162)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:01:58 +01:00
FredApps 6d22e36196 Merge branch 'main' of https://github.com/storytold/wordcraft into pr-5559
# Conflicts:
#	ROADMAP.md
#	docs/gaps.md
#	docs/layout-parity.md
2026-11-04 15:54:52 +01:00
ReesandClaude Opus 5.5 958867b7e8 Walk Print Markup balloon anchors once (#3614) (#4785)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:51:39 +01:00
FredApps 8b9f726ea7 Merge main (688cce47) into ODT PR 2026-11-04 13:44:24 +01:00
midasdfandClaude Opus 5.5 2d6dff96cc Layout: 行頭禁則 lists small kana and the prolonged sound mark (JIS X 4051 §4), with the emergency-break gap documented (#2558)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:31:39 +01:00
FredApps 773ef21f9f Merge origin/main into the ODT branch (gap 19)
# Conflicts:
#	ROADMAP.md
#	crates/layout/src/tests.rs
#	docs/gaps.md
#	docs/layout-parity.md
2026-11-04 11:57:31 +01:00
Aleksander HelgakerandClaude Opus 5.5 ba58e70cc8 Flag document fonts that aren't installed (#4770)
* Gap 4: flag document fonts that aren't installed

The font box (ribbon, mini toolbar, both Font dialog boxes) shows a
warning mark when the current font isn't installed; its hover note
names the font drawn instead. The status bar says "N fonts not
installed" and lists each with the faces drawn instead, plus Add Font….
Agents get `font.missing` and `format.state.fontShownAs`.

Layout records, where it picks a face per text segment, the fonts the
shown text asks for that aren't installed and the faces drawn instead
(ParaLayout::substituted, DocLayout::substituted_fonts), so hidden
text, unused styles and orphaned stories never count; list labels,
field results and East Asian and complex-script text do, and so does a
missing roman font drawn in Cambria. Metric-compatible substitutes and
symbol fonts drawn as Unicode aren't flagged. Bounded for hostile
files: 64 pairs a paragraph, 1024 a document, names kept to 256
characters, 20 rows in the popup.

Ported from alehel/wordcraft#1 onto current main; English strings only
(translations belong in storytold/craft-i18n).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: missing-font review fixes

- Judge a padded font name by the face layout drew for it, and skip blank names.
- Name the face CJK text is drawn in for a missing East Asian font, not Cambria.
- Keep the missing-fonts list per window, so several windows don't rebuild it every frame.
- Expose the font box's warning to screen readers as its description.
- Count missing fonts with craft-i18n plurals (`i18n::tn`).
- One table of metric twins (`fonts::word::METRIC_TWINS`): a missing font is drawn in an
  installed twin first, and the warning reads the same table.
- Find the status-bar item by part of its label in the click test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: warn for a font picked at the caret; drop fonts of hidden list items

- At a collapsed caret the font box warns for a missing font typing will use, also at the end
  of text in another font (it waited for the first letter).
- A list item that shows nothing (#3982) reports no missing font: its number was shaped, and
  its font recorded, before layout dropped it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: say where Add Font… is, and that it needs the font file

The font box's note said "Add Font… installs it": Add Font… opens a file picker at the end of
the list, so it only helps with a font file at hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: docs say the popup lists 20, and when the font box warns ahead

CodeRabbit review: the status-bar popup lists the first 20 missing fonts, then a count; the font
box also warns at a collapsed caret for a font no shown text uses yet; `fontShownAs` is the
font's substitute, fallback faces aside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: the Font dialog warns for a missing font over a selection

CodeRabbit review: the dialog's boxes show the fonts OK applies, which no shown text may use
yet (an Asian text font over Latin text, a font typed into the dialog), so they name the
substitute whatever is selected. The ribbon's box over a selection still reads the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: the Asian font box names the face CJK text gets

CodeRabbit review: the status bar's list is by font name, so for a font Latin text also uses
it named the Latin text's substitute. The Asian text box now always looks up the face layout
draws Chinese, Japanese and Korean text in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: gaps.md status on the current line, missing-font scope and twin wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: gaps.md says which missing fonts are flagged and what font.missing lists

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gap 4: font.missing lists what the current view lays out

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:48:48 +01:00
alehelandClaude Opus 5.5 56c85834f1 Merge origin/main into the ODT branch (gap 19)
Conflicts kept both sides. Where the two branches met:
- Para.mark: both added it. It stays CharProps (ODT needs it); HTML's
  `<p>&nbsp;</p>` converts its Fmt, and to_doc reads it in one place.
- Arrowheads: the PR's line ends (top left to bottom right) with main's
  arrowhead styles.
- inline_rect: the PR's depth below the baseline with main's rule room.
- Spacing and Cell literals take main's new fields.
- The zip entry guard (#3660) also covers the encrypted ODT paths, which
  parsed a package before it; regression test in tests_more/encryption.rs.
- chunks_exact to as_chunks in odt/frames.rs and odt/path.rs (clippy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:51:35 +01:00
Martin BallandClaude Sonnet 5.5 bff0d1d925 #3867 follow-up: header repeats count canvas members, one budget per layout; contours and chain widths don't depend on cache history (#4385)
* #3867: review — header repeats, picture outlines and linked-box widths bounded the same way every layout

- Header repeats: a Clip (floating Drawing Canvas) counts its members, depth-bounded, so a 1000-member canvas no longer counts as one item (CodeRabbit thread 4373103686); the budget is per layout (Ctx), not per table.
- Tight-wrap outlines: each cache entry stores its picture's pixel cost; a cached hit is charged to the layout's decode budget like a decode, so cold and warm layouts (and the CLI) wrap alike.
- Linked boxes: widths are budgeted by laid-out work (MAX_FLOW_WORK), not 16 widths, so ordinary stories keep exact widths for every box.
- header_rows.contains -> index compare; layout-parity doc revision.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

* #3867: review — a picture that fails to decode still costs its pixels when cached; header repeats restored on retry

- contour: Alphas records the pixels each picture charged (costs map), also when the body then fails to decode, so the cache entry's cost is right and warm layouts charge it like cold ones.
- Snap saves and restores ctx.header_repeats so balance_pass retries don't keep their charge.
- Tests: truncated large PNG cold vs warm; canvas header row end to end through place_table; linked-chain bound <= 8; vertical box's layout differs from every other box's.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

* #3867: CodeRabbit — header and linked-box budgets count every clip descendant

placed_cost counts iteratively (explicit stack, stops past the limit) and is shared by flow_work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

* #3867: docs — progress log, gap 20 note for layout resource caps and linear-time passes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

* #3867: review — roadmap gives each cap its unit: header items, wrap-cache pixels, linked text box lines and items

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #3867: CodeRabbit — the vertical-box check runs past the work cap too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #3867: review — a linked chain's work counts the story it lays out, not only the lines it shows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #3867: review — header-row cost import follows main's explicit layout imports (#4501)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #3867: review — a test pins the header repeats that column balancing gives back; docs keep main's caps row and both gap status lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 00:45:19 +00:00
Claude 8d5485c8bd M5 #19: ODF interpretation profile (WordCraft / LibreOffice / custom)
What ODF 1.3 defines, including XSL's initial values for fo: properties,
is followed whatever the settings: unstated page margins are now 0, and
padding without a border takes room in every ODT document. What the
standard leaves to the application (Part 3 §16.2) is an OdfProfile:
WordCraft's own choices by default, LibreOffice's, or custom values.
The profile fills in only what a file leaves unsaid; stated values win
under every preset (tests_more/profile.rs).

Cases: header height, how its height and spacing combine, dynamic
spacing, tabs from the indent, column gap, shape fill, line and
hairline, page frames against a paragraph, in-line frames against a
paragraph or page, and lines of only pictures.

- doc: odf_profile module; Settings::odf (OdfLayout) replaces odf_layout
- formats: import_with / import_flat_with take a profile
- engine: Prefs::odf, file.options `odf`, opened files follow it
- ui: File > Options > Advanced > OpenDocument files (presets, custom cases)
- cli: WORDCRAFT_ODF=wordcraft|libreoffice
- docs: odf-interpretation.md lists every case (law, open, writer
  choices, Google Docs and ONLYOFFICE findings); parity docs, gaps,
  ROADMAP updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 20:54:05 +00:00
Claude be2666da15 Gap 19: ODT padding without a border, unequal columns, column breaks
Found with LibreOffice's report templates and one-page résumé.

Padding without a border:
- LibreOffice gives a paragraph's padding room on every side, with or
  without a border; measured, 0.5 in of padding and no border moves the
  text 0.5 in in and down. WordCraft dropped it.
- The reader now keeps it (an invisible border holding the space; a new
  `Borders::takes_room()`), and the writer writes it back.
- The layout adds it to the spacing or indent under the document
  setting, now renamed `Settings::odf_layout` to cover all of
  LibreOffice's layout rules. `ResolvedPara::padding` carries it, so
  Word documents are unchanged.

Columns:
- Unequal columns (`style:column` relative widths and indents, on pages
  and sections) were made equal. They now keep their widths and are
  written back as shares. The writer leaves out `fo:column-gap` beside
  them, because LibreOffice ignores a page's column widths when a gap is
  stated (checked).
- Columns with no stated gap have none, where they had Word's 0.5 in.
- A paragraph starting a column (`fo:break-before="column"`) now starts
  with Word's column break, written back on the paragraph.

The report templates round-trip to 0-11 differing pixels (from up to
9,600), and the résumé to 24 (from 6,286) with its text the same.
Across the corpus the round-trip difference falls from 155k to 82k
pixels. WordCraft's own rendering difference falls from 469k to 428k,
and no document gets worse.

gaps.md row 19 and the ROADMAP log are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 15:39:32 +00:00
Claude 959770fa13 Gap 19: ODT lines of only pictures are as tall as the pictures
LibreOffice sizes a line holding only in-line pictures by the pictures
alone. Its font's ascent and descent don't count: measured in
LibreOffice, a 3.6 pt rule's line is 3.6 pt, where WordCraft (following
Word) made it 15.5 pt.

- A new document setting, `Settings::object_lines_fit_objects`, makes
  the layout size such lines by the pictures alone. The ODT reader turns
  it on, so other documents keep Word's layout. An equation or any text
  on the line keeps the font's height.
- A document with no page layout at all (no styles.xml) opened on Word's
  Letter page with 1 inch margins. It now uses the region's paper with
  LibreOffice's 2 cm margins.

WordCraft's own rendering of LibreOffice's elegant agenda comes close to
LibreOffice's: its pixel difference halves, from 17,428 to about 8,950.
Across the corpus WordCraft's rendering difference falls from 487k to
469k. No document gets worse, and round trips are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 14:51:51 +00:00
Claude 8f65b867c0 Gap 19: ODT tables fit the margins and keep their spacing; header spacing kept
Found with LibreOffice's report templates.

Tables:
- A table filling the margins (`table:align="margins"`) but wider than
  them ran off the page. It is now fitted to the text width, its columns
  in proportion. Checked in LibreOffice, which fits such a table with or
  without relative column widths; the old test's claim otherwise is
  corrected.
- A table's own space above and below (`fo:margin-top` and
  `fo:margin-bottom` on the table) was dropped. It is now read into new
  `TableProps::space_before`/`space_after`, laid out (added to the
  spacing of the paragraphs around it, as LibreOffice does) and written
  back.

Headers and footers:
- A header taller than its band lost its spacing from the body, moving
  the text up. LibreOffice keeps the spacing below the content unless it
  is dynamic (`style:dynamic-spacing`, which behaves as Word does). New
  `SectionProps::header_gap`/`footer_gap` hold it. The layout keeps it
  past a tall header or footer, and the writer writes it back. Word keeps
  no gap, so DOCX documents are unchanged.

Across the 125-document corpus, the round-trip pixel difference falls
from 241k to 185k, and WordCraft's own rendering's difference from
LibreOffice's falls from 562k to 484k. No document changes page count or
gets worse; one report template falls from 7,646 to 602 pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 13:38:36 +00:00
Claude 5cdf2608bf Gap 19: ODT tables fixed layout and starting a page; a page break before a table's first row moves it
- A table starting a page (`fo:break-before="page"` on the table,
  LibreOffice's agenda minutes) didn't. Its first paragraph now has a
  page break before it, and it is written back on the table.
- Layout: a page break before the paragraph in a table's first row
  now moves the whole table to the next page, as Word does. It was
  ignored; this matters for Word documents too.
- ODT tables are now fixed layout (Word's `w:tblLayout`).
  LibreOffice's columns never fit their content.

LibreOffice's 6orange agenda now round-trips with the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 10:08:59 +00:00
499fc6289e Columns: the line between them is drawn for selected text, as tall as the section's text (#5178) (#5183)
* #5178: the line between columns is drawn for a section starting mid-page

Columns for selected text are a continuous section in the middle of a page, and
the line between columns was only added when a page started, down to the page
bottom. The lines are now drawn per section on each page, from the top of its
text there to the bottom of its tallest column, when the page or the section ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #5178: the line between columns spans the text's lines, not its paragraph spacing

Review: the line started above the section's first line (its space before) and
ended below its last (its space after). It now runs from the highest text line or
table cell of the section on the page to the lowest, falling back on the flow's
extent when there are none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #5178: the column lines find the section's text by place, and draw nothing without it

Review: the section's items were the page's items after an index, which objects
behind the text (inserted at the front) shift; the flow's extent drew a line
where the section had only spacing; overflow lines of exact-height cells counted.
The text is now the page's top-level body lines and table cells below the
section's top; lines in cells count by their cell's box; no text, no line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #5178: the column lines take the section's text by its blocks

Review: a floating table of an earlier section placed lower on the page would
pass a filter by place. The section's text is now the page's items of top-level
blocks from the section's first block on the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:39:35 -05:00
Brandon ThomasandClaude Opus 5.5 bf80a31d10 docs: merge the Tables row duplicated by the #5145/#5161 doc merge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:23:38 -05:00
a64d9748f2 Tables: a row isn't split to leave a picture cell empty beside its text; it moves on whole, as in Word (#4272) (#5161)
When a row's first part on a page that already holds something would leave
a cell empty because that cell's inline picture (one line, never split) or
first line runs past the room, the row moves on whole instead of leaving the
other cells' text beside the empty cell. An at-least row grown taller than its
minimum by its picture used to pass the #1742 check (room >= declared minimum)
and then split, the picture going on alone. At the top of a page such a row
still splits.

Co-authored-by: Harrison Smith <harrison@hsmith.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:23:01 -05:00
c395760959 Tables: a middle or bottom cell of a row split across pages lines up on the first page (#5145)
Measured in Word for Windows: when a table row breaks across pages, a cell
aligned to the middle or bottom whose text all fits in the part of the row
left on the page is aligned within that part, as tall as the other cells'
text there (with its space after), and a cell whose text doesn't fit runs
from the top of the cell with the rest on the next page. WordCraft kept the
offset the whole row's height gave the cell, so a short centred label
beside a long cell went to the top of the next page and left its cell empty
on the first, and a long centred cell started far down the page.

The vertical offset each cell's content got is kept with the row, so a split
moves the content back to the top before it decides what fits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:21:25 -05:00
6ab1023ed6 Layout: tab leader dots spaced and placed as in Word (#5144)
* Layout: tab leader dots spaced and placed as in Word

Measured in Word for Windows with six fonts at four sizes: Word spaces leader
dots by the dot's advance at the font size in whole 600-dpi pixels (the size
itself in whole pixels), so a TH SarabunPSK dot at 16 pt (2.59 pt) is drawn
every 2.64 pt and a Tahoma one at 22 pt every 6.6 pt. The dots sit on the
page's grid of that pitch, from the first place after the text before the
tab to the last one that ends before the text after it. WordCraft drew them
at the unrounded advance, half a dot after the text and 1.2 dots short of
the number, so a table of contents line had a dot or two more than Word's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: each leader dot's place comes from its grid index

The leader's dots were placed by adding the pitch to the last place, so on
a long leader the rounding of each addition added up. From 72 to 528 pt at
2.64 pt it came to more than the 0.001 pt allowed at the end, and the last
dot, which ends exactly on 528 pt, was left out. `leader_xs` now works out
each place as its grid index times the pitch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: name the leader slack and glyph limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: leader dots use the font size rounded to whole pixels

Word rounds the font size to the nearest 600-dpi pixel before it rounds the
dot's advance (Arial 14 pt: 116.7 px, so 117, dots every 3.96 pt); taking the
floor drew them 3.84 pt apart, as measured at 11, 14, 17, 20 and 26 pt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* leader_xs: saturate the glyph range end

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:19:41 -05:00
639b2342c2 Lists: a level above counts its first number at the first deeper item (#5108)
* Lists: a level above counts its first number at the first deeper item

Word counts a list level that hasn't been used yet as started at its first
number when a deeper item comes first: after 10.1-10.4 with no level-1 item
before them, the next level-1 item is 11. WordCraft showed 10 in the deeper
labels but didn't count it, so the next level-1 item repeated it ("10.
Qualifications", then "10. Penalties" in a tender document whose sub-items
come first).

Counters::next_label now starts each unstarted level above the one it counts,
at that num's start override if it has one, else the level's start. Measured
in Word with ListFormat.ListString: 1.1, 1.2, 2., 3.; (1), (2), 2. when the
deeper text doesn't show level 1; 1.1.1., 1.2., 2. from level 3.
a_list_s_own_level_definitions_win expected the old count; its level 0 item
after level 1 items is now 2., as in Word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Lists: record the counting fix in the progress log and gaps

AGENTS.md asks for the progress log and the gap list when work lands. The
ROADMAP progress log has the entry; gaps.md records that no ranked gap
changed. Each with its status line and revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Lists: test a num's start override for the level above

The deeper-item-first test now also gives the num a start override for
level 0 (10) under level-1 items: 10.1., 10.2., then 11. It fails if the
override is ignored for the level above (1.1., 1.2., 2.).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test a start override of 5 and a restart after a level-1 item

Review: 5.1., 6., 6.1. with the num's start override, and level 2 restarting below the next level-1 item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:18:14 -05:00
VoiceLessQandBrandon Thomas 4a3586c686 Paragraph side borders lie outside the text (#5036) (#5088)
* #5036: paragraph side borders lie beyond their distance from the text

push_para centred the left and right rules on the distance, so a 3 pt border
at no distance reached 1.5 pt into the text and, drawn after the lines, covered
the first letter. In a heading whose border matches its shading the letter
looked cropped, on the page and in the ribbon's style tiles. All four lines now
lie outside the distance, as the top and bottom ones did. Top and bottom reach
the corners and the shading fills out to the outer edges, so no seam shows
where rules and shading meet.

* #5036: pin the right border on its own in the side borders test

The test only compared the right border with the end of the top rule, and both come from the same value, so a wrong right edge still passed. Check it against the line's own bound.

* docs: progress lines for #5088

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:14:59 -05:00
Fin WarmanandBrandon Thomas e5f2885ecc Use the body boundary for header margin anchors (#2485)
* T280: anchor header drawings below tall headers

* #2485: share rendered header boundary with pins and obstacles

* docs: layout-parity status line for #2485

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:11:56 -05:00
Claude b87c80cdc7 Layout: text in a turned text box turns with it
A turned text box (free rotation) drew its frame turned and its text
upright. Its text now turns with it about the box's centre, as Word
draws it, on screen and in PNG and PDF export: upside down when the
box is flipped across, never mirrored by a side-to-side flip.

PDF export draws item by item, to tag each paragraph, so it turns each
of the box's lines the same way, and tags the turned lines as their
paragraph's text rather than as decoration.

Clicks and the caret inside a turned box still go by its upright text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 17:42:07 +00:00
Claude 45b852ad4a Layout: in-line drawings move with their run's raise or lowering
An in-line picture, shape or chart always stood on the baseline,
whatever its run's position. Word moves an in-line picture with its
run's position (`w:position`, Font › Advanced › Position):
- lowered, it reaches below the baseline (at most until its top is on
  the baseline) and the line's descent grows to hold it;
- raised, it stands clear of the baseline at its own height.

A drawing's cluster now reaches `obj_h` above the baseline and `obj_d`
below it (negative when raised). It is painted, hit-tested and wrapped
from both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 17:24:15 +00:00
a4bcf05e11 Layout: a kept heading goes over with a three-line paragraph that widow control moves whole (#4991)
Keep with next asked for two lines of the paragraph after a kept heading under widow/orphan
control. A three-line paragraph cannot split at all there (two lines and a widow, or an orphan
and two), so it moved over whole and left the heading alone at the foot of the page. Word moves
the heading with it: ask for all three lines in that case.

Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 10:49:46 -04:00
ReesandBrandon Thomas a4b1682b41 Index laid-out objects during connector rerouting (#3631) (#4622)
* #3631: index laid-out objects during connector rerouting

* docs: keep the gaps.md note in the status line and revision table

* docs: drop a stray blank line

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-30 00:13:32 -04:00
.CooCooandBrandon Thomas 39ae5f6778 Layout parity: field results take \* DollarText, \* Hex and \* ArabicDash (#4361)
* Layout parity: field results take \* DollarText, \* Hex and \* ArabicDash

docs/layout-parity.md gets a row for the three field switches that #4226
added, a revision entry and a new status-line clause. The row describes
main as it is after 8c035fbb, where \* ArabicDash formats any number
field and table formulas, not PAGE alone.

* Layout parity: dollar text keeps its digits only past 999,999.99

Cardinal and ordinal text keep their digits past 999,999. `\* DollarText` writes
results up to 999,999.99 (crates/layout/src/fields.rs dollar_text).

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 22:22:55 -04:00
Fin WarmanandBrandon Thomas f658c872a2 Tables: share spanning minima by preferred widths (#1883)
* Tables: share span minima by preferred widths (#1883)

* 1222: Match nested preferred span weights

* docs: word the #1883 layout-parity entries

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:40:41 -04:00
Martin BallandClaude Opus 5.5 1b485e1f90 #3907: CodeRabbit — a kept paragraph is measured against the next page's body (a different first-page header)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:24:50 +00:00
Martin BallandClaude Opus 5.5 5225569d44 #3907: review — an empty mark taking no room, or stacked past the page bottom, draws no list label; an empty mark after a text paragraph closing a section stays quiet
dpkruse's Genuine Steps Certificate: a bulleted text paragraph closes a
two-column continuous section, then an empty bulleted one closes another.
Word draws no bullet for the empty one; it is now quiet (takes no room) and
draws no label, still counted. A stacked mark that doesn't fit keeps its
line for the caret but draws nothing below the margin (CodeRabbit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:04:05 +00:00
Martin BallandClaude Opus 5.5 7757828295 #3907: continuous sections running off a page go onto the next; empty marks in a row stack to the page bottom
A continuous section moves the column top down below the text above. at_top()
then held at every new section start, so a paragraph that did not fit was
placed anyway, as on an empty page. A run of one-line continuous sections ran
off the bottom of page 1 (100 of them reached 2,480 pt down a 792 pt page).
The overflow checks for paragraphs now use fresh(): at the top with nothing
above on this page. at_top() still drives the spacing rules, unchanged.

Empty paragraphs that close a continuous section right after another
section's mark took no room. Measured in Word on synthetic files ("Start", N
such marks, "End"): 10 put "End" about 9 cm down page 1; 50, 100 and 200 all
put it on the first line of page 2, the same with 24 pt marks or no paragraph
spacing. So each takes its line while it fits, then stays at the page's
bottom. A mark right after text stays quiet, as measured before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-27 09:56:35 +00:00
aiongg 95ba3d07b4 docs: update paths of moved files 2026-10-26 21:16:49 +00:00
Dylan M. Taylor c0627f2b0b Merge branch 'main' into sec/b 2026-10-25 21:16:51 -04:00
Martin BallandClaude Opus 5.5 87d3ea5363 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:21 -04:00
Martin BallandClaude Opus 5.5 0f1a127e63 Findings 39, 70, 59, 46, 61, 44, 62: record the new font limits where
the matching parity rows and the MCP write-root list live.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:11 -04:00
Martin BallandClaude Opus 5.5 1ae38ec2be #3692: review — stacked linked boxes share one layout; past the cap a box takes one in its own direction
Stacked text is laid out at STACK_WIDTH whatever the frame, so its flow key now is too.
The nearest-layout fallback only picks layouts of the box's TextVert; the first box in a
direction is laid out (at most a few more layouts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 32e4900bc5 #3692: a linked chain past 16 widths reuses the nearest; opaque pictures and budget refusals don't cost a later picture its outline
Ultrareview finding 6: boxes past a chain's 16th distinct width (compared
by exact float equality) showed no text, which resized boxes hit easily.
Widths are now told apart to a twip, and past the cap a box takes the
nearest layout already made (same direction, widest that fits, else the
narrowest). Work stays bounded at 16 story layouts.

Ultrareview finding 7 (also raised by CodeRabbit): opaque non-JPEG
pictures were decoded and charged to the per-layout pixel budget though
they give no outline, and a budget refusal was cached in LayoutCache as a
permanent "no outline". The header (read via into_decoder, PNG tRNS
included) now says whether a picture can be transparent before anything is
charged; Alphas::outline reports OverBudget distinctly from Ok(None), and
Ctx::wrap_outline (via picture_outline) doesn't keep a refusal in
LayoutCache::outlines. Within one layout a refused picture is remembered
so its header isn't re-read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 ed78539488 Progress log and layout-parity notes for audit findings 7, 11, 35, 38.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 0674573b45 #3692: progress log and layout parity note for linear-time lookups
Findings 1, 3, 5, 13, 23, 28, 29 and 30 (#3706): one Progress log row, one Performance row in layout-parity.md, revision-history rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
skylar taylor-barrick f7c49cb0f8 egui-037: reconcile main and protect IME undo and wrapped layout 2026-10-25 16:15:05 +01:00
MarekSpodBiedry 14f0eec255 Stretch justified lines that end in a manual line break (#3924) (#4023) 2026-10-25 10:27:35 -04:00