Commit Graph
1028 Commits
Author SHA1 Message Date
52190491ff Picture Format › Crop: crop handles on the canvas (#1447)
* Crop handles: Picture Format › Crop crop mode on the canvas, picture.crop takes the new size

ui.crop toggles crop mode for the selected (unturned) picture: black crop
handles trim a side or two, keeping the scale and the visible part in place;
the cropped-away part shows dimmed; Esc, Enter, a click outside or Crop again
keep the crop. A drop is one picture.crop with width/height, one undo step;
a floating picture moves by what a left/top crop trims.

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

* Crop handles: flipped pictures trim the side under the handle, stale drags drop unapplied

Review round 1: the crop maths, the dimmed preview and the floating
position shift map the on-screen side to the source side (flip H swaps
left/right, flip V top/bottom). A drag remembers its picture and is
dropped unapplied if that picture is no longer the one in crop mode.
Sizes stay within 1-4000 pt at the same scale. Pointer-path tests drive
a real handle drag and a click outside. The dim is darker, and the
canvas keeps following a crop drag's pointer.

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

* Docs after rebasing on main

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

* Crop handles: ui-parity row merged with main; #1242's Cell Size note moved to the Tables 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-14 19:08:44 -04:00
c80f51e810 DOCX: draw VML preset shapes such as Word's right arrow (#1377) (#1442)
* DOCX: draw VML preset shapes such as Word's right arrow (#1377)

A VML v:shape that takes its geometry from a preset shapetype
(type="#_x0000_t13", o:spt) was dropped on import. Its preset number now
comes from the shape's o:spt, its v:shapetype, or Word's _x0000_tNN id,
and the presets whose geometry matches a DrawingML preset WordCraft draws
are read with their fill, outline and size (paint left out by the shape
comes from its shapetype): rectangle, rounded rectangle, ellipse,
diamond, isosceles triangle, five-point star, right arrow, line and
straight connector, heart, text box, and the flowchart process, decision
and connector. Saving writes them as DrawingML presets. Text in a preset
keeps the shape around it. MSOSPT numbers from [MS-ODRAW] 2.4.24.

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

* DOCX: draw unmatched VML presets as their nearest shape (#1377)

VML preset numbers now map to the DrawingML preset of the same shape and
go through preset_kind, so a preset WordCraft has no shape for (left, up
and down arrows, right triangle, callouts, four-point star...) is drawn
as the nearest one, as DrawingML presets are, instead of being left out.
The exact mappings are unchanged. WordArt text shapes, picture frames
without a picture and non-primitive shapes are still not drawn.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:08:24 -04:00
85aa9cdea4 #1376: a word too wide for the strip beside a floating picture moves on whole (#1439)
* #1376: a word too wide for the strip beside a floating object moves on whole

Text in the narrow strip beside a square-wrapped picture was split mid-word
("docu" | "mentazione") when the strip was wider than the 20pt minimum but
narrower than the word. A span narrowed by a floating object is now used only
if the line's first word fits it; otherwise the word tries the row's next span,
or goes below the object. A word wider than the whole line still breaks in
place, and the 20pt minimum measured for floating tables is unchanged.

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

* #1376: a line after a skipped strip starts its row; word width skips hyphenation points

A line placed in a row's second span after the first was skipped was marked as
beside a line before it, so line numbering and pagination treated it as the
second half of a row. The word measured against the full line now runs to the
next real break, not the first hyphenation point, and a row with nothing on it
jumps below an object that actually crosses the column.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:08:04 -04:00
18ecff6fbb wordcraft --control checks its port (#1426) (#1436)
* wordcraft --control checks its port (#1426)

--control took whatever followed it as the port and dropped it when it
didn't parse, so `--control --sample` lost --sample, `--control abc`
started without the control channel, and a bad value cleared
WORDCRAFT_CONTROL_PORT. A flag after --control is now left for the loop,
and a missing, non-numeric, zero or out-of-range port prints an error
naming it and exits with status 2, after the loop so --version still wins.

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

* wordcraft --control names any negative number it rejects (#1426)

A value after --control counts as the port when it starts with a digit
or a minus and a digit, so a negative too long for i64 is named in the
error instead of left in the file list. NonZeroU16 replaces the
separate zero check.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:07:45 -04:00
NathanaelandClaude Opus 5.5 e3ed7da1be #967: keep with next, keep lines together, page break before and widow control list their {"value"?: bool} (#1427)
* #967: para.keepNext, keepLines, pageBreakBefore, widowControl document {"value"?: bool}

The four commands already set the property when given a bool value and
toggle without one, but listed their params as {}. List them as format.bold
does and pin the behaviour with a test.

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

* #967: test that the params mention value, not the whole string

Other toggles annotate it, e.g. "(omit to toggle)"; a later hint should not
break this test.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:07:26 -04:00
rabis7575andClaude Opus 5.5 68d15dafba Layout: formats past 65,536 in one paragraph keep their own style (#1340) (#1421)
The index was a u16 clamped to u16::MAX while the style was still
appended, so every distinct format past the 65,536th in a paragraph
drew with the last one that fit: its font, size, link and tracked-change
marking. It is now a u32, like the glyph ranges beside it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:07:15 -04:00
MarekSpodBiedry 910a2bb312 Convert Text to Table refuses to drop fields or a table in the selection (#1318) (#1416) 2026-10-14 19:07:04 -04:00
MarekSpodBiedry 1024fc471b DOCX: cap the depth of header/footer parts that reference further headers/footers (#1322) (#1415) 2026-10-14 19:06:52 -04:00
MarekSpodBiedryandBrandon Thomas 81c52fecc0 Outline Promote, Demote, Move Up and Move Down add one undo step instead of two (#1363) (#1403)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:06:41 -04:00
a09162603d Styles gallery shows Word's quick styles for DOCX files that mark none, e.g. from LibreOffice (#1238) (#1394)
* #1238: Styles gallery falls back to Word's quick styles when a DOCX marks none

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

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

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

* Docs after rebasing on main

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-14 19:06:29 -04:00
MarekSpodBiedry 3a201c8246 Document::delete_range checks both ends before editing, so an error leaves the document unchanged (#1350) (#1410) 2026-10-14 19:01:47 -04:00
MarekSpodBiedry d1b14504dc Restart Numbering keeps the list's own level definitions (#1351) (#1409) 2026-10-14 19:01:36 -04:00
MarekSpodBiedry 4262276418 Print to PDF and PNG export replace the file only after it is fully written (#1355) (#1408) 2026-10-14 19:01:25 -04:00
MarekSpodBiedry 825538fc8b MCP type_text reports a refused paragraph break instead of typing the next line (#1356) (#1407) 2026-10-14 19:01:12 -04:00
MarekSpodBiedry f8d06ebb27 RTF import decodes Unicode escapes in font names (#1360) (#1406) 2026-10-14 19:01:01 -04:00
7188d8ae04 Save preferences through a temporary file and log write failures (#1361) (#1404)
* Save preferences through a temporary file and log write failures (#1361)

* Move the preferences test module to the end of main.rs (clippy::items_after_test_module)

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-14 19:00:49 -04:00
MarekSpodBiedryandBrandon Thomas 7591283266 Mouse wheel keeps scrolling while a text selection is being dragged (#1388) (#1402)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:00:37 -04:00
rabis7575andClaude Opus 5.5 cf8aea6886 xtask assets: fail when git ls-files fails, read raw NUL-separated paths (#1364) (#1401)
The attribution check spawned `git ls-files` but never looked at its
exit status: outside a usable checkout, or on a git error, the empty
output read as "no files" and the check passed with nothing checked.
A non-zero status is now an error with git's message.

The list is read NUL-separated (`-z`): without it git quotes a name
with non-ASCII letters ("assets/\303\251.png"), which matched no file
on disk and was silently left out of the check.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:00:24 -04:00
krinry f732c1a370 fix(geom): measurement parser rejects overflow to infinity during unit conversion (#1359) (#1398) 2026-10-14 19:00:12 -04:00
rabis7575andClaude Opus 5.5 b574cb9638 Paragraph dialog: leave mixed formatting alone, apply as one undo step (#1346) (#1397)
Over differently formatted paragraphs the dialog showed the caret
paragraph's values and OK gave them to every selected paragraph, through
para.rtl/ltr, para.align and para.set (three undo steps). It now reads
every selected paragraph: a field they share shows as before, a field
they differ in shows blank (number boxes empty, alignment empty, check
boxes neither on nor off, no direction picked) and OK leaves it as each
paragraph has it until the user sets it. OK sends only the fields that
changed, as a single para.set.

para.set takes "onPage": alignment and indents as seen on the page,
which each paragraph turns into its own start and end once `props` is
applied, so mixed directions get the right sides in one command. The
line spacing and space boxes no longer pull an out-of-range value from
the document (line spacing 6) into their range, which counted as an
edit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:00:01 -04:00
31b622ad79 Scripts can't close a document with unsaved changes without force (#1268) (#1391)
* #1268: programmatic file.close on unsaved work errors unless force is passed

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

* #1268: guard file.close in every control form; force must be boolean true

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>
2026-10-14 18:59:41 -04:00
rabis7575andClaude Opus 5.5 7052cfc80d An empty .docx opens as a blank document that saves back to it (#1384) (#1387)
File Explorer's New › Microsoft Word Document makes a zero-byte .docx.
file.open read it as a Word package, failed, and left the untitled
document in place, so Save asked for a name. An empty file (from disk or
as bytes) now opens as the Normal-template blank document File › New
gives (region's paper, editing language) and keeps its path.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:59:30 -04:00
23e26628ed Insert Hyperlink: Link to › Place in This Document (#1234) (#1370)
* Insert › Link › Place in This Document: link.targets, links to bookmarks, headings and the top (#1234)

* Insert Hyperlink: Link to › Place in This Document (#1234)

The Insert Hyperlink dialog gets Word's "Link to:" choice. Place in This Document
lists the top of the document, its headings (indented by level) and its
bookmarks, and links to the chosen one; the text to display follows the choice
until it is typed over. It uses insert.link with `top`, `heading` or `bookmark`
and link.targets for the list.

* Docs: update the progress docs for this change

* Docs: put the new sentence in the WordCraft / evidence column

* i18n: Insert Hyperlink link-target strings in every catalog

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-14 18:59:19 -04:00
2c33a66ca9 Word's text watermark in DOCX: drawn on screen and in the PDF, and kept on save (#1290) (#1328)
* #1290: read Word's VML text watermark from DOCX headers

Word's Design › Watermark is a VML shape with a v:textpath in the headers.
The reader dropped it, so screen and PDF showed no watermark. It now maps
it to the document's watermark model (text, font, fill colour,
semitransparent from v:fill opacity, diagonal when turned), which the
renderers already draw. Only Word's watermark shape (PowerPlusWaterMarkObject
id) or text centred on the page both ways counts; other header WordArt,
body WordArt and shapes inside text boxes are left alone. Writing it back
to DOCX is still missing (it was already lost on save).

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

* #1290: write the watermark to DOCX as a VML text shape in the headers

The DOCX writer dropped settings.watermark, so a watermark read from Word
(or set in WordCraft) was lost on save. Every header part now starts its
first paragraph with a VML shape (ECMA-376 Part 4) whose v:textpath carries
the text, font, fill colour, semitransparency (v:fill opacity) and diagonal
turn, centred on the margins behind the text, with the reader's
PowerPlusWaterMarkObject id so it reads back as the watermark, not a shape.
The shape type is our own: a straight path the text follows, fitted to the
box. Slots the pages show with no header of their own or linked from an
earlier section (default; first page with a title page; even pages with
even/odd headers) get a header holding only the watermark; linked sections
stay linked. A blank watermark isn't written. Check Compatibility no longer
warns about watermarks.

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

* #1290: give the watermark's text path a top and a bottom edge so Word draws it

With a single line as the shape type's path, Word loaded the watermark as
WordArt (right text, size and turn) but drew nothing. Checked black-box in
Word: diagonal, horizontal and header-less documents now show it.

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

* #1290: test the watermark through a Word-shaped file, table-only and even-page headers

Read → write → read of a file with Word's watermark keeps the watermark, the
headers' text and paragraphs and one shape per header; header WordArt that
isn't a watermark doesn't become one. A header holding only a table gains no
line; with odd and even pages an even header is added and later sections
stay linked.

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

* DOCX: read VML freeforms (v:shape with a path) as freeform shapes

Legacy VML shapes with their own `path` (ECMA-376 Part 4), such as rule
lines in headers, were dropped on open and lost on save. They are now
read as freeforms: m/t/l/r/c/v/x/e commands in the coordsize/coordorigin
space, nf/ns, v:fill and v:stroke opacity; arcs drawn straight, formula
references read as 0. Saved as DrawingML custom geometry.

Tests: header rule read and round-tripped; hostile paths and coordinate
spaces stay finite and never panic.

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

* DOCX: VML freeforms whose path uses formulas aren't drawn

Formulas (@n, from v:formulas) aren't evaluated; reading them as 0 drew stray lines. Parity row states the fill-opacity limit (none at 0, otherwise opaque, as for DrawingML fills).

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

* DOCX: VML freeforms reuse the watermark's vml_fraction

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

* #1290: move the VML freeform reader out of this PR

The three VML freeform commits (b7a6cc7, d8fa665, 945cbb4) aren't part of
#1290; they go to their own pull request so this one stays on the
watermark.

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-14 18:59:01 -04:00
a21b069390 fix(ui): drag a Behind Text picture or shape over the text it overlaps (#1313)
A picture with Wrap Text > Behind Text could not be dragged where it overlapped text, in the body or in a header/footer. Pressing it did nothing; once it was selected some other way (Selection Pane, arrow keys) the arrow keys moved it, but the mouse never started a drag. Reported from the field (#82).

Root cause: the hit test deliberately lets body text win over an object behind it (crates/layout/src/hit.rs: 'objects behind the text only where there's no text'), so objects::pointer never began a frame drag.

Fix: grab_at/pointer now treat a Behind Text picture, shape or group as a drag candidate even where text covers it, in body and header/footer stories. The press does not take the gesture at once: it becomes an object drag only after the pointer passes the drag threshold (pending_behind), so a plain click still places the text caret in the overlapping text. Text boxes are unchanged (their inside is their text; only their border grabs them).

Verified with three egui_kittest UI tests driving real pointer events, plus a headless ui_shot run against the real app:
- floating_picture_can_be_dragged_in_front_of_or_behind_text
- floating_header_picture_can_be_dragged_in_front_of_or_behind_text
- behind_picture_dragged_over_text_without_being_selected_first
Full wordcraft-ui-egui suite: 365 passed.

Docs: ROADMAP progress log and revision, docs/ui-parity.md, docs/gaps.md (#3).

Co-authored-by: Muthiullah Hib <thye911@mail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:58:44 -04:00
Eduardo SoaresandBrandon Thomas 41cf3c7563 Restrict Editing: filling in forms opens fillable content controls (#1077) (#1309)
* #1077: forms protection fills content controls (w:edit="forms")

* #1077: forms walk stays conservative for unclosed control markers

* #1077: forms protection edges — nested locks, orphan markers, tables, empty interiors

* #1077: forms protection — formFields state and Find Next Region over fillable controls

* #1077: forms protection round-trip, password guard, docs

* #1077: final review fixes — lock-param gate, per-list pairing, docs, pane perf

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:58:25 -04:00
f47cd975ae UI: view-mode tooltips, SmartArt labels and TOC style names follow the interface language (#1272)
* UI: view-mode tooltips, SmartArt labels and TOC style names follow the interface language

Found while translating the interface:
- The status bar's Web/Print/Read view buttons showed their internal ids (`readMode`) as tooltips;
  they now show the translated names ("Web Layout", "Print Layout", "Read Mode").
- SmartArt layout tiles drew each label on one line, so long labels (and long translations) ran out
  of the 96 px tile. A label wider than the tile now breaks at the space nearest its middle; the tile
  is 96 px high to make room for the second line.
- Table of Contents › Options listed built-in styles (Normal, Title, Caption …) in English in every
  language; they now use the same display names as the Styles pane. The document keeps its names.

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

* TOC options: the style list's scroll bar takes its own room

A floating scroll bar was drawn over the level boxes and hid their right edge. The list now uses a
solid scroll bar, so the list is as wide as its widest label and level box plus the bar.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:58:07 -04:00
ahutsunshinandBrandon Thomas 2ffe707b03 refactor: deduplicate ZIP and OLE magic number constants in io.rs (#1260)
Both signature_contradicts and open_by_content defined identical local
ZIP and OLE magic number consts. Extract them to module-level ZIP_SIG
and OLE_SIG with doc comments so there is a single source of truth.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:57:56 -04:00
e0a1346590 Layout: inside/outside floats swap sides on even pages; Page Setup shows mirror margins (#1181) (#1239)
* Layout: inside/outside floats swap sides on even pages; Page Setup shows mirror margins (#1181)

Floats aligned inside or outside, or placed relative to the inside or
outside margin, were always put as on an odd page, so shapes Word puts
in a book's outside margin all came out on the right. They now follow
the page they're on (ECMA-376 ST_AlignH, ST_RelFromH): on an even page
the inside is the right side. Floating tables (w:tblpXSpec) too.

With mirrored margins, even pages are laid out as odd ones and then
shifted; float positions now allow for that shift, so floats placed
from the page edge stay put (the "known limit" in #452) and the left
and right margin areas are each page's own.

Mirrored margins read from a file were invisible in the UI: the Margins
menu ticks Mirrored when they're on, and Page Setup gets a Mirror
margins check box (labelling the margins Inside and Outside), applied
through a new `mirror` param of layout.margins.

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

* i18n: Mirror margins in every catalog

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-14 18:57:44 -04:00
Brandon ThomasandClaude Opus 5.5 cf140ae600 CI: Select All + Delete test presses Select All until the document is selected
#1202's test selected all once with the caret in a table; since #1478,
Select All in a table grows a step at a time (cell, row, table,
document), so one press selected only the cell and the table stayed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:25:20 -04:00
5f2e18e1cd fix(ui): prevent options pane labels from overflowing (#1266) (#1301)
* fix(ui): prevent options pane labels from overflowing (#1266)

Clip each navigation label to its fixed row while preserving the full
accessible name, click region and hover/selection backgrounds.

Cover real painting bounds and row interactions in English and Russian
at 100%, 125% and 150% interface size with 2x native display scaling.

Co-Authored-By: Codex <noreply@openai.com>

* Test: Options nav labels under the absolute interface size (#1142)

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

---------

Co-authored-by: Zaxaerith <189452125+Zaxaerith@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:17:18 -04:00
neronek22andneronek22 ae8048e3f1 Improve Polish spelling suggestions for final consonant devoicing (#1298)
Co-authored-by: neronek22 <aantoszek@gmail.com>
2026-10-14 18:17:07 -04:00
40cf85c25b Insert › Bookmark: Delete and Go To, laid out as Word's (#1236) (#1277)
* Insert › Bookmark: Delete and Go To, laid out as Word's (#1236)

The Bookmark dialog had only Add: a bookmark could not be deleted, and a
click in its list jumped to the bookmark at once. It is now laid out as
Word's (observed in Word 2019 on Windows): the name box over the list, Add,
Delete and Go To beside them, Sort by Name or Location and Hidden bookmarks
below. A click in the list picks a name; Add (Enter) adds and closes; Delete
and Go To leave the dialog open, and after a delete Cancel reads Close. The
dialog opens on the bookmark around the caret, and Add is off for a name
the command would refuse.

insert.deleteBookmark {name} removes a bookmark's start and end wherever
they are, also across paragraphs, as one undo step, keeping the selection
on its text; from the command search without a name it opens the dialog.
Adding a bookmark under a name in use goes through the same removal: it
looked for the old end only in the start's paragraph, so a bookmark across
paragraphs left a stray end behind. edit.goto with a bookmark selects its
text, as Word's Go To does, instead of placing the caret at its start.

Closes #1236

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

* Insert › Bookmark: Ctrl+click on a link to a bookmark keeps a caret (#1236)

Word's Go To selects a bookmark's text, but Ctrl+click on a link to a
bookmark only puts the caret at the bookmark's start (checked in Word 2019
on Windows). Following a link runs edit.goto, which now selects, so it
collapses the selection to its start.

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

* Resolve the doc conflict markers left by the previous 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-14 18:16:55 -04:00
NetterSilverandBrandon Thomas 44787764a9 Table Layout: the Cell Size boxes apply to the whole selection (#1242) (#1276)
* Table Layout: the Cell Size boxes apply to the whole selection (#1242)

table.rowHeight and table.columnWidth read the caret's cell only. With the whole
table selected the caret is in the last cell, so only the last row's height and
the last column's width changed.

Both now use the cells the selection covers (selected_cells): every selected
row gets the height and every grid column the selected cells touch gets the
width, as one edit. Without a selection that spans cells they still change the
caret's row and column.

* Docs: update the progress docs for this change

* Docs: put the new sentence in the WordCraft / evidence column

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:16:35 -04:00
ahutsunshin 48ab796792 feat: add SVG and ICO MIME types to mime_of helper (#1262)
The mime_of helper is missing SVG (most common web vector image) and
ICO formats, causing SVG data URIs and HTML img references to be marked
as generic application/octet-stream. Add image/svg+xml and image/x-icon.
2026-10-14 18:16:16 -04:00
7658819bf7 Desktop: a window too big for its screen shrinks to fit when it opens (#1215) (#1253)
eframe shrinks the start size to the monitor's, but not by the frame, the
title bar or the taskbar, and Windows places a new window away from the
screen's corner. On a laptop screen the first window's bottom, with the end
of the scroll bar, sat under the taskbar or past the screen's edge, so a
drag of the scroll bar stopped where the pointer met the edge. The window
was saved that size and opened so again on every launch.

On the frame that first knows the monitor, a window that doesn't fit with
48 pt to spare on each side (WindowGeometry::fit_on_monitor) gets a smaller
size, never below the minimum, and is centred. A window the system placed
counts its position too; a restored one, maybe on another monitor, only its
size. Maximized, full-screen and minimized windows are left alone. Checked
on Windows with no saved settings: the window now opens wholly on screen.

Closes #1215

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:16:05 -04:00
e5780698f2 Accessibility: Windows Magnifier follows the caret onto a new line (#1119) (#1247)
Windows Magnifier keeps the text cursor in view through UI Automation: the
caret's own box has no width, so it takes the box of the character after
the caret, and when that has none either, of the two characters before it.
The canvas described a line's end mark (the paragraph mark, a line break)
with no width, so on the empty line Enter or Shift+Enter makes Magnifier
looked at the end of the line above and stayed there until two more
letters were typed.

A line's end mark now takes the room a selection shows a paragraph mark
with: PARA_MARK_WIDTH (5 pt, now shared with selection_rects) at the line's
visual end (Line::mark_box). Checked live with Windows Magnifier, its view
read with MagGetFullscreenTransform: after Enter and Shift+Enter it moves
to the new line at once.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:15:46 -04:00
ahutsunshinandBrandon Thomas 5ffd7a99f5 fix: remove unreachable empty-string branch in open_bytes_with match (#1258)
The match arm included | "" but ext.is_empty() is already handled above
by the guard that routes empty extensions to open_by_content. This branch
could never be reached, making it dead code that misleads readers.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:15:27 -04:00
78c37cafa7 Cut copies to the system clipboard from every control (#1256)
* Cut copies to the system clipboard from every control

The ribbon, the context menu and the native menu's Cut deleted the
selection but wrote nothing to the system clipboard: only egui's Cmd+X
Copy/Cut event put the text there, in each UI caller that handled it.
Move the write into WordApp::run (execute), where edit.copy and edit.cut
already return the text, and drop the per-caller copies so nothing writes
twice.

Regression test: run edit.cut and edit.copy and check the text reaches
the context's clipboard output.

* Test Cut publishes to the clipboard from every entry point (#1265)

The one clipboard sink is WordApp::run; assert the ribbon button path
(run_from_ribbon) and a real Cut key event both reach it, alongside the
existing generic-dispatch test the context menu, native menu, command
search and CLI/MCP all go through.

* rustfmt

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-14 18:15:15 -04:00
ahutsunshin 36553301d8 fix: remove dead-code unwrap_or magic string in percent_decode (#1257)
Replace from_utf8(h).unwrap_or("zz") with .ok() and let_chaining.
The fallback was unreachable because h is always valid UTF-8 (it comes
from slicing a valid UTF-8 input string). The magic string "zz" made
the intent unclear to readers.
2026-10-14 18:01:25 -04:00
.CooCooandBrandon Thomas 00c8ee3407 Fonts: the interface CJK fallback skips faces without outlines (#1250)
* Fonts: the interface CJK fallback skips faces without outlines

The interface CJK fallback has listed PingFangUI.ttc from
FontServices.framework as its last macOS candidate since #495. The skrifa
crate reads glyph outlines only from the glyf, CFF, CFF2 and VARC tables.
On macOS 27.0.1 the 32 faces in PingFangUI.ttc have none of them and keep
their outlines in Apple's hvgl table. pick_face counts coverage from each
face's cmap alone. The file's first face maps all 1,114 CJK characters of
the zh-hans and zh-hant catalogs. When no earlier candidate maps all of
them, find_ui_font returns that face for a Chinese interface built without
craft-fonts, and the interface would draw the characters it takes from
that face as blank space.

pick_face now skips a face with none of those four tables, and find_ui_font
goes on to the next candidate. VectorCraft's font scan has the same check
(storytold/vectorcraft#781).

* Contributors: add dotCooCoo (Robert Lee)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:01:13 -04:00
fabiodalez-devandBrandon Thomas d129fa2daa i18n(it): translate the 24 strings other catalogs added since #598 (#1244)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:00:54 -04:00
Jeremy YaoandClaude Fable 5.1 c3bdcd73b4 Harden against hostile-document DoS and non-web link export (#1235)
* Harden against hostile-document DoS and non-web link export

A security pass found several small files that make the app allocate or
compute without bound when opened (GUI, wordcraft-cli, or the MCP server),
and link targets leaving on export without the scheme check the HTML and
Markdown writers already apply. None lets a document run code or read files;
all are denial-of-service or credential-leak-on-click. Each fix bounds an
attacker-controlled size and lands with a regression test, per the
never-crash standard.

- layout: a table repeats its header rows on every page it spills onto, so
  many tall header rows paginate to O(headers x pages) placed items. Cap the
  total header-row repeats per table (lib.rs). 2 KB docx: 1.1 GB -> 124 MB.
- formats/model, html: an HTML table of many <tr> with large rowspan expanded
  to rows x 64 covered cells, and `model::table` pre-allocated a full
  rows x cols grid only to clear it. Cap rows at MAX_ROWS before
  insert_covered, bound its total work, and build the grid template with one
  row. 640 KB html: >1.5 GB -> 214 MB (a plain large table is faster too).
- docx/read/freeform: a custom-geometry Bezier segment took any number of
  points, making de Casteljau flattening quadratic. Cap points per segment.
  3 KB docx: 12 s -> 0.15 s.
- docbin: footnote/header/text-box story tables and the piece table can name
  the same backing text many times. Hold the total characters materialised to
  twice the WordDocument stream size (a ceiling no real file reaches, since
  each char costs >=1 stream byte). 45 KB doc: >1.2 GB -> 14 MB.
- render, pdf, formats/rtf: document pictures were decoded with no dimension
  limit, so a tiny file declaring a huge image could allocate before the size
  check. Decode through image::Limits with a side cap first.
- doc/link, formats/rtf, odt, latex: route exported hyperlink targets through
  is_safe_href (as HTML/Markdown already do), and extend it to reject
  protocol-relative (//host) and UNC (\host) references. Keeps file:,
  javascript:, ms-msdt: and UNC targets from being written into exported RTF,
  ODT, LaTeX and PDF where a reader would follow them (UNC leaks NTLM on
  Windows).

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

* Align picture-decode limits with the bounds already in force

Review of the previous commit: `image::load_from_memory` and
`load_from_memory_with_format` already applied `Limits::default()`, whose
512 MB allocation cap bounded every picture decode. The PDF change had set
`max_alloc = None`, loosening that; restore the default. The render and RTF
side caps (16 384 px) newly rejected wide-but-short pictures that fit in
memory, so render now uses the pixmap's own `u16` side limit (what
`premul_pixmap` enforced after decoding, applied before it instead) and the
RTF re-encode keeps only the explicit allocation cap. Net effect: rejection
moves to header time with no change to the sizes accepted. Also fix a
comment: the header-row repeat cap is per table, not per document.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 18:00:35 -04:00
Marco Arnulfo 1190fa8881 Ctrl+wheel zoom keeps the place under the pointer instead of scrolling the page away (#1226) 2026-10-14 18:00:23 -04:00
Marco Arnulfo 6dd027fcfa Ctrl+Q resets the paragraph without also closing the window (#1220) 2026-10-14 18:00:11 -04:00
Marco ArnulfoandBrandon Thomas 0c5804767c Layout menus mark the current margins, orientation, size and columns (#1214)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 17:59:59 -04:00
Marco Arnulfo f7dc68d543 Navigation pane headings align left, indented under their parent (#1204) 2026-10-14 17:59:47 -04:00
9710db73da Right-click on a table of contents entry offers Update Field (#1203)
* Right-click on a table of contents entry offers Update Field (part of #1197)

* Test: the TOC context menu check allows more than one Paste item

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-14 17:59:36 -04:00
Marco ArnulfoandBrandon Thomas 70a4917973 Tables: Select All then Delete removes a table the document starts with; right-click Delete offers Delete Table again (#1202)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 17:59:17 -04:00
8ae23cee29 Desktop: a maximized window no longer flashes on and off at start-up on Windows (#23) (#1194)
* Desktop: a maximized window no longer flashes on and off at start-up on Windows (#23)

winit shows a window the moment it creates it maximized, even though eframe starts it hidden
until the first frame. A window saved as maximized therefore appeared and disappeared about four
times in a third of a second before settling. On Windows it now opens at its normal size, still
hidden, and the first frame maximizes it (WindowGeometry::maximize_after_open). Other platforms
are unchanged. Measured by polling the window's visibility every 15 ms: before, show/hide four
times; after, hidden until the first frame, shown once.

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

* Contributors: add my display name

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 17:59:04 -04:00