docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole.
crates/pdf/src/lib.rs: t01's vector_text kept before t05's watermark (box area signature). Test files and canvas.rs tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
crates/metafile/src/lib.rs: re-export both TextLayout (#1915) and crop_window.
ROADMAP.md, docs/layout-parity.md: main's status lines kept; the PR's log rows added.
ROADMAP.md: status line once (this change first, then main's); progress-log and history rows union in date order
docs/layout-parity.md: status line once (this change first, then main's); history rows union in date order
crates/proof/src/lang.rs: main's English-dialect helpers kept; code() doc lists `de`.
crates/proof/src/lib.rs: hostile-input test loops over main's languages plus De.
crates/ui-egui/src/options.rs: language_name maps EnGb/EnCa and De to no dictionary name.
ROADMAP.md: main's status lines; both progress-log and revision rows kept.
docs/gaps.md: main's status line; gap 8 row is main's with German hyphenation added; revision rows unioned.
docs/layout-parity.md: main's status line, Tabs and Drop caps rows; the PR's Hyphenation row; revision rows unioned.
docs/localization-parity.md: main's status line; revision rows unioned.
* #2617: centre equation-array rows without &
A row with no alignment mark sat in the first column and was right-aligned
to it, widening that column and pushing the aligned rows right (Word's
"First Property of Radicals" case equation). Word centres such a row in the
array; it no longer widens the aligned columns.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #2617: review — test a lone eq-array row wider than the aligned columns
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* #1500: shape fills keep gradients and transparency (model, DOCX read/write)
A shape's, text box's and canvas's fill is now a `Fill` (solid with
transparency, or a gradient kept as the file states it) instead of one
opaque colour, so gradients and transparency survive opening and saving.
- doc: `fill::Fill`/`Gradient` with sanitizing for hostile input; `.json`
keeps writing opaque solid fills as `[r,g,b]` and reads both forms;
`ShapeExtra::outline_fill` keeps an outline gradient.
- docx read: `a:solidFill` with `a:alpha`, `a:gradFill` (stops, `a:lin`,
`a:path`/`fillToRect`, `tileRect`, `rotWithShape`, `flip`), theme fill
styles through `a:fillRef` 1-999/1001+ with `phClr`, `a:grpFill`,
`satMod`, outline gradients, VML `v:fill` gradients and opacity, canvas
backgrounds.
- docx write: `a:solidFill` + `a:alpha`, `a:gradFill` as read.
- engine/docbin/layout: call sites; drawing still uses the main colour
(next commit draws gradients).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: draw shape gradients and transparency (layout, render, PDF)
- layout: WordArt's paint becomes a shared `display::Paint` (solid,
linear, radial); `paint_for` lays a fill over the shape's frame
(linear at its angle, `scaled` honoured; path gradients circular from
their focus to the farthest corner). Shapes, freeforms and canvas
backgrounds use it.
- render: one `set_paint` for shapes, paths and WordArt; radial
gradients; soft edges fade the real fill.
- pdf: axial and radial shadings with per-stop opacity.
- Review fixes: nested groups without a fill pass theirs on to
`a:grpFill`, text-box contents don't take a group's fill, `phClr`
transparency only on `phClr` colours, VML focus ends consistent and
opacity clamped, `rotWithShape`/`scaled` written only when set,
Shape Outline clears an outline gradient, shared stop sanitizing.
- docs: file-format, layout parity, gaps #1, roadmap progress log.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: shape.fill sets transparency and gradients on every selected shape
- shape.fill (now cmd/shape_fill.rs) takes {"color", "transparency"?} or {"gradient": {stops,
type, angle, focus, rotateWithShape}}; a colour alone keeps the transparency; values are
sanitized; applies to every selected shape, text box and canvas as one undo step.
- Formatting (shape.fill, shape.effects) keeps a Shift+click multi-selection, as in Word.
- view.formatShapePane toggles the Format Shape pane.
- Session::undo_mark/merge_undo_since: a pane drag that applies as it goes undoes in one step.
- Helpers for the pane: params(fill), stop add/remove/move, directions, variations and
presets (variation and preset colours provisional until compared with Word).
- Catalog: Shape Fill Gradient, Format Shape Pane.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: Shape Fill › Gradient and the Format Shape pane
- Shape Fill › Gradient: No Gradient, Light and Dark Variations of the shape's colour, More
Gradients… (opens the pane).
- Format Shape pane (right): No fill / Solid fill (colour, transparency) / Gradient fill
(presets, type, direction, angle, stops bar with add/remove/drag and arrow keys, per-stop
colour, position, transparency, rotate with shape). Drags preview live, one undo step.
- Right-click › Format Shape… on shapes, text boxes and canvases.
- Gradient swatches have accessible names; new labels translated (own wording, clean room).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: preset gradients are our own design (clean room)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: Gradient variations, starting gradient and directions as Word 365 does them
Observed in Word 365 (behaviour and values only):
- Shape Fill › Gradient: a No Gradient swatch, then 13 Light and 13 Dark Variations in a
grid five wide, in Word's order; light stops tinted 66/44.5/23.5% with saturation ×1.6,
dark shaded 30/67.5/100% with ×1.15; linear ones scaled; corners with their tile rectangle.
- Format Shape › Gradient fill starts from the theme's first accent (four stops, 0/74/83/100%).
- Corner directions in the pane get the same tile rectangle; Add/Remove sit by the stops bar.
- shape.fill carries "scaled" and "tile"; docx exports color_modified (lumMod, lumOff, tint,
shade, satMod) so variations use the reader's own colour maths.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: review fixes for the shape fill ribbon controls
- Gradient variations are built from the colour the shown variation came from, so picking
variations in turn, or No Gradient, doesn't drift the colour.
- A drag's undo mark is forgotten while the Format Shape pane is closed (a stale mark could
merge later undo steps).
- Dragging on the stops bar away from a handle no longer moves the selected stop; the arrow
keys are taken by the bar (no focus moves or shape nudges).
- Direction and Type changes keep a linear gradient's "scaled" and an imported tile
rectangle; only a corner direction sets its tile.
- Colour modifiers (lumMod, lumOff, tint, shade, satMod) live on Rgb in the doc crate, shared
by the DOCX reader and the gradient builders.
- Previews build their stop list once per swatch, not per vertex.
- Translations for the pane's hint, direction and preset names.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: adapt to main: caret contrast samples gradient fills; upstream tests use Fill/Paint
- Paint::at gives a paint's colour and opacity at a point; the caret's background sampling
(#299) uses it for shape fills, now gradients or transparent, and the Format Shape
previews use it too.
- Upstream tests (#886 shapes survive saving, render) build fills as Fill/Paint.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1500: adapt to main after the rebase: VML presets, rounded corners, new catalogs
- vml_fill takes the shape's shapetype: a VML preset shape (#1377) takes its fill, opacity and
gradient from its shapetype when it states none of its own, as it already took its colour.
- Rounded-rectangle corners (#1476) and the gradient paints meet in render, PDF and the caret's
background sampling; ShapeExtra keeps both `corner` and `outline_fill`.
- Tests that landed on main since build fills as Fill/Paint (VML presets, cover pages, VML
freeforms, path arrowheads, PDF and render shapes).
- The 40 Shape Fill / Format Shape strings in the hr, ko, sv and vi catalogs, which main
gained since.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz
* #1500: docs: shape gradients and transparency, Shape Fill › Gradient and the Format Shape pane
Re-applied on main's docs after the rebase: progress log, file-format, layout and UI parity
rows, gaps #1, status lines and revision history; parity checklist regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz
* #1500: a theme picture fill falls back on the reference colour; VML freeform opacity note
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz
* #1500: adapt to main after the second rebase: VML shapetype tests compare Fill; parity checklist regenerated
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz
* #1500: one VML fraction parser; a preset takes its gradient from its shapetype
Review notes: story.rs's watermark/freeform opacity reuses shape_style's vml_fraction (clamped at the callers) instead of a second copy; a test pins a VML preset whose v:fill gradient comes from its v:shapetype, and the 65536ths form of a fraction.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz
* #1500: adapt to main after the third rebase: the shape duplicate preview test fills with Fill
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz
* #1500: review — harness test: right-click a shape, Format Shape… opens the pane and keeps the shape selected
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #1500: review — theme keeps every fill style a fillRef can reach (999 per list), still bounded
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* #2048: text above a float's anchor wraps around it
A floating object joined the page's wrap areas only when layout reached
its anchor paragraph, so paragraphs above the anchor ran behind a picture
placed higher up the page (fixed on the page, or moved up from its
paragraph). Layout now notes such floats and lays the pass out again
(at most 3 times) with their wrap areas known from the page's start.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #2048: early-float wrap settles when the anchor moves to the next page
Review follow-up. Rewrap passes add to the wrap areas pages start with
instead of replacing them, and a pass is done when every float found
above its anchor sits inside the room its page started with. A float
whose wrap pushes its own anchor onto the next page has no fixed point;
the first page keeps the room and the float goes with its anchor, so
the passes no longer swap between the two pages. The last allowed pass
moves any float still not covered into its seeded room, or down to its
paragraph's top, so the returned layout never has text under it.
Tests: the reviewer's page-boundary case, and a last pass with nothing
seeded keeping floats clear of earlier text.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8veLveuUsJJm6UEcQPEuW
* #2048: review — the early-float page-boundary case is pinned for 4 to 18 paragraphs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #2048: review — a column break into another width keeps the floats where the first piece drew them
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* #1740: triple borders draw as three lines on screen and in PDF
Triple borders (tables, paragraphs, the ### AutoFormat line) were drawn as
double lines. Layout now emits a Triple stroke; the renderer and the PDF
exporter draw three thin lines across the line's direction, so vertical
table borders get three side-by-side lines too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1740: rebase onto main — triple border strokes use the line's f64 ends
Main now converts the line ends to f64 once and draws through a shared
offset-line closure; the triple stroke uses both instead of dereferencing
the shadowed values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #1740: review — PDF triple-border test checks the three strokes are distinct and 3 widths apart
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Word sizes a superscript or subscript's line at the full font size and
leaves out the vertical alignment's raise or drop; a w:position raise or
lowering still grows the line. WordCraft added the drop to the line's
descent and used the shrunk size, so lines with a subscript came out taller
and lines of sub/superscript alone shorter than Word's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
contour::picture_outline turned the crop into a pixel window on its own,
so a sliver or overlapping crop gave an empty window and Tight/Through
wrapping fell back to the frame while the renderers draw a 0.5% sliver.
It now uses wordcraft_metafile::crop_window, with the upper pixel edges
rounded up. A PDF test covers the bitmap and metafile paths of the export
(CodeRabbit on #3247).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word for Mac spaces Symbol lines at 1.222 em (measured at 11 and 20 pt)
even with Apple's Symbol installed, whose own line is one em. Symbol's
metrics now apply when the resolved face is one em tall as well as when
the font is substituted (fonts::word::symbol_line).
A crop that kept under 5% of a picture's axis was widened to 5%, so a
header bar cropped to 2.7% of a page-sized WMF came out half as tall and
the text box over it fell below it. The rasteriser, the PDF writer and
the metafile placement now share wordcraft_metafile::crop_window, which
keeps down to 0.1% of an axis and keeps junk crops finite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux the run fallback loaded the script's preferred family (Noto Sans
Thai, which has no Latin letters) and stopped: every character had a face,
so an installed font mapping the whole run (IBM Plex Sans Thai) was never
tried, and a Thai + Latin run was split across two faces. Three Thai tests
failed on machines with Noto Sans Thai installed.
When no loaded face covers the run, the search now looks through the
coverage index for the first installed file mapping the whole run. It
loads that file's whole family, since path order can reach the Bold
file before the Regular. It also looks at the loaded faces again even
when it loaded nothing itself: a concurrent search may just have
loaded the font and taken its files off the index, and the miss would
otherwise be cached.
Regression test: a_run_no_preferred_face_covers_whole_loads_an_installed_font_that_does
(synthetic fonts, independent of what is installed).
A table dragged by its move handle floats and the empty paragraph after it
moves up beside it; a click below the table then landed in a cell of its last
row (the nearest line). Pages now list their floating body tables, and
DocLayout::hit leaves out a floating table's cell lines when the point is
outside the area its cells cover, as long as other lines are left. Checked
against Word 2019 for Windows: clicks below the table and Ctrl+End go to the
paragraph beside or after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
To find the offset from its anchor paragraph, a dropped object was
first laid out at the paragraph's top. Near the bottom of a page that
pushed the paragraph onto the next page, so the object ended up about
a page below the drop point, cut off at the page edge.
Now it is first placed where it was dropped, and the paragraph's
position is read from that layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix#1974: the caret and clicks land between the letters of a ligature
The glyph's advance is shared evenly among its letters (mirrored in
right-to-left text); ligature letters keep their character spacing.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix#1974: a long word never wraps inside a ligature
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix#1974: a hyphenation point never splits a ligature
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* #1974: ligature tests use a bundled font that draws fi and ffi as ligatures
They assumed the installed default-font stand-in draws ligatures; Liberation Sans (Linux) draws
none, so they failed there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The top and bottom borders of a paragraph now take their distance plus width
in the text flow, as in Word, and the shading fills that room. Neighbouring
paragraphs with the same borders and indents make one box: the top border goes
above the first only, the bottom one below the last, and the shading and side
borders run on through the space between them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: a word after a slash wraps whole, like Word (#2963)
UAX #14 (LB13) forbids a break between a space and a slash, and the break
after a slash is dropped on purpose (Word keeps "and/or" whole). Together
"caption /word" had no break opportunity at all, so a line too short for
both filled letter by letter and split the word. Add the break before a
slash that follows whitespace and starts a word.
Co-Authored-By: Claude <noreply@anthropic.com>
* Layout: no break before a slash after a no-break space (#2963)
The break before a slash word took any whitespace, U+00A0, U+2007 and
U+202F included, so "Nr. /12" could wrap after the no-break space.
Only a breakable space counts now. The parity checklist lists the rule
on its own row as untested against Word.
Co-Authored-By: Claude <noreply@anthropic.com>
* Layout: test the slash word in a fixed-width table cell (#2963)
The report came from a table cell, where the cell's own width is the
line width.
Co-Authored-By: Claude <noreply@anthropic.com>
* Docs: gaps and typing parity for the slash-word break (#2963)
Doc wording taken from #2994 (maxm11), with the test names of this PR.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude (ThinkSystem) <claude@thinksystem.local>
Co-authored-by: Claude <noreply@anthropic.com>