* M6: a date typed without separators fills a yyyymmdd field
parse_date read each run of digits as one number, so 20261018 in a
yyyymmdd field was a single number with nothing left for the month and
the day, and the keystroke check refused it. Every date field on forms
that use the YYYYMMDD date format (US Army forms such as DA Form 31 among
them) refused every valid date.
order() now records each field's fixed width (4 for yyyy; 2 for yy, mm,
dd, HH, MM and ss) and whether the next field follows with nothing in
between. parse_date keeps runs of digits as text and splits a run between
such joined fields by those widths. Formats with separators, month names,
literal digits (dd.mm.26) and two-digit years behave as before.
A run of digits too long for a u32 used to be dropped silently, which let
"2026 99999999999" pass for yyyy; it is now read like any other number and
refused when it is left over or does not fit.
* chore(contributors): credit Eric Kaden by real name
* M6: separated runs of digits stay whole in a joined date format
With separators each run of digits is one field's number, as before this
branch: "2026-10-18" in a yymmdd field reads the year 2026, where the
first commit split the run into 20 and 26 and refused the date. A run is
now split between joined fields only when fewer runs were typed than the
fields still to fill, so "2610" and "261018" still split.
* M6: what is left of a split run of digits fills its field exactly
"202610018" in a yyyymmdd field split off 2026 and 10 and read the rest,
018, as day 18, so a nine-digit date passed. The rest of a split run now
has to be its field's width; a number typed on its own ("2026 10 018")
keeps its leeway.
* M6: a number typed on its own is split only when its rest fills the next field
The optional time fields of a format like yyyymmddHHMM counted as fields
still to fill, so "2026 10 018" split the day 018 into 01 and 8 and was
refused; on main the time defaults to 00:00. A run typed on its own is
now split only when what is left can fill the next field; the rest of a
run that was already split is split as before.
* M6: describe the undelimited-date tests without naming one form
The tests and their comments now say what they check (fields written side by side, such as yyyymmdd) instead of naming the form that showed the problem, and use a generic field label.
* M6: count date and time fields apart when deciding to split a run
Optional time fields counted towards the fields still to fill, so
"2026-10-18" in a yymmddHHMM field split the year into 20 and 26 and was
refused, where main reads it with the time left out. A run typed on its
own is now matched against the fields of its own kind: date runs against
date fields, time runs against time fields. What is left of a split run
goes on being split along the fields written side by side, so
"20261018143005" still fills yyyymmddHHMMss. A mix such as
"2026 1018 1430" (year apart, month and day joined, then a time) is still
refused, as on main.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The calendar took its month once, when it opened, so a date typed while
it was open showed only after leaving the field and coming back. It now
moves to a date as soon as the text becomes one, and stays on the month
it shows while the text is half typed or unchanged, so its arrows still
turn the months.
* M14: keep long menus usable in short windows
* Menus: scroll each top-level menu once, from its caller, so View's list isn't nested in a second scroll area
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>
* Viewer: accelerate coarse wheel scrolling (#1072)
Line/Page notches moved a fixed eased step, so a fast spin felt slow.
Accumulate one frame of coarse input per axis, scale by arrival rate
(1.0 idle, up to 6.0 spinning), and apply it directly like precise
touchpad input.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Viewer: add only the fast-spin surplus on top of egui smoothing
Single notches flow through egui untouched, so isolated input matches
the scroll speed preference exactly; page units are never boosted.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
---------
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
Recovery snapshots are unsaved copies of open documents. The folder was
made with the default mode, so on a shared Linux or macOS machine other
accounts could list it and read the snapshots of documents that are not
encrypted. The folder is now made owner-only (0o700), and a folder an
earlier version made is tightened when an instance starts or writes a
snapshot, as image_pick does for its folder. A volume that can't change
modes (FAT, some network shares) works as before, with one warning in
the log.
* M4: Ctrl-drag copies pages in the Organize grid (#2997)
Dragging thumbnails in the Organize page grid with Ctrl held (Option on
macOS) drops copies at the gap instead of moving the pages: a "+" badge
by the pointer, the copies land at the gap selected, and one undo step
takes them out. The insertion point joins the engine edit
(Edit::DuplicatePages { at }) so the copy is one undo step rather than a
duplicate plus a move.
page_duplicate takes a 1-based "at" (the first copy becomes page "at"),
so agents can drive the same thing, and the UI control channel's
ui.drag modifiers now reach the input state -- which gestures that read
it (this copy-drag, and Alt-drag column select) need.
Closes#2997.
* fix(ui): take the grid's copy modifier from the runtime OS (#3092 review)
Option-drag in the Organize grid moved the pages instead of copying them in a Mac
browser: `cfg!(target_os = "macos")` is false in the wasm build, and eframe sets the
context's OS from the browser. `copy_modifier` now takes the context and asks
`commands::mac_shortcuts`, as the shortcut labels do.
The drag helpers in `tests/editing.rs` and `tests/control.rs` picked the modifier the
same compile-time way, so they would have sent Ctrl on a Mac host; both read the
context's OS now. The new regression test
`option_copies_and_ctrl_moves_when_the_context_reports_macos` drives a Mac context —
Ctrl moves, Option copies — and fails without this change.
ROADMAP.md, docs/roadmap.md: union of both sides' lines, each once.
parity/acrobat-features.toml: the XFA entry has both sides' evidence and notes (#2604 page areas, #2813 CLI layout).
* M3: type an exact zoom into the rail's zoom field
The rail's zoom percentage was a read-only label. It is now a text field:
Enter applies a typed percentage (150, 150%, 62.5 %), Escape or unparsable
input keeps the current zoom. The control channel's zoom option uses the
same parser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: document parse_zoom_percent
* i18n: Vietnamese for the zoom field's tooltip
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
In a region that mixes sizes (a 24 pt title over 12 pt lines), the
inline editor's caret on a small row was as tall as the region's first
size and reached into the row below. egui draws the caret at least one
row of the TextEdit's font tall, and that font had the paragraph's first
size. Each row already carries its source row's height, so the
TextEdit's font is now half the region's smallest size and only sets
that floor.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ROADMAP.md, docs/roadmap.md: union of both sides' log lines and milestone rows, each line once.
parity/acrobat-features.toml, crates/xfdf/README.md: next's text, a superset of main's (#1917, #2686 plus #2688).
crates/edit/src/tests.rs: both sides' new tests (#2558 and #3084).
crates/ui-egui/src/editing.rs: the picked picture is cleared on every edit, and a new drawing commits its raster (#3149).
Organize pages ▸ More ▸ Reverse pages (and the command palette) reverses
the selected pages among their own positions, or the whole document when
fewer than two pages are selected. It is one undoable step, needs the
assemble permission like the other page edits, and is available headless
as the page_reverse automation tool (pages optional, default all).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Hand tool scrolled by the page widget's drag delta from inside the
page view's ScrollArea. egui applies that delta when the area ends, after
the frame's pages are laid out and drawn, so the page showed each move a
frame late and trailed behind the pointer. (Until the page view stopped
animating its scrolls, the delta was also eased in over several frames,
which is the stutter #924 and #1992 describe.) On top of that, the points
the pointer moved before egui tells a drag from a click were dropped, so
the page never caught up with the grab point.
Pan before the area lays the pages out instead: when the page widget is
being dragged with the Hand tool, set the scroll offset from the last
offset minus this frame's pointer delta, or minus everything moved since
the press on the frame the drag is recognised. The page now moves point
for point with the pointer in the frame it moves, and still stops where
it is let go (no fling). Other scrolls (go to page, links, zoom anchors,
compare sync) are untouched and still win when asked for in the same
frame.
Seven tests failed at random on slow or busy machines, main included
(FreeBSD run 38834256439 at 5fdd820b). They now count the work or wait
for the state they check; no assertion is loosened and nothing retries.
- spot_colour_images_run_their_tint_transform_once_per_sample_value
counts tint-transform runs (exactly 256, not one per pixel) through
vendored hayro-interpret patch (34), color::tint_transform_runs, a
thread-local counter, instead of a 10 s deadline.
- shadings_with_large_sampled_functions_paint_quickly: no counter tells
the flat table from the hash map (both read every sample per build),
so the build each paint does is timed against reading its samples on
the same thread, fastest of 8 rounds each, and must stay under 10x
(flat table 1.1x, hash map 109x), instead of a 10 s deadline.
- page_contents_and_nested_forms_share_one_budget: the budget is
checked by its warnings; the deadline is only a hang guard, now 600 s.
- The pen-stroke tests hold finished renders back from the view
(DocView::hold_rasters, for tests) while they look at the settling
stroke, then wait by state for its raster.
- files_and_quit_from_the_operating_system waits for the tab before
deleting the file the open worker reads.
- image_pixels_follow_the_frame_while_a_corner_is_dragged waits for the
page raster on screen before each drag.
Product fix (#1858): a released drawing stayed painted only until any
raster of its page arrived. In full screen its edit is applied a frame
later, so a raster already under way could end the preview first. A
settling drawing is now marked committed when its AddAnnotation lands,
only rasters after that end its preview, and a failed edit drops it.
An attachment opened as a tab is named with the same plain file name as
its Save suggestion, and Export certificate names the file the same way
from the certificate subject. Writing files into a chosen folder refuses
any name with a separator, a drive, or that is . or .., so no caller can
write outside the folder.
The first fix dropped a stale pick while painting the next frame. The window only redraws on
the next input, so after deleting a picture in Edit text & images its highlight stayed on screen
until the user clicked. Clear the pick in apply_edit, in the same step as the edit, and ask for a
repaint. drop_stale stays for the paths that change the document without apply_edit (Undo, Redo).
An edit-level test checks the pick right after apply_edit with no frame in between; it fails
without the change. The earlier one-frame test could not tell, the test harness redraws more
often than the app does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With the Select tool a click picks a picture and draws a blue highlight over it. The pick was
only cleared by Escape, a drag or another click. Picking a picture, then entering Edit text &
images and deleting it left the highlight on the empty spot, with nothing left to click to
remove it.
The pick now remembers the document's edit generation and is dropped once the document has been
edited since (a delete, a move, Undo), the same condition under which the copy shortcut already
refused it as stale.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Read mode hides the tools, not the navigation panes, as Acrobat's does
and as switch_mode already says, but the floating quick toolbar was
still drawn over the page there. It is now hidden in Read mode, and
entering Read mode puts the current tool down (back to Select), so no
markup tool stays active without a toolbar to show it. The right rail,
the page navigation and the V/H shortcuts are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): validated signature display, snapshot tab reuse
Signature widgets with layered appearances baked the unverified state
into the page, so a valid signature kept reading 'Signature Not Verified'
while the banner and panel said valid. Validated verdicts are now drawn
into the icon and caption layers in the display copy only: the signed
bytes are untouched, saving keeps the original appearance, and files
without those layers or without a valid verdict render exactly as before.
Other valid signatures still get the verdict mark over their widget,
sharing one style with the Signatures panel, and hide it again once the
appearance itself shows the verdict.
View signed version and View revision opened a new tab on every click,
and every tab showed the document title, so repeats piled up identical
tabs. Repeats now reuse the snapshot tab, snapshot tabs read like their
source plus the suffix in the tab strip, window title and Combine picker,
and carry a notice bar naming the revision with a Close that asks about
saving first.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Address review comments on validated appearances and trust refresh
Count every field when checking shared appearance streams, so a
caption shared with an unvalidated field is never rewritten for the
valid one alone.
Pick the icon slot by drawing instead of length, and only when a
single layer qualifies. Bare rectangles never count as the icon, and
ambiguous or missing slots fall back to the overlay badge.
Revalidate and invalidate together through one helper, so trusting a
certificate always drops rendered pages. Trust alone still marks
nothing dirty.
Tests: mixed-verdict sharing, longer decorative borders, competing
and doubled layers, capture-before preservation, trust invalidation
without zooming, and signed Session round trips with rendering,
saving and passwords.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Prove the icon slot geometrically and cover real layered signatures
A lone line drawing is still not the icon: frames and borders span the
box while the status mark lives in a corner of it. The icon now has to
sit strictly inside the appearance frame and cover at most half of it;
anything else falls back to the overlay badge. Widgets without a
measurable frame veto the rewrite the same way.
Tests: parsed extents and inhabitant checks as units, both reported
border repros plus a frameless widget, and real layered signatures
built with cms::sign_detached through Session open, trust, rendering,
save and reopen, plain and password-protected.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Identify the icon by shape instead of geometry
A lone line drawing is still not the icon, but size and position were
the wrong signals: the real status mark is large, and geometry blocked
it. Frames, borders and panels are rectangles by nature while the mark
is not, so a drawing made only of axis-aligned rectangles is decoration
however it is stroked. Singularity, shared-stream and template checks
are unchanged; anything ambiguous still falls back to the overlay badge.
Tests: rectangularity as a unit, nested line-drawn frames, and a
triangular fixture mark in place of the rectangular one.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Budget the appearance walk and keep the marks with the bytes
The walk that finds a signature's icon and caption capped depth,
layers and fields, but not the work: a dictionary whose entries all
point at the next one is visited once per entry per level, so a small
hostile file never finishes opening. The walk now counts its visits
and gives up, and a field whose tree cannot be read to the end stops
the whole pass, because a layer we never read may be the one two
fields share and a rewritten shared layer puts one field's verdict on
another field's page. The sharing count is exact again with that: two
widgets of one field pointing at the same appearance claim it once,
or the field vetoes its own rewrite.
A mark used to outlive the bytes it described. A failed display write
left the page showing the file's own caption while the verdict still
claimed its state was drawn; a metadata edit dropped the mark while
the page kept drawing the state, so the badge landed on top of it. The
write fallback now drops the marks and logs the loss, and metadata
carries them over.
Tests: a self-referential appearance tree, which never finished before
the budget, two widgets sharing one appearance, a metadata edit that
keeps its mark, a save that goes through the writer instead of
short-circuiting on the opened file, and the snapshot bar's Close
button pressed for real.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Cite the new regressions in parity, restore main's installer note
The appearance entry now cites the bounded walk and the save through
the writer, and says what a tree that cannot be read to the end does.
The installer entry carried main's older text, which still claims
signed installers are planned; main's newer note is taken verbatim.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: preserve rounded decoration beside raster status glyphs
* M9: place appearance progress beside signature history
* M9: retain appearances with ambiguous curved layers
* M9: keep signature verdicts in viewer overlays
Defer generic appearance rewriting: caption text and a unique drawing do not establish an icon role. Preserve original artwork for saves, image and text exports, and both print modes while retaining snapshot reuse and trust refresh. Cover chamfered borders and mixed signer/caption streams through signed Session round trips.
---------
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
The find-step shortcuts sat after the canvas's "egui wants keyboard
input" check, so they did nothing while the find field had the focus
(Enter worked). They now run before it, while a search is open, as in
Acrobat and Preview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
The bar beside selected text closed after Highlight, Underline, Strikethrough,
Add comment and Replace text because those consume the selection. Copy kept the
selection, so the bar stayed open. Remember the copied selection and hide the
bar until the selection changes; the words stay selected for ⌘C or another
action.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The jump scrolls to MARGIN - GAP above the page. A page that fits has
nothing to scroll, and egui clamps the offset only after drawing the
frame, so the page showed 14 pt too high for a frame and then hopped
down. Clamp the jump to the scroll range before handing it to egui.
At Fit Page in single-page view, every wheel turn forward drew one
blank frame before the new page appeared.
- Lay out the shown row again after the wheel is read. The jump to the
new page was measured from the old row's place and scrolled both out
of view; going back it came out negative and was clamped, so only
forward turns flickered. The edge-turn deferral that worked around
this goes.
- Prefetch the rows either side of the shown one, so the next page is
rendered before the turn instead of showing blank paper.
* Fix main UI regressions in paging, fonts and test expectations
* Wait for asynchronous dropped-document preparation in regression
---------
Co-authored-by: Codex <codex@openai.com>
* Comments panel: page groups fold from their chevron (fixes#1554)
* Comments panel: translate the fold-all buttons in every catalog
"Collapse all pages" and "Expand all pages" were missing from the 21
catalogs, so the covers_ui_literals tests failed; the catalogs' hashes
are re-pinned in ATTRIBUTION.toml.
* Add the Vietnamese entries for Collapse/Expand all pages; refresh catalog hashes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M6: make list boxes directly selectable and scroll to selection (#1524)
- In-place selection: List boxes are rendered directly on the page and
selectable in place. Clicking an option row selects it immediately;
Ctrl/Cmd-clicking toggles items in multi-select lists, and Shift-clicking
selects ranges, without opening a popup menu.
- Appearance and scroll (/TI): When options are selected, /TI (top index)
is automatically adjusted so the selected items are scrolled into view
within the widget's visible rows, and option rows and selection highlights
are drawn into the appearance stream.
- Unit and UI tests: Added tests in pdfcraft-forms verifying /TI scrolling
and appearance generation for multi-select lists, and UI tests in
pdfcraft-ui-egui covering in-place click and Ctrl-click toggling.
Also ensured test harness in reload.rs explicitly sets language to en.
Fixes#1524
* List boxes: keep the engine's page appearance, consume arrow keys
Drop the overpaint of every fillable list box with an egui mock (it ignored the
PDF's fonts, colours and /MK /R; the engine redraws the appearance with the new
/TI), and consume ArrowUp/ArrowDown in the list overlay so they don't also
scroll the page.
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>
* M7: grow text boxes and edit existing text
Text boxes now grow to the page edge and vertically as needed, retaining their size when saved. Clicking native or added text in either text tool edits the existing item instead of layering a duplicate over it.\n\nCo-authored-by: OpenAI gpt-6.1-sol <noreply@openai.com>
* #1539: build against main's Document::added() accessor and draft-aware prepare
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>