Brings the branch up to date with main (47f42ba0), which moved 37 commits since
the merge base, including page-edge shadows (#2088), full rewrite after page
delete (#4279) and the licence-check stage (#2212). No conflicts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Resolve all 13 merge conflicts between upstream/main and main
- Preserve full image editing suite: OBB, crop, proportional resize, arrange, and context menu actions
- Preserve text editing formatting tools: bullet/numbered lists, subscript/superscript, and line spacing
- Cleanly integrate upstream's canvas modularization, font updates, and styling changes
- Refactor arrange_image and run_edit_page_image helpers to satisfy clippy cognitive complexity limits
- All engine, edit, and UI integration test suites passing cleanly
Use the unified Format Text panel and current page-input context instead of restoring old monolithic UI code. Preserve main's pointer routing, editor accessibility and preferences; retain colour, save and input regressions. Hosted CI must validate this refreshed head.
* feat(ui): menus from craft-menu, native on macOS
One menu definition (crates/ui-egui/src/menus.rs) built from the command
registry drives the native macOS menu bar (craft-menu's muda backend) and
an in-window egui bar on Windows, Linux and the web, which takes the place
of the "Menu" button at the start of the top bar.
- File, Edit, View, Sign, Window and Help, with separators and submenus.
Edit gains Cut, Paste, Delete and Select All (they act as their keys
do) and a Pages submenu; View gains a Tools submenu built from the All
tools groups; Sign holds Fill & Sign and the certificate commands;
Window lists the open documents.
- Toggles are check items and the page layouts, side panels and themes
radio items, with their state from the app.
- File > Open Recent lists the recent files (file.open_recent with an
index).
- A menu click and its shortcut run the same dispatch, including the
form-field deferral (#166) and macOS Cmd+W closing the window (#869).
Dialogs and the palette grey the menus through craft-menu's gate.
- Quit goes through craft-quit, so the unsaved-changes prompt runs.
- craft-ui, craft-quit, craft-menu and craft-keymap pinned to
storytold/craft-libs 70ade1fb.
Menu state and dispatch follow #2610 by @hajdukste; the menu tree and the
view and panel items follow #3870 by @RivanJarjes.
* test(ui): the menu model, native dispatch and the in-window bar
- Unit tests for the menu definition: every registry menu command is
listed once with its registry shortcut; check and radio items follow
the app; a dialog greys document commands and drops their clicks; the
recent files and open documents are listed once each; a native menu
click (through a fake craft-menu backend) runs its command once.
- The kittests that opened the old "Menu" button use the menu bar, and
the ones that look up a label the bar's titles share (Edit, File,
Sign) skip the bar.
- The in-window bar takes its titles' width, so the mode tabs, search
and buttons keep their place in the top bar.
- Edit menu clicks that stand for keys (Copy, Cut, Delete, Select All)
reach this frame's input from `logic`, where test hosts see them too.
* test(ui): the language switch test opens File in the menu bar
Translated titles and rows carry their mnemonic ("ファイル(F)", "開く(O)…").
* feat(ui): the menu bars run Quit themselves, the bar shrinks to its titles, and AppKit's window list stays off
craft-libs main edc41f6 (craft-libs#354): MenuBarUi runs Quit and Minimize, shrink_to_fit sizes the in-window bar, Key::parse reads the registry's arrow and minus glyphs, and mac::install_with(.., window_list(false)) keeps the Window menu to PdfCraft's own document list.
* fix(ui): the menu command filter is an if, for clippy
* test(ui): drop a needless borrow in the edit-tab helper
* test(ui): two tests follow the menu bar
The tests were wrong, not the app. The Preferences language test clicked the old Menu button; it opens Edit in the menu bar and its Preferences row. The prepare Actions test counted the Edit labels: the narrower menu bar leaves room for the Edit tab, so the rows' Edit buttons come third and fourth.
* Record successful PdfCraft web application check
* E21: bound startup requests and validate font and license inputs
* E21: separate pending control completion and gesture regression phases
* M-egui037: enforce control admission and per-family font notices
* M-egui037: qualify capture fixes and document migration behavior
* egui-037: identify current and historical qualification results
* refactor(ui): side panels on craft-ui task panes and the shared rail
The Tools pane and the open side panel are docked craft-ui task panes
(craft_ui::panes::PaneHost, Columns policy, no title bars, not movable),
and the rail is craft_ui::panes::Rail with the page navigation cluster
as its foot. On the right, outermost first: the rail, Tools when
Preferences > Tools panel side is Right, the side panel, the page.
Read mode uses the same panes without Tools; All tools full view still
covers them. Floating, tab groups and drag-in are gone with docking.rs.
- Widths are resizable within the old limits and saved under a new
side_panels preference (version 1) with the Tools side and open state.
The old docking value is read once as plain JSON and dropped on save.
- ui.dock becomes ui.panel {panel: tools|side, open, width};
ui.dock.reset resets the side panels ("Reset side panels").
- F4 shows or hides the side panel (reopening the last one) and
Shift+F4 the Tools panel, as View menu commands.
- craft-ui pinned to e9630ae7 (craft-libs#309).
* fix(ui): side panels clippy lint and the dragged-width test's grab offset
* chore(deps): pin craft-ui at 7c42807, whose pane resize edges work over the page
* chore(deps): pin craft-ui at craft-libs main ea4c823, with panes::Rail
When undoing or redoing an edit (such as Arrange), re-resolve any active image selection to match the same image's XObject, name, and position rather than leaving the selection stuck on the old index (which could point to another image). If ambiguous or no longer present, clear the selection.
* M14 #4253: remember the annotation tool pin across restarts
* M14 #4253: clarify roadmap timezone and Markdown formatting
* Keep main's roadmap status lines; only the progress-log entry is new
* Take main's forms.rs: main already gives each form test its own folder
* Roadmap: drop the form-test folder note (main already has its own)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Conflicts:
- crates/ui-egui/src/i18n/mod.rs: kept main's side (main dropped the per-language translation tests; the PR's drawing_scale_labels_are_translated_in_every_catalog is not carried over).
- parity/acrobat-features.toml: measure.distance evidence: kept main's paths (tests/it/), appended the PR's two new entries.
* Tests: UI tests wait for background work by time, not frame count (#4186)
A frame count is not a time: on a busy runner the worker had not finished
the open, or the page text had not arrived, within 30 frames or 50 hover
rounds. support::step_until (#[must_use]) steps frames until a condition
holds or 30 s pass, sleeping briefly between frames, and says whether it
held; each test asserts on that with its own message.
- control_open_replies_pending_while_the_worker_opens_and_focuses_once_it_has
and mode_tabs_keep_the_hand_over_page_text_scrolled_under_them wait with it,
and so does control.rs call() for its reply.
- support::settle waits for every pending render and fails if the pages are
not rendered within 30 s. It replaces all the render-pending settle loops
(40 to 3000 frames) and the local settle/wait_for_pages wrappers, except
text_select's double-click wait, which is held to 8 frames on purpose to
stay inside egui's multi-click window, and view.rs's loops that already
wait by a deadline.
- wait_until is built on step_until, keeping its diagnostic message.
- tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value already
waited 30 s for its file. It failed because its "same-frame" case shared
its scratch directory with typing_and_saving_in_the_same_frame_saves_the_last_keystrokes,
which runs in the same test process (since ee1ee704, #1045) and deleted
the directory mid-save. forms' and pickers' scratch() now give every call
a directory of its own.
Fixes#4186
* M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385
The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.
(cherry picked from commit 1f8811761a)
A frame count is not a time: on a busy runner the worker had not finished
the open, or the page text had not arrived, within 30 frames or 50 hover
rounds. support::step_until (#[must_use]) steps frames until a condition
holds or 30 s pass, sleeping briefly between frames, and says whether it
held; each test asserts on that with its own message.
- control_open_replies_pending_while_the_worker_opens_and_focuses_once_it_has
and mode_tabs_keep_the_hand_over_page_text_scrolled_under_them wait with it,
and so does control.rs call() for its reply.
- support::settle waits for every pending render and fails if the pages are
not rendered within 30 s. It replaces all the render-pending settle loops
(40 to 3000 frames) and the local settle/wait_for_pages wrappers, except
text_select's double-click wait, which is held to 8 frames on purpose to
stay inside egui's multi-click window, and view.rs's loops that already
wait by a deadline.
- wait_until is built on step_until, keeping its diagnostic message.
- tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value already
waited 30 s for its file. It failed because its "same-frame" case shared
its scratch directory with typing_and_saving_in_the_same_frame_saves_the_last_keystrokes,
which runs in the same test process (since ee1ee704, #1045) and deleted
the directory mid-save. forms' and pickers' scratch() now give every call
a directory of its own.
Fixes#4186
Quitting with several unsaved documents moves from one question to the next
without the prompt closing, so a question that opened in the background after
one the user saw asked for nothing (review feedback on #4155).
close_and_continue now marks the next document's question as new. A request
that still stands carries over to it instead of being sent again.
Quitting from the Dock menu (or logging out, or an agent's ui.quit) while
another app is in front opens "Save changes?" in a window the user can't see,
so it looked as if PdfCraft hadn't quit. The prompt now sends a critical
user-attention request on the frame it opens while the window isn't focused:
the Dock icon bounces until PdfCraft is active, the Windows taskbar button
flashes until the window is in front, and X11 sets the urgency hint. Coming
back, or answering, sends Reset. This is what AppKit does by itself when an
inactive app presents a modal panel; PdfCraft's prompt is drawn by egui, so
AppKit never sees it.
* Build each crate's integration tests as one binary
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs, for apps/pdfcraft, apps/pdfcraft-cli, automation, cos,
crypt, engine, render, sign and ui-egui. ui-egui's ui_group binary and
the support modules move into tests/it/ too. Crates with a single test
file keep it. The test executables go from 150 to 52, and every crate
runs the same number of tests as before.
ui-egui perf keeps its own binary (own process): its frame-time budgets
would charge each measurement for the other tests running beside it.
What the move needs:
- include_str!/include_bytes! and #[path] paths gain one "../".
- ui-egui: one GPU lock in tests/it/main.rs replaces the five per-file
ones (content, document_links, editing, fill_sign, view), so one
process creates one wgpu device at a time. The drop-line test in ui.rs
and the screenshot blocks behind PDFCRAFT_*_SHOT(S) and
CRAFT_UI_DOCKING_DIR, which render without holding it, take it too.
- pdfcraft winit_wayland_scroll: the vendored winit source it compiles
names crate::event, so that shim lives in main.rs.
- parity evidence names the new paths.
* Name the one-binary test paths in docs and comments
- READMEs, docs, ATTRIBUTION and source comments name tests/it/<file>.rs.
- The oracle README's example filters with --test it encryption::qpdf_agrees.
- pdfcraft's GPU lock comment points at the UI tests' shared lock in
crates/ui-egui/tests/it/main.rs.