Commit Graph
170 Commits
Author SHA1 Message Date
f24a5b22b2 Tables: drags and Shift+arrows over cells select whole cells (#722)
* Tables: select, insert and move with the mouse; double-click a border to AutoFit

Like Word, on top of the border resizing:

- Selection areas: just above a column the pointer becomes a down arrow,
  left of a row the row arrow, along a cell's left edge (inside it) the
  cell arrow. A click selects the column, row or cell; a drag selects every
  one up to the pointer. Selected cells are shaded as cells, empty ones too.
- Insert buttons: left of a row border and above a column border a "+"
  shows with the border marked; a click adds a row or column there and puts
  the caret in it.
- Move handle past the table's top-left corner: a click selects the table;
  a drag shows a dashed outline and, on release, the table floats where it
  is dropped with the text wrapping around it (Word's behaviour). One undo
  step. Nested tables and tables outside the main text are only selected.
- A double-click on a column border fits the column left of it to its
  contents (the right neighbour gives or takes the room; on the right edge
  the table's width changes, up to the right margin).
- Table Layout > Select > Select Column (catalog 418/449).

Whole-cell selection: a column (or a rectangle of cells over several rows)
is not one run of the text, so selecting it makes a block of its cells'
paragraphs (`CellRange` on the existing `ColumnBlock`). Bold, alignment,
Delete, typing, copy and numbering act on those cells only (a numbered
column is one list); table commands (shading, merge, delete columns) take
the rectangle; Tab moves on without clearing it; Undo brings the selection
back. Cells of one row, or whole rows, stay an ordinary stream selection.
The format painter no longer clears a block's text (edit.pasteFormat was
handled like a paste). The mini toolbar stays hidden over whole-cell
selections and during these drags: pinned above the selection it covered
the column areas, the "+" buttons and the move handle.

Engine: crates/engine/src/cmd/table_mouse.rs - table.selectAt,
table.insertAt, table.moveDrag, table.fitColumn (page coordinates, so
agents and the control channel can do the same) and table.selectColumn;
hit testing reuses table_resize's drawn geometry, and borders keep priority
for resizing. 10 engine tests (areas, a column edited cell by cell,
numbering, rows/cells/drags, insert buttons, move, fit, format painter,
hostile params).
UI: crates/ui-egui/src/table_mouse.rs, wired into the existing canvas mouse
handler (no extra ui.interact areas, so clicks in a cell's text still place
the caret); kittest tests with real pointer events: column click, row drag,
"+" button, move-handle drag in one undo step, double-click fit.
New labels translated in all eleven catalogs. Progress docs updated: gaps,
ui-parity, target-app-parity, ROADMAP, parity checklist.
`cargo xtask ci` passes (fmt, clippy, test, assets, layers, wasm).

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

* Tables: a selected column's word count is its cells'; merged cells shade once

With whole cells selected (a column, or a rectangle that isn't one run of
the text) the status bar said "12 of 23 words": `Session::selected_text`
took everything between the selection's ends. It now takes each piece's
own text, a line each, for a column block too, so the status bar, Word
Count and the other callers see what is selected.

Since #669 a vertically merged cell is one box as tall as its rows, with a
`covered` box for each continuation row; shading selected cells skips
those, so a merged cell isn't shaded twice. `selected_paths` follows
main's `para_paths_in` (#418): a selection ending at the start of the next
paragraph doesn't take it.

- Test: a selected column's text is its three cells' and Word Count says
  three words.

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

* Tables: double-click AutoFit and the "+" buttons work as in Word

Checked against Word (2019, Windows):

- A double-click on a column border fits the column left of it to its
  contents and the columns right of it keep their widths, moving with the
  border, so the table narrows or widens (up to the right margin). The
  right neighbour used to give or take the difference.
- A "+" insert button selects the new row or column (the caret used to
  go into it); typing then goes into its first cell. If the table can't
  take another row or column, nothing changes.
- A double-click along a cell's left edge (the cell selection area)
  selects its row.

- Tests: engine (fit keeps the columns on the right; insert selects the
  new row or column, typing lands in its first cell) and kittest (real
  double-click on a border; "+" selects the row; double-click by a cell's
  edge selects the row).

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

* Tables: drags and Shift+arrows over cells select whole cells

An ordinary drag, Shift+click or Shift+arrow from one table cell into
another now selects the rectangle of whole cells between them, as Word
does, instead of the run of text between the two points: bold, alignment
and Delete leave the cells outside the rectangle alone (they already did
for Select Column and the table's selection areas). Between a cell and
text outside its table the selection takes whole rows, from the cell's
row out to the table's edge, and the text beyond. Extending again goes on
from where the selection really started and ends: dragging back into the
first cell selects its text again, Shift+Up after Shift+Down shrinks the
rectangle, and from a nested table's cell into another cell of the table
around it the outer table's cells are taken.

- Engine: `table::extend_selection` (the rule, down through nested
  tables) and `Session::sel_ends` (where a widened selection really
  runs); every caret command run with `extend` goes through it in
  `column::dispatch`, so agents get the same with `caret.*`.
- UI: a text drag extends through `caret.set` (still straight to the
  session, so nothing scrolls on every frame); the view follows the real
  caret.
- Tests: engine (drag across cells and back, nested tables, Shift+Down
  and Shift+Up through and past a table, Shift+click, from text into a
  table) and a kittest with a real mouse drag.

Progress docs updated: gaps (the known gap from #637 is gone),
ui-parity, target-app-parity, ROADMAP. `cargo xtask ci` passes.

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

---------

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:34:10 -04:00
284818b467 Tables: the right-click menu offers the table's commands (#694)
* Tables: select, insert and move with the mouse; double-click a border to AutoFit

Like Word, on top of the border resizing:

- Selection areas: just above a column the pointer becomes a down arrow,
  left of a row the row arrow, along a cell's left edge (inside it) the
  cell arrow. A click selects the column, row or cell; a drag selects every
  one up to the pointer. Selected cells are shaded as cells, empty ones too.
- Insert buttons: left of a row border and above a column border a "+"
  shows with the border marked; a click adds a row or column there and puts
  the caret in it.
- Move handle past the table's top-left corner: a click selects the table;
  a drag shows a dashed outline and, on release, the table floats where it
  is dropped with the text wrapping around it (Word's behaviour). One undo
  step. Nested tables and tables outside the main text are only selected.
- A double-click on a column border fits the column left of it to its
  contents (the right neighbour gives or takes the room; on the right edge
  the table's width changes, up to the right margin).
- Table Layout > Select > Select Column (catalog 418/449).

Whole-cell selection: a column (or a rectangle of cells over several rows)
is not one run of the text, so selecting it makes a block of its cells'
paragraphs (`CellRange` on the existing `ColumnBlock`). Bold, alignment,
Delete, typing, copy and numbering act on those cells only (a numbered
column is one list); table commands (shading, merge, delete columns) take
the rectangle; Tab moves on without clearing it; Undo brings the selection
back. Cells of one row, or whole rows, stay an ordinary stream selection.
The format painter no longer clears a block's text (edit.pasteFormat was
handled like a paste). The mini toolbar stays hidden over whole-cell
selections and during these drags: pinned above the selection it covered
the column areas, the "+" buttons and the move handle.

Engine: crates/engine/src/cmd/table_mouse.rs - table.selectAt,
table.insertAt, table.moveDrag, table.fitColumn (page coordinates, so
agents and the control channel can do the same) and table.selectColumn;
hit testing reuses table_resize's drawn geometry, and borders keep priority
for resizing. 10 engine tests (areas, a column edited cell by cell,
numbering, rows/cells/drags, insert buttons, move, fit, format painter,
hostile params).
UI: crates/ui-egui/src/table_mouse.rs, wired into the existing canvas mouse
handler (no extra ui.interact areas, so clicks in a cell's text still place
the caret); kittest tests with real pointer events: column click, row drag,
"+" button, move-handle drag in one undo step, double-click fit.
New labels translated in all eleven catalogs. Progress docs updated: gaps,
ui-parity, target-app-parity, ROADMAP, parity checklist.
`cargo xtask ci` passes (fmt, clippy, test, assets, layers, wasm).

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

* Tables: a selected column's word count is its cells'; merged cells shade once

With whole cells selected (a column, or a rectangle that isn't one run of
the text) the status bar said "12 of 23 words": `Session::selected_text`
took everything between the selection's ends. It now takes each piece's
own text, a line each, for a column block too, so the status bar, Word
Count and the other callers see what is selected.

Since #669 a vertically merged cell is one box as tall as its rows, with a
`covered` box for each continuation row; shading selected cells skips
those, so a merged cell isn't shaded twice. `selected_paths` follows
main's `para_paths_in` (#418): a selection ending at the start of the next
paragraph doesn't take it.

- Test: a selected column's text is its three cells' and Word Count says
  three words.

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

* Tables: double-click AutoFit and the "+" buttons work as in Word

Checked against Word (2019, Windows):

- A double-click on a column border fits the column left of it to its
  contents and the columns right of it keep their widths, moving with the
  border, so the table narrows or widens (up to the right margin). The
  right neighbour used to give or take the difference.
- A "+" insert button selects the new row or column (the caret used to
  go into it); typing then goes into its first cell. If the table can't
  take another row or column, nothing changes.
- A double-click along a cell's left edge (the cell selection area)
  selects its row.

- Tests: engine (fit keeps the columns on the right; insert selects the
  new row or column, typing lands in its first cell) and kittest (real
  double-click on a border; "+" selects the row; double-click by a cell's
  edge selects the row).

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

* Tables: the right-click menu offers the table's commands

Right-clicking a table cell now offers what Word's table menu does, all
through existing commands: Insert (rows above/below, columns left/right),
Delete (cells, rows, columns, table), Select (cell, column, row, table),
Merge Cells, Split Cells, Distribute Rows, Distribute Columns, AutoFit
(contents, window, fixed column width), Text Direction, Cell Alignment
(the nine alignments) and Table Properties… (opens the dialog, as the
ribbon's button does).

A right-click beside selected cells now moves the caret there, as in
Word: with a column selected, the cells between its first and last cell
in the text (the other columns of those rows) counted as "inside the
selection", so the selection stayed and the menu acted on the column
instead of the cell clicked. A right-click on a selected cell keeps the
selection, so Cell Alignment, Distribute and the rest act on it.

- UI only (crates/ui-egui/src/canvas.rs): the table part of
  `context_menu()` and a small check before the right-click caret logic
  (separate from it, so it doesn't overlap #521's change to that block).
  Table Layout's nine cell alignments are one shared list (`CELL_ALIGN`
  in ribbon.rs) for the ribbon grid and the menu.
- Two new labels, "Cell Alignment" and "Table Properties…", translated
  in all eleven catalogs; the rest reuse the ribbon's labels.
- Tests: kittest with real pointer events: right-click in a cell, the
  menu's table entries, Select > Select Column; with a column selected,
  a right-click on it keeps it and Cell Alignment > Align Bottom Right
  aligns just the column (one undo step), and a right-click beside it
  drops it.

Progress docs updated: ui-parity (context menus 55% -> 60%), gaps,
target-app-parity, ROADMAP. No new commands, so the catalog is unchanged.
`cargo xtask ci` passes.

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

* Tables: the right-click menu's table items follow the selection, as in Word

Checked against Word (2019, Windows): Word's table items in the context
menu depend on what is selected, and there is no Select or Cell
Alignment submenu among them.

- Caret in a cell: Insert, Delete Cells, Split Cells, Text Direction,
  Table Properties…
- Several cells: Insert, Delete Cells, Merge Cells, Text Direction,
  Table Properties…
- Whole columns: Insert, Delete Columns, Merge Cells, Distribute Rows,
  Distribute Columns, Text Direction, Table Properties… (whole rows the
  same with Delete Rows; not checked in Word).
- The whole table: Insert, Delete Table, Distribute Rows and Columns,
  AutoFit, Text Direction, Insert Caption…, Table Properties…

Insert lists the columns first, as Word does. Select and Cell Alignment
are gone, and with them the shared CELL_ALIGN constant in the ribbon;
"Insert Caption…" replaces "Cell Alignment" in all fifteen catalogs.
Word's Border Styles and Insert Cells… aren't here yet.

- Tests (kittest, real right-clicks): the menu with the caret in a cell
  and with the whole table; a selected column keeps its column menu and
  Text Direction turns only it; beside it the menu is a cell's again.

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

---------

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:29:45 -04:00
Brandon ThomasandClaude Opus 5.5 994162fdff Shift+F10 opens the page's context menu at the caret (#821) (#920)
The right-click menu could only be opened with the mouse. Shift+F10,
Windows' keyboard way to the context menu (the Menu key's equivalent),
now opens the same menu just below the caret, with the spelling
suggestions and synonyms for the word there; Escape closes it.
ui.contextMenu opens it for scripts and agents.

The Menu key itself still does nothing: egui-winit drops that key
before the app sees it, so it needs an upstream change (or a winit
hook) to reach the same path.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 10:28:22 -04:00
8ca285ca9d Tables: select, insert and move with the mouse; double-click a border to AutoFit (#637)
* Tables: select, insert and move with the mouse; double-click a border to AutoFit

Like Word, on top of the border resizing:

- Selection areas: just above a column the pointer becomes a down arrow,
  left of a row the row arrow, along a cell's left edge (inside it) the
  cell arrow. A click selects the column, row or cell; a drag selects every
  one up to the pointer. Selected cells are shaded as cells, empty ones too.
- Insert buttons: left of a row border and above a column border a "+"
  shows with the border marked; a click adds a row or column there and puts
  the caret in it.
- Move handle past the table's top-left corner: a click selects the table;
  a drag shows a dashed outline and, on release, the table floats where it
  is dropped with the text wrapping around it (Word's behaviour). One undo
  step. Nested tables and tables outside the main text are only selected.
- A double-click on a column border fits the column left of it to its
  contents (the right neighbour gives or takes the room; on the right edge
  the table's width changes, up to the right margin).
- Table Layout > Select > Select Column (catalog 418/449).

Whole-cell selection: a column (or a rectangle of cells over several rows)
is not one run of the text, so selecting it makes a block of its cells'
paragraphs (`CellRange` on the existing `ColumnBlock`). Bold, alignment,
Delete, typing, copy and numbering act on those cells only (a numbered
column is one list); table commands (shading, merge, delete columns) take
the rectangle; Tab moves on without clearing it; Undo brings the selection
back. Cells of one row, or whole rows, stay an ordinary stream selection.
The format painter no longer clears a block's text (edit.pasteFormat was
handled like a paste). The mini toolbar stays hidden over whole-cell
selections and during these drags: pinned above the selection it covered
the column areas, the "+" buttons and the move handle.

Engine: crates/engine/src/cmd/table_mouse.rs - table.selectAt,
table.insertAt, table.moveDrag, table.fitColumn (page coordinates, so
agents and the control channel can do the same) and table.selectColumn;
hit testing reuses table_resize's drawn geometry, and borders keep priority
for resizing. 10 engine tests (areas, a column edited cell by cell,
numbering, rows/cells/drags, insert buttons, move, fit, format painter,
hostile params).
UI: crates/ui-egui/src/table_mouse.rs, wired into the existing canvas mouse
handler (no extra ui.interact areas, so clicks in a cell's text still place
the caret); kittest tests with real pointer events: column click, row drag,
"+" button, move-handle drag in one undo step, double-click fit.
New labels translated in all eleven catalogs. Progress docs updated: gaps,
ui-parity, target-app-parity, ROADMAP, parity checklist.
`cargo xtask ci` passes (fmt, clippy, test, assets, layers, wasm).

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

* Tables: a selected column's word count is its cells'; merged cells shade once

With whole cells selected (a column, or a rectangle that isn't one run of
the text) the status bar said "12 of 23 words": `Session::selected_text`
took everything between the selection's ends. It now takes each piece's
own text, a line each, for a column block too, so the status bar, Word
Count and the other callers see what is selected.

Since #669 a vertically merged cell is one box as tall as its rows, with a
`covered` box for each continuation row; shading selected cells skips
those, so a merged cell isn't shaded twice. `selected_paths` follows
main's `para_paths_in` (#418): a selection ending at the start of the next
paragraph doesn't take it.

- Test: a selected column's text is its three cells' and Word Count says
  three words.

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

* Tables: double-click AutoFit and the "+" buttons work as in Word

Checked against Word (2019, Windows):

- A double-click on a column border fits the column left of it to its
  contents and the columns right of it keep their widths, moving with the
  border, so the table narrows or widens (up to the right margin). The
  right neighbour used to give or take the difference.
- A "+" insert button selects the new row or column (the caret used to
  go into it); typing then goes into its first cell. If the table can't
  take another row or column, nothing changes.
- A double-click along a cell's left edge (the cell selection area)
  selects its row.

- Tests: engine (fit keeps the columns on the right; insert selects the
  new row or column, typing lands in its first cell) and kittest (real
  double-click on a border; "+" selects the row; double-click by a cell's
  edge selects the row).

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

---------

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:15:31 -04:00
55bfc33418 Undo history list beside the Quick Access Toolbar's Undo (#701) (#855)
* #701: Undo history list beside the Quick Access Toolbar's Undo

A drop-down arrow next to Undo lists recent steps, newest first; hovering a
row highlights it and every newer step, the footer reads "Undo N Actions"
(Cancel when none is highlighted), and a click undoes them all at once.

Engine: Session::undo_history (newest first, capped at UNDO_LIST_MAX = 100)
and Session::undo_steps; edit.undoTo {"count": n} (0, negative and non-number
counts rejected, counts past the depth clamped, stops at protection) and the
read-only edit.undoHistory for CLI and MCP. Catalog: Undo List.

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

* i18n: Hungarian and French strings for this change

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

* #701: Bulgarian for the Undo history strings; drop a duplicate Turkish entry

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:05:06 -04:00
8c67ef7c28 Start on File › Home when launched with nothing to open (#766) (#853)
* #766: start on File › Home (Start screen) when launched with nothing to open

Desktop and web start on Backstage Home (Blank document, templates, Recent)
when no file, --sample or --control is given; Esc/back leaves the empty blank
document. File › Options › General › Start-up checkbox (ui.startScreen, kept
between runs, default on). WordApp::new is unchanged; hosts opt in via
WordApp::start_screen. ui_shot: {"empty":true,"startScreen":true}.

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

* i18n: Hungarian and French strings for this change

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

* #766: Bulgarian for the Start screen setting

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:00:28 -04:00
6f35313f68 #819: Alt+Q moves to "Tell me" command search (#849)
Word for Windows' Alt+Q opens the "Tell me" box; in WordCraft it did nothing.
Interface commands (ui.*) aren't in the engine's keymap, so keys.rs gets a
small table of interface shortcuts, checked before the keymap from the page
and from the rest of the window. Alt+Q opens command search
(ui.dialog {"name": "commands"}) and its key text isn't typed. Not on macOS,
where Option+Q types a character.

Also fixes the ROADMAP progress log, whose separator row sat below two rows.

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-12 09:30:18 -04:00
1677c2d9cf Ribbon groups fit above their labels (View › Show, Developer › Controls, Layout › Paragraph) (#910)
View › Show stacked four check boxes in a column made for three: since
Hide White Space (#645) joined Ruler, Gridlines and Navigation Pane, the
fourth row was drawn over the "Show" group label. It now sits in a second
column.

A new test draws every tab in every interface language and fails on any
group taller than the ribbon's content area (CONTENT_H, 66 pt). It found:

- Developer › Controls, 75 pt: 3 pt after each row of small buttons. The
  rows now fill the height with no gaps, like every other stack.
- Layout › Paragraph (#654), which panicked in debug builds: `add_space`
  between grid cells trips egui's "add_space makes no sense in a grid
  layout" assertion. The 8 pt gap before the Spacing column now sits
  inside those cells, so it looks the same. The group was also 68.5 pt:
  1.5 pt between rows instead of 3 fits it.

- widgets.rs: under cfg(test), group_titled records any group whose
  content is taller than CONTENT_H.
- ribbon.rs: ribbon_groups_fit_above_their_labels.
- ROADMAP progress log and docs/ui-parity.md revision history.
- contributors/people.toml: my own entry.

Co-authored-by: Manuel Carmona <328347120+manucarcor@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 09:27:44 -04:00
fccc09e4c9 fix(ui): draw non-Latin file names in the window caption (#840)
The painted title uses a Latin-only face, so names in other scripts were shown as question marks. A covering fallback face is now loaded for the caption.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 09:23:47 -04:00
Brandon ThomasandClaude Opus 5.5 90515abd1e Keytips: Word for Windows' tab letters (#922)
Observed in Word for Windows (Microsoft 365): pressing Alt badges the
tabs File F, Home H, Insert N, Design G, Layout P, References S,
Mailings M, Review R, View W, Help Y, and the Search box Q.

Before, WordCraft used Design L, References R, Review S and View V,
and File and Help had no letter, so Alt, W or Alt, R went to the wrong
tab for anyone used to Word. Now the letters match; Alt, F opens the
Backstage and ends keytips.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 09:23:21 -04:00
MarekSpodBiedryandBrandon Thomas bc60cbaadb Zoom after One Page keeps one page per row (#768) (#832)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 09:05:40 -04:00
325e5a6157 M8: Truncate long Navigation headings in resizable pane (#759)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:59:54 -04:00
241877d666 Add paginated Reading Mode with smooth column navigation (#740)
* M8: reflow reading mode into viewport-sized pages

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: navigate reading columns with wheel, keys and page controls

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: keep Outline in current view and add transient reading layout controls

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: replace reading ribbon and align navigation to columns

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: animate reading column turns and retain visible text through resize

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: name reading navigation Outline and complete its translations

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: consolidate Reading Mode progress notes

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: keep reading clicks from scrolling the selected column

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: reveal reading caret only when outside the viewport

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: hide floating formatting toolbar in Reading Mode

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

---------

Co-authored-by: ChatGPT (GPT-6) <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:33:19 -04:00
7cccdf0f38 KeyTips: Quick Access Toolbar badges, saved option and native Alt fix (#620) (#709)
* M8.1: Quick Access Toolbar KeyTips (#620)

* M8.1: Handle native Alt events for KeyTips (#620)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:24:36 -04:00
af622ac843 Layout › Paragraph: fields fit above the title and line up (#654) (#703)
* #659: one drop-down marker (the dropdown icon) on every ribbon button

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

* #658: menu buttons grey out while their command is disabled

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

* #653: large-button labels end above the group title row

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

* #654: Layout › Paragraph fields fit above the title and line up

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 06:16:15 -04:00
4a1acb6c3f Ribbon: large-button labels end above the group titles (#653) (#699)
* #659: one drop-down marker (the dropdown icon) on every ribbon button

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

* #658: menu buttons grey out while their command is disabled

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

* #653: large-button labels end above the group title row

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 06:13:17 -04:00
3c8840556a Menu buttons grey out while their command is disabled (#658) (#698)
* #659: one drop-down marker (the dropdown icon) on every ribbon button

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

* #658: menu buttons grey out while their command is disabled

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 06:09:16 -04:00
c1e1c06297 #659: one drop-down marker (the dropdown icon) on every ribbon button (#697)
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-12 06:05:30 -04:00
dd66d5171b Dark theme: gallery previews no longer bright white (#663) (#706)
* #656: dialog buttons, Discord pill, eraser and search highlight use theme tokens

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

* #663: gallery previews dimmed in the dark theme; dark page under Switch Modes

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 05:42:58 -04:00
d510d9a41a #656: dialog buttons, Discord pill, eraser and search highlight use theme tokens (#705)
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-12 05:39:06 -04:00
de8a3daf31 Fix paragraph alignment focus (#690)
* Fix paragraph alignment focus

Fixes #686

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Review fixes for #690: every ribbon command button returns focus to the document

Instead of four hard-coded alignment ids, big, small and split buttons and ribbon menu
items hand keyboard focus back to the canvas after running any engine command (ui.*
commands open dialogs and panes, which take focus themselves). The test also clicks Bold.
The ui-parity.md note moves into the change log.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:32:58 -04:00
1fa5333b26 M15: Polish interface, and Polish spelling, grammar and hyphenation (#346)
* M15: Polish interface, and Polish spelling, grammar and hyphenation

Interface
- Polish is the tenth interface language ("Polski", pl.tsv, 1,045 entries);
  pl, pl-PL and pl_PL.UTF-8 system locales select it automatically.
- Right-click menu labels now go through translation in every language.
- Review > Language is a menu (English US/UK, Polski, Do not check spelling
  or grammar); the status bar shows the language at the caret.
- With a Polish interface, new blank documents and opened .txt/.md files
  default to pl-PL.

Proofing
- Each run is checked in its own language (w:lang, resolved through
  styles): squiggles, F7, review.issues, suggestions and hyphenation.
  Text in languages without proofing tools is no longer flagged as English.
- Polish spelling from the SJP.PL Hunspell dictionary (Apache-2.0 option),
  read by a new Rust affix engine (crates/proof/src/affix.rs); 875 KB asset,
  ~4.5M word forms. Regenerate with the pl_dictionary example.
- Polish suggestions: case, missing diacritics, rz/ż, ó/u, ch/h confusions,
  missing spaces (napewno -> na pewno), edit distance.
- Conservative Polish grammar rules: repeated words, spacing, doubled
  punctuation, sentence capitals, missing comma before że/który/ponieważ/
  aby/żeby/jeśli…, we/ze forms.
- Polish hyphenation with the hyph-utf8 hyph-pl.tex patterns (MIT option);
  pattern keys widened to 20 letters, TeX exceptions parsed; the heuristic
  no longer treats ł, ż, ś… as vowels.

review.language validates and normalizes tags, can set noProof alone and
the document default; format.state reports lang and noProof.

Tests for every part, docs and ATTRIBUTION.md/NOTICE updated;
cargo xtask ci passes (fmt, clippy, test, assets, layers, wasm).

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

* Layout: rustfmt the test module order left unformatted by #391

cargo fmt --check failed on main after #391 added mod tests_grid.

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

* Review fixes for #346: read Polish spelling from the installed Hunspell dictionary

- Polish spelling uses the Hunspell pl_PL dictionary installed on the
  system (OS packages, Homebrew, LibreOffice, ~/.local/share/hunspell, or
  WORDCRAFT_DICTIONARIES) instead of a bundled 870 KB SJP.PL file;
  without one, Polish text gets no spelling marks, like Word without the
  language's proofing tools. .aff/.dic are size-capped and decoded from
  UTF-8 or ISO 8859-2 (crates/proof/src/hunspell.rs).
- The dictionary can be replaced at runtime (install_dictionary) or
  re-searched (reload_dictionary); an empty one accepts every word.
- Tests use small synthetic dictionaries written for them, so they don't
  depend on what the machine has installed.
- The bundled pl.dic, its ATTRIBUTION/NOTICE entries and the generator
  example are removed; docs describe the installed dictionary.

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

* M15: Polish catalog covers the interface text merged from main

The merge of main (98456ac) brought ~575 new interface strings (charts,
SmartArt, content controls, Developer tab, AutoCorrect, outline view,
protection, compare, print, accessibility, references and Language/
Hyphenation dialogs, Draw additions). pl.tsv lacked them, so
polish_covers_every_key_of_every_other_catalog and
every_tab_and_command_is_translated failed. All are now translated.

cargo xtask ci: all 6 steps pass.

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-12 04:04:03 -04:00
ad8c264420 Tables: resize by dragging column borders, row borders and the corner handle (#217) (#600)
Like Word: the pointer turns into a resize arrow over a column border or a
row's bottom border, and a small handle shows past the table's bottom-right
corner. Dragging shows a dashed guide; releasing runs the new
`table.resizeDrag` command (page coordinates), so a drag is one undo step
and the selection is kept. Esc cancels a drag.

- Column border: the right neighbour gives or takes the difference, so the
  table keeps its width; Shift, or the table's right edge, changes only the
  column on the left. Columns stay at least 6 pt wide.
- Row border: sets the row's "at least" height; the margins and border bands
  the layout adds are measured and taken off, so the row is drawn at the
  dragged height.
- Corner handle: scales every column and the rows on that page.
- Widths come from the drawn layout, so autofit and percentage widths don't
  make the result jump; a merged cell hides the border it covers.
- Borders are hit-tested in the existing canvas mouse handler, only within a
  few pixels of a border, so clicks in cells still place the caret.

Engine: crates/engine/src/cmd/table_resize.rs (7 tests).
UI: crates/ui-egui/src/table_resize.rs, wired into canvas.rs; kittest tests
drive real pointer events through the whole window (one undo step per drag,
selection kept, clicks in cells still move the caret, Esc cancels).
The command label is translated in all nine interface catalogs.
Progress docs updated: gaps, ui-parity, target-app-parity, ROADMAP.
`cargo xtask ci` passes (fmt, clippy, test, assets, layers, wasm).

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:38:05 -04:00
c08d9f8598 Accessibility Options: Animation effects setting that follows the system (#791)
* Options: Animation effects setting that follows the system

The app eased wheel scrolling, Ctrl+wheel zoom and its fades whatever the
system's animation setting was: with Windows' Animation effects off, the
page still smooth-scrolled.

File > Options > Accessibility > Animation effects: Use system setting
(the default), On or Off; `ui.animations` for agents; kept in ui.json.

- Off: a wheel notch scrolls the page its whole distance at once, egui's
  own scroll areas get the notch whole instead of eased in, Ctrl+wheel
  zoom steps at once, and egui's fades and scroll-to glides are off.
  Touchpad momentum and the caret blink are unchanged.
- Use system setting asks the host (`Services::animations_enabled`) at
  start-up and when the window regains focus. Windows: the Windows
  Runtime's UISettings, a safe API of the `windows` crate already in the
  build. Web: `prefers-reduced-motion`. macOS and Linux/BSD aren't read
  yet (docs/gaps.md); there it means on.
- The eight new strings are in all eleven interface catalogs.

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

* Animation effects off: zoom steps land finished in one frame

Follow-up to the Animation effects setting: zooming could still look
animated with the setting off.

- Ctrl+wheel zooms in whole steps, each at once. A mouse-wheel notch is
  two steps in its frame, the same zoom as before. Turns that arrive in
  small pieces (a touchpad pinch that Windows reports as Ctrl+wheel, a
  high-resolution or free-spinning wheel) add up to a step of about 10%
  first, where each piece used to zoom a little and the zoom glided.
- A page re-rendered for a zoom step is drawn on the pixel grid in that
  frame. It used to be drawn off the grid, slightly soft, and snap sharp
  a frame later.
- The frame a zoom changes in asks for the next one at once, so the
  status bar's percentage and slider follow in the next frame. They
  could lag until the caret's next blink, up to half a second.

The last two apply whatever the setting.

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

* Animation effects: follow the system's setting on macOS, Linux and the BSDs

"Use system setting" only read Windows and the browser; elsewhere it
meant on.

- macOS: System Settings > Accessibility > Display > Reduce motion,
  through NSWorkspace (a safe call in objc2-app-kit 0.3, already in the
  build).
- Linux and the BSDs: the XDG settings portal over D-Bus (zbus, already
  in the build through AccessKit). Three settings say whether animations
  play, and any one saying no turns them off: the cross-desktop
  reduced-motion preference, GNOME's enable-animations and KDE Plasma's
  animation speed. A thread reads them at start-up and follows the
  portal's change signal. Where no portal answers, or it knows none of
  them, KDE's kdeglobals and GTK's settings.ini are read instead.
- The UI now asks the host every frame instead of at start-up and on
  focus, so a change in the system's settings is followed while the app
  runs. Hosts answer from memory: Windows and macOS are asked again at
  most once a second, the web keeps its media query, and Linux/BSD is
  kept current by its thread.

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

* Animation setting on Linux: ask every setting again when one changes

KDE's portal announces a new animation speed only as the cross-desktop
reduced-motion preference, not under its own kdeglobals name. Keeping
each setting's last announced value left KDE's own one stale: after
going from instant back to normal speed, animations stayed off.

Every setting is now read afresh on any relevant change, and the
settings files' answer stands whenever the portal has none.

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

* Caret glide follows Animation effects; its canvas test turns the setting off, then on

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:24:58 -04:00
d69c94efcf Linux: open/save works without a portal file dialog (#576) (#583)
* Linux: built-in file browser when the portal file dialog is unavailable (#576)

rfd 0.17 on Linux/BSD asks the xdg-desktop-portal FileChooser and falls back to
zenity; with neither (labwc with only xdg-desktop-portal-wlr, no zenity) it
returns None, the same as Cancel, so Open/Save As/Insert Picture silently did
nothing. The dialog thread now checks first (zenity on PATH, or the portal's
FileChooser interface via dbus-send) and answers with an error when neither is
there; the app then finishes the job in its own egui file browser (folder
field, Up/Home, folders and files of the chosen type, file name, type filter,
replace confirmation) with a hint on how to get the system dialog.
File > Open > Browse in WordCraft... (Linux/BSD) and `ui.fileBrowser` reach
it at any time, even while a system dialog never showed up;
WORDCRAFT_FILE_DIALOG=builtin forces it. Native only (not wasm32).

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

* Turkish and Russian strings for the file browser (#576)

Co-Authored-By: Claude Sonnet 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-12 02:19:04 -04:00
cbd9291909 Caret: glide and fade like Word for Windows; File › Options › Advanced › Animation turns it off (#570)
* Add arcanii to the contributor names

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

* Caret: glide and fade like Word for Windows; File › Options › Animation turns it off

The caret no longer jumps and switches on and off. A key or typing glides
it to its new place in 100 ms along cubic-bezier(0, 0, 0, 1), the same for
one character and for a whole line; a second key before it arrives makes
it snap to where it was heading and set off from there. Clicks, and moves
made by scripts or the control channel, don't glide. The blink is a pair
of 100 ms fades (530 ms on, 530 ms off), and a key or a click turns the
caret on from however visible it was.

These are Word for Windows' numbers (16.0.20326, 100% zoom, 144 Hz),
measured black-box: a synthetic document in a separate Word instance, its
window captured frame by frame while key and mouse messages moved the
caret. Fourteen consecutive frames match the curve to 0.003. Word for Mac
was not measured.

Nothing glides when the view changed (zoom, a resized window, scrolling,
another document), when the caret wasn't drawn the frame before, or when
a move starts or ends off screen.

File › Options › Animation › "Animate the caret (it glides and fades)"
turns it off: ui.caretAnimation {"value"} for agents, kept in ui.json, on
by default, translated in the nine catalogs. Off, and whenever egui's
animations are off (the headless harness, so ui_shot and scripted
screenshots), the caret jumps and blinks exactly as before.

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-12 02:15:02 -04:00
Eduardo SoaresandBrandon Thomas 833d0b00c2 #755: Line Numbers and Hyphenation measure in the Options unit (#795)
File > Options > Advanced > Measurement units (#485) missed these two dialogs:
the Line Numbers "From text" distance and the Hyphenation zone read and
sent inches whatever the preference was. They now convert through
options::unit() like every other dialog; params still send points.

Reported by @Ledity.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:32:44 -04:00
a520fb362d Track Changes: show deletions in balloons when 'Show revisions in balloons' is on - balloons now work (#734)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:08:26 -04:00
1490d4e594 Dark theme: readable text on the accent (#655) (#704)
* #657: input borders reach 3:1 contrast in both themes

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

* #655: dark-theme accent controls use dark on_accent text (6:1)

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:51:53 -04:00
b2491a28dd #657: input borders reach 3:1 contrast in both themes (#696)
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-12 00:43:19 -04:00
219b125eeb #661: Table of Authorities buttons labelled, with Update Table (#693)
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-12 00:41:03 -04:00
1b96e04b2c Add fonts at run time: Add Font…, drop a font file, font.add (#582) (#668)
* Add fonts at run time: Add Font…, drop a font file, font.add (#582)

Rebased onto main as one commit (the branch had merge commits from main).

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

* #582: font test no longer depends on the system's fallback font

The FreeBSD runner has no system fonts, so a missing family fell back to
Source Serif 4, the font the test compared against. The test now adds a
renamed JetBrains Mono under a name without Serif/Sans/Mono (which picks a
fallback of that kind), with narrow-letter sample text: monospaced, it
measures unlike any proportional fallback.

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:38:20 -04:00
381c475edc M-print: File > Print preview, page range, odd/even, copies and collate (#641) (#674)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:30:58 -04:00
39f51ca292 More Colors… and a Colors dialog for custom colours (#606) (#666)
Rebased onto main as one commit (the branch had merge commits from main).

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-11 23:27:34 -04:00
14080c614c Drag after a double/triple-click extends the selection by words/paragraphs (#549) (#649)
Rebased onto main as one commit (the branch had merge commits from main).

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-11 22:58:11 -04:00
dab5092e83 Font dialog: separate Asian and Latin text fonts (#505) (#536)
* Font list: installed Chinese fonts are listed, by their Chinese names in a Chinese interface (#505)

The font database read only the English family name of each font, so a
Chinese interface listed SimSun, Microsoft YaHei and WenQuanYi Zen Hei
by their English names and never as 宋体, 微软雅黑 or 文泉驿正黑, and a
document naming a font by its Chinese name fell back to another font.

- fonts: keep each face's localized family names (name IDs 16 and 1,
  every face of a collection, read during the scan without loading the
  file); FontDb::families_in(lang) lists them in place of the English
  names for a matching interface language (zh-hans, zh-hant, ja, ko);
  face, has_family and styles accept either name.
- ui: the ribbon and mini toolbar font boxes show the interface
  language's names and store the English name.
- East Asian font: choosing a Chinese, Japanese or Korean font also sets
  it as the East Asian font (w:eastAsia), read and written in DOCX;
  layout draws CJK text in a run's installed East Asian font.

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

* Font dialog: separate Asian and Latin text fonts (#505)

Word's Font dialog sets the font of Chinese, Japanese and Korean text
apart from the font of Latin text; WordCraft's had one font box.

- Font dialog: an "Asian text font" box (the installed Chinese, Japanese
  and Korean fonts, by their interface-language names, type to find)
  above the "Latin text font" box, shown for a Chinese or Japanese
  interface, East Asian text in the selection or a selection with an
  Asian font. OK sets only the fonts changed, in one undo step.
- format.font takes "eastAsia" (w:eastAsia) besides "name"; either or
  both; format.state reports fontEastAsia.
- fonts: the scan reads each font's OS/2 code pages, so
  FontDb::east_asian_families lists CJK fonts without loading them.
- Tests: dialog sets each font on its own; layout sets CJK text in the
  run's Asian font and Latin text in its Latin font (skipped without an
  installed CJK font); command and undo; the scan finds a CJK font.

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-11 21:42:56 -04:00
f422d6ccbb Mini toolbar: every button fits, flush font box, proper dropdown arrows, dark-mode font list (#535) (#540)
- Two rows like Word's mini toolbar (font, size, grow, shrink; B I U S, highlight,
  font colour, four alignments), sized from the ribbon widgets' real metrics so no
  control is clipped any more (the one-row bar cut off everything after Align Left).
- Combo lists drop from the box's left edge instead of from the arrow.
- The dropdown triangle fills its box, so it no longer shrinks to a dot beside
  combos and split buttons.
- Font list entries are an ink mask tinted with the theme's text colour, so they no
  longer sit on white strips in dark mode.

Tests: the bar drawn over a real selection contains every control and is exactly as
wide as them; font entries take the theme text colour (dark and light) and paint no
background; the ink mask keeps coverage and drops the paper.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:19:26 -04:00
7484a6bd11 Find and Replace: special characters (^p, ^t, …) and wildcards (#543) (#553)
- Find what understands Word's ^ codes (^p ^t ^? ^# ^$ ^^ ^l ^m ^n ^b ^+ ^= ^s ^~ ^- ^w ^g ^% ^v,
  ^nnn / ^unnnn); Replace with adds ^& (found text) and ^c (clipboard). `special: false` reads
  the text literally.
- Use wildcards: ? * [abc] [a-z] [!x] {n} {n,} {n,m} @ < > ( ), \1-\9 in Replace with; always
  matches case, like Word. Patterns are translated to the linear-time regex engine (no
  catastrophic backtracking), length and counts capped, bad patterns are errors.
- ^p matches paragraph marks between sibling paragraphs; a ^p in the replacement splits the
  paragraph. Replace All stays one undo step.
- Match prefix / suffix, Ignore punctuation / white-space characters.
- Dialog: Use wildcards checkbox, Search Options with the new options and a Special menu that
  inserts codes into the focused field (a shorter list with wildcards); strings in all 9 catalogs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:15:31 -04:00
46d5e99b3e Styles: Update to Match Selection takes the selection's formatting (#555) (#562)
styles.updateToMatch copied only the direct run/paragraph formatting at the
caret onto the style, so text whose look came from its own style (Normal,
Title, ...) changed nothing; it then wiped that paragraph's direct formatting
without applying the style, so the selection lost its look too.

Now the style takes the selection's effective formatting (paragraph style
chain, character style chain, direct formatting) as a diff against what the
style currently gives, keeping its outline level and numbering. The selected
paragraphs take the style and drop only the direct formatting it now carries.
The Home styles gallery tiles get the right-click menu (Update to Match
Selection, Modify...).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:06:14 -04:00
900c0f4412 Outline view: Outlining tab, levels, collapse/expand, Show Level (#493) (#499)
* Outline view: Outlining tab, levels, collapse/expand, Show Level (#493)

- View › Outline is its own view mode: the body on one long sheet, each paragraph indented
  by its outline level behind a symbol drawn in code (circled plus for a heading with content,
  circled minus for one without, a dot for body text); paragraph indents and alignment hidden.
- Collapsed headings hide their subtree and get a wavy line; Show Level 1-9 / All; Show First
  Line Only; Show Text Formatting off draws everything in the Normal style. All of it is view
  state (ViewState.outline), not saved; collapsed headings are found again by their text after
  edits, and a caret entering hidden text moves on to the next shown paragraph.
- Commands: outline.promoteToHeading1/promote/demote/demoteToBody/level/moveUp/moveDown,
  outline.expand/collapse/toggle/selectSubtree, outline.showLevel (+ showLevel1-9, showAll),
  outline.firstLineOnly, outline.showFormatting, outline.close, outline.state. In Outline view
  Move Up/Down and Promote/Demote take a collapsed heading's hidden content along.
- Keys: Alt+Shift+Plus/Minus expand/collapse, Alt+Shift+1-9 and Alt+Shift+A Show Level.
- Outlining tab (next to File while in Outline view): level box, promote/demote, move,
  expand/collapse, Show Level, the two check boxes, Close Outline View. Clicking a symbol
  selects the heading with its content; double-clicking expands or collapses it.
- Catalog: Outlining tab rows (421/453, 92.9%); translations in all nine catalogs; docs.

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

* Docs: drop the stale shortcut and dialog rows left by the rebase (#493)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:54:56 -04:00
e46133817a Templates and Add-ins: attached template, update styles, Organizer (#377) (#382)
* Templates and Add-ins: attached template, update styles, Organizer (#377)

- Model + DOCX: Settings keep the attached template (w:attachedTemplate's external
  relationship of the settings part) and w:linkStyles; both round-trip, written in
  CT_Settings order (after w:mirrorMargins, before w:trackRevisions).
- Engine: tools.templates (state), tools.attachTemplate, tools.detachTemplate,
  tools.linkStyles, tools.updateStyles (copy the template's styles and defaults, one
  undo step; path or base64 data), styles.organizer (list), styles.copyFrom (copies
  missing bases, linked halves and the lists a style numbers with), styles.rename.
  The template path comes from the document, so it is never read automatically (not on
  open either): only an explicit tools.updateStyles / attach with update reads it.
- UI: View > Templates > Templates and Add-ins dialog (path, Attach..., automatically
  update document styles, Organizer...) and a two-list Organizer (Copy, Delete,
  Rename...); strings in every catalog, Estonian included.

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

* Review fixes for #382: never follow a template path to another machine

A document's w:attachedTemplate can name \\host\share\T.dotx, //host/share or
file://host/share. Opening it (Update Styles, Organizer) would make Windows
authenticate to that host over SMB and leak the user's NTLM credentials.
local_path now returns None for network paths; regression test added.

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

* #377: refresh catalog count and parity checklist after rebase

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:35:06 -04:00
2e5ecb916b File › Options: Word's panes with working settings (#485) (#492)
File › Options is now a pane list (General, Display, Proofing, Save,
Language, Accessibility, Advanced, Agents), one function per pane in
crates/ui-egui/src/options.rs. Every setting shown is wired:

- General: theme, interface size (#475), mini toolbar on selection, dark page, community
  button, user name and initials (comments use the initials).
- Display: tab characters, spaces, paragraph marks and hidden text shown
  with Show/Hide ¶ off; rulers; print the page colour; update fields
  before printing.
- Proofing: ignore words in capitals / with numbers / web and file
  addresses (proof::Options, passed through layout), check spelling and
  mark grammar as you type, custom dictionary add/remove (kept in ui.json).
- Save: AutoSave. Language: interface language. Accessibility: Read Aloud
  speed and citations. Advanced: typing replaces selection, Insert key
  overtype, overtype (text.overtype), recent documents count, measurement
  units (dialogs, Layout ribbon indents and rulers).

file.options reads and sets the engine-side settings headlessly and only
opens the page when given no setting. ui.json loads leniently: a bad value
falls back to its default instead of dropping every setting.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:17:50 -04:00
88a45329b2 Right-click on a picture or shape: Wrap Text submenu (#513) (#521)
Right-clicking a picture, shape, text box or chart selects it (as a
click would) and its context menu offers Wrap Text: In Line with Text,
Square, Tight, Through, Top and Bottom, Behind Text, In Front of Text
(current one checked), then Move with Text / Fix Position on Page.

The latter two are a new engine command, arrange.moveWithText, which
re-anchors a floating object's vertical position to its paragraph or
the page without moving it. The menu items live in one shared function
(ribbon::wrap_text_items, WRAP_STYLES) for the ribbon to reuse.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:11:31 -04:00
b0431eadc2 Paragraph dialog: mirror indents, same-style spacing, suppress line numbers, don't hyphenate (#512) (#519)
- Model: `ParaProps::mirror_indents` (`w:mirrorIndents`, ECMA-376 §17.3.1.18), read and
  written in DOCX; resolved `mirror_indents` and `suppress_line_numbers` (so styles count).
- Layout: a paragraph with mirrored indents moves over by its indents' difference on even
  (left-hand) pages, so its left (inside) indent is on the right there.
- Paragraph dialog: Mirror indents (Left/Right become Inside/Outside), Don't add space between
  paragraphs of the same style, and Formatting exceptions (Suppress line numbers, Don't
  hyphenate) on Line and Page Breaks; OK sends only the options that changed, in the same
  `para.set` as the indents and spacing.
- Strings in all nine catalogs; Reveal Formatting names mirrorIndents.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:50:28 -04:00
91b960ed74 Accessibility: the document canvas exposes its text to screen readers (#488) (#489)
The canvas is now the AccessKit document node: body paragraphs become
paragraph, heading (with level) or list item (with level, grouped into
lists, list numbers as list markers) nodes holding one text run per laid-out
line with character lengths, positions, widths and word starts; tables are
table/row/cell nodes, pictures and charts image nodes named by alt text,
hyperlinks link nodes around their runs. The caret and selection are an
AccessKit text selection on the document node.

Only pages on screen and one either side are described, capped at 6000
nodes; nothing is built unless AccessKit is active, and the tree is rebuilt
only when the layout, selection, described pages or their screen place change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:11:05 -04:00
4b5ed293d7 Compare, Restrict Editing and AutoCorrect Options dialogs (#412) (#476)
* Compare, Restrict Editing and AutoCorrect Options dialogs (#412)

- Compare / Combine Documents dialog: original (this document or a file) and
  revised document, label changes with, More >> comparison settings, word or
  character level, show changes in. review.compare honours case changes,
  white space, formatting (tracked formatting changes), tables (compared and
  kept, no longer dropped) and character level; the LCS no longer drops text
  past its size cap. Unsupported settings are accepted and reported.
- Restrict Editing pane: formatting restrictions (locked styles,
  w:formatting), editing restrictions, exceptions for everyone
  (w:permStart/permEnd), Start Enforcing Protection with an optional password
  (SHA-512, salt, spin count 100000; ECMA-376 §17.15.1.29) and Stop
  Protection. Enforced in Session::run: outside the exceptions nothing may
  change; undo can't lift protection; passwords never land in Repeat/macros.
  DOCX round trip of protection, hash attributes, locked styles and ranges.
- AutoCorrect Options dialog: replacement list (add/replace/delete, built-in
  entries too), capitalization options with exceptions, AutoFormat As You
  Type switches, Math AutoCorrect list. Saved per user in ui.json; damaged
  values read as defaults. Names of days are capitalised.

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

* Restrict Editing: verify Word's legacy protection hash; refuse unsupported forms (#412)

- Stop Protection checks Word's legacy w:documentProtection hash
  (w:cryptAlgorithmSid/w:hash/w:salt/w:cryptSpinCount) like Word: the password
  becomes the legacy 32-bit key (ECMA-376 Part 4 §14.8.1 initial code array and
  encryption matrix), its reversed bytes as an upper-case hex string are hashed
  with the salt prepended and the iteration number appended. A wrong password
  is rejected; the form no longer unlocks with any password.
- Unsupported or damaged hashes (MD2/MD4/MD5 and other SIDs, w:algIdExt,
  unknown algorithmName, spin counts above 10,000,000, bad base64) refuse with
  "This password type isn't supported" and the protection stays on. A
  protection without a hash still stops without a password.
- Known-answer tests: the specification's "Example" key 0x64CEED7E and an
  independently computed SHA-1 hash; pane message in all 9 catalogs.

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-11 17:10:02 -04:00
1a0db9ec65 Dialogs: Line Numbers, Hyphenation, Language, Envelopes and Labels (#407) (#414)
* Dialogs: Line Numbers, Hyphenation, Language, Envelopes and Labels (#407)

- Line Numbers (Line Numbering Options…): add numbering, start at, from
  text (auto or a distance), count by, restart each page/section or
  continuous; `layout.lineNumbers` takes start/countBy/distance.
- Hyphenation Options: automatic, words in CAPS, hyphenation zone,
  consecutive-hyphen limit; kept in DOCX (w:hyphenationZone,
  w:doNotHyphenateCaps, w:consecutiveHyphenLimit) and honoured by
  layout. Manual…: `layout.manualHyphenation` finds words at line starts
  that would fit hyphenated on the line above; Yes inserts an optional
  hyphen, No skips.
- Language: localized language list, do not check spelling or grammar,
  detect language automatically (per-user preference), Set As Default
  (document default language).
- Envelopes: delivery/return address, common sizes (#10, #9, Monarch,
  DL, C4, C5, C6, B5); Add to Document inserts an envelope section
  before the letter. Labels: common sheets (Letter and A4), full page or
  a single label at row/column, one per recipient; New Document builds
  the sheet with gap columns.
- Ribbon: Line Numbering Options…, Hyphenation Options…, Manual…; the
  Language, Envelopes and Labels buttons open their dialogs.

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

* Remove duplicate Label: keys in ru and tr (#407)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 16:15:53 -04:00
d5bcb9b490 Accessibility: alt text for every object, decorative flag, more checker rules (#386) (#396)
- Model: Float carries AltText (title, decorative; the description of shapes and groups);
  InlineObject::alt_text/set_alt_text/is_decorative; TableProps.description. Capped input.
- DOCX: wp:docPr / pic:cNvPr / wps:cNvPr descr, title and the [MS-ODRAWXML] adec:decorative
  extension read and written for pictures, shapes, text boxes, groups and members, charts;
  OLE objects get their alt text in the kept XML; w:tblCaption + w:tblDescription.
- Commands: object.altText {text?, title?, decorative?} (picture.altText kept as alias),
  table.altText {title?, text?}, view.accessibilityPane, view.altTextPane.
- Checker (cmd/accessibility.rs): alt text on every object and table (decorative passes),
  heading order, header rows, blank table rows/columns, merged cells, WCAG contrast,
  floating objects, blank paragraphs; issues carry pos/select and fix actions.
- UI: Accessibility pane (grouped, jump-to, fix buttons), Alt Text pane, Picture/Shape
  Format > Accessibility > Alt Text, right-click > View Alt Text...; strings in all catalogs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 16:01:20 -04:00
56f9456f03 References dialogs: Caption, Index, Table of Contents, Create Source (#402) (#410)
* References dialogs: Caption, Index, Table of Contents, Create Source (#402)

- Caption: labels (built-in plus New/Delete Label, kept with the interface
  settings), above/below the item, leave the label out, Numbering (format,
  chapter number from a heading level with a separator) -> SEQ \* \s and
  STYLEREF n \s fields; Update Field renumbers SEQ with \s restarts, \r, \c, \h.
- Index: indented/run-in, columns, right-aligned page numbers with tab leader,
  letter headings -> INDEX \h \c \e \r; Mark Index Entry (subentry,
  cross-reference, page-range bookmark, bold/italic page numbers, Mark All)
  -> XE \t \r \b \i. The index is rebuilt honouring them; columns become a
  continuous section of their own. Index commands move to cmd/index.rs.
- Custom Table of Contents: page numbers, right align, tab leader,
  hyperlinks, levels, Options (heading styles, other styles with levels,
  outline levels, TC fields), Modify for TOC 1-9 -> TOC \o \h \z \u \t \f
  \l \n \p; updating honours them (hyperlinks to _Toc bookmarks).
- Source Manager and Create/Edit Source: source types with their fields,
  show every bibliography field (new Source fields); Insert Citation menu
  lists the sources and Add New Source.

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

* fmt (#402)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 15:56:41 -04:00
4ca58d67e1 Draw: Lasso Select, Add Pen, Ink to Shape and Ink Replay (#395) (#408)
* Draw: Lasso Select, Add Pen, Ink to Shape and Ink Replay (#395)

- draw.lasso: with no points picks up the lasso; with a loop on a page it
  selects the strokes mostly inside it and drawings whose centre is inside.
  draw.lassoMove / draw.lassoColor / draw.lassoDelete move, recolour and
  delete the selection (one undo step each); it lasts until the document
  changes otherwise. In the app: drag a loop, drag the selection to move it,
  Delete deletes, Esc lets go, the Color menu recolours it.
- draw.addPen / draw.removePen: a pen gallery (kind, colour, width; capped,
  validated) shown after the built-in pens, kept in ui.json; damaged entries
  are dropped on reading. Right-click a gallery pen to remove it.
- draw.inkToShape: toggles Ink to Shape (pen and pencil strokes that look
  like a line, rectangle/square, ellipse/circle, triangle or polygon of up to
  eight corners become that shape, in the same undo step); with ink
  lasso-selected it converts the selection (draw.convertInk). Our own
  recogniser (cmd/ink_shape.rs): chord test for lines, closure test,
  ellipse RMS fit, closed RDP simplification with corner merging.
- draw.replay: Ink Replay plays the strokes back in drawing order with a
  Rewind / Play-Pause / Forward / Close bar; pages are drawn without ink
  meanwhile. Each new stroke records its drawing order (Freeform::order),
  written to DOCX in the drawing name (`Ink Pen 3 #12`) and read back.

Catalog 402/434 -> 406/434 (93.5%). New strings in all nine catalogs.

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

* Regenerate parity checklist after rebase (#395)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 15:53:47 -04:00