* 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>
#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>
* 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>
* 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>
* 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>
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.
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>
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>
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>
* 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>
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.
* 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>
* 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>
* 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>
* 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>
Table Design and Table Layout were in the keytip tab list inside a table but had no letter, so no badge showed and no key reached them. They now take Word's two-letter badges, JT and JL: J waits for the second letter and narrows the tab badges to the J ones, then the tab's control badges show as on any other tab.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Word forms keep their legacy form fields and rows' skipped grid columns when saved (part of #1174)
* Tests: field initializers name the new form field
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>
failed_pruning_cannot_push_a_record_beyond_the_discovery_limit failed
once in a full workspace run: a record whose lock is briefly held (by a
child another test is spawning, which shares the descriptor until it
execs) is skipped rather than reported, so scan() returned Ok. The test
now retries for up to a second before asserting.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M10: retain local crash recovery on current main
* i18n: Backstage Recover 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>
* Russian proofing: spelling, suggestions, grammar and word division
Runs tagged ru get Russian tools instead of none:
- spelling against an installed ru_RU Hunspell dictionary (not bundled;
LibreOffice dict-ru folders searched; KOI8-R and Windows-1251 decoded),
with ё optional, hyphenated words and solid compounds understood, не with
adjectives, participles and verbal nouns, prefixed verbs, abbreviations
with endings and camel case left alone, plus our supplement of modern
words with their declensions (assets/spelling/ru-extra.txt);
- suggestions tuned for Russian: е/ё, letter groups, common confusions,
missing spaces, typos and a stem scan;
- grammar rules with Russian messages (repeated words, spaces, sentence
capitals with Russian abbreviations, likely missing commas before
clause openers, doubled punctuation);
- Russian word division from the spelling rules, written as code.
* Options: Russian in the dictionaries list (ProofLang::Ru arm)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Магда М.Г. <mail@mgmagda.ru>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
docbin decrypts XOR-obfuscated, RC4 and RC4 CryptoAPI files after verifying the
password against the stored verifier; the engine reuses the .docx password
prompt errors. Written from [MS-DOC] and [MS-OFFCRYPTO]; synthetic tests only.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #778: .xls and legacy-encoding recipient lists; filter and sort recipients
- Recipient lists: legacy Excel 97-2003 .xls (BIFF8 reader on the cfb crate already in the
tree); Excel 5.0/95 and password-protected .xls refused with a clear message; a CSV named
.xls reads as text, an HTML one gets an error. .xls added to the picker filters (desktop,
web, built-in browser) and drop handling.
- Text lists (and plain-text import) detect legacy encodings when not UTF-8/UTF-16: GBK/GB18030,
Big5, Shift_JIS, EUC-KR, Windows code pages (encoding_rs + chardetng, MIT/Apache); an
`encoding` param overrides.
- mailings.filterRecipients / mailings.sortRecipients: field, comparison (equal, notEqual,
less, greater, lessOrEqual, greaterOrEqual, blank, notBlank, contains, notContains), value,
And/Or joins; sort keys. Preview, Finish & Merge and labels use only the matching records
in sort order. Edit Recipient List edits every record and keeps the filter.
- Recipient List dialog: Filter and Sort section and a merge-order column; catalog rows;
strings in every catalog; docs.
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>
* Review fixes for #885: total order for recipient sort, Bulgarian strings
- compare_cells now ranks numbers (compared numerically) before text (compared
case-insensitively), so it is a total order; a column mixing 9, 10 and 1a
could make slice::sort_by panic. Regression test sorts mixed columns.
- Bulgarian catalog: the 24 Filter and Sort strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vi: mail merge Filter and Sort strings
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>
* #775: STYLEREF fields in headers and footers show each page's result
Running headers/footers with STYLEREF (e.g. the current chapter title) are
evaluated per page at layout time (ECMA-376 §17.16.5.59): the page's first
paragraph of the style (\l: last), else the nearest before, else after.
Style names/ids, heading levels (STYLEREF 1), character styles, and the
\n \r \w \s \t \p switches. Body STYLEREF fields update with the other
fields. STYLEREF added to Insert › Quick Parts › Field with its options.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #775: STYLEREF error text in our own words
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #882: translate the STYLEREF Field dialog strings
Adds the style picker label, the six STYLEREF switch labels and the
STYLEREF description to all 18 interface catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* STYLEREF: reuse the style index while the body and styles are unchanged
Headers with STYLEREF rebuilt and hashed the body's style index on every
layout pass (echelon's follow-up note on #882). The layout cache now keeps
the index with the body blocks it came from (shared, so an edited block is
a different pointer) and the styles/numbering hash, and rebuilds it only
when either changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vi: STYLEREF field strings
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>
hdiutil makehybrid -hfs gave every file and folder in the image a
com.apple.FinderInfo, so WordCraft.app failed codesign --verify --deep
--strict inside the released DMG and after copying it out with ditto.
- Build the DMG with hdiutil create -srcfolder -fs HFS+ -format UDZO
(retried: attaching a device is flaky on CI runners).
- Clear extended attributes on the bundle before signing.
- After signing the DMG, mount it read-only and strictly verify the app
inside it and a ditto copy of it (plus spctl when notarizing), so the
release job fails if this regresses.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* README: install on Gentoo from the ::snakebyte overlay
* Contributors: credit switch87 as Gert Pellin
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* test(engine): drop a needless drop() that clippy 1.97 rejects (drop_non_drop)
* Comments: a click inside a selected balloon moves the caret in its text (#1013)
- The canvas under the markup area also "contains" the pointer, so a press on the balloon counted as a click on the page and left it
- Presses over a balloon card now belong to the card, in both the deselect check and the document click in mouse()
- Test: press, drag and release inside the selected balloon; a click on the page still leaves it
* Comments: an option to show only the selected text's comment (#1055)
- Track Changes Options › Comments: "In the margin" (default, as in Word) or "Only for the selected text"
- The second mode takes comments out of the margin column; the comment under the caret, or the clicked one, floats beside the page, over its edge in a narrow window
- The floating card has its own foreground layer, so presses on it never reach the canvas
- review.trackingOptions takes commentDisplay: margin|selected
- comment_range_at_caret: the comment whose range holds the caret
- Strings in all catalogs; tests for the option, the helper and the card's clicks
* vi: comment display options
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>
* DOCX: merge settings.xml into the original instead of regenerating it (#1034)
A save regenerated word/settings.xml from the model, so every setting the
model doesn't hold was dropped: compatibility options (lost in 18 of 18
real-world Word files), rsids, document variables, proofing state and the
colour scheme mapping.
The reader now keeps the original part when it holds such settings, and
the writer merges into it: the original root and every unowned child stay
byte for byte; owned children follow the model in CT_Settings order; in
w:compat only compatibilityMode is replaced. The privacy options are the
model's since #1062, so they are owned too. Kept children that point at a
relationship (a mail-merge data source) are dropped. Anything unexpected
falls back to the generated part. Files WordCraft wrote itself keep
nothing extra.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DOCX: carry unmodelled parts and keep comment ids, style defaults, latent styles, list ids and app props (#1034)
Builds on the settings.xml merge.
- Parts the package or main document relate to that WordCraft neither
models nor writes (webSettings, stylesWithEffects, people, add-in task
panes, the thumbnail, future parts) are carried verbatim with their
relationships and content types; relationship types the writer
produces, and any the body refers to by id, are not (HANDLED_RELS).
- commentsIds / commentsExtensible: durable ids and UTC dates are read
into the model and regenerated against the written paragraph ids.
- w:default on localized default styles, w:latentStyles, lists'
nsid/tmpl/multiLevelType, and app.xml Template/Company/Manager/
HyperlinkBase round-trip; counts are not carried, and Company/Manager
are dropped when the file asks to remove personal information.
- Comment reference marks use the document's "annotation reference"
style id instead of a hard-coded CommentReference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: przemek-arch <263159043+przemek-arch@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#1069's the_bar_scrolls_under_the_ribbon_not_over_it expects the bar to
slide under the ribbon with a selection scrolled above the document;
#1501 (landed first) anchors the bar to the visible selection only, so
the selection has no anchor there and the test fails on main. Ignored
with the reason until the owner picks the behaviour.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
File › Options › Proofing gets a Dictionaries folder: a folder of Hunspell
.aff/.dic files searched before the system locations, saved with the settings
and applied at start-up. An app started from Finder or the Dock doesn't see
WORDCRAFT_DICTIONARIES, so this is how it finds a dictionary kept in a folder
of the user's own. Changing it reloads the Polish and Portuguese dictionaries
and checks the text again. The pane lists the dictionary file each language
found, or that none was found, so missing spelling marks are explained.
Agents set it with file.options {"dictionariesFolder": path | "auto"}; a
missing folder or a relative path is refused and changes nothing.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Clippy 1.96: simplify boolean guards, drop a needless drop()
Rust 1.96's clippy flags nonminimal_bool in find_pattern, table_mouse and
table_resize, and drop_non_drop in a ribbon test; `cargo xtask ci` failed at
clippy on main. Same logic, rewritten as clippy suggests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: combo boxes join their dropdown arrow (field rounded left, arrow right)
The font and size boxes looked apart from their arrows: the text field was
rounded on all four corners and the arrow on none, leaving a notch where they
meet. The field now rounds only its left corners (in every widget state,
including focus and hover) and the arrow only its right ones, so the two read
as one control. Applies to every combo drawn by widgets::combo/font_combo:
the ribbon's font, size and outline-level boxes, the mini toolbar, and the
Font and Symbol dialogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: one divider between a combo's field and its arrow
The field's right border and the arrow's left border stood side by side, a
divider twice as wide as the rest of the outline. The arrow's box is now
painted under the field and reaches one border width beneath it, so the
field's right border is the only line between them (in the field's focus
colour while typing). The test draws with the app's light and dark themes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Clippy 1.96: simplify boolean guards, drop a needless drop()
Rust 1.96's clippy flags nonminimal_bool in find_pattern, table_mouse and
table_resize, and drop_non_drop in a ribbon test; `cargo xtask ci` failed at
clippy on main. Same logic, rewritten as clippy suggests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: mini toolbar steps aside for the ribbon
The mini toolbar covered the ribbon's drop-downs (the font size list opened
under it). A press outside the document and the bar (the ribbon, a pane, the
rulers, the status bar) now puts the bar away until the selection changes or
the next press in the document; presses on popups (the bar's own lists, an
open ribbon list over the page) neither hide nor restore it. The bar also
moves from the Tooltip layer to Middle, so every popup and menu draws over
it, as its comment always said.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: mini toolbar scrolls under the ribbon, not over it
With text selected, scrolling the selection up toward the ribbon carried the
mini toolbar along on top of the rulers and ribbon. The bar is now clipped to
the document area: it slides under the rulers and ribbon (or the status bar)
with the text and goes once none of it shows. Its area is constrained to the
document for hit testing too, so the hidden part no longer swallows clicks
and drags meant for the ruler or the ribbon underneath.
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>
* test(engine): drop a needless drop() that clippy 1.97 rejects (drop_non_drop)
* Comments: a click inside a selected balloon moves the caret in its text (#1013)
- The canvas under the markup area also "contains" the pointer, so a press on the balloon counted as a click on the page and left it
- Presses over a balloon card now belong to the card, in both the deselect check and the document click in mouse()
- Test: press, drag and release inside the selected balloon; a click on the page still leaves it
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#1052's test asserted that every Hebrew font falls back to David, but
only the Windows fallback list has it, so it failed on Linux and macOS.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A save regenerated word/settings.xml from the model, so every setting the
model doesn't hold was dropped: compatibility options (lost in 18 of 18
real-world Word files), rsids, document variables, proofing state and the
colour scheme mapping.
The reader now keeps the original part when it holds such settings, and
the writer merges into it: the original root and every unowned child stay
byte for byte; owned children follow the model in CT_Settings order; in
w:compat only compatibilityMode is replaced. The privacy options are the
model's since #1062, so they are owned too. Kept children that point at a
relationship (a mail-merge data source) are dropped. Anything unexpected
falls back to the generated part. Files WordCraft wrote itself keep
nothing extra.
Co-authored-by: przemek-arch <263159043+przemek-arch@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>