CodeRabbit on #4560:
- The bookmark titles are the document's text, so the edit now needs
copying permission as well as assembly, and Document::text_styles (the
dialog's samples) is empty when copying is withheld; the automation
text_styles tool already checked it.
- A heading went on to the next row whenever it was close below in the
same style, so same-style headings in two columns on staggered rows ran
together into one bookmark. A line continues a heading only when it lies
under it.
- Lines found their style by a scan over every style so far; they are now
indexed by font, weight, slant and quarter-point size, so a page with
thousands of styles no longer makes the scan quadratic.
Regression tests for the first two (each fails without its fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
Main took integration batch 6 (#4566). Conflict in edit_text_ui.rs: the
overlay keeps this PR's view_id and main's switch to interact_pos for
clicks outside the document.
Bookmark options > New bookmarks from text styles... lists the styles the
document's text is set in (font without its subset tag, the size shown,
bold, italic, how many lines, a sample), the largest first, and takes one
style per bookmark level. Every line in a level's style becomes a
bookmark of that level under a new first "Untitled" bookmark, as from
structure; a heading set on several rows is one bookmark, and each goes
to the top of its heading.
The detection lives in engine::text_style_bookmarks on top of
edit::text_lines, reading each page top to bottom then left to right, so
two headings on one row stay two. text_styles and
bookmark_from_text_styles reach it headlessly, and the command
bookmark.from_text_styles opens the dialog. OutlineEntry gets an
optional top for the /XYZ destination; structure bookmarks keep theirs.
If the copy's editor had closed before or during Redo, the check for the
other tab's typing passed on an empty text. It now requires the editor
to be open before Redo and after it.
Edit ▸ Undo with typing in a paragraph or added text box that the page
doesn't have yet applied the typing (a new edit, which dropped the
document's waiting Redo), closed the box and undid the typing all at
once; the key takes it back a step at a time in the box. Menu Undo now
goes to the box first, as Redo does, and is enabled while the box holds
such typing. Undo and Redo share typing_step for the box's own history.
The dialog previews a colour as a tint of the page's thumbnail (main's #3009). It tinted every
page, so with Replace unticked a page that keeps its own white backdrop previewed coloured,
though OK leaves it white. A page a picture covers (a scan) previewed coloured too.
background_tint() now shows no colour where the page will still hide the background after OK:
a scan, or a white backdrop with Replace unticked. It reads the page from the dialog's backdrop
check (BackdropCheck::found, for the current version of the document only); a page the check
hasn't reached yet previews coloured, as before.
Tests, both seen failing with the fix reverted:
- marks_ui::tests::the_preview_shows_a_background_only_where_ok_lets_it_show (the rule);
- it::marks::the_background_preview_follows_the_replace_checkbox (what the dialog paints, per
page, ticked and unticked).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- crates/render/src/inspect.rs: this branch's reply-linking methods and main's NoRotate
rectangles (#3016) both go in the Annotation impl; the annotation reader takes this
branch's loop with main's no_rotate field.
- crates/automation/src/comments.rs: this branch's HashMap import, main's import list.
- ATTRIBUTION.toml: each catalogue's source note keeps main's secured-document clause and
this branch's reply-count clause; ATTRIBUTION.md regenerated (cargo xtask assets --write).
- The 33 catalogues: main's new rows, then this branch's reply-count row.
main's #3016 tests build Annotation field by field; they now set parent and grouped.
Conflicts: optimize_ui.rs imports (this branch's RichText, main's Compatibility; both kept)
and the organize.document-properties parity entry: both evidence lists kept, main's XMP note
kept, and the tab list now names main's Custom tab (#4387) instead of saying there is none.
* fix(ui): text typed into a new text box counts as unsaved work
Closing a tab or quitting while the only unsaved thing was text in a new Add text box skipped the save prompt and lost the text. The box now counts like fill text does: only when it holds non-blank text.
* fix(ui): saving on close keeps text typed into a new text box
The renderer reports when a page's picture is incomplete or approximate
(content cut at the page's safety budget, a colour painted in a fallback
colour), but only `pdfcraft-cli render` showed it. `page_render` and
`render_preview` returned the PNG alone, so a script or MCP client could
not tell that the image was not the whole page.
Both tools now return a JSON part after the PNG when the page has
warnings: `{ "page": 1, "warnings": [...] }`, with the sentences the CLI
prints. Warnings are strings, like every other tool's `warnings`, so
`command_run` can add its own to the same list. A page without warnings
still returns the PNG alone, so existing callers see no change.
The viewer notice the issue also asks for is left for a separate change.
Conflict in automation/tests/it/automation.rs: this PR's two aligns
tests and main's editable combo box test (#4451) were added at the same
place; all three are kept.
#4430's merge with main left mod type3_text_clip before mod tiling_cell_placement,
so main's fmt check failed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The page pointer has the pointer only where egui has the pages on top, and egui knows what
is drawn over them from the layers it met in the frame before. Dialogs are drawn after the
pages, so on the frame one opens egui has not met its layer yet. For that frame the pages
kept the pointer, and a press that landed then was theirs: a link's tip showed over the
dialog, and a click placed a stamp or followed a link under it. main's page_covered had
kept the link's tip and click off the page while a dialog was open, from a hand-kept list.
PagePointer::read now also takes whether a dialog is open: PdfCraftApp::modal_open, the
list the page press and the edge scroll already check, the Unlock prompt included. With one
open the pages see no pointer and no press, except a drag they already have, which goes on
to its release. The palette is not a dialog and the page beside it keeps its tools, so it
is left to egui's layers, from the frame after it opens.
Tests: tests/it/pointer.rs hovers a link and opens the Unlock prompt (no tip in that
frame), and clicks with the Stamp tool in the frame the prompt opens (no stamp). A
page_pointer unit test hides the pointer and a click under a dialog, and keeps a drag the
pages have until its release. With the fix reverted, all three fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A click on a check box or radio button focuses it, so Tab, Space and the arrows reach it
(#2223). The page hands the click to the field, and the field overlay toggles the box in
the same frame unless the click landed elsewhere. That check read hover_pos, which is None
on the frame egui reports a tap: egui-winit sends PointerGone with every touch release,
and winit reports Windows pen input as touch. So a finger or pen tap counted as a click
away. The focus closed and the toggle was dropped: a tap left a check box unticked and
unfocused, and a tap on a radio button selected nothing. The check now uses the overlay's
clicked_outside, which reads interact_pos, as the other field editors do.
Tests: touch_tap_ticks_a_check_box runs again. tests/it/editor_taps.rs taps a focused
check box and the other button of a focused radio group, each with a mouse click as the
control. Each checks that the focus stays and the tap toggles, and for the radio group that
the arrows still move within it. With the fix reverted, the three tap tests fail and the
controls pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form field editors and the Edit text paragraph editor are drawn over the pages,
outside the page pointer, and close on a click away. With a finger, or a pen on
Windows (winit reports it as touch), they closed on taps inside them:
- A tap on a combo box opened its list and closed it in the same frame, so a choice
could not be picked by touch.
- A tap on a list box row picked it, but the list lost focus, so the arrow keys no
longer moved the choice.
- A tap on a date field's calendar arrows closed the field.
- With a paragraph open in Edit text, a tap on a Format text control that only
opens a list, such as Line spacing, applied and closed the paragraph, and the list
went with it.
Two things were wrong. The combo and list boxes judged the click by hover_pos, and
the paragraph's "outside the document" by latest_pos. Both are None on the frame egui
reports a tap, since egui-winit sends PointerGone with every touch release. They now
read interact_pos, which keeps the tap's position on that frame (egui's own
clicked_elsewhere does the same). And egui 0.36 reports a lost focus for two frames.
The text field editor and the paragraph editor judged their exceptions again on the
second frame, when a tap has no position left and a mouse may have moved on. They now
judge a loss once, on its own frame (edit_text_ui::lost_focus_once, with a flag kept
per editor). A focus taken later in a frame is still seen on the next one, as before.
Tests: tests/it/editor_taps.rs taps with support::input::touch_tap, which sends
the release and PointerGone in one frame as egui-winit does, and pairs each tap with a
mouse click as the control. That click leaves the pointer in the window: kittest's
drop_at sends PointerGone a frame later, which the second-frame judging also turned
into a click away. With the source reverted, the four tap tests fail at their bug
assertion and the controls pass. Reverting one part at a time fails only its own
tests: hover_pos in the combo and list check, egui's lost_focus in the field editor
(the calendar) or in the paragraph editor, and latest_pos for the paragraph's outside.
A tap inside an open text field already worked and has a guard test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A one-line non-negotiable in CLAUDE.md, next to Layering, so agents and contributors
know the rule before the test tells them: page modules read the pointer through
PagePointer (crates/ui-egui/src/page_pointer.rs), never hover_pos, interact_pos,
latest_pos, press_origin or the button reads. It names the test that enforces it,
page_pointer::tests::page_modules_read_the_pointer_only_through_page_pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain #[test] in page_pointer.rs reads the source of the page modules (the per-page
chain in canvas::document_area and the modules it calls) and fails on any raw read of
egui's pointer state: hover_pos, interact_pos, latest_pos and press_origin, Context's
pointer_*_pos shortcuts, and the primary_* and any_* button reads. Its message names the
file and line and points to PagePointer. 24 open PRs add such reads to these modules, and
three of them (#2137, #1709, #2235) merge cleanly on top of the snapshot, so they would
quietly bring back the touch and overlay bugs it fixed. CI's existing cargo test
--workspace runs it on Linux and Windows.
To pass, the snapshot also carries the button state (primary_down and any_down, global as
before), and the page-input button reads that were left use it or the frame's press edge:
drag ends in Prepare, redact, the Link tool, added content, Edit text and objects, the
comment grab check, a text field opening on the press, and the press that makes a page
current. Code in these files that is not page input is listed, each with its reason, in
NOT_PAGE_INPUT: the field, paragraph and added-text editors (Areas of their own), the
signature dialog's Scan tab, Organize Pages, the page size hint, comment key handling, and
four view gates in document_area that run before the pages' widget exists (zoom anchor,
drawing edge scroll, glide interruption, full screen's click to turn the page), now small
named functions. Prepare a form's calculation order overlay (calc_order_ui) is a page module
too, and takes the pages' pointer for its hover. The test also fails
on a list entry that reads no pointer state any more, and on a source file that takes
PagePointer without being listed as a page module.
Seen failing: a raw hover_pos() put back into zoom_snap.rs, and main's raw read put back
into the stamp block of document_area, are reported with their lines, as are a stale list
entry and crop.rs left off the module list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page view had no single reading of the pointer. About twenty page modules each
read egui's raw state (hover_pos, latest_pos, interact_pos, press_origin) and tested it
against an unclipped page rectangle. That state knows nothing of what is drawn over the
pages, and hover_pos is None on the frame a finger or a Windows pen lifts: egui-winit
sends PointerGone with every touch release, and winit reports Windows pen input as
touch. So a tap placed no stamp, followed no link and ticked no check box. A touch drag
of a comment never committed, and the next drag anywhere on the page moved the comment.
The Stamp crosshair showed on the command palette, on the quick bar and on the chrome
over a page scrolled under it, a link's tip floated over the Unlock prompt, and a
double-click on a dialog's backdrop selected the word under it.
canvas::document_area now reads one PagePointer per frame from the pages' content
widget (step 1 of the page pointer router in the cursor-handling review):
- pos: interact_pos, which keeps the position through a touch release, only while egui
has the pages under the pointer with nothing over them (contains_pointer, and the top
layer there is the pages', as the closed#2626 did) or while the pages hold a press.
- origin: where the held press began, also on the frame that brings both the press and
its release. This replaces the text layer's raw-event fallback (#2100).
- edges: the frame's primary presses and releases on the pages, in order.
It reaches the page modules through comments::PageCx and as an argument: the stamp
blocks, text layer, page press and link and comment hover in canvas.rs; comments and the
signature drag preview; forms and Prepare a form; Fill & Sign and the Sign area; crop,
marquee zoom, snapshot, camera and measure; redact and the Link tool; added content,
Edit text, images and objects. page_covered, a hand-kept list of what covers the pages
that missed the Unlock prompt, is gone. The pages' hover tip is click-through, as a
tooltip is, so a quick move onto it does not take the pointer off the link under it.
Tests: tests/it/support/input.rs sends pointer input in the frames the platform delivers
it in (a touch tap and drag, a click in one frame, a double-click). kittest otherwise
runs one frame per event, so the suite never saw a touch release. tests/it/pointer.rs
holds the review's probes and #2626's layer tests. With the fix reverted, the
tap (stamp, link, check box), touch drag, palette, quick bar, chrome, Unlock tip and
dialog double-click tests fail, and the mouse-click and one-frame-click controls pass.
The tip test fails with the tip interactable, and each page_pointer unit test fails
under a mutation of the part it pins. The probes for later steps stay ignored: the
link hand and the I-beam over other tools' targets (step 2), paragraph edges (step 3)
and Escape mid-drag (step 4). touch_tap_ticks_a_check_box is ignored until the
check box focus fix that follows: since #2223 a tap on a check box counts as a click away
from its focus.
tests/it/comments.rs: the multiline markup hover test (#3541) hovered a point below the
window on a page rotated 270 degrees, and its tip showed only because the hover was
tested against the page's whole rectangle. Its harness now zooms to 100 %, so every
rotation shows the whole page, and the hover helper asserts the point is on screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A combo box with the Edit flag (bit 19) opened only its list, so a value
that isn't one of its options could not be typed on the page, although
form_fill already took one. ISO 32000-2 12.7.5.4 and Table 233: with Edit
set, "the combo box shall include an editable text box as well as a
drop-down list".
- An editable combo box now opens a single-line text box over the field
as well as its list. Enter, Tab or a click away commits the text as
Edit::SetFieldValue with a Choice, the path form_fill and the list use,
so undo, field scripts and the appearance follow; a pick from the list
still sets an option.
- The editor starts on the shown text of the picked option, and typing a
shown text picks that option (the engine maps it to its export value).
- A combo box without Edit is unchanged: a drop-down list only.
- Tests: kittest typing into an editable combo (Enter, list pick, Tab,
save, undo) and a non-editable one refusing typing; automation
form_fill takes free text only with Edit, drawn before and after save.
- Parity: form.fill-choice notes and evidence.
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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>
The special colourant name All refers to every colourant of the output
device at once, and on an additive device its subtractive tint is
complemented before it is applied (ISO 32000-2 §8.6.6.4). The vendored
colour code treated All as an ordinary spot colourant and drew it through
its tint transform, so a /DeviceCMYK transform giving cyan drew 0.5 scn
as light cyan instead of a 50% gray.
Vendored hayro-interpret patch 66: Separation::convert_f32 writes the gray
1 − t for each tint of an All space without evaluating the tint transform.
Fills, strokes, images and shadings in that space all convert through it,
and so does the RGB print raster. /None still paints nothing, other names
still use their alternate, and a DeviceN space naming All keeps its
alternate (the clause defines the name for Separation spaces).
Tests: crates/render/tests/it/separation_all.rs (synthetic).
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ISO 32000-2 9.3.6: for a Type 3 font, modes 3 and 7 shall not be
rendered, the other modes render the glyph descriptions, and modes
4 to 7 add nothing to the clipping path. Vendored hayro-interpret
patch 42 drew every mode but 3 filled, so 7 Tr painted the glyphs
in the fill colour (black by default) wherever a following fill
did not cover them.
Patch 64: show_glyph keeps mode 7 for Type 3 glyphs and reports
them as clipping text (GlyphDrawMode::Clip, painting nothing, kept
by extraction), and clip_glyph's Type 3 arm is a deliberate no-op.
Modes 0-2 and 4-6 still render filled.
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* refactor(xfa): replace model wildcard imports with explicit names (Sonar S2208)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(xfa): complete explicit model imports
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(xfa): drop unused model imports
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Import Corner, which main's parse.rs now uses
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(ui): header, footer and watermark previews step through the pages
The marks dialogs preview the current page, or the first page the range
selects when it leaves the current one out, with no way to look at
another page. Arrows beside the page label step the preview to the
previous or next page the range selects. The page stepped to resets
each time the dialog opens.
* fix(ui): the marks preview forgets a stepped-to page the range leaves out