From a review of this branch:
- Escape mid-drag with a text markup tool (Highlight, Underline, Strikeout, Squiggly, Replace
Text) or Redact over text left the half-made selection on the page: the cancel took the Escape
that would have cleared it. It is dropped now. The marks made from a selection at its end
(after the page tools, which the release blocking did not reach) also wait for a cancelled
press's button; egui ends its own drag on Escape without a drag stop, so they were not made
in practice, but nothing but egui stopped them.
- The open paragraph's frame took the press above and below the text only: its sides still gave
edge grips (a closed paragraph's rewrap handles) on the Editor layer, over the frame. Edges now
belong to closed paragraphs only.
- A press in the frame right after a release kept the last press's owner until a page claimed
it, so a drag from the margin beside a page could move the comment just clicked. A new press
now drops the last owner before the pages give it theirs.
- A release and a new press in one frame no longer leave the new press cancelled.
- Added content is picked up with the tools that act on it (Select, Add text, Edit text), not with
every tool while Edit a PDF is open.
- The layer docs in page_cursor.rs say what lies on each layer now.
New tests an_open_paragraphs_frame_takes_the_box_beside_its_text and
a_drag_from_beside_the_page_right_after_a_click_leaves_the_comment_alone (the release and the
next press in one frame) fail on the previous commit. escape_cancels_a_highlight_being_dragged_over_text
(with a control: the drag highlights without Escape) passes there too, for the reason above, and
stays as a guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "Page presses" non-negotiable in CLAUDE.md, beside "Page input" and "Page cursor": a page
module decides what of its own is under a point in one hit test (a Hit from
canvas/page_router.rs) that gives its cursor, its tip and its press, and acts on a press only
when the router says it is its own, never by testing the press point again or by asking
another module; Escape cancels any press the pages hold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit text's paragraph edges were tested twice: by edit_text_ui for the cursor, on the page's
paragraphs, and by object_ui for the press, on its own object inventory (grabs_edge). Where the
two inventories disagree, the resize arrow shows and a press starts an invisible area selection
instead (#2276, the review's step-3 probe). The open paragraph's frame showed the Move cursor
4 px out and took the press 6 px out.
Each of these parts now has one hit test, in the page router's table, and acts only on the
press it was captured on:
- edit_text_ui::hit: a paragraph's edges (Handle), its body (Text; Editor for the open one).
- edit_text_ui::picture_hit and picture_handle_hit: a picture's body on the Text layer, asked
before the paragraphs, so text on top of artwork takes the press; the selected picture's
corners and rotate knob on Handle, asked after them, so they win over text.
- object_ui::hit: the objects selected together (Placed) and the open paragraph's frame
(Handle), with one reach (FRAME_REACH, 6 px) for the cursor and the press. An area selection
starts only where nobody's hit claims the press.
- content_ui::hit: added text and pictures (Placed; the selected one's corners on Handle), over
form fields; Add text starts a new box only where nobody's hit claims the press.
image_owns_pointer and content_ui::select_grabs go, with the edge test object_ui kept a copy of
(grabs_edge). The page's hits move into canvas::PageHits.
Un-ignores a_paragraph_edge_resizes_wherever_it_shows_the_resize_cursor: it passes on main
already (the inventories agree for its paragraph since the probe was written), so it stays as a
guard rather than as proof. New test an_open_paragraphs_frame_shows_move_wherever_a_press_takes_the_box
fails on the previous commit (the cursor is Default there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forms: forms_ui::hit is the fields' only hit test (cursor and press). The press opens a text
field (#2213) and its click acts on the field the press was on, from the page router, instead
of the field under the release point: a press on one check box released over the one beside it
ticked the second. Now it ticks neither, as a button pressed and released off itself does
nothing. Forms no longer wait on comments::select_grabs: a comment that moves (a note, a shape,
a signature, a Fill & Sign mark) hits on the Placed layer, over form fields, so it still takes a
press over a field (a signature can be dragged off the field it covers), and text markup stays
on the Comment layer, under them, so a click on a box under a highlight ticks the box.
select_grabs goes.
Prepare Form: prepare::hit (the selected field's corner handles, then a field, on Placed so a
field being moved stays over the comments as before) gives the cursor, and the drag, click and
double click act on what the press was on; a press held by something else is not Prepare's.
New tests: a_press_on_one_check_box_released_over_another_ticks_neither (fails without this
commit: the second box is ticked) and a_check_box_under_a_highlight_takes_the_click (fails with
markup on the Placed layer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Sign area, marquee zoom and snapshot, crop and redaction boxes, and the stamps, act where
nothing drawn over the page holds the press (the page router has no owner for it), rather than
wherever the press lands on the page. Fill & Sign already asks the router (fill_grabs).
Their drags start on the primary button and end with its release, however egui reported it:
its drag stop, or the button found up. They started on any button, so a right-drag drew a
signature rectangle and opened the signing dialog (and drew a crop or a marquee), and the end
waited for a pointer position, so a drag whose release came without one never ended.
New test a_right_drag_draws_no_signature_rectangle, seen failing with the drag started on any
button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page router (canvas/page_router.rs): a page module answers "what of mine is under this
point?" in one hit test, which gives a Hit (an owner, a layer, a cursor, a tip). On the frame of
a press, the hits at the press point decide who it belongs to, by PR 2's table (the highest
layer the tool reaches), and the router holds that capture until the button is up and its
release has been seen; automation's cancel and Escape drop it. The pointer's hits claim the
cursor and the tip, after the tools that are not converted yet, as before.
Comments: comments::hit is the module's only hit test, with each tool's filter (Select: point
handles, resize handles, then the comment; Fill & Sign: the same over its marks only; shape
tools: the selected comment's handles only). The Select tool's drag, click and double click act
on the comment and part the press was captured on, instead of testing the press origin and the
release point again; fill_grabs and select_grabs ask the router while the button is down. The
topmost comment is the one drawn last (/Annots order), not the last in inspection's top-edge
order: over stacked comments the tip named the top one while a click selected the one
underneath. Text markup declines the drag, so a click on a highlight selects it and a drag
across it still selects the text.
Links: the link hover is a hit (hand and target), and a click follows the link its press was
on. Text selection starts unless the press is held by something that keeps its drag (a link, a
comment that moves); it tested for a link under the pointer at the moment egui called the press
a drag, so a drag from text onto a link selected nothing.
Un-ignores a_click_on_stacked_comments_selects_the_one_drawn_on_top; new test
a_drag_from_text_onto_a_link_selects_the_text. Both fail with their fix reverted (inspection
order for the topmost comment; the link test at the pointer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Escape cancelled a comment gesture, Prepare's marquee, measuring and the camera tool, each in its
own module; with the Sign area, crop, marquee zoom, a redaction box, the Link tool's rectangle,
a paragraph or picture drag, added content, an object move or a text selection it did nothing,
and the release then drew, placed or opened what the drag had begun (the step-4 probe: Escape
mid-rectangle still opened the signing dialog).
document_area now handles Escape once, before the pages: while the pages hold a press, it drops
every pointer gesture (DocView::cancel_pointer_gestures, as automation's cancel does) and marks
the press cancelled. Until the button is up the page tools take no input from it, so its
release neither clicks (a stamp, a Fill & Sign mark) nor ends a drag. Escape without a press
keeps its meanings (close the editor, clear the selection, back to the Select tool).
Un-ignores escape_cancels_a_signature_rectangle. Two new tests: Escape mid-press keeps the
release from placing a stamp, and Escape mid-drag abandons a crop rectangle. All three fail
with the cancel taken out; the stamp test also fails with the cancel kept and the release let
through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four holes CodeRabbit found on 648dd428:
- alignment_frame framed a one-row paragraph when any alignment entry
was set, including one past the draft's rows, and then moved rows
with no alignment of their own to the column's edge. It now takes the
draft and frames only when every draft row is aligned, by its own
alignment or the paragraph's.
- remap could not tell which of two same paragraphs an edit took
("A\nA" to "A"), and kept the deleted one's alignment. remap_at takes
the caret where the edit ended, and the typing path (follow_typing,
both editors) passes it from the box's state. The caret only chooses
between readings that fit both texts.
- Both editors judged a focus loss by where a click landed, so focusing
a Format text control by keyboard or a screen reader (no pointer)
closed the box before the alignment was chosen. They now also judge
by where the keyboard went (focus_pos): focus outside the document
view, or on the added-text box's own buttons, keeps the box open as a
click there does.
Each change has a test seen failing before it.
The wrap's space count has to survive an incremental save and a full rewrite, and a width-only text_edit has to reflow with that count. A recorded editor session narrows the box and widens it again.
wrap() already dropped the whole gap at a wrap point (#4199); rejoining it always guessed a single space back, so two or more typed spaces silently became one, and a Chinese/Japanese word split (which has no space of its own) could gain a spurious one. wrap() now records how many separator characters it consumed at each wrap (0 for a CJK split); the /PCSoft marker carries it as /N (a file saved before this, or by another writer, reads as 1); reflow_text and the mixed-style row editor insert exactly that many spaces back instead of assuming one.
craft_menu writes "⋯" (U+22EF) itself for the menu of titles that don't fit, through the app's own label callback. No font this app ships or falls back to has that glyph, and egui 0.37's MissingGlyphPolicy::Panic crashes on it as soon as the bar is narrow enough to need it: every editing test that narrows the window while a paragraph is open hit this. The ordinary ellipsis the rest of the app already uses everywhere else is available and reads the same.
wrap()'s own test for mixed exotic whitespace fed through it directly, bypassing the caller's CR/CRLF normalization, needed its expected value updated for #4199's keep-every-space design, landed on main after this branch's base.
Editing either end of one text object, then saving and editing again, has to leave the other paragraph in place. The library tests did not drive text_edit and doc_save.
The paragraph-key helper has to carry reflow text, which a width change uses. ClosePath on an empty subpath is a match guard so the edit crate still passes clippy.
A partial rewrite put the paragraph marker around the shared ET, so the neighbour in that text object joined the paragraph and the next edit deleted it.
An unmarked paragraph was closing its marker at the following line's BT, which is after that line's BDC. The close now follows this paragraph's last ET.
Three or more rows at one repeated step stay one paragraph when the file has no TL, which is how LibreOffice writes each line. A rewrite that leaves a source row still marks the whole paragraph, so the neighbour survives a second edit. Dragging the box width keeps the line breaks.
The join limit follows the text object's TL when it has one, checked against the measured baseline gap. A rewrite of every row is marked so a new row one leading above the next paragraph does not absorb it.
A gap of 2.4 times the font size was still under the 2.5 times join limit, so two paragraphs became one and editing the first deleted the second. Once a paragraph has two rows, a wider gap than its own leading stays the next paragraph.
Two holes in alignment_frame:
- A column's right edge came from the runs that start at its left
edge, so a row of several styles ended at its first run, and the
column could fail to hold a title wider than that run. It now takes
the right edge of each complete row (row_rect).
- A row with no column to align in was framed by its own box, which
the rewrite also used as the width its text wraps at: changing an
isolated row's text together with an alignment wrapped it at its old
width, word by word. When the text changes and no column holds the
row, alignment_frame now returns None, so the row keeps the width it
has without the alignment. With the text unchanged the own box is
still the frame, so aligning twice is still a no-op.
alignment_frame measured a one-row paragraph's alignment in the frame
of the nearest overlapping block. A full-width heading closer to the
title than the title's own body column gave it a page-wide frame, so
centring or right-aligning the title moved it out of its column. It now
considers every overlapping block whose column holds the row and takes
the narrowest, the nearest on a tie, so a page-wide heading gives the
title no frame while a narrower column that holds it does. When no
column holds the row it is still measured in its own box.
The comment about page-wide headers named an exclusion the filter
never made; the doc comment now says what is chosen and why.
When Document::comment_layer gave nothing (a comment with nothing to draw, or
a document that can't be read), prepare_move asked again on every pointer
frame of the drag, and each time it copies and writes out the whole
document. The failure is now remembered for that drag (edit generation, page,
index); the next drag asks again.
A render that fails no longer drops the layer either: only its pixels go, so
the frame alone follows the pointer as before, the failed scale isn't waited
for, and another scale renders the same layer again without making it anew.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now gives page modules one pointer per frame (#2685); the comment drawn where it goes
follows `cx.ptr.pos`, as its frame does, instead of egui's hover position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A NoRotate comment on a quarter-turned page isn't drawn in its /Rect; its layer's tile and
its offset now come from the box it is shown in, as the frame it follows does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving a rectangle, an oval or any other comment showed only its blue
frame going, the shape staying where it was until release. The comment is
now rendered alone once as the drag starts (Document::comment_layer: a
read-only copy with only that comment, no page content) and laid over the
page where it goes; the page beneath keeps showing it where it was until
the move is applied, as a CAD program's move does. Rendering the page
without it would cost a whole page's render at every drag on a heavy plan.
Stamps and Fill & Sign marks keep their own layers. annot: isolate_annotations
for several comments of a page, so a group can be drawn together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eframe runs only App::logic while a window or browser tab is hidden, so a
?file= download that finished then opened the document without the titles
changing. The titles are now set at the end of logic as well as in ui.
Prepare's arrow nudge read the frame's Alt, so an Alt+arrow whose Alt was
let go in the same frame nudged the field instead. Each press's own
modifiers now decide, as for objects, pictures and comments.