427 Commits
Author SHA1 Message Date
6cf0aec0ff Fix bulleted list toolbar button active state (#651)
* Fix bulleted list toolbar button active state (#642)

Add para.state command to return paragraph formatting state (bullets, numbering, multilevel) and update ribbon to use it for list button active states. Previously, the bullets, numbering, and multilevel list buttons never showed as active even when the cursor was inside a list item.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Review fixes for #651: list buttons from the paragraph's own level

- The Bullets/Numbering state reads the paragraph's list level (ilvl), not
  level 0, and Multilevel is on when the list numbers its levels together
  (a label showing another level's number, or legal numbering); the old
  level-0 '%2.' check was never true.
- Computed from the paragraph props the ribbon already resolves
  (cmd::para::list_buttons) instead of a new para.state command.
- Engine test.

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

* #651: list-button test starts legal numbering from plain text

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

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@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:08:11 -04:00
2f58d441b6 Numeral display modes: Arabic, Hindi, Context and System digits (view.numeral) (#501)
* Numeral display modes: Arabic, Hindi, Context and System digits

Display-only digit substitution (Word File > Options > Advanced >
Numeral): ASCII and Arabic-Indic digits are substituted before shaping
while clusters keep indexing the original text, so find, spelling,
copy/paste and saved files see the typed digits.

- layout: NumeralMode (arabic|hindi|context|system) with System resolved
  from LC_ALL/LANG, threaded through LayoutOptions/ParaEnv with cache
  invalidation; Context follows the surrounding text
- engine: view.numeral command + catalog row
- tests: layout tests_numeral, engine command test

* Options page: Numerals picker in File > Options > Display

ComboBox for the view.numeral mode (Arabic/Hindi/Context/System),
mirroring Word's File > Options > Advanced > Numeral. Display-only,
session-scoped like the other Display options.

* fmt: collapse numeral direction branch

* #501: translate the Numerals option label in every catalog

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

---------

Co-authored-by: abdo4312 <abdo4312@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:04:43 -04:00
1c188ba140 M1: preserve independent complex-script DOCX formatting (#366)
Keep inherited complex-script size, bold and italic independent from Latin
properties and retain explicit values during DOCX round trips. Preserve
ordinary formatting-command behavior and add regression coverage.
Resolve eight existing Clippy warnings without changing behavior so all
six required CI gates pass.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:55:44 -04:00
326d695335 Linux: default to XWayland for file drag-and-drop on Wayland desktops (#220)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:49:37 -04:00
Brandon ThomasandClaude Opus 5.5 715f85c612 Tests: linear-time checks measure CPU time, not wall clock under load (#850)
The five tests that assert linear layout/export/hyphenation by comparing
the cost of an n-sized and a larger input (layout: unequal formats, hidden
separators with auto hyphenation, hidden float anchors; pdf: fragmented
hidden headings; proof: hyphenating a long word) timed both with wall
clock, fastest of three. On a loaded machine (CI, shared dev boxes, the
test harness running other tests in parallel) a preempted big run, timer
resolution on the small run, or a tight 4x/8.0 limit (hyphenation measured
~7.2 when linear) made them fail now and then.

Each now uses a shared cost_ratio helper (test-only, no production change):
- CPU time of the test thread from /proc/thread-self/schedstat on Linux, so
  time spent waiting for a CPU is left out (wall clock elsewhere);
- the small input runs `scale` times per timing so both timings are long
  against the clock's resolution;
- small and big runs alternate, five rounds after a warm-up, cheapest kept;
- 8x the input everywhere: linear ~8x, quadratic ~64x, limit 24x.

Measured under load (load avg ~15-35 on 32 cores): 60 linear ratios in
7.7-8.8. With the old quadratic code injected locally each test fails:
unequal formats (uncapped style bucket scan) ~90-104x, hidden separators
(hyphenator rescan per point) ~36x, float anchors (run walk restarted per
anchor) ~188x, PDF headings (left-out range scan per char) ~37x,
hyphenation 50-65x.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:37:03 -04:00
ba9bbb48dd Open Word documents with WordCraft by default (file associations + Options button) (#304)
* Open Word documents with WordCraft from the system (#295)

Register WordCraft for Word, RTF and ODT files on Windows and Linux without
taking over anyone's defaults, and add File > Options > General > "Make
WordCraft the default for Word documents..." where the system allows it.

- Windows MSI: a ProgId per type (.docx .docm .dotx .dotm .doc .dot .rtf
  .odt, plus .md and .txt as Open With candidates only) with the app icon
  and `"wordcraft.exe" "%1"`, OpenWithProgids, Applications\wordcraft.exe
  SupportedTypes, and Capabilities + RegisteredApplications so WordCraft
  shows in Settings > Default apps. New component with its own GUID.
- Linux: the desktop entry declares the macro-enabled, template and legacy
  Word types and text/rtf; AppStream lists the media types. deb, rpm,
  AppImage and both Flatpak manifests use this same file.
- macOS: Info.plist also lists legacy .doc.
- The Options button runs `ui.makeDefaultApp` through a new
  Services::make_default_app hook: Windows opens
  ms-settings:defaultapps on WordCraft's page; Linux and BSD run
  `xdg-mime default ai.storyteller.wordcraft.desktop <types>` (refused
  inside Flatpak or without an installed desktop entry); macOS shows the
  Finder Get Info hint; the web shows nothing. The status bar reports the
  outcome. Strings translated in every catalog.

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

* Review fixes for #304: keep Make default out of programmatic dispatch

ui.makeDefaultApp is refused when run programmatically (control channel, MCP, macros,
scripts); only a click on File > Options > General > Make WordCraft the default… reaches
the host's hook, so no agent can change the user's system default apps or open system
settings. Test: programmatic_calls_cannot_change_the_default_apps.

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

* Default-app strings in the newer catalogs; the button's test looks for a General-pane label

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 04:30:57 -04:00
f4dfc8eb85 Fix CJK fallback mixing serif/sans faces within one run (#567)
* Fix CJK fallback mixing serif/sans faces within one run

Per-character fallback picked the first covering face in load order, so a
run without an East Asian font could draw JIS-covered ideographs in a
bundled Japanese Mincho and simplified-only ones in the SC sans, mixing
serif and sans glyphs even inside one word. Pick one face covering all
of the run's East Asian characters when available, keeping the
per-character fallback when none does.

* Review fixes for #567: cache the run-level CJK fallback

- fallback_for_east_asian is cached per (character set, excluded face,
  bold, italic) and dropped when the loaded faces change or the system
  fallback is switched, like the per-character cache, so relayout doesn't
  redo the search; the cache is bounded in entries and in key size.
- loaded_fallback and loaded_covering share one fallback_order helper.
- Test for the cache.

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-12 04:24:03 -04:00
d3e2e184c2 Linux: the display theme asks the desktop, not winit (#298)
* Linux: the display theme asks the desktop, not winit

winit reports no system theme on Linux — `ActiveEventLoop::system_theme()` is
`None` there on every desktop — so "Use system setting" never saw the desktop's
choice and fell back to light. A new `desktop_theme` module asks the XDG
Desktop Portal's Settings interface for
`org.gnome.desktop.interface color-scheme` on a thread of its own, so no
D-Bus call is ever made from the frame loop, and the app follows the answer,
including a change made while it runs.

The colour scheme is asked for under its GNOME schema name first, which
GNOME's portal and XFCE's both answer, then under the older freedesktop one,
which KDE answers. A desktop that answers neither leaves the user's own choice
alone, exactly as before. macOS and Windows are untouched: winit answers there.

* Review fixes for #298: one D-Bus connection; watcher started by the app

- The watcher keeps one session-bus connection for its lifetime instead of opening a new
  one every 4 s; a connection whose calls all fail is dropped and reopened on the next poll.
- WordApp::new no longer starts the watcher. The desktop app calls
  WordApp::watch_desktop_theme at start-up, so unit tests, kittest and ui_shot don't spawn
  D-Bus pollers and what they draw doesn't depend on the host's desktop theme (test:
  a_new_app_does_not_watch_the_desktop).
- A change wakes the window (request_repaint), so an idle app follows a theme switch.

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

* Test: name the empty Vec's type (a new dependency adds a PartialEq impl for char)

---------

Co-authored-by: GyuriX <gyurix@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 04:18:29 -04:00
199ac88a2e i18n: add Czech (cs) interface translation (#387)
* i18n: add Czech (cs) interface translation

Translate the 1,246-entry interface catalog clean-room from the English source labels (cross-referenced with PdfCraft, VectorCraft, and PhotoCraft Czech catalogs) and register Czech (Čeština) for system locale detection and persisted selection. Add tests for Czech locale resolution, full catalog key parity, count-neutral word count formatting, and bundled UI font glyph coverage for Czech diacritics.

# Conflicts:
#	ROADMAP.md

* Czech: translate interface strings added on main since this PR

Adds 221 Czech entries so cs.tsv again has the full zh-hans key set
(2,021 entries) and drops the stale "Wildcards (regex)" row; updates the
documented entry count.

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>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
2026-10-12 04:09:20 -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
55f7b894c6 Add German (de) interface language (#574)
* Add German (de) interface language

New catalog crates/ui-egui/src/i18n/de.tsv (1,307 entries), key set
byte-identical to zh-hans.tsv; original translations written from the
meaning of the English UI strings in standard German word-processor
vocabulary; MIT OR Apache-2.0.

- LangInfo for de (Deutsch) registered in i18n::LANGUAGES
- Tests: German locale resolution (de, de-DE, de-AT, de-CH,
  de_DE.UTF-8@euro), saved-preference/document-content isolation,
  catalog key-set parity with zh-hans, count-neutral wording,
  bundled-font glyph coverage for ÄäÖöÜüß
- Count strings use count-neutral wording (Wörter: {n}), the
  convention the other catalogs' tests pin
- Docs: localization-parity.md, gaps.md, roadmap.md, ROADMAP.md,
  README.md, control-protocol.md; ATTRIBUTION.md row for de.tsv

cargo test -p wordcraft-ui-egui i18n: 28 passed; --lib: 162 passed.

* rustfmt after merging main

* German: translate interface strings added on main since this PR

Adds 636 German entries so de.tsv again has the full zh-hans key set
(2,021 entries) and drops the stale "Wildcards (regex)" row; updates the
documented entry count.

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

* Fourteen interface languages with German and Italian

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
2026-10-12 03:55:05 -04:00
99fbd1861a i18n: add Italian interface (it) (#598)
* i18n: add Italian interface (it)

New it.tsv catalog with 1,378 entries: every key of the existing catalogs plus 51 strings that go through tl! but were in no catalog yet (File › Info properties, File › Home greeting, list levels, Equation tab gallery and tooltips, Draw Table, About tabs). Ribbon tabs, groups and command names use the names Italian Word users know; all other text is written from the meaning of the English.

Registers it in LANGUAGES (it, it-IT, it-CH resolve here), adds locale, saved-preference, coverage and bundled-font tests, and updates README, control protocol, localization parity and roadmap. Refs #594.

* i18n(it): shorter Wrap Text Sides label

Lati del testo a capo matches its parent menu, Testo a capo.

* i18n(it): translate the 590 strings main added since the rebase

Options, accessibility, alt text, content controls, document grid, templates, index, citations and the other labels every catalog gained on main. The Italian coverage test (every key of the other catalogs) passes again.

* i18n(it): Issue is Fascicolo in the bibliography source form

* i18n(it): translate the 351 strings main added (equation gallery, built-in styles, paper sizes, animation settings, file browser)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:46:44 -04:00
6801d89065 .doc import: inline pictures, floating pictures and basic shapes (#551) (#558)
* .doc import: inline pictures, floating pictures and basic shapes (#551)

Word 97-2003 files kept their text but dropped every picture and drawing.
Pictures and drawings are now read from the OfficeArt records ([MS-DOC],
[MS-ODRAW]):

- Inline pictures: the PICF's OfficeArtInlineSpContainer (MM_SHAPE /
  MM_SHAPEFILE) and its FBSE/BLIP, with crop and alt text from the shape
  properties; the older raw-image path stays as a fallback.
- Floating pictures and shapes: PlcSpaMom/PlcSpaHdr anchors joined with
  the shapes of the OfficeArtContent drawings (fcDggInfo), placed by the
  SPA bounds, bx/by or posrelh/posrelv/posh/posv, wrap type and
  fBelowText; rotation and flips; pictures from the BLIP store.
- Shapes: rectangle, rounded rectangle, ellipse, diamond, triangles, star,
  block arrows, heart, lines/connectors, with fill/line colour and width;
  groups flattened into one group space; text boxes linked to their
  PlcftxbxTxt story parts by spid.
- BLIPs: PNG, JPEG, TIFF as-is, DIB converted to PNG, EMF/WMF inflated
  (the metafile renderer draws them), PICT kept as a sized placeholder.
- Hostile input: record lengths clamped to their parent, nesting depth
  and record counts capped, decompression size-limited, picture bytes
  capped per document as before.

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

* Review fixes for #558: miniz_oxide as a workspace dependency

Restores the change @echelon pushed in 04b59287, which a rebase dropped: miniz_oxide is declared once in the root [workspace.dependencies] and docbin uses it with workspace = true.

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 03:42:18 -04:00
Rees 4611afba16 SCRUM-28: preserve imported TOC result ranges (#617) 2026-10-12 03:29:32 -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
skylar taylor-barrick a38caa4b15 Print: preserve native workers across upstream platform shutdown changes 2026-10-12 08:37:53 +02:00
ReesandBrandon Thomas b0f3d52d80 SCRUM-26: preserve custom XML stores through saves (#612)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:29:16 -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
ReesandBrandon Thomas 1f09547f4e SCRUM-34: protect original files during native saves (#621)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:11:13 -04:00
95abff4205 MacOS window close (#539)
* MacOS window close

The red close button was aborting inside AppKit. The close button now cancels that in-loop window drop, saves preferences, and quits on the next turn of the main queue the same way Command-Q does.

* Review fixes for #539: remove the control key file before the macOS quit

Merge main. The new close path quits through terminate:/exit, which skips
on_exit, so it now removes the control key file as on_exit does (prefs were
already saved). Clippy-checked for aarch64-apple-darwin.

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-12 02:05:32 -04:00
520bc2863e Ribbon and context-menu Paste insert the system clipboard's text (#584) (#593)
* Ribbon and context-menu Paste insert the system clipboard's text (#584)

* Review fixes for #593: ribbon Paste of copied picture files inserts the pictures

Picture files copied in a file manager reach the clipboard as their paths;
ribbon/context-menu Paste now pastes the pictures (as Ctrl+V does) instead
of the path text. Test included.

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-12 01:48:02 -04:00
maxandBrandon Thomas 9c21eb07df fix(ui): the horizontal ruler mirrors for right-to-left paragraphs (#692) (#756)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:42:58 -04:00
f0431abd23 M8.1: Bibliography DOCX round-trip: support extended fields (#410) and upstream sync (#532) (#790)
* M8.1: Bibliography sources DOCX round-trip

Store bibliography sources in package-root custom XML (customXml/item1.xml) with datastore item properties (customXml/itemProps1.xml), relationships, root customXml relationship (_rels/.rels), and content type overrides per ECMA-376 §15.2.

Map internal source types to Word b:SourceType values (Book, BookSection, JournalArticle, ArticleInAPeriodical, InternetSite, DocumentFromInternetSite, ElectronicSource, Report, ConferenceProceedings, Case, Art, Film, Interview, Patent, Performance, SoundRecording, Standard, Thesis, Misc) and parse them back. Write SelectedStyle and StyleName attributes on b:Sources. Use safe slice indexing for author names parsing. Scan customXml relationships and customXml/item*.xml on read, extracting sources from b:Sources in the bibliography namespace.

Add roundtrip integration tests in crates/docx/tests/roundtrip.rs verifying all parts, relationships, and attributes, update second-generation kitchen sink stability test, add hostile-input tests in crates/docx/tests/malformed.rs, and verify integration with citation styles in engine citations test.

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

* Review fixes for #532: bounded source reading, real citation style

- Read at most 10,000 bibliography sources and dedupe them with hash sets, so a hostile
  file with ~100k b:Source elements can't make opening quadratic; each custom XML part is
  read at most once (64 parts at most). Test: hostile_bibliography_source_count_is_capped.
- Drop the rt::BIBLIOGRAPHY relationship branch (not an OPC relationship type) and the
  stray comment in write_as.
- Write the document's citation style (References > Style) in b:Sources/@StyleName and
  @SelectedStyle instead of always APA: Settings::bib_style records it, the style
  command sets it, opening a file reads it back into the session.
- The b: namespace goes with the other non-body namespaces (it isn't declared on
  document.xml), matching #612.

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

* Keep the custom XML properties relationship apart from customXml

So #612, which adds the same rt::CUSTOM_XML line, merges with this branch without a
conflict.

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

* clippy: pass parse_persons by value (needless_borrows_for_generic_args)

* rustfmt after merging main

---------

Co-authored-by: yefan911 <yefan911@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:39:24 -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
maxandBrandon Thomas 0b950a7f0b fix(ui): dragging a selection auto-scrolls near the viewport edges (#688) (#758)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:20:30 -04:00
maxandBrandon Thomas d9b76d77f9 fix(ui): Save As saves under the filename typed in the dialog (#689) (#757)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:18:16 -04:00
65c9b2e5ec speech: launch Windows PowerShell by full path for Read Aloud (#792)
Read Aloud on Windows started `powershell` by name, so the program search
decided which executable ran. Start
%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe instead
(C:\Windows when SystemRoot is unset), as PdfCraft already does for
rundll32. The command line and stdin handling are unchanged.

Adds speech::tests::powershell_is_launched_from_the_system_folder.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:16:07 -04:00
9d7a972d0b #771: a page total in the footer doesn't make the body lay out again (#773)
foot_top_for measured the footer with total tracking still on, so a
NUMPAGES/SECTIONPAGES footer counted as a page total shown in the body and
every layout ran a second full pass. Footer totals are filled in after
pagination, like the header's, so the measurement now turns tracking off
the way body_top_for does.

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 01:12:26 -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
Debarghaya MitraandBrandon Thomas 9a58b5ba33 Web: add wordcraft.ico as favicon via Trunk (#717)
Registers assets/app-icon/wordcraft.ico in apps/wordcraft-web/index.html
using Trunk's rel=icon asset pipeline.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:04:33 -04:00
37318985b5 Layout: multiple line spacing adds its leading below the text, like Word (#719)
Benchmarked against docxide-pdf's 390 Word-referenced fixtures, the
largest systematic gap was the vertical position of text: WordCraft put
the baseline one descent above the bottom of the line box, so all the
extra leading of 1.15 or double spacing went above the text (1.8pt at
1.15-spaced 12pt). Word keeps the text one ascent below the line top and
puts the extra leading below it; below single spacing it shrinks the
whole box.

The hhea line gap now counts toward the ascent instead of being split
half above, half below. Line heights stay the same, but every descent
is half a line gap smaller, so text in exact, at-least and grid-snapped
lines (which keep the descent below the baseline or centre the glyph
box) sits up to half a line gap lower: 0.25pt for 12pt Times New Roman.
On the suite that brings those lines closer to Word as well (exact:
5 fixtures better, 1 worse; at-least: 5 better, none worse).

On the suite: mean Jaccard 29.2 -> 33.5%, SSIM 49.4 -> 50.3%, median
baseline offset from Word +0.68 -> -0.06pt, documents within 1pt
35 -> 51%.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:58:37 -04:00
MarekSpodBiedryandBrandon Thomas 92d6165d4c Let dialogs be dragged by their title bar instead of pinning them to the screen centre (#732) (#748)
* Let dialogs be dragged by their title bar instead of pinning them to the screen centre (#732)

* rustfmt after merging main

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:54:25 -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
skylar taylor-barrick 1d0dff03a4 Print: preserve page planning and pass native copy collation to spooler 2026-10-12 06:50:19 +02:00
MarekSpodBiedryandBrandon Thomas cad4138e7e Backspace still erases while Shift is held (#707) (#747)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:50:14 -04:00
MarekSpodBiedryandBrandon Thomas 4fb9c2cb2a Ink: draw the live pen stroke in the dark-page colour while drawing, not only after lifting the pen (#730) (#746)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:48:02 -04:00
05679497ea Pictures: WMF drawings fill their frame like Word plays them (#739)
Word plays a WMF into the picture's box the way a GDI player does: the
placeable header's bounds are only the first window, mapped onto a
viewport of the picture's size, and the file's own SETWINDOWORG and
SETWINDOWEXT records replace that window. WordCraft mapped the header's
bounds through the file's final mapping instead, which broke three
pictures in docxide-pdf's fixtures:

- bulgarian_farmland_allocation_order: the coat of arms draws in a
  window centred on the origin, nowhere near the header's bounds, so
  the lion was blown up into a solid black box.
- ukrainian_municipal_heating_resolution: a portrait window replaced
  the landscape bounds, so the emblem was drawn at a fifth of its size.
- A plain WMF in anisotropic mode that sets a window but no viewport
  kept the 1 x 1 default viewport and shrank to a point; its first
  window is now its target.

Files that set their own viewport, or use the fixed-scale map modes,
are read as before.

SETROP2 is now read too: with R2_NOP, shapes and paths draw nothing
(the coat of arms first draws its bounding rectangle that way, which
showed as an outline). The mode is saved and restored with the DC.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:45: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
8bd3427012 View: Hide White Space between pages in Print Layout (#645) (#684)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:34:30 -04:00
Brandon ThomasandClaude Opus 5.5 19684cff26 layout: make Myanmar fallback tests deterministic (#807)
tests_complex::myanmar_syllables_are_single_clusters and
myanmar_in_a_latin_font_falls_back_to_a_regular_myanmar_face failed about
1 run in 4 of the full layout suite (cluster starts [0, 3, 6, 12, 18], "one
face for the word") but passed alone.

Root cause: a race in FontDb's system font fallback, not the tests. Tests
share FontDb::global(), and layout asks for a fallback per character from
several threads. A search marks a preferred family ("Padauk" for Myanmar) as
tried before loading it; a second search racing it skipped the family as
already tried, fell through to the coverage index, and loaded the first
cataloged file in path order that maps the character (Padauk-Bold.ttf on
its own). Characters of one syllable then fell back to different faces, so
the shaper split the syllable and the word's faces differed. Separately, a
search's answer was cached under the face count read after the search, so
an answer chosen before another thread's load could be cached as current.

Fix (crates/fonts):
- System fallback searches run one at a time (a `searching` lock). A search
  that waited re-checks the loaded faces first, so it uses the family the
  previous search loaded instead of loading another file.
- The fallback cache keys an answer by the face count it was chosen from,
  and doesn't cache an answer from before a concurrent load.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:29:58 -04:00
Brandon ThomasandClaude Opus 5.5 46e694f5f5 Release workflow: pass the Azure signing secrets so Windows builds are signed (#752)
Since the release workflow moved to storytold/craft-common-actions, it has
passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads
values from the release environment, but only for names the caller lists, so
the AZURE_* names that Trusted Signing uses came through empty and Windows
builds would ship unsigned. Same fix as storytold/craft-launcher#174.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:27:33 -04:00
0918a52e57 docs: fix coordination error in ROADMAP.md (#715)
Co-authored-by: lunar-me <lunar-me@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:20:52 -04:00
85698c4d79 Security: pictures travel in the reply, exported links keep only safe targets (#678, #679) (#681)
- `ui.render` and `ui.screenshot` without a `path` return the PNG as base64 `png` in the
  reply instead of writing a fixed name into the shared temporary folder, where another
  user could have planted a link to one of this user's files; the MCP bridge's
  `render_page` and `screenshot` use that and no longer round-trip through
  `temp_dir()/wordcraft-mcp-*.png`. A given `path` still works as before (#678).
- One allow-list, `wordcraft_doc::link`: `is_web_or_mail_link` for Ctrl/⌘-click and
  `is_safe_href` (web, mail, `#bookmark`, relative path; schemes read the way browsers
  do, so `java\nscript:` is a script) for the HTML and Markdown exporters and the HTML
  importer. Scripts and other schemes export as plain text (#679).
- Tests: the allow-list, exporters with hostile links, the MCP bridge against a fake app
  (no `path` ever asked for), the control channel's in-reply PNGs.

Co-authored-by: petycomppety <30414255+petycomppety@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:57:08 -04:00
62b58d1f87 Design tab: style set gallery gets its own box so it can't overlap Colors and Fonts (#638) (#673)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:42:20 -04:00
f7a84fbfb8 Print Markup: comments in PDF, print, images and text copies (#589) (#667)
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:38:29 -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