refactor(ui): tell comments and Fill & Sign whether the press is theirs

Comments ran last in the page view's chain of `||`, so only when no earlier module had taken the
frame, and asked the router themselves (`grabs`, `fill_grabs`, `draw_grabs`), with a hit test of
their own for hover. A link, a field or added text taking the frame first hid a comment's
gesture end, its Escape and the click that unselects it.

Comments now run on every frame and are told the page's `PagePress`:
- a press on a comment (its body, a handle or a point) goes to `select_input` with the part,
  with the Select tool, a shape tool (the selected shape's handles) or Fill & Sign (its marks);
- the drawing tools act only on a press nobody holds (`Gestures::free`);
- a click on anything but a comment leaves comments unselected.

`grabs`, `fill_grabs`, `draw_grabs` and `cancelled_drag` (with the raw button read in `keys` it
needed) are gone; `grabs_points` is `reaches_points`. Fill & Sign leaves the press to a mark
when the press is someone's, or, with no button down, when a mark is the top hit under the
pointer. The page's hover is that top hit (`PageInput.hover`), and the comments' hover outline
follows it, so it no longer shows under a field or added text that would take a press.

Behaviour change (the user's ruling, 2026-10-31): a click on a form field, or on added text,
leaves the selected comment unselected, as a click on a link or the page already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alehel
2026-11-02 20:38:52 +01:00
co-authored by Claude Opus 5.5
parent 1780f33350
commit 84fe5e54b3
11 changed files with 164 additions and 99 deletions
+2 -1
View File
@@ -1496,7 +1496,8 @@ fn draw_page(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<'_>, i
let slot = PageSlot { i, r, xf };
// A cancelled press takes nothing until the button is up.
let input = if view.press_cancelled { PageInput { consumed: true, area_tool: false } } else { page_tool_input(ui, view, frame, &slot) };
let input =
if view.press_cancelled { PageInput { consumed: true, area_tool: false, hover: None } } else { page_tool_input(ui, view, frame, &slot) };
let painter = ui.painter();
let pcx = comments::PageCx { page: i, xf: &xf, ptr, info, tool, prefs, allowed, hidden: comments_hidden, redact_mark };
let preview_target = (matches!(tool, QuickTool::Select | QuickTool::Fill(_)) && !comments_hidden).then_some(view.comments.selected).flatten();
+24 -13
View File
@@ -16,6 +16,8 @@ pub(super) struct PageInput {
pub(super) consumed: bool,
/// Redact or Highlight draws boxes off text.
pub(super) area_tool: bool,
/// What a press under the pointer would take (the top hit there), for hover painting.
pub(super) hover: Option<crate::page_router::Hit>,
}
/// Common geometry and pointer state for a tool acting on one page.
@@ -153,10 +155,13 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
frame.out.stamp_placed = true;
}
}
// Over a mark already placed, the Fill & Sign tool picks it up (comments, below).
// A mark already placed takes its own press (comments, below), and with no button down the
// tool's preview hides over one, where a press would pick it up.
let over_mark = matches!(tool, QuickTool::Fill(_))
&& (matches!(press, PagePress::Mine(_)) || !ptr.any_down && pointer.and_then(|p| top(hits.at(view, p), tool)).is_some());
if let QuickTool::Fill(ft) = tool
&& allowed
&& !comments::fill_grabs(&pcx, view)
&& !over_mark
{
match crate::fill_sign::page_input(
cx,
@@ -283,17 +288,23 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
{
view.links.context = Some((i, index));
}
let consumed = on_objects || on_edit_text || on_link || on_content || boxing || on_field || comments::page_input(ui, resp, &pcx, view);
// The cursor and the tip from what lies under the pointer, after the tools claimed theirs (the
// last claim on a layer stands).
if frame.env.unobstructed
&& let Some(p) = pointer
{
for h in hits.at(view, p) {
view.cursor.claim_with_tip(h.layer, h.icon, h.tip);
}
// Comments, every frame: their own presses, a commenting tool's, and a click elsewhere that
// leaves them unselected.
let on_comment = comments::page_input(ui, resp, &pcx, press, view);
let consumed = on_objects || on_edit_text || on_link || on_content || boxing || on_field || on_comment;
let hover = pointer.filter(|_| frame.env.unobstructed).and_then(|p| claim_under(&hits, view, p));
PageInput { consumed, area_tool, hover }
}
/// The cursor and the tip from what lies under the pointer at `p`, after the tools claimed theirs
/// (the last claim on a layer stands). Returns the top of it, what a press there would take, for
/// hover painting.
fn claim_under(hits: &PageHits<'_>, view: &mut DocView, p: Pos2) -> Option<crate::page_router::Hit> {
let under = hits.at(view, p);
for h in &under {
view.cursor.claim_with_tip(h.layer, h.icon, h.tip.clone());
}
PageInput { consumed, area_tool }
top(under, hits.tool)
}
/// The link `owner` is, by its `/Annots` index.
@@ -701,7 +712,7 @@ pub(super) fn page_overlays(ui: &mut egui::Ui, view: &mut DocView, frame: &mut P
if input.area_tool {
crate::redact_ui::paint(painter, i, view, pointer);
}
comments::paint_page(ui, painter, &pcx, view);
comments::paint_page(ui, painter, &pcx, view, input.hover.as_ref());
if editing_content {
crate::content_ui::paint_page(painter, &xf, i, info, added, view, pointer);
}
@@ -217,14 +217,13 @@ const RAW_READS: [&str; 21] = [
/// Functions in the page modules that are not page input and read egui's pointer state
/// themselves: (file, function, why).
const NOT_PAGE_INPUT: [(&str, &str, &str); 11] = [
const NOT_PAGE_INPUT: [(&str, &str, &str); 10] = [
("canvas/area.rs", "zoom_anchor", "a pinch zooms the whole view, and is read before the pages' widget exists"),
("canvas/area.rs", "drawing_edge_pointer", "scrolls the view at its edges, read before the pages' widget exists"),
("canvas/area.rs", "glide_interrupted", "a press anywhere stops the touchpad's glide"),
("canvas/area.rs", "presentation_click", "full screen's click to turn the page, read before the pages' widget exists"),
("canvas/bars.rs", "page_size_hint", "the page size hint sits in the view's corner, not on a page"),
("canvas/organize.rs", "organize_grid", "Organize Pages shows thumbnails, not pages"),
("tools/comments/input.rs", "keys", "keyboard handling after the pages: a drag cancelled with Escape ends once no button is down"),
("tools/forms_ui.rs", "overlay", "the field editor is an Area of its own over the page"),
("tools/content_ui.rs", "editor", "the added-text editor is an Area of its own over the page"),
("tools/edit_text_ui.rs", "overlay", "the paragraph editor is an Area of its own over the page"),
@@ -416,7 +415,7 @@ fn hit_test_calls(line: &str) -> Vec<String> {
/// The functions that call a hit test: (file, function, why). Only `PageHits::at` asks at the
/// press point; the others ask where the pointer is.
const HIT_TESTERS: [(&str, &str, &str); 4] = [
const HIT_TESTERS: [(&str, &str, &str); 3] = [
(
"canvas/page_input.rs",
"hits",
@@ -424,7 +423,6 @@ const HIT_TESTERS: [(&str, &str, &str); 4] = [
),
("canvas/page_input.rs", "page_overlays", "a link's click counts only released over the link its press was on"),
("tools/forms_ui.rs", "page_input", "the field's tooltip under the pointer, and a click counts only released over the widget its press was on"),
("tools/comments/input.rs", "grabs", "with no button down, whether a comment would take a press under the pointer, for hover painting"),
];
/// What ignores the press the pages hold until the button is up.
@@ -493,7 +491,6 @@ fn page_modules_act_on_a_press_only_through_the_router() {
("tools/edit_text_ui.rs", "page_input"),
("tools/edit_text_ui/image.rs", "image_input"),
("tools/object_ui.rs", "input"),
("tools/comments/input.rs", "select_input"),
] {
let source = std::fs::read_to_string(src.join(file)).expect("page input module");
let body = source.lines().skip_while(|line| declares(line) != Some(function)).take_while(|line| *line != "}").collect::<Vec<_>>().join("\n");
+1 -2
View File
@@ -402,8 +402,7 @@ mod tests {
let doc = session.get(id).expect("open");
let index = doc.info.annotations.iter().find(|a| a.subtype == "Sound").expect("listed").index;
let ctx = egui::Context::default();
let mut cv = CommentView::default();
cv.gesture = Some(Gesture::Move { page: 0, index, from: pos2(0.0, 0.0) });
let mut cv = CommentView { gesture: Some(Gesture::Move { page: 0, index, from: pos2(0.0, 0.0) }), ..CommentView::default() };
let mut drag = SignatureDrag::default();
let key = Some((doc.edit_generation(), 0, index));
drag.prepare_move(&ctx, doc, &cv, None, false, 1.0);
+1 -1
View File
@@ -37,7 +37,7 @@ pub(crate) use geometry::{corner_ratio, page_rotation, resized};
pub use geometry::{drawn_text_box_rect, text_box_rect};
#[cfg(test)]
use input::hover_tip;
pub(crate) use input::{context, context_hit, describe, fill_grabs, hit, keys, nudge_keys, page_after_text, page_input};
pub(crate) use input::{context, context_hit, describe, hit, keys, nudge_keys, page_after_text, page_input};
pub use menu::{CanvasAction, fill_picker, recent_custom_colours, remember_custom_colour, status_badge, swatch_grid};
#[cfg(test)]
use menu::{RECENT_CUSTOM, same_colour};
@@ -20,10 +20,10 @@ pub(super) fn reshapable(a: &Annotation) -> bool {
&& !a.intent.as_deref().is_some_and(|i| i.ends_with("Dimension"))
}
/// Whether `tool` picks up a line's end and corner handles: the Select and Fill & Sign tools.
/// Whether `tool` reaches a line's end and corner handles: the Select and Fill & Sign tools.
/// The drawing tools draw from where they are pressed, so a new line starts at the end of the
/// one just drawn rather than dragging it.
pub(super) fn grabs_points(tool: QuickTool) -> bool {
pub(super) fn reaches_points(tool: QuickTool) -> bool {
!matches!(tool, QuickTool::Comment(t) if t.draws() || t.clicks_points())
}
+52 -63
View File
@@ -1,43 +1,47 @@
use super::geometry::{
CARET_SIZE, HANDLES, clamp_to, corner_ratio, drawn_box, grabs_points, handle_pos, is_markup, point_handle, reshapable, reshaped, resizable,
CARET_SIZE, HANDLES, clamp_to, corner_ratio, drawn_box, handle_pos, is_markup, point_handle, reaches_points, reshapable, reshaped, resizable,
resized, snapped_caret,
};
use super::paint::{drawn_shape, edit_anchor, erase, new_comment};
use super::state::{Composer, ComposerKind, Gesture, PageCx};
use super::{Annotation, CommentPrefs, CommentTool, DocInfo, DocView, Edit, Pos2, QuickTool, Rect, Shape, vec2};
use crate::page_cursor::{Layer, Subject};
use crate::page_router::{CommentPart, Hit, Owner};
use crate::page_router::{CommentPart, Hit, Owner, PagePress};
/// Comment input on one page, before text selection runs. Returns `true` when the pointer
/// gesture belongs to commenting (text selection must ignore it).
pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>, view: &mut DocView) -> bool {
/// Comment input on one page, before text selection runs. `press` is what this page has to do with
/// the press the pages hold: a press on a comment ([`hit`]) is that comment's, with any tool, and
/// one nobody holds is a commenting tool's. Returns `true` when the pointer gesture belongs to
/// commenting (text selection must ignore it).
pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>, press: PagePress, view: &mut DocView) -> bool {
let (pointer, origin) = (cx.ptr.pos, cx.ptr.origin);
let page_rect = cx.xf.rect;
let pressed_here = origin.is_some_and(|o| page_rect.contains(o));
let free = press == PagePress::Free;
let pressed_here = free && origin.is_some_and(|o| page_rect.contains(o));
let over_page = pointer.is_some_and(|p| page_rect.contains(p));
let fill_grab = fill_grabs(cx, view);
let draw_grab = draw_grabs(cx, view);
let owned = match press {
PagePress::Mine(Owner::Comment { index, part }) => Some((index, part)),
_ => None,
};
let caret_text = if cx.tool == QuickTool::Comment(CommentTool::Caret) { view.page_text(cx.page) } else { None };
let cv = &mut view.comments;
if cv.gesture.is_some() && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
cv.gesture = None;
cv.cancelled_drag = true;
return true;
}
if cv.cancelled_drag {
return true;
}
let gestures = Gestures { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page };
// A comment being moved, resized or reshaped on this page keeps the frame until it ends.
let moving =
matches!(cv.gesture, Some(Gesture::Move { page, .. } | Gesture::Resize { page, .. } | Gesture::Reshape { page, .. }) if page == cx.page);
let gestures = Gestures { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page, free };
match cx.tool {
QuickTool::Comment(_) if draw_grab => select_input(ui, resp, cx, view, pointer, origin),
// The selected shape's handles, and Fill & Sign's marks, take their own presses.
QuickTool::Comment(_) | QuickTool::Fill(_) if owned.is_some() || moving => select_input(ui, resp, cx, view, owned, pointer, origin),
QuickTool::Comment(tool) if tool.draws() => gestures.draw_shape(view, tool),
QuickTool::Comment(tool) if tool.clicks_points() => gestures.click_points(view, tool),
QuickTool::Comment(CommentTool::Callout) => gestures.callout(view),
QuickTool::Comment(CommentTool::Attach) => gestures.attach(view),
QuickTool::Comment(CommentTool::Caret) => gestures.caret(view, caret_text.as_deref()),
QuickTool::Comment(tool @ (CommentTool::Note | CommentTool::TextBox)) => gestures.note_or_box(view, tool),
QuickTool::Select => select_input(ui, resp, cx, view, pointer, origin),
QuickTool::Fill(_) if fill_grab => select_input(ui, resp, cx, view, pointer, origin),
QuickTool::Select => select_input(ui, resp, cx, view, owned, pointer, origin),
_ => false,
}
}
@@ -68,7 +72,7 @@ pub(crate) fn hit(cx: &PageCx<'_>, view: &DocView, p: Pos2, on_link: bool) -> Op
};
let bodies = matches!(cx.tool, QuickTool::Select | QuickTool::Fill(_));
if let Some(a) = view.comments.selected.filter(|(page, _)| *page == cx.page).and_then(|(_, i)| cx.get(i)).filter(|a| pick(a) && cx.allowed) {
if grabs_points(cx.tool)
if reaches_points(cx.tool)
&& reshapable(a)
&& let Some(point) = point_handle(cx, a, p)
{
@@ -119,32 +123,6 @@ pub(crate) fn context(cx: &PageCx<'_>, p: Pos2, index: Option<usize>, view: &mut
view.comments.selected = index.map(|i| (cx.page, i));
}
/// Whether the comments take the press on this page (or, with no button down, would take one
/// under the pointer), so the tools beneath leave it: a live move, resize or reshape, or a press
/// held by a comment that keeps its drag.
fn grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
if matches!(view.comments.gesture, Some(Gesture::Move { page, .. } | Gesture::Resize { page, .. } | Gesture::Reshape { page, .. }) if page == cx.page)
{
return true;
}
if cx.ptr.any_down {
return matches!(view.router.owner(cx.page), Some(Owner::Comment { .. })) && view.router.keeps_drag();
}
// Only the Select tool lets a link through a highlight, and it doesn't come here.
cx.ptr.pos.and_then(|p| hit(cx, view, p, false)).is_some_and(|h| !h.declines_drag)
}
/// With a Fill & Sign tool, a Fill & Sign mark already on the page is picked up as with the Select
/// tool ([`hit`]), so the tool does not place a mark there.
pub(crate) fn fill_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
matches!(cx.tool, QuickTool::Fill(_)) && grabs(cx, view)
}
/// With a shape tool, the selected comment's handles are picked up ([`hit`]).
fn draw_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
matches!(cx.tool, QuickTool::Comment(_)) && grabs(cx, view)
}
/// The tip for the comment under the pointer, with any tool, beside that tool's own cursor: Acrobat
/// shows a comment's tooltip whatever tool is active. Not with Fill & Sign, whose placement stays
/// clear; not while drawing freehand, where it would cover the next stroke (#429); and not while a
@@ -180,13 +158,19 @@ pub(super) fn hover_tip(a: &Annotation, prefs: &CommentPrefs) -> Option<String>
Some(if body.is_empty() { who } else { format!("{who}\n{body}") })
}
/// The Select tool: select, move, resize, edit, delete, on the comment the press was on (its
/// owner, from [`hit`]). The cursor comes from [`hit`] too.
fn select_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>, view: &mut DocView, pointer: Option<Pos2>, origin: Option<Pos2>) -> bool {
let owner = match view.router.owner(cx.page) {
Some(Owner::Comment { index, part }) => cx.get(index).map(|a| (a, part)),
_ => None,
};
/// The Select tool: select, move, resize, edit, delete, on the comment the press is on and the
/// part of it (`owned`, from [`hit`]). The cursor comes from [`hit`] too. A click on anything else
/// (a link, a field, added text, the page) leaves comments unselected.
fn select_input(
ui: &egui::Ui,
resp: &egui::Response,
cx: &PageCx<'_>,
view: &mut DocView,
owned: Option<(usize, CommentPart)>,
pointer: Option<Pos2>,
origin: Option<Pos2>,
) -> bool {
let owner = owned.and_then(|(index, part)| cx.get(index).map(|a| (a, part)));
let cv = &mut view.comments;
let mut consumed = false;
if resp.drag_started()
@@ -262,8 +246,8 @@ fn select_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>, view: &mu
// (egui clears the press origin on release, so clicks are located by the pointer.)
let on_page = pointer.is_some_and(|p| cx.xf.rect.contains(p));
if resp.clicked() && on_page {
// A click elsewhere (a link beneath text highlighting included: it follows the link)
// leaves comments unselected.
// A click elsewhere (a link beneath text highlighting included: it follows the link; a
// form field; added text) leaves comments unselected.
match owner {
Some((a, _)) => {
cv.selected = Some((cx.page, a.index));
@@ -326,13 +310,21 @@ struct Gestures<'a> {
pointer: Option<Pos2>,
origin: Option<Pos2>,
page_rect: Rect,
/// The press began on this page, and nobody holds it: the tool's.
pressed_here: bool,
over_page: bool,
/// Nobody holds the press, and it began on this page, or no button is down.
free: bool,
}
impl Gestures<'_> {
/// The tool's own click: one nobody holds.
fn clicked(&self) -> bool {
self.free && self.resp.clicked()
}
fn draw_shape(&self, view: &mut DocView, tool: CommentTool) -> bool {
let Self { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page } = *self;
let Self { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page, .. } = *self;
let cv = &mut view.comments;
if !cx.allowed {
return false;
@@ -380,7 +372,7 @@ impl Gestures<'_> {
// it as a one-point ink stroke (a dot). The appearance uses round line caps, so a
// single PDF point is visible and remains editable like any other drawing.
if matches!(tool, CommentTool::Ink | CommentTool::FreehandHighlight)
&& resp.clicked()
&& self.clicked()
&& over_page
&& let Some(p) = pointer
&& let Some(shape) = drawn_shape(tool, &[cx.to_user(p)])
@@ -412,7 +404,7 @@ impl Gestures<'_> {
cv.undo_point();
return true;
}
if resp.clicked()
if self.clicked()
&& over_page
&& let Some(p) = pointer
{
@@ -492,7 +484,7 @@ impl Gestures<'_> {
}
fn attach(&self, view: &mut DocView) -> bool {
let Self { resp, cx, pointer, over_page, .. } = *self;
let Self { cx, pointer, over_page, .. } = *self;
let cv = &mut view.comments;
if !cx.allowed {
return false;
@@ -500,7 +492,7 @@ impl Gestures<'_> {
if over_page {
view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Crosshair);
}
if resp.clicked()
if self.clicked()
&& over_page
&& let Some(p) = pointer
{
@@ -511,7 +503,7 @@ impl Gestures<'_> {
}
fn caret(&self, view: &mut DocView, caret_text: Option<&pdfcraft_render::PageText>) -> bool {
let Self { resp, cx, pointer, over_page, .. } = *self;
let Self { cx, pointer, over_page, .. } = *self;
let cv = &mut view.comments;
if !cx.allowed {
return false;
@@ -519,7 +511,7 @@ impl Gestures<'_> {
if over_page {
view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Text);
}
if resp.clicked()
if self.clicked()
&& over_page
&& let Some(p) = pointer
{
@@ -573,7 +565,7 @@ impl Gestures<'_> {
}
}
}
if resp.clicked()
if self.clicked()
&& over_page
&& let Some(p) = pointer
{
@@ -587,9 +579,6 @@ impl Gestures<'_> {
/// Delete / Escape handling for comments (only while no text field has focus).
pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView, tool: &mut QuickTool, allowed: bool) {
if !ctx.input(|i| i.pointer.any_down()) {
view.comments.cancelled_drag = false;
}
if ctx.egui_wants_keyboard_input() {
return;
}
+18 -10
View File
@@ -1,12 +1,24 @@
use super::geometry::{HANDLES, grabs_points, handle_pos, is_markup, reshapable, reshaped, resizable};
use super::state::{Gesture, PageCx, SETTLE_SECS};
use super::geometry::{HANDLES, handle_pos, is_markup, reaches_points, reshapable, reshaped, resizable};
use super::state::{CommentView, Gesture, PageCx, SETTLE_SECS};
use super::{
Annotation, Color32, CommentTool, CornerRadius, DocInfo, DocView, Edit, LineEnding, NewAnnotation, Pos2, QuickTool, Rect, SELECT_BLUE, Shape,
Stroke, color32, vec2,
};
use crate::page_router::{CommentPart, Hit, Owner};
/// Paint comment selection, hover and gesture previews on a page.
pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, cx: &PageCx<'_>, view: &DocView) {
/// The comment outlined under the pointer: the one a press there would take (`hover`, the top hit),
/// with the Select tool or, over its own marks, Fill & Sign. Not the selected comment, nor while a
/// comment is moved, resized or drawn, nor one under a form field or added text that would take
/// the press.
pub(super) fn hover_outline<'c>(cx: &'c PageCx<'_>, cv: &CommentView, hover: Option<&Hit>) -> Option<&'c Annotation> {
let Some(Owner::Comment { index, part: CommentPart::Body }) = hover.map(|h| h.owner) else { return None };
let tool_outlines = matches!(cx.tool, QuickTool::Select | QuickTool::Fill(_));
(cv.gesture.is_none() && tool_outlines && cv.selected != Some((cx.page, index))).then(|| cx.get(index)).flatten()
}
/// Paint comment selection, hover and gesture previews on a page. `hover` is the top hit under the
/// pointer.
pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, cx: &PageCx<'_>, view: &DocView, hover: Option<&Hit>) {
let cv = &view.comments;
let pointer = cx.ptr.pos;
// A redaction mark shows its fill while the pointer is over it, as Acrobat's does, so the
@@ -21,11 +33,7 @@ pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, cx: &PageCx<'_>
painter.rect_filled(rect, CornerRadius::ZERO, fill);
}
}
if cv.gesture.is_none()
&& let Some(a) = pointer.and_then(|p| cx.hit(p))
&& (cx.tool == QuickTool::Select || matches!(cx.tool, QuickTool::Fill(_)) && a.fill_sign)
&& cv.selected != Some((cx.page, a.index))
{
if let Some(a) = hover_outline(cx, cv, hover) {
for r in cx.screen_rects(a) {
painter.rect_stroke(r.expand(2.0), CornerRadius::same(2), Stroke::new(1.0, SELECT_BLUE.gamma_multiply(0.7)), egui::StrokeKind::Outside);
}
@@ -45,7 +53,7 @@ pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, cx: &PageCx<'_>
return paint_gesture(painter, cx, view);
}
let shift = ui.input(|i| i.modifiers.shift);
if cx.allowed && grabs_points(cx.tool) && reshapable(a) {
if cx.allowed && reaches_points(cx.tool) && reshapable(a) {
return paint_points(painter, cx, view, a, pointer, shift);
}
let r = cx.adjusted_rect(a, cv.gesture.as_ref(), pointer, shift, view.pending_edit.as_ref()).expand(2.0);
@@ -133,8 +133,6 @@ pub struct CommentView {
/// Drawings just released: shown from their points until the page raster that includes the
/// committed annotation arrives, so the stroke never vanishes in between (#1858).
pub(crate) settling: Vec<Settling>,
/// Escape ends egui's drag too; ignore its synthetic release until the mouse is up.
pub(super) cancelled_drag: bool,
pub composer: Option<Composer>,
/// Reply being typed under the selected card.
pub reply: String,
@@ -499,3 +499,39 @@ fn text_boxes_fit_their_text() {
assert_eq!(long[2], 300.0);
assert!(long[3] - long[1] > 50.0);
}
/// The hover outline goes round the comment a press under the pointer would take (the top hit
/// there): not a highlight under a form field, which takes the press.
#[test]
fn the_hover_outline_goes_round_what_a_press_would_take() {
use super::paint::hover_outline;
use crate::page_cursor::Layer;
use crate::page_router::{CommentPart, Hit, Owner};
let page = PageInfo { width: 200.0, height: 200.0, label: String::new(), crop: [0.0, 0.0, 200.0, 200.0], rotation: 0 };
let xf = PageXform { rect: Rect::from_min_size(Pos2::ZERO, vec2(200.0, 200.0)), rot: 0, pw: 200.0, ph: 200.0 };
let highlight = Annotation { rect: [50.0, 50.0, 150.0, 150.0], ..annotation("Highlight", None, false) };
let info = DocInfo { pages: vec![page], annotations: vec![highlight], ..Default::default() };
let prefs = CommentPrefs::default();
// The pointer is over the highlight.
let ptr = PagePointer { pos: Some(pos2(100.0, 100.0)), ..PagePointer::default() };
let cx = PageCx {
page: 0,
xf: &xf,
ptr: &ptr,
info: &info,
tool: QuickTool::Select,
prefs: &prefs,
allowed: true,
hidden: false,
redact_mark: ([0.0; 3], 1.0),
};
let on = |owner, layer| Hit::new(owner, layer, egui::CursorIcon::PointingHand);
let comment = on(Owner::Comment { index: 0, part: CommentPart::Body }, Layer::Comment);
let field = on(Owner::Field { field: 0, widget: 0 }, Layer::Field);
let cv = CommentView::default();
assert!(hover_outline(&cx, &cv, Some(&comment)).is_some(), "the highlight a press there selects");
assert!(hover_outline(&cx, &cv, Some(&field)).is_none(), "not under a field that takes the press");
assert!(hover_outline(&cx, &cv, None).is_none(), "nor where nothing would take it");
let selected = CommentView { selected: Some((0, 0)), ..CommentView::default() };
assert!(hover_outline(&cx, &selected, Some(&comment)).is_none(), "the selected comment has its own frame");
}
+26
View File
@@ -1129,3 +1129,29 @@ fn a_right_click_on_a_comment_with_a_drawing_tool_opens_its_menu() {
right_click(&mut h, on_comment);
assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "the comment is selected for its menu");
}
/// The check box `agree` of [`checkbox_pdf`], with a rectangle comment at x 150..250, y 100..150.
fn comment_and_checkbox_pdf() -> Vec<u8> {
pdf(&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R] /DA (/ZaDb 0 Tf 0 g) /DR << /Font << /ZaDb 5 0 R >> >> >> >>",
PAGES,
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 6 0 R] >>",
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [100 200 120 220] /P 3 0 R /F 4 /MK << /CA (4) >> /DA (/ZaDb 11 Tf 0 g) >>",
"<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>",
"<< /Type /Annot /Subtype /Square /Rect [150 100 250 150] /C [1 0 0] /F 4 /T (Ana) >>",
])
}
/// A click on anything but the selected comment leaves it unselected, a form field included, as
/// a click on a link or on the page does. A field's click used to keep it selected.
#[test]
fn a_click_on_a_form_field_deselects_the_selected_comment() {
let mut h = open(comment_and_checkbox_pdf(), "150", false);
let comment = at(&h, 200.0, 125.0);
mouse_click(&mut h, comment);
assert_eq!(h.state().views[0].comments.selected, Some((0, 1)), "the comment is selected");
let check_box = at(&h, 110.0, 210.0);
mouse_click(&mut h, check_box);
assert_eq!(h.state().views[0].comments.selected, None, "a click on the check box leaves it unselected");
assert_eq!(agree(&h).len(), 1, "and ticks the box");
}