Commit Graph
146 Commits
Author SHA1 Message Date
08e8d4ade0 Table sizing: Height/Width boxes, AutoFit to contents and a Table Properties dialog (#287)
* Table sizing: Height/Width boxes, real AutoFit Contents, Table Properties dialog (#44)

- Table Layout > Cell Size gets Height and Width boxes (inches) for the caret cell's row
  and column; each edit runs table.rowHeight / table.columnWidth, a drag is one Undo.
- AutoFit Contents now measures every column's narrowest (longest word) and widest
  (unwrapped text) content with the layout's text shaping, shares the text width the way
  Word does, and writes the widths into the table grid and cell widths.
- Table Properties opens a dialog (table width, alignment, indent; row height and rule,
  break across pages, header row; column width; cell width and vertical alignment). OK
  applies only what changed as one table.properties step; with settings the command still
  works headlessly, and without them it reports the current values.
- table.columnWidth clears the table's preferred width so the column keeps the new size;
  table.rowHeight takes an optional exact rule.

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

* Progress docs: Table Properties dialog and AutoFit Contents landed (#44)

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

* Table sizing: saturating column-span sum in sync_cell_widths

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

* Merge fix: one region_text (main's) after #287 met the landed table-style code

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-10-10 14:52:08 -04:00
c0bbcbac24 Charts and SmartArt: draw them instead of dropping them (#292)
A chart or SmartArt diagram in a DOCX was dropped on import: it took no
room, so the text after it moved up (five of the six chart documents in
docxide-pdf's fixtures came out a page short), and nothing was drawn.

They are now an InlineObject::Graphic: it takes its room, floats and
wraps like a picture, and draws a list of neutral items (shapes, paths,
one-line texts, pictures) that layout scales to the object's box.

- Charts (read/chart.rs) are drawn from the values the chart part
  caches: clustered, stacked and 100% bar and column charts, line, area,
  pie, doughnut and scatter charts, with title, legend, gridlines, axis
  labels and Excel number and date formats.
- SmartArt (read/diagram.rs) is drawn from the drawing Word stores
  beside the diagram: preset shapes, theme colours, text and pictures.
- Theme colour handling is shared (read/drawing_color.rs).
- Each chart or diagram part is read once, only through a relationship
  of its type, under a per-file budget of items.
- Tagged PDF tags them as figures with their alternative text, and the
  accessibility check reports charts without one.

Not yet: charts and diagrams aren't written back when saving a DOCX
(they weren't before either), so the editor doesn't let you move or
resize them. Radar, stock, 3-D and combination charts draw their first
supported chart type only; SmartArt pictures aren't clipped to their
shape.

docxide-pdf bench (365 documents, vs main + #135): page counts right
320 -> 326 (all five chart documents and one SmartArt document), Jaccard
passes 208 -> 215, SSIM passes 76 -> 79; eight documents better, none
worse.

Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:39:41 -04:00
Valentin KleinandClaude Opus 5.5 4a75c57f58 Comment balloons edit in place, and the ruler drags page margins (#167) (#289)
Clicking a comment balloon in the markup area selects it like Word: its text
becomes editable in place, with Reply, Resolve/Reopen and Delete, and replies
are shown (and editable) under the comment they answer. Each edit is written
back through a new review.editComment command, one undo step per editing
session; the Comments pane uses the same path (it used to write the text
without an undo step) and gains a Reply button.

The margin edges on the horizontal ruler (left/right) and the vertical ruler
(top/bottom) can be dragged to set the section's margins through
layout.margins, one undo step per drag, snapped to 1/16 inch and clamped so
at least an inch of text remains. Indent markers keep priority where they
overlap an edge. Setting margins by number no longer switches off mirrored
margins; only a preset decides that.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:38:06 -04:00
88ad402581 Gridlines: the document's own grid spacing, and table gridlines on by default (#69, follow-up to #243) (#283)
View › Gridlines now draws the drawing grid at the document's own spacing:
DOCX keeps it in settings.xml as w:drawingGridHorizontalSpacing and
w:drawingGridVerticalSpacing (ECMA-376 §17.15.1.44/45), which WordCraft now
reads (clamped) and writes (only when it differs from the default), with a
round-trip test. Documents that don't say use the spec's 180 twentieths of
a point (1/8 inch) instead of a fixed 0.5 cm; across and down can differ.

Table Layout › View Gridlines (table.viewGridlines) is on by default, as in
Word, so tables without borders still show their cells.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:31 -04:00
Valentin KleinandClaude Opus 5.5 12a4da3be5 Make the floating header text box test independent of installed fonts (#284)
The test put a two-line header 36pt from the top of the page and checked
that the body still starts at the 72pt top margin. How tall those two
lines are depends on the font the default text resolves to: with the
installed Carlito-style fonts they fit, but with only the bundled
fallback fonts (as on the FreeBSD CI VM) they end at 72.6pt, so the
header correctly pushed the body down and the control check failed.

Start the header 18pt from the top instead, which leaves the two lines
plenty of room above the margin with any font. The checks are unchanged:
the control body starts at the top margin, the floating box changes
neither the body start nor the page count, and the same box inline
still pushes the body down.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:36:33 -04:00
e9a38a88ca Pictures: draw WMF and EMF pictures instead of a grey box (#293)
Windows metafiles (WMF, EMF), common for logos, crests and diagrams in
older documents, couldn't be decoded and showed as grey boxes, on screen
and in PDF.

The new crate wordcraft-metafile (layer 1) reads them from the public
[MS-WMF] and [MS-EMF] specifications into paths and bitmaps, and places
them in a picture's box; render and pdf paint the result as vectors.
It covers the drawing records Office pictures use (lines, polygons,
curves, rectangles, ellipses, arcs, path brackets), pens and brushes,
the mapping modes and world transforms, and bitmaps in DIB form with the
usual raster operations. Input is treated as hostile: every read is
bounded, and a byte budget caps what one picture may allocate.

Not yet: text records (ExtTextOut) and EMF+ records are skipped, so a
metafile whose content is mostly text shows only its lines and fills.

docxide-pdf bench (365 documents, vs main + #135): the twelve documents
with WMF/EMF pictures show them; the pictures are a small part of their
pages, so the scores move little (Jaccard passes 208 -> 209, five
documents up to 3 SSIM points better, none worse).

Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:35:32 -04:00
23395e7fc0 Tables: autofit by content, and floating tables inside cells (#291)
Word sizes the columns of a table that may follow its content (tblLayout
not fixed) from what is in them; we kept the stored grid as it was:

- A table with no preferred width anywhere (no tblW, no tcW: what
  generators write) now gets its columns from their content: each its
  widest line if all fit, else shared out between longest word and
  widest line, as Word does (case51: a nested table no longer fills its
  cell, and the document is two pages like Word's).
- In other autofit tables no column is narrower than its longest word;
  the columns with room give up the difference ("ON" no longer breaks
  into "O/N" in case91).

Floating tables (w:tblpPr) inside a table cell were laid out in the
text flow. They now stand at their position in the cell and take no
room; the text after them wraps around them, a table that may not
overlap goes below the ones in its way, and the cell grows to hold
them. A row whose lines all fit but whose nested floating table runs
past the page bottom splits there instead of moving to the next page.

Text now flows beside a floating object into gaps of 20pt or more
(was 36pt): Word puts a paragraph beside a floating table in a 21.3pt
gap but not in an 18.6pt one (docxide-pdf case139-148 vary just that).

docxide-pdf bench (365 documents, vs main + #135): Jaccard passes
208 -> 227, SSIM passes 76 -> 86, page counts right 320 -> 324; three
documents slightly worse (case149 -4 SSIM: Word moves the whole floating
table to the next page, we split it; two others -1 or less).

Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:35:00 -04:00
Valentin KleinandClaude Opus 5.5 8622ae8cec Desktop File › Print opens the PDF in the system viewer (#15) (#286)
On desktop, the Print button on File › Print now renders the document
to PDF, writes it to a fresh wordcraft-print-*.pdf in the OS temp
folder and opens it without blocking the window: in Preview on macOS,
and in the default PDF app on Linux/BSD (xdg-open, gio, ...) and
Windows (via the `open` crate, already in the tree). The user prints
from there through the system print dialog. If no viewer can be
started, the status bar says so and the file is removed. Print files
older than an hour are cleaned up off the UI thread at start-up and
after each print.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:34:11 -04:00
Brandon ThomasandClaude Opus 5.5 6b40846789 i18n: translate the remaining untranslated interface strings; test the font fallback cache with legacy names (#276)
* i18n: translate the remaining untranslated interface strings; test the font fallback cache with legacy family names (#233, #239)

Follow-ups from integrating #233 and #239: every tl! string now has a row in every
catalog, and a test pins that loading a face by its legacy family name drops the
fallback cache.

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

* i18n: Serbian rows for the newly translated strings

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:31:59 -04:00
Brandon ThomasandClaude Opus 5.5 eb31860917 Progress docs: target-app parity, gaps, alpha gate, readiness by audience (#277)
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, layout, hardware, UI and localization parity docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:11:53 -04:00
Valentin KleinandClaude Opus 5.5 a99dddcbdc Show/Hide ¶: paragraph mark no longer overlaps text in mixed-direction paragraphs (#278)
* Show section breaks with Show/Hide ¶ (#42)

With formatting marks on, the last paragraph of a section now ends in a
dotted double rule with the break's name centred in it (Section Break
(Next Page), (Continuous), (Even Page), (Odd Page)) instead of a plain ¶.
The type shown is the start type of the section that follows. The mark
is screen-only: PDF export skips it, and print/PNG never turn marks on.

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

* Show/Hide ¶: put the paragraph mark past mixed-direction text (#274)

The paragraph mark (and the section-break mark) was anchored at the caret
position after the paragraph's logically last character. When that text
runs against the paragraph's direction (Hebrew ending a left-to-right
paragraph, Latin ending a right-to-left one) that position is inside the
line, so the mark was drawn on top of a letter. The mark now sits at the
line's visual end for the paragraph's direction: right of the rightmost
glyph in a left-to-right paragraph, left of the leftmost in a right-to-left
one. Pure left-to-right and pure right-to-left lines are unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:56:32 -04:00
Valentin KleinandClaude Opus 5.5 da18ae8933 File › Info: Title, Author and the other property fields keep what you type, and browser saves advance the save stamps (#260, #262) (#268)
The Info page edited a copy of the document properties that was re-read
from the document every frame and only written back when a field lost
focus, so each keystroke vanished on the next frame and nothing was ever
committed. The fields now commit through file.properties as you type
(undoable, one step per visit to a field, marks the document dirty).

The web build's Save writes the bytes itself instead of running
file.save, so it never stamped the modified time, last-modified-by and
revision. Both paths now share Session::stamp_save.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:55:50 -04:00
Valentin KleinandClaude Opus 5.5 f6a6ded2cc Tests no longer depend on the installed fonts; PDF text of field results with ligatures (#261)
On a Linux machine with Carlito and Noto Naskh Arabic installed, four
layout and PDF tests failed on main:

- field_results_are_extractable_text found a real bug: a field result is
  laid out as one cluster, and the display list gave each of its glyphs one
  character. A ligature (Carlito, the Calibri substitute, joins "ti") took
  only its first letter and shifted the rest, so the PDF text read
  "Sectio  3.01 fir details3". The layout now keeps each glyph's text range
  from shaping, so a ligature carries all its letters. A new test pins this
  with bundled Source Sans 3 ("fi", "ffi"), whatever fonts are installed.
- fragmented_hidden_text_lays_out_like_the_visible_text compared visible
  text in alternating bold runs with an all-regular paragraph, so it only
  passed when the default font's bold happened to break lines the same.
  The plain paragraph now has the same bold runs.
- persian_letters_join_and_brackets_mirror counted glyphs, but Noto Naskh
  Arabic draws a dotted letter as two glyphs (the dotless form and the
  dot). It now compares letters by shaping cluster.
- glyph_runs_carry_their_text_for_pdf_export expected لا as one ligature
  glyph; Noto Naskh Arabic draws the lam and the alef separately. The
  ligature check now applies when the font forms the ligature.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:55:28 -04:00
8ac57f0312 i18n: add Serbian, both scripts (sr, sr-latn) (#250)
Serbian is officially digraphic — Cyrillic and Latin are both in
everyday use, with Cyrillic the default per BCP 47 when no script is
given. Adds both as separate catalogs, same as the existing
Simplified/Traditional Chinese split handles script rather than
region:

- sr-latn.tsv: the actual translation, written clean-room from the
  plain meaning of each English label (never from Microsoft Word's or
  another product's Serbian localisation), covering the full existing
  catalog (874 entries, same key set as zh-hans.tsv).
- sr.tsv: mechanically transliterated from sr-latn.tsv by a small
  script (not committed — a one-off; the mapping is the standard
  digraph-aware Serbian Latin→Cyrillic table). Brand/product/format
  names (WordCraft, PDF, Discord, Zotero, SmartArt, file extensions…)
  and `{placeholders}` are protected from transliteration, same names
  that stay in Latin letters in every other catalog. Hand-editing this
  file isn't the workflow — change sr-latn.tsv and re-transliterate.
- mod.rs: registers `sr` (Cyrillic) and `sr-latn`. No special-casing
  needed in `candidates()` for the script split, unlike Chinese's
  region-implies-script fallback — `sr-Latn-RS` already produces
  `sr-latn` as a candidate before falling back to plain `sr`, and a
  bare `sr`/`sr-RS` (no script tag, the common case) resolves straight
  to `sr` since that's the registered default-script code.
- tests.rs: locale-matching coverage for both scripts, full-catalog
  parity with the reference language, and bundled-font glyph coverage
  for both Cyrillic and Serbian Latin's extended letters (š č ć ž đ),
  mirroring the existing Ukrainian/Portuguese tests.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:47:38 -04:00
15f5ad274e Column Selection: Alt+drag selects a block of text (#237)
select.column (Mod+Shift+F8) selects the same x range on a run of lines,
from two positions, two page points (Alt+drag), or by toggling column
mode where the arrow keys extend the block. Copy/Cut give one line per
row, Delete/Backspace remove only the block, character formatting and
Change Case apply per row, typing/paste replace the block. Any ordinary
caret move drops it. F8 keys now reach their shortcuts in the UI.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:47:06 -04:00
e71c2b6c5b Style Inspector: styles.inspector pane, styles.inspect query and per-level clearing (#236)
styles.inspector shows/hides the Style Inspector pane and returns the
paragraph style, character style and the direct formatting that differs
from each; styles.inspect is the pure query; styles.inspectorClear
resets one level (paragraphStyle, paragraphFormatting, characterStyle,
characterFormatting) as a single undo step. The pane opens from the
Styles pane, with a clear button per level. Parity floor raised to 87%.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:46:39 -04:00
8bc4e6c999 Paste Special: paste the clipboard as formatted text, RTF, HTML or unformatted text (edit.pasteSpecial) (#235)
New edit.pasteSpecial command ({"as"?, "text"?, "html"?, "rtf"?}). Without "as" it
lists the clipboard's formats and asks the front end for the dialog; with "as" it pastes in
that format. HTML/RTF go through the wordcraft-formats importers, with their lists, styles and
images merged into the document under fresh ids. Respects Track Changes (now also inside pasted
tables) and caps payloads at 16 MB. UI: Paste Special dialog (radio list + result hint),
Mod+Alt+V and Paste menu entry; strings in all four catalogs. Parity 355/405, floor 87.5%.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:46:21 -04:00
9c3ad84d33 Advanced Find: every match, Reading Highlight, search within a selection (#234)
Home › Editing › Find › Advanced Find (edit.advancedFind) opens the Find
and Replace dialog. With params it returns every match in the body, or
only inside the selection ("in": "selection"), and "highlight": true
turns on Reading Highlight, which marks all matches on the page and
follows edits until it's turned off. The ribbon's Find button becomes a
menu with Find, Advanced Find and Go To; the Find dialog gets a Reading
Highlight toggle. Parity 355/405.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:45:35 -04:00
d8f71959d3 Create and modify your own table styles, saved in the .docx (#146) (#256)
New Table Style and Modify Table Style (Table Design > Table Styles) make
real table styles in the document's style sheet: name, based on, and
formatting for the whole table, the header row and banded rows (borders,
fill, bold, font colour). The table at the caret takes the new style, it
shows in the gallery, and other tools that read table styles by name
(pandoc's custom-style, for one) see it.

- engine: table.newStyle / table.modifyStyle with documented params
  (regions take borders, fill, bold, italic, colour, size, font; bandSize;
  para); table.style only accepts table styles now.
- doc/layout: table styles gain header-row and row-band cell borders,
  row-band character formatting and rows per band, applied by layout.
- docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are
  read and written, so custom styles round-trip.
- ui: a compact dialog from the Styles menu next to the gallery; table
  style tiles redraw when a style changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:45:11 -04:00
19e2e37ca6 View › Gridlines shows the drawing grid; Table Layout › View Gridlines outlines cells (#69) (#243)
View › Show › Gridlines only outlined table cells, so with no table on the
page it seemed to do nothing. It now draws a light 0.5 cm grid over the
page's text area, inside the margins, in Print Layout.

Table Layout › View Gridlines gets its own command, table.viewGridlines,
with its own view flag, and draws the table cell outlines as before. The
ribbon button, keytip and parity catalog point at it.

Both are screen-only overlays drawn by the canvas; print, PDF and image
exports never show them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:44:09 -04:00
69eaf9925c View tab: Zoom In/Out buttons that step from the shown zoom (#67) (#232)
Zoom In/Out stepped from the stored manual zoom, which goes stale while a
fit mode (Page Width, One Page, Multiple Pages) sizes the page: from a 163%
Page Width, Zoom In jumped to 110%, i.e. zoomed out. The canvas now keeps
view.zoom equal to the shown zoom while a fit mode is on, so Zoom In/Out,
the Zoom dialog and view.state all start from what the user sees.

The View › Zoom group gains Zoom In / Zoom Out buttons (code-drawn icons;
the catalog already lists them there), and the Zoom dialog's slider is
logarithmic and wider so a short drag no longer jumps from 100% to 480%.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:43:42 -04:00
c97b568d62 Touchpad scrolling follows the fingers and coasts after a flick; mouse wheel scrolls further (#122) (#252)
The document canvas used egui's built-in wheel handling: touchpad deltas
lost their first and last events, Wayland compositors leave kinetic
scrolling to the app so a flick stopped dead, and a wheel notch scrolled
only 40 px.

The canvas now scrolls itself (crates/ui-egui/src/scroll.rs): touchpad
pixel deltas move the page 1:1 in the same frame; on Linux/BSD a flick
keeps coasting with time-based exponential decay, stopped by a click, a
key, a wheel notch or the end of the document (macOS, Windows and
browsers already send their own momentum); a wheel notch scrolls about
three lines (56 px at 100%, gently scaled with zoom) and still eases in.
Ctrl+wheel zoom and Shift+wheel sideways scrolling are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:43:00 -04:00
40c5d51bf9 Mailings: Start Mail Merge and Rules open menus, and Match Fields and Check for Errors show what they found (#259)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Reject All removes paragraph breaks inserted with Track Changes on (#229)

Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

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

* Table Layout: Text Direction turns cell text (#226)

The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.

Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.

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

* File dialogs no longer freeze the window on Linux (#94)

Open, Save As, Export and Insert/Change Picture used rfd's blocking
FileDialog on the UI thread with no parent window. On Linux the
xdg-desktop-portal call blocked the event loop until the dialog closed,
and the unparented dialog could open behind WordCraft, so the app looked
frozen.

The desktop app now shows rfd::AsyncFileDialog parented to the window
(set_parent with eframe's Frame, no unsafe) and waits for the answer on
a background thread, which hands it back through a channel and wakes the
UI. ui-egui gains a backend-agnostic Services::file_dialog hook: the app
remembers what the dialog was for and finishes it when the answer
arrives (open the file, save, export, insert or change the picture, turn
AutoSave on). "Save changes?" -> Save on a new document now runs the
held-back New/Open/Close only once the later Save As has saved; a
cancelled Save As cancels it. Only one file dialog is open at a time;
further clicks are ignored until it is answered.

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

* Select Recipients opens a recipient list instead of a parameters error (#240)

Clicking Mailings › Select Recipients ran mailings.recipients with no data, so the
status bar showed "`csv`, `path` or `rows` required" and nothing opened.

The button is now a menu: Type a New List… opens a Recipient List dialog (an
editable table of fields and entries), and Use an Existing List… opens the file
picker for a CSV/TSV/text file (desktop: loaded by path; web: the picked file
comes through the inbox and loads as recipients instead of opening as a
document). User-run commands that lack their input open a dialog instead of
failing: Select Recipients and Edit Recipient List open the Recipient List,
Insert Merge Field and Find Recipient open small dialogs of their own. Scripts
and agents still get the command's error, never a dialog.

mailings.recipients and mailings.editRecipients take an optional `fields` list,
so a typed list keeps its column order and rows can be arrays.

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

* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)

The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.

- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
  Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
  CJK font (desktop only): a short per-platform list of files in the system font folders
  (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
  header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
  collection), so egui never sees a file it can't parse.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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

* Mailings: Start Mail Merge and Rules are menus; Match Fields and Check for Errors show results (#240)

Start Mail Merge opens a menu of the document kinds the engine makes (Letters,
E-mail Messages, Envelopes, Labels, Directory, Normal Word Document) with the
current one checked. mailings.start now rejects kinds it doesn't know and takes
`normal`.

Rules opens a menu of the merge rules the engine has: Merge Record # and Next
Record insert straight away; If…Then…Else… and Skip Record If… open a small
dialog for the field, value and texts. Run by the user without a field (or
without a rule), mailings.rules opens that dialog; scripts and agents still get
the default.

Match Fields shows which recipient-list field fills each address part, and
Check for Errors lists merge fields the recipient list lacks, each in a dialog.
Edit Recipient List is disabled until there is a recipient list.

The new strings, and the ones the Recipient List dialogs added, are in every
interface catalog.

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

* #248: read the installed CJK UI font only when CJK text is shown

A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).

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

* #247 on #246: Use an Existing List… goes through the non-blocking file dialog

#246 removed the desktop's blocking pick_open; the recipient list picker now asks
the host's file dialog hook (AfterPick::Recipients → load_recipients), and the
desktop dialog offers a Recipient lists filter (csv/tsv/txt).

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

* i18n: translate the recipient list and file dialog strings (#247, #246)

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-10-10 13:08:44 -04:00
3684289699 Select Recipients lets you type a list or pick a CSV, instead of a parameters error (#247)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Reject All removes paragraph breaks inserted with Track Changes on (#229)

Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

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

* Table Layout: Text Direction turns cell text (#226)

The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.

Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.

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

* File dialogs no longer freeze the window on Linux (#94)

Open, Save As, Export and Insert/Change Picture used rfd's blocking
FileDialog on the UI thread with no parent window. On Linux the
xdg-desktop-portal call blocked the event loop until the dialog closed,
and the unparented dialog could open behind WordCraft, so the app looked
frozen.

The desktop app now shows rfd::AsyncFileDialog parented to the window
(set_parent with eframe's Frame, no unsafe) and waits for the answer on
a background thread, which hands it back through a channel and wakes the
UI. ui-egui gains a backend-agnostic Services::file_dialog hook: the app
remembers what the dialog was for and finishes it when the answer
arrives (open the file, save, export, insert or change the picture, turn
AutoSave on). "Save changes?" -> Save on a new document now runs the
held-back New/Open/Close only once the later Save As has saved; a
cancelled Save As cancels it. Only one file dialog is open at a time;
further clicks are ignored until it is answered.

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

* Select Recipients opens a recipient list instead of a parameters error (#240)

Clicking Mailings › Select Recipients ran mailings.recipients with no data, so the
status bar showed "`csv`, `path` or `rows` required" and nothing opened.

The button is now a menu: Type a New List… opens a Recipient List dialog (an
editable table of fields and entries), and Use an Existing List… opens the file
picker for a CSV/TSV/text file (desktop: loaded by path; web: the picked file
comes through the inbox and loads as recipients instead of opening as a
document). User-run commands that lack their input open a dialog instead of
failing: Select Recipients and Edit Recipient List open the Recipient List,
Insert Merge Field and Find Recipient open small dialogs of their own. Scripts
and agents still get the command's error, never a dialog.

mailings.recipients and mailings.editRecipients take an optional `fields` list,
so a typed list keeps its column order and rows can be arrays.

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

* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)

The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.

- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
  Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
  CJK font (desktop only): a short per-platform list of files in the system font folders
  (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
  header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
  collection), so egui never sees a file it can't parse.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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

* #248: read the installed CJK UI font only when CJK text is shown

A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).

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

* #247 on #246: Use an Existing List… goes through the non-blocking file dialog

#246 removed the desktop's blocking pick_open; the recipient list picker now asks
the host's file dialog hook (AfterPick::Recipients → load_recipients), and the
desktop dialog offers a Recipient lists filter (csv/tsv/txt).

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

* i18n: translate the recipient list and file dialog strings (#247, #246)

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-10-10 13:07:39 -04:00
7dcfa07c15 File dialogs (Open, Save As, Export) no longer freeze the window on Linux (#94) (#246)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Reject All removes paragraph breaks inserted with Track Changes on (#229)

Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

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

* Table Layout: Text Direction turns cell text (#226)

The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.

Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.

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

* File dialogs no longer freeze the window on Linux (#94)

Open, Save As, Export and Insert/Change Picture used rfd's blocking
FileDialog on the UI thread with no parent window. On Linux the
xdg-desktop-portal call blocked the event loop until the dialog closed,
and the unparented dialog could open behind WordCraft, so the app looked
frozen.

The desktop app now shows rfd::AsyncFileDialog parented to the window
(set_parent with eframe's Frame, no unsafe) and waits for the answer on
a background thread, which hands it back through a channel and wakes the
UI. ui-egui gains a backend-agnostic Services::file_dialog hook: the app
remembers what the dialog was for and finishes it when the answer
arrives (open the file, save, export, insert or change the picture, turn
AutoSave on). "Save changes?" -> Save on a new document now runs the
held-back New/Open/Close only once the later Save As has saved; a
cancelled Save As cancels it. Only one file dialog is open at a time;
further clicks are ignored until it is answered.

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

* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)

The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.

- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
  Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
  CJK font (desktop only): a short per-platform list of files in the system font folders
  (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
  header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
  collection), so egui never sees a file it can't parse.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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

* #248: read the installed CJK UI font only when CJK text is shown

A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).

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-10-10 13:05:44 -04:00
8b49ea7546 Table Layout › Text Direction turns the cell's text (#226) (#245)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Reject All removes paragraph breaks inserted with Track Changes on (#229)

Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

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

* Table Layout: Text Direction turns cell text (#226)

The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.

Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.

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

* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)

The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.

- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
  Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
  CJK font (desktop only): a short per-platform list of files in the system font folders
  (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
  header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
  collection), so egui never sees a file it can't parse.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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

* #248: read the installed CJK UI font only when CJK text is shown

A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).

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-10-10 13:05:04 -04:00
18d3a39278 Chinese interface shows every character instead of boxes on Windows (#248)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)

The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.

- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
  Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
  CJK font (desktop only): a short per-platform list of files in the system font folders
  (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
  header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
  collection), so egui never sees a file it can't parse.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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

* #248: read the installed CJK UI font only when CJK text is shown

A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).

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-10-10 13:04:18 -04:00
f4a694ecf0 Font list offers every installed weight (Light, Medium, SemiBold, Black…), as Word does (#239)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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-10-10 13:02:40 -04:00
7b8c38fa62 Contributors: add my own entry (dkhlapov) (#238)
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:26:53 -04:00
9643dcb621 Font picker: build only the visible rows, cache font fallbacks (#121, #59) (#233)
Opening the font menu built a row, and so rendered a preview, for every
installed family: each preview loads that family's font files, so the menu
kept repainting until every font on the system was read into memory (1.1 GB
for 305 families here; far more on large collections), which froze the app.
The combo list now uses ScrollArea::show_rows, so only rows in view are built.

Layout asked FontDb for a fallback face once per character the font lacks,
and each lookup sorted every loaded face, so text in a script the font lacks
(Thai) slowed down with every font loaded. Fallback lookups are now cached
per (character, excluded face) until more faces load: 5000 Thai characters
lay out in 5 ms instead of 96 ms with all fonts loaded.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:24:54 -04:00
Valentin KleinandClaude Opus 5.5 7040751a54 Windows installer: show a finish page after installing (#198) (#257)
The MSI had no UI, so Windows Installer showed only its basic progress box and closed
without saying the install had worked. It now uses WiX's standard WixUI_Minimal dialogs:
a welcome page with the license (MIT or Apache-2.0, built at packaging time from
LICENSE-MIT and LICENSE-APACHE), progress, and a finish page with an optional
"Launch WordCraft" checkbox. The release and Windows ARM64 workflows install the WiX
UI and Util extensions the build now needs. UpgradeCode and component GUIDs are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:23:03 -04:00
Valentin KleinandClaude Opus 5.5 075b828c3d Zoomed out, pages sit side by side in rows like Word's Print Layout (#123) (#251)
When the window is wide enough for more than one page at the current zoom,
Print Layout now flows pages left to right in rows (two, three or more per
row as you zoom out) instead of a single narrow column. One page per row is
kept whenever two don't fit, and for One Page / Page Width; Multiple Pages
keeps at least two; Read Mode stays a two-page spread. All clicks, selection,
caret scrolling, object drags, balloons and rulers already map through the
actual page rects, so they follow the new positions. When the column count
changes, the caret's page is scrolled back into view.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:22:46 -04:00
Valentin KleinandClaude Opus 5.5 61fac127ed Make Backstage Home and New different pages (#141) (#254)
Home greets you with Blank plus a short row of templates, a "More templates"
link to New, and the Recent list. New shows the whole template gallery with a
search box and no Recent list. New strings are in every interface catalog.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:22:06 -04:00
Valentin KleinandClaude Opus 5.5 ca83ba7be8 macOS title bar: centre the traffic lights and space the Backstage back button (#225, #222) (#255)
AppKit keeps the traffic lights in its standard 28 pt title-bar band at the
top of the window, but the integrated title bar was 38 pt tall, so the lights
sat above its centre line. Moving them would need AppKit calls (unsafe), so
on macOS the title bar is now that band's height, with the search box and
account button shrunk to fit. Content starts after the lights plus the same
gap they keep from the top edge.

The Backstage covers the title bar, and its back button sat right under the
traffic lights. On macOS its top band is now a title bar: the back button sits
beside the lights, centred with them at the same inset as the main title
bar's controls, and the rest of the band drags the window. Linux and Windows
are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:21:41 -04:00
302528d67e Web: use the browser clock everywhere on wasm (insert weekday, PDF dates, layout timing, UI timers) — follow-up to #214 (#230)
#214 gave now_iso the browser's clock on wasm; the other clock helpers were
still stubs there: Insert Date's weekday was always Thursday, the PDF
creation date was left out, and the layout and UI millisecond clocks
returned 0, so browser AutoSave's timer never fired, the caret didn't blink
and status messages never cleared.

The engine's clock is now one now_unix() (std on native, js_sys::Date in the
browser, as #214 does) that now_iso and the weekday share; the pdf, layout
and ui-egui clocks read js_sys::Date the same way on wasm. Refs #188.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:20:56 -04:00
Valentin KleinandClaude Opus 5.5 4c4dc14a54 Reject All removes paragraph breaks inserted with Track Changes on (#229) (#244)
Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:19:52 -04:00
Valentin KleinandClaude Opus 5.5 cc64f3df2d Add a System interface theme that follows the OS appearance (#115) (#249)
The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:18:50 -04:00
Valentin KleinandClaude Opus 5.5 e53b618584 Absolutely positioned VML text boxes float instead of sitting inline (#149) (#242)
A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:18:07 -04:00
Valentin KleinandClaude Opus 5.5 68364f75c8 Tests: shapes and pictures can be clicked, resized, dragged and nudged (#228)
Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:17:06 -04:00
004286e4b7 Cross-references point at real bookmarks, and Update Field refreshes them (#227)
insert.crossReference wrote every field as `REF _Ref \h` with a literal
bookmark name and created no bookmark, so the fields resolved to nothing
when updated or opened in Word. Now:

- Headings, figures, tables and equations get a hidden `_Ref<number>`
  bookmark around the target (label and number only for show=number),
  reused by later references with the same extent.
- Bookmark targets use the bookmark's own name and show its text, not
  its name.
- references.updateFields refreshes REF (bookmark text) and PAGEREF
  (bookmark page).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:43:15 -04:00
6565a0f0ce Headless: file.setAuthor takes a name; review.trackChanges lists its value param (#90) (#224)
file.setAuthor now documents {"name": string} in the command list, trims the
name, strips control characters, caps it at 255 characters and refuses a blank
name. wordcraft-cli run and headless mcp take --author NAME. review.trackChanges
lists {"value"?: bool} so scripts find the explicit, idempotent form.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:43:05 -04:00
85da7e40e0 web: print straight to the system print dialog, not just export (#209)
* web: print straight to the system print dialog, not just export

The web build's Print backstage page only let you save a copy in another
format — there was no way to actually send the document to a printer short
of exporting a PDF and opening it yourself. Add a dedicated Print button
that exports to PDF in memory and opens it in a hidden iframe, then calls
the iframe's own print() once it has loaded, which is the standard way to
drive the browser's native print dialog on a PDF without a download or a
popup window.

- ui-egui: new Services::print hook (web: hand PDF bytes to the browser's
  print flow directly, no download).
- ui-egui backstage: "Print" button on the Print page when the host
  implements the hook; goes through wordcraft_engine::io::save_bytes
  directly rather than the existing save/download command path, since we
  want raw bytes in memory, not a file write.
- wordcraft-web: implement the hook (Blob -> object URL -> hidden iframe ->
  iframe.contentWindow.print() on load).

Desktop builds are unaffected (no Services::print set there, so the
button doesn't appear - file.print still opens the Print page as before).

* web print: make printing the ui.print command, dispatched by id

The backstage Print button now runs `ui.print` instead of exporting and
calling the host hook inline, so the control channel, MCP bridge and command
search reach the same path. Like ui.openFileDialog it is the one explicit
call that opens system UI; without a print hook it is an error. Status
strings get rows in all six catalogs.

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

* web print: drop the previous print's iframe and object URL

Each Print appended a hidden iframe holding a fresh blob URL of the whole
document's PDF and never released it, so copies piled up in the page. The
frame now has a fixed id; the next print revokes its URL and removes it
first, and a failure part-way through cleans up after itself.

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

* web print: test that Print needs the host's print hook

ui.print errors without Services::print and hands the hook a PDF with it;
a failing hook reports why. The Print page shows the Print button only when
the hook is set, and the button prints through the command.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:40:16 -04:00
919d74bee6 Shift+F3 runs Change Case again, not Find Previous (#186) (#221)
* Shift+F3 runs Change Case again, not Find Previous (#186)

edit.findPrevious and format.changeCase were both bound to Shift+F3.
Registry::by_shortcut returns the first match, so Find Previous won
and Change Case never ran from the keyboard. Find Previous keeps
Mod+Shift+G. A new test fails if any two commands share a shortcut.

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

* Contributors: credit vangel76 by their GitHub profile name

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-10-10 09:27:11 -04:00
58ae1d5685 Use the browser clock for now_iso on wasm (comment and tracked-change dates) (#214)
Fixes #188.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:27:02 -04:00
24388e2c24 macOS: open documents from Finder, the Dock and open -a (#27) (#223)
* macOS: open documents from Finder, the Dock and `open -a` (#27)

Double-clicking a document, Open With, dropping on the Dock icon and
`open -a WordCraft letter.docx` send the app a kAEOpenDocuments Apple
event instead of command-line paths. winit 0.30 doesn't handle it, so
AppKit answered "WordCraft cannot open files in the Word Document format".

Register the handler with the audited fmv-macos-events crate that PdfCraft
and PhotoCraft already use: it does the Objective-C interop (unsafe, which
this workspace forbids) behind a safe main-thread API and leaves winit's
delegate alone. Opened paths go through `file.open` like File > Open, so
unsaved changes are asked about first; a quit event closes the window,
which asks the same. The registration lives in main() until the event loop
returns, so the event that launched the app is caught too.

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

* macOS open: say in the status bar when a file can't be opened, and leave Backstage after opening

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

---------

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:26:52 -04:00
Saeed AzarehgarandClaude Opus 5.5 5d86dec250 Right-to-left and Persian text support (bidi, shaping, RTL paragraphs, DOCX) (#207)
* feat(doc,docx): complex-script run properties and RTL OOXML round-trip

Model the formatting Word applies to Persian, Arabic and Hebrew text:
cs, font_cs, size_cs, bold_cs, italic_cs and lang_bidi on CharProps,
resolved with Word's fallbacks (ResolvedChar::complex). Size, bold and
italic pair with their complex-script values so a run's size or bold
also covers its Persian characters. Add doc::bidi script helpers, and
keep ZWNJ and Arabic marks inside words for word selection.

DOCX: read and write w:rFonts/@w:cs, w:szCs, w:bCs, w:iCs, w:cs and
w:lang/@w:bidi in schema order; stop using the cs font as the Latin font.
Tests round-trip Persian text with ZWNJ, digits and mixed Latin.

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

* feat(fonts): directional shaping and Persian font fallback

Add shape_run to shape a run in a known bidi direction (Arabic joining,
mirrored brackets). Prefer Persian-capable fonts (Vazirmatn, Noto Naskh/
Sans Arabic…) for right-to-left fallback, pick bold/italic fallback faces,
and map common Persian document fonts (B Nazanin, B Lotus, B Titr,
IRANSans…) to open substitutes.

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

* feat(layout): Unicode Bidirectional Algorithm and right-to-left paragraphs

Run UAX #9 per paragraph (unicode-bidi), shape each level run in its
direction, keep marks and ZWNJ with their letter and a word in one
fallback face. After line breaking, apply rules L1/L2 per line and give
each cluster a visual box; right-to-left paragraphs are laid out from
their start edge and mirrored, so indents, tabs, alignment, list labels,
drop caps, floats and borders sit on the right.

Caret, hit testing and selection use visual geometry (split selections,
caret at bidi boundaries, DocLayout::visual_step for arrow keys). Display
items carry glyph-to-text ranges, which the PDF writer now uses. Plain
left-to-right lines are unchanged. No hyphenation of RTL words.

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

* feat(engine,ui): paragraph direction commands and RTL editing

Add para.ltr / para.rtl (Word's text direction buttons) that change only
the reading order, and make the alignment buttons visual in right-to-left
paragraphs (para.align also takes start/end). Arrow keys move visually in
bidirectional text, Ctrl+arrows follow reading order, and Bold/Italic/
Size/Font cover complex-script text. Ribbon buttons with code-drawn
icons, a mirrored ruler for RTL paragraphs, translations, catalog,
parity and roadmap updates.

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

* chore(contributors): add Saeed Azarehgar to people.toml

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

* fix(engine): para.align with an explicit value sets it, without toggling

Only the alignment buttons toggle back to the start edge when pressed
again. para.align (used by the Paragraph dialog, scripts and agents) now
sets the requested alignment, as seen on the page, in each selected
paragraph's own direction. Previously pressing OK in the dialog with
Center or Right selected reset the paragraph to the start edge.

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

* feat(ui): Direction option in the Paragraph dialog

Add Word's Direction (Right-to-left / Left-to-right) to the Paragraph
dialog's General section. The dialog now shows alignment and Left/Right
indents as seen on the page, mapping them to the paragraph's logical
start/end values on OK, so an unchanged OK no longer flips a
right-to-left paragraph. OK logic moved to apply_paragraph, with tests;
ja/zh translations.

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

* test(layout): allow hanging trailing spaces in the RTL display test

A wrapped right-to-left line's trailing spaces hang past its end edge
(the left margin), as left-to-right lines hang them past the right. The
test failed whenever the loaded fallback fonts made a line end there;
it now checks only visible glyphs.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:26:41 -04:00
a9eb233a9d Track changes: keep other authors' deletions, credit paragraph splits, exact edit snapshots (split from #54) (#125)
* Track changes: a tracked delete keeps other authors' deletions

Deleting a range with Track Changes on marked every run in it as deleted
by the current author, also text another author had already deleted.
That author's deletion was lost: the change list showed it under the
wrong name, and rejecting the second author's changes brought back text
the first author had deleted. Text that is already deleted now keeps its
deletion and its author, like Word.

* Track changes: a tracked paragraph split credits the new mark to its author

Pressing Enter with Track Changes on inserts a paragraph mark by the
current author, but the paragraph after the split took its mark's
revisions from the text at the caret. Inside or at the end of another
author's insertion, the original paragraph mark then showed as inserted
by that author. The paragraph after the split now keeps the original
mark's revisions; only the new mark is the splitting author's.

* Engine: edit snapshots that take commands back exactly

Session::edit_snapshot and Session::restore let a caller run a command
and take it back without a trace: the document, the selection, the undo
and redo stacks, the open typing group, a pending join_next_undo and
the dirty flag come back as they were, and the layout is recomputed.
Edit > Undo cannot do this: it leaves a redo step, loses the redo stack
the command cleared, and closes the typing group.

At the undo limit (500 steps) a command pushes the oldest step out. The
snapshot keeps the oldest steps when the stack is near the limit and
restore puts them back, so the stack is exact there too (up to four
commands between snapshot and restore).

Restore is exact for commands that add undo steps; edit.undo and
file.new/file.open in between are outside its contract (documented).
The snapshot destructures every Session field, so a new field does not
build until someone decides whether a snapshot must cover it.

Session::undo_depth and the snapshot's doc/dirty/undo_depth accessors
tell a caller whether the command changed anything.

* Tests: hostile_params_never_panic leaves out review.readAloud

Read Aloud hands the document's text to the system's speech synthesizer
(say, PowerShell, spd-say or espeak), so running the test made the
computer speak the sample document. Like the file commands, it is left
out of the sweep.

* Undo: evictions at the limit are counted everywhere edit snapshots rely on them

Merging main brought two paths #125's eviction counter didn't know: push_undo (typing parity, #204) dropped the oldest step without counting it, and a failed command (#153) put the whole stacks back but left the count raised. Either made restore() bring back a step that had never left. Both now keep the count right; restore_is_exact_at_the_undo_limit covers each (both fail without the fix).

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

---------

Co-authored-by: lpgn <2556951+lpgn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:07:32 -04:00
10eb8798cd Equations: Word (OMML) import, layout and save, plus an equation editor like Word's (#191)
* M5.5: Word equations (OMML) import, lay out and save like Word

Equations were flattened to plain text on import. Now OMML is read into a
math tree (fractions, scripts, radicals, n-ary operators, delimiters,
functions, limits, accents, bars, boxes, equation arrays, matrices,
phantoms), laid out with the OpenType MATH table (Cambria Math when
installed, else STIX Two Math and other open math fonts), drawn to screen
and PDF, and written back as the original OMML. Display equations sit on
their own centred line. insert.equation parses the linear format into
structure.

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

* M5.5: equation editor like Word's — Equation tab, in-place editing, # numbers

Insert › Equation now opens an equation for in-place editing, with the
Equation tab: ready-made equations, Unicode/LaTeX input and conversion
(professional ↔ linear), Normal Text, eight symbol sets, and the eleven
structure galleries (fraction, script, radical, integral, large operator,
bracket, function, accent, limit and log, operator, matrix) with tiles
rendered by the math engine. Typing builds up like Word (a/b, x^2, √, ∑…
then space), Math AutoCorrect turns \alpha, \sqrt… into symbols, Tab moves
between empty slots, arrows/Backspace work through structures, and the
right-click menu has structure commands (fraction type, radical degree,
limit position, matrix rows/columns, remove brackets…). A trailing #(n)
numbers a display equation with the number at the right margin; # alone
numbers it automatically. DOCX output keeps numbers Word-compatible.

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

* i18n: equation editor strings in Spanish, Brazilian Portuguese and Ukrainian

The catalogs that landed on main after #191 was opened lacked the Equation
tab and equation command labels.

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

* Equations: our own names and order for the built-in equations

Clean room: the ready-made equation list used another product's gallery entry
names and order. Same formulas (and ids), with our own descriptive labels,
grouped by subject (geometry, algebra, calculus, trigonometry, probability).

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

* ROADMAP: equations row matches the landed equation editor

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

* fmt after merge

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-10-10 09:04:52 -04:00
2982e75544 Tables: cell text takes its table style's formatting; derived table styles keep their base (#104)
Word's Table Grid style sets single spacing and no space after
(<w:pPr><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr>).
Cell text ignored it and used the document defaults (8 pt after, 1.08
lines in Word's default template), so rows came out ~60% taller than in
Word: an 80-row table took 3 pages instead of 2. A table style based on
Table Grid that adds red bold text showed no red, no bold and no borders.

- Style resolution has a table-style level (resolve_para_in,
  resolve_char_in), in ECMA-376 §17.7.2 order: document defaults, table
  style, paragraph style chain, character style, direct formatting. Any
  property the paragraph style chain sets, Normal included, beats the
  table style; the table style only replaces the defaults. (So a document
  whose Normal sets its own spacing keeps tall rows, as in Word.)
- StyleSheet::table_style merges a table style's based-on chain: pPr, rPr
  and the conditional parts, borders edge by edge. Layout used the first
  style in the chain with any table parts and dropped the rest.
- Layout passes the table style's paragraph and run formatting, with the
  cell's header row / total row / first column formatting on top, to each
  cell paragraph. Conditional run formatting now sits at the table-style
  level too, under the paragraph style, instead of over it.
- DOCX: the wholeTable region's pPr/rPr go to the style's own formatting
  and its cell shading to a new whole-table fill; tblPr shd and tblCellMar
  of a table style are read and written (as wholeTable shading and
  tblCellMar).

New tables use Table Grid, so tables made in WordCraft get Word's compact
rows too.

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 08:57:42 -04:00
e4456efb84 Text boxes and floating objects: editing, drag/resize, wrapping, overflow, footnotes (#46)
* fix text boxes: type into a new box, click in and out, Escape leaves it

Insert › Text Box left the caret in the body after the box and clicks inside a
box never reached its story, so its text could not be edited.

- insert.textBox puts the caret in the new box (when anchored in a top-level
  body paragraph, the only place layout shows box text today).
- Layout emits a hit-test-only Placed::TextBox area; story_at prefers a box
  under the point (body text wins over boxes behind it) and ignores box lines
  outside their box.
- Canvas: click enters a box, clicking outside goes back to the body (not while
  editing a header/footer); double-click in a box selects a word there.
- Escape in a box puts the caret just after it (Document::text_box_anchor).
- Editing a box doesn't dim the body or show "Editing header/footer".
- Tests: engine (caret in box, typing, undo, table cell unchanged), layout
  (story_at inside/outside/behind), doc (anchor lookup).

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

* fix pasted text boxes sharing the original's text

A copied fragment only carried the text box's part id, so a pasted box showed
the original's story: editing either changed both, edits made after copying
leaked into the paste, and pasting into another document (or Insert › Text
from File) pointed at whatever part had that id there, e.g. a header.

- Fragment carries copies of the text box stories it shows (nested boxes too).
- insert_fragment gives every pasted box a new story of its own; a box whose
  story can't be found keeps none rather than showing an unrelated part.
  Nesting is depth-limited and one paste creates at most 4096 stories.
- Insert › Text from File uses Document::body_fragment, which brings the
  file's text boxes along.
- Tests: doc (own story, copy-time snapshot, other document with clashing
  ids, box in a table, fragment without parts, self-nested box), engine
  (copy/paste/type/undo through commands).

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

* word count includes text boxes, footnotes and endnotes (Word's default)

The status bar and Word Count dialog counted only the body, so words typed
into text boxes (and notes) were missing.

- Document::counted_stories / word_count_including_notes: the body plus the
  text boxes, footnotes and endnotes it actually shows (nested too); orphaned
  stories (deleted or cut boxes), headers, footers and comments don't count.
- Session::count_notes (default on) with Session::word_count for the status
  bar; review.wordCount takes {"includeTextBoxes"?: bool} and reports it.
- Word Count dialog: "Include text boxes, footnotes and endnotes" checkbox.
- Document::word_count stays body-only (docx Words property, NUMWORDS, stats).
- Tests: doc (used vs orphaned stories, nesting, self-nested box), engine
  (default on, toggle sticks, deleting a box drops its words).

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

* canvas: select, drag to move and resize pictures, shapes and text boxes

Click a picture or shape (or a text box's border) to select it: a frame with
eight handles. Drag it to move it, drag a handle to resize it (pictures keep
their proportions from a corner, Shift toggles), arrows nudge a floating one
(Ctrl/Alt: 1 pt), Escape cancels a drag. While a text box's text is edited its
frame shows dashed with handles. Typing or Enter with a text box selected
goes into it.

Reusable pieces, no per-kind code:
- layout: Placed::Object, one hit area for every picture/shape/text box
  (replaces the text-box-only item); DocLayout::object_at / object /
  text_box / find_object (page-hinted, no whole-document scans per frame).
- engine: arrange.bounds {width,height,x,y,page} (floating objects keep their
  anchors on the same page; inline ones float where dropped; another page
  re-anchors to the nearest line) and arrange.nudge; objects::min_size shared
  with the UI.
- ui: frame.rs, a document-agnostic transform frame (handles, hit testing,
  drag/resize maths, painting); objects.rs wires it to document objects.
- geom: Rect::expand. control: ui.press / ui.release for half-way drags.

Rendering: nothing is laid out or rasterised while dragging. The preview is
the object's own pixels from the cached page texture (moves, picture
resizes) or a light outline (text box/shape resizes); the drop is one
command, one relayout, one undo step.

Tests: frame maths, layout hit areas, engine move/resize/nudge across
anchors and pages, hostile values.

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

* moving an object anchors it to the paragraph it's dropped on, so text wraps

A floating object only pushes aside text from its anchor paragraph down, and a
move kept the old anchor: drag a text box above its paragraph and the text
there ran straight under it.

- arrange.bounds now anchors the object to the paragraph under its top edge
  (the last one starting at or above it on that page) and positions it
  relative to that paragraph's column and top, as Word does: the text it
  lands on wraps, and it moves with that text. On a page where no paragraph
  starts it anchors to the nearest line, positioned on the page.
- layout: Placed::Object / ObjectHit carry the paragraph origin that column-
  and paragraph-relative offsets start from.
- engine: edit_obj / edit_float change an object at a position (with_obj
  uses it).
- Test: a box dropped over an earlier paragraph anchors there and the lines
  beside it stop short of it.

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

* text boxes hide text that doesn't fit, as in Word

Text past a text box's bottom spilled out over the page below it.

- layout: fit_box keeps what fits in a box: whole lines that fit (a line that
  only partly fits is hidden too) and other items starting inside; the first
  line always stays so even a tiny box shows something. Done in layout, so the
  canvas, PNG and PDF agree and hidden text can't be clicked; no renderer
  changes. Reusable for anything laid out into a fixed height.
- BOX_INSET_X / BOX_INSET_Y name the text box internal margins (were 7.2/3.6
  literals).
- Test: overflow hidden, every shown line inside the box, a tall box shows
  everything, a minimum-size box shows its first line.

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

* square wrap flows text on both sides of a floating object

Text only used the wider side of a floating picture, shape or text box; Word's
Square wrap fills both sides when there's room.

- para: row_spans gives the spans of a row around the objects in its way (each
  at least 0.5" wide, else the line goes below); a row cut in two is two Lines
  sharing top and height, the second marked `beside`. Everything per line
  (alignment and justification per side, hit testing, caret, selection,
  drawing, PDF) works unchanged; a row is as tall as its tallest part.
- layout: a row stays on one page; line numbers count rows, not parts.
- Exclusions that don't overlap a line horizontally no longer narrow it.
- Test: text both sides of a mid-column box (same row, left then right,
  hittable), a too-narrow gap stays empty.

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

* drop a deleted or cut text box's text from the document

Deleting or cutting a text box left its story in Document::parts: never shown
or saved to .docx, but listed by document.inspect and kept in memory.

- doc: prune_text_boxes removes the text box stories no box shows (nested
  ones too); free without text boxes, one paragraph pass otherwise. Session
  runs it after every editing command. Undo snapshots and the clipboard hold
  their own copies, so undo and cut/paste are unaffected.
- doc: reachable() walks the stories objects lead to; counted_stories (word
  count) and prune_text_boxes share it, and it walks blocks directly instead of
  building paragraph paths.
- Tests: orphaned and nested-orphan boxes go, shown ones (body, nested, in a
  header) stay; delete then undo, cut then paste.

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

* word count's include-text-boxes setting survives a restart

The Word Count dialog's "Include text boxes, footnotes and endnotes" was a
session field, so it reset to on every launch.

- engine: Prefs, the editing preferences the front end saves between runs
  (count_notes moves there; Session::prefs is the one source of truth).
- ui: SavedPrefs pairs UiState with the engine Prefs; WordApp::saved_prefs /
  restore_prefs. UI fields stay at the top level of ui.json, so existing files
  still load (the new `editing` section defaults).
- wordcraft app: saves and restores SavedPrefs instead of UiState alone.
- Tests: an old prefs file loads with Word's default; the setting round-trips.

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

* text boxes and floating objects outside the body; footnotes in text boxes

Text boxes in table cells, headers, footers, footnotes and other text boxes
showed an empty frame (or nothing), and floating pictures and shapes there
weren't drawn. Footnotes inserted inside a text box were never numbered or
placed.

- layout: place_objects, one routine for the pictures, shapes and text boxes
  in a paragraph's lines (floating ones drawn, every one's area, text boxes'
  text), used by body paragraphs and by layout_box (cells, headers, footers,
  notes, box contents). Floats are positioned against an AnchorFrame (column,
  paragraph, margins, page); headers and footers pass where the page is, so
  page/margin-anchored objects land right (a footer is laid out again only
  when it has floating objects); elsewhere they're placed in their container.
  Nested boxes show MAX_TEXT_BOX_DEPTH (doc) levels deep.
- Placed::Object / ObjectHit carry their story; DocLayout::object(&Pos),
  object lookups include headers and footers, header_footer_text_box_at.
  A part's lines are looked up in headers and footers too (caret, typing).
- Footnotes: numbered in reading order with text boxes where they are
  (Document::objects_in_reading_order); a box's footnotes go on the page of
  its line (Document::notes_in_text_box), sharing the body's reservation.
- engine: insert.textBox always puts the caret in the new box; disabled
  inside a text box (as in Word).
- canvas: click into a header/footer's text boxes while editing it; leaving a
  box returns to where it is; body dimming computed once per frame and used
  for both the render and its cache key. objects: only body objects are
  selectable, only body paragraphs' objects movable; frames off the body
  show without handles.
- Tests: boxes in header (inline and page-placed), footer, cell and note show
  text and take the caret; footnotes in a box numbered and placed in order;
  a box in a table cell takes the caret and its text.

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

* word count: a selected object counts the whole document, in the dialog too

With a picture, shape or text box selected the status bar showed the
document's total but Review > Word Count counted the selection (0 words).

- engine: cmd::objects::object_selection, "the selection is exactly one
  object"; review.wordCount counts the document then. The UI's
  objects::selected now uses it (one definition).
- Test: an object selection counts the document; a text selection itself.

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

* review: bound nested text box expansion; tidy-ups

Blocker: laying out a text box lays out the boxes inside it, which could fan
out unboundedly (a box whose shapes show itself, or chains of boxes each
showing the next many times) - 30 shapes made 1.7M layout items. Footnote
numbering's walk fanned out the same way.

- doc: BoxBudget, one bound for both: a box isn't expanded inside itself,
  nesting stops at MAX_TEXT_BOX_DEPTH, an outermost box expands at most 64
  boxes inside it (so one hostile box can't starve the next page's header
  boxes) and a pass at most 20,000.
- layout: Ctx carries a BoxBudget for the whole layout; place_objects asks it
  before laying out a box's text. DocLayout::text_boxes reports how many were.
- doc: walk_objects (objects_in_reading_order, notes_in_text_box) uses it too.
- Tests (crafted documents): a box showing itself 30 times lays out once; a
  five-level chain fanning out 30 ways stays within the budget, for layout and
  for the walk; a box inside a box still shows its text. Weakening the budget
  makes them fail (20,000 boxes / 600,001 visits).

Tidy-ups:
- para: row spans in a VecDeque (pop_front, not remove(0)).
- fit_box: a text box's text area excludes both the top and bottom margins.
- theme: Tokens::handle / handle_shadow for the frame handles (light, dark);
  the drag preview's opacity is a named constant.

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

* box budget: no overall cap; bound the .docx writer too; tests

An audit of the previous commit found:

- The 20,000-box overall cap starved ordinary long documents: one budget per
  layout, spent by body boxes first, then one per page for header boxes (two
  for footers with floats), so a long document's header boxes went blank on
  every page. It wasn't needed: only nesting grows exponentially, and
  MAX_NESTED caps that per outermost box, keeping the work linear. Removed.
- The .docx writer still expanded a shape's story only depth-limited (a box
  showing itself 30 times saved as ~28,000 nested copies, ~7 MB zipped). It
  now uses the same BoxBudget, and only for text box parts.
- Layout expanded a shape's story whatever its part kind (a shape pointing
  at a header showed the header's text); now text box parts only, as the
  walk does. The story's blocks are cloned only once the budget agrees.
- Translations of "Count words in text boxes and notes" said footnotes only;
  now footnotes and endnotes, in each catalog's own terms.
- Tests: no timing assert or leftovers; a long document with 400 boxes and a
  header box shows all of them on every page; notes_in_text_box follows
  nested boxes once; saving a self-showing box stays small (fails at 7 MB
  without the bound).

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

* Arrange: nudging or dragging an aligned float keeps it in place first

main (#136) reads w:align into Float.h_align/v_align, which overrides the offset; the PR's
nudge only changed the offset and drag only reset the anchors. Nudge now converts an
aligned axis into a column/paragraph offset that keeps the object where it is, and a drag
clears the alignment. Test included.

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

* Fonts: a lookup that races another thread's load of the same family no longer falls back

FontDb::face fell back to Source Sans 3 when its load_cataloged added nothing, which happens
when another thread loaded the family between the miss and the load. word::resolve memoises
that, so the wrong font stuck for the process; it made layout tests on main flaky
(justified_lines_shrink_spaces_in_word_2013_mode, space_shrinking_is_only_for_justified_text
failed 6 of 15 runs on origin/main). Look again after loading whatever it returns. Regression
test races 8 threads on fresh databases.

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

* Layout tests: text box and two-sided wrap tests measure what they mean

square_wrap_flows_text_on_both_sides compared xs.last() (the end of a trailing space, which
may hang past the edge) with the edge; compare the last non-space cluster's right edge.
text_box_hides_text_that_does_not_fit compared a tall box's lines with another document's
line count; compare with its own.

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

---------

Co-authored-by: Weeb Labs <weeblabs@VFIEVOX3.Router>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 08:51:38 -04:00