mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 21:48:09 -06:00
test(ui): page modules read the pointer only through PagePointer
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cc4726f6e6
commit
3ff8222d72
@@ -724,10 +724,9 @@ pub(crate) fn panel(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
/// The calculation order on page `page`: the links of the field in focus, with the fields it has
|
||||
/// nothing to do with faded, and the steps of the focus and its linked fields; with nothing in
|
||||
/// focus, the steps of the fields whose place is wrong. A form with many calculations stays
|
||||
/// readable either way. Returns the field under the pointer.
|
||||
/// readable either way. Returns the field under the pages' `pointer`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn paint_page(
|
||||
ui: &egui::Ui,
|
||||
painter: &egui::Painter,
|
||||
xf: &PageXform,
|
||||
page: usize,
|
||||
@@ -736,6 +735,7 @@ pub(crate) fn paint_page(
|
||||
view: &CalcOrderView,
|
||||
generation: u64,
|
||||
selected: Option<&str>,
|
||||
pointer: Option<Pos2>,
|
||||
) -> Option<String> {
|
||||
let listed = view.current(generation)?;
|
||||
// Each field's first widget on this page.
|
||||
@@ -749,7 +749,6 @@ pub(crate) fn paint_page(
|
||||
if on_page.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let pointer = ui.input(|i| i.pointer.hover_pos());
|
||||
let hovered = pointer.and_then(|p| on_page.iter().rev().find(|(_, r)| r.contains(p))).map(|(n, _)| (*n).to_owned());
|
||||
let at: HashMap<&str, Rect> = on_page.iter().copied().collect();
|
||||
let zoom = xf.zoom().clamp(0.5, 3.0);
|
||||
|
||||
@@ -2836,11 +2836,8 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
// Full screen: a click on the document goes forward a page and a shift-click back, as in
|
||||
// Acrobat, unless the click did something else (a link, a word selected). Decided below,
|
||||
// once the pages have had the click.
|
||||
let present_click = view.presentation.is_some()
|
||||
&& navigation_enabled
|
||||
&& ui.rect_contains_pointer(navigation_rect)
|
||||
&& ui.input(|i| i.pointer.primary_clicked())
|
||||
&& app.quick_tool == QuickTool::Select;
|
||||
let present_click =
|
||||
view.presentation.is_some() && navigation_enabled && presentation_click(ui, navigation_rect) && app.quick_tool == QuickTool::Select;
|
||||
// Shift as held now, or as the click's own event carried it (a synthetic click).
|
||||
let present_back = present_click
|
||||
&& ui.input(|i| {
|
||||
@@ -2851,7 +2848,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
});
|
||||
if zoom_delta.is_finite() && zoom_delta > 0.0 && zoom_delta != 1.0 && navigation_enabled && ui.rect_contains_pointer(navigation_rect) {
|
||||
let z = view.zoom * zoom_delta;
|
||||
match ui.input(|i| i.pointer.hover_pos()) {
|
||||
match zoom_anchor(ui) {
|
||||
Some(p) => view.zoom_at(z, p),
|
||||
None => view.set_zoom(z),
|
||||
}
|
||||
@@ -2867,7 +2864,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
// Drawing a comment: the pointer in a band along any edge (or just past it) scrolls the view
|
||||
// that way, so a point beyond what is shown can be reached without letting go of the tool.
|
||||
let edge_delta = if unobstructed && matches!(view.comments.gesture, Some(comments::Gesture::Draw { .. })) {
|
||||
let (pointer, dt) = ui.input(|i| (if i.pointer.any_down() { i.pointer.latest_pos() } else { i.pointer.hover_pos() }, i.stable_dt));
|
||||
let (pointer, dt) = drawing_edge_pointer(ui);
|
||||
crate::autoscroll::edge_scroll(pointer, avail, dt)
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
@@ -3007,7 +3004,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
let may_glide = !paging_owns_vertical && !middle_gesture && navigation_enabled && unobstructed && animates;
|
||||
let over_page = ui.rect_contains_pointer(avail);
|
||||
ui.input(|i| view.fling.read(&i.events, now, may_glide && over_page));
|
||||
let interrupted = ui.input(|i| i.pointer.any_pressed() || i.events.iter().any(|e| matches!(e, egui::Event::Key { pressed: true, .. })));
|
||||
let interrupted = glide_interrupted(ui);
|
||||
if !may_glide || interrupted || key_scroll != 0.0 || auto_delta != Vec2::ZERO || zoom_delta != 1.0 {
|
||||
view.fling.stop();
|
||||
}
|
||||
@@ -3260,7 +3257,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
// A press on a page makes it the current one (as in Acrobat), so a page command with
|
||||
// no selection (Rotate, Delete) acts on the page the reader pointed at, not on a page
|
||||
// gone to earlier that still shows whole beside it.
|
||||
let pressed = unobstructed && ui.input(|i| i.pointer.primary_pressed()) && ui.rect_contains_pointer(avail);
|
||||
let pressed = unobstructed && ptr.pressed().is_some();
|
||||
let mut pressed_page = None;
|
||||
if resp.secondary_clicked() {
|
||||
// Set again below when the right-click lands on a link.
|
||||
@@ -3477,7 +3474,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
// Over a mark already placed, the Fill & Sign tool picks it up (comments, below).
|
||||
if let QuickTool::Fill(ft) = tool
|
||||
&& allowed
|
||||
&& !comments::fill_grabs(ui, &pcx, view)
|
||||
&& !comments::fill_grabs(&pcx, view)
|
||||
{
|
||||
match crate::fill_sign::page_input(
|
||||
ui,
|
||||
@@ -3536,7 +3533,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
use crate::forms_ui::FieldPointer;
|
||||
let field = if tool == QuickTool::Select
|
||||
&& !(editing_content && can_modify && crate::content_ui::select_grabs(&xf, &ptr, i, info, &added, view))
|
||||
&& !comments::select_grabs(ui, &pcx, view)
|
||||
&& !comments::select_grabs(&pcx, view)
|
||||
{
|
||||
crate::forms_ui::page_input(ui, &resp, &xf, &ptr, i, info, &form, can_fill, view)
|
||||
} else {
|
||||
@@ -3851,7 +3848,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
}
|
||||
if calc_order {
|
||||
let selected = view.prepare.selected.as_ref().map(|(name, _)| name.as_str());
|
||||
if let Some(name) = crate::calc_order_ui::paint_page(ui, painter, &xf, i, info, &form, &view.calc_order, generation, selected) {
|
||||
if let Some(name) =
|
||||
crate::calc_order_ui::paint_page(painter, &xf, i, info, &form, &view.calc_order, generation, selected, pointer)
|
||||
{
|
||||
calc_hover = Some(name);
|
||||
}
|
||||
}
|
||||
@@ -4716,6 +4715,31 @@ fn set_layers(app: &mut PdfCraftApp, index: usize, changes: &[(LayerOp, (u32, u1
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a pinch or ⌘-scroll zooms the view about: the pointer, wherever it is over the view. Not
|
||||
/// page input: it is read before the pages' widget exists, once `rect_contains_pointer` has found
|
||||
/// the view under the pointer.
|
||||
fn zoom_anchor(ui: &egui::Ui) -> Option<Pos2> {
|
||||
ui.input(|i| i.pointer.hover_pos())
|
||||
}
|
||||
|
||||
/// The pointer and frame time that scroll the view while a comment is drawn. Not page input: it is
|
||||
/// read before the pages' widget exists, and the pointer may be past the view's edge.
|
||||
fn drawing_edge_pointer(ui: &egui::Ui) -> (Option<Pos2>, f32) {
|
||||
ui.input(|i| (i.pointer.latest_pos(), i.stable_dt))
|
||||
}
|
||||
|
||||
/// A press anywhere, or a key, stops the touchpad's glide (#531).
|
||||
fn glide_interrupted(ui: &egui::Ui) -> bool {
|
||||
ui.input(|i| i.pointer.any_pressed() || i.events.iter().any(|e| matches!(e, egui::Event::Key { pressed: true, .. })))
|
||||
}
|
||||
|
||||
/// Full screen's click on the document, which turns the page: a primary click this frame with the
|
||||
/// view on top under it in `area`. Not page input: it is read before the pages' widget exists, and
|
||||
/// the turn waits until the pages have had the click.
|
||||
fn presentation_click(ui: &egui::Ui, area: Rect) -> bool {
|
||||
ui.rect_contains_pointer(area) && ui.input(|i| i.pointer.primary_clicked())
|
||||
}
|
||||
|
||||
/// Whether `p` lies in the bottom-left quadrant of `area`, where the page size shows (#1855).
|
||||
fn in_size_hint_zone(area: Rect, p: Pos2) -> bool {
|
||||
p.x >= area.min.x && p.x < area.center().x && p.y > area.center().y && p.y <= area.max.y
|
||||
|
||||
@@ -815,8 +815,8 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>,
|
||||
let page_rect = cx.xf.rect;
|
||||
let pressed_here = 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(ui, cx, view);
|
||||
let draw_grab = draw_grabs(ui, cx, view);
|
||||
let fill_grab = fill_grabs(cx, view);
|
||||
let draw_grab = draw_grabs(cx, view);
|
||||
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)) {
|
||||
@@ -1085,20 +1085,20 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>,
|
||||
/// moves it and the selected one's handles resize it. Elsewhere the tool places its mark. While
|
||||
/// the button is down the press decides; otherwise the pointer (egui clears the press origin on
|
||||
/// release, so a click is located by the pointer).
|
||||
pub(crate) fn fill_grabs(ui: &egui::Ui, cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
matches!(cx.tool, QuickTool::Fill(_)) && grabs(ui, cx, view, |a| a.fill_sign)
|
||||
pub(crate) fn fill_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
matches!(cx.tool, QuickTool::Fill(_)) && grabs(cx, view, |a| a.fill_sign)
|
||||
}
|
||||
|
||||
/// With the Select tool, a comment that would move (not text markup) is picked up before a form
|
||||
/// field under it, so a signature placed over a field can still be dragged off it.
|
||||
pub(crate) fn select_grabs(ui: &egui::Ui, cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
cx.tool == QuickTool::Select && grabs(ui, cx, view, |a| !is_markup(&a.subtype))
|
||||
pub(crate) fn select_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
cx.tool == QuickTool::Select && grabs(cx, view, |a| !is_markup(&a.subtype))
|
||||
}
|
||||
|
||||
/// With a shape tool (rectangle, oval, line, polygon…), the selected comment's resize handles
|
||||
/// are picked up as with the Select tool, so the shape just drawn can be resized straight away
|
||||
/// (#2338). Only the handles: a press anywhere else, the shape itself included, still draws.
|
||||
pub(crate) fn draw_grabs(ui: &egui::Ui, cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
pub(crate) fn draw_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
let QuickTool::Comment(tool) = cx.tool else { return false };
|
||||
if !(tool.draws() || tool.clicks_points()) || tool == CommentTool::Eraser || cx.hidden {
|
||||
return false;
|
||||
@@ -1111,12 +1111,12 @@ pub(crate) fn draw_grabs(ui: &egui::Ui, cx: &PageCx<'_>, view: &DocView) -> bool
|
||||
if cv.gesture.is_some() {
|
||||
return false;
|
||||
}
|
||||
press_point(ui, cx).is_some_and(|p| on_handle(cx, view, p, |_| true))
|
||||
press_point(cx).is_some_and(|p| on_handle(cx, view, p, |_| true))
|
||||
}
|
||||
|
||||
/// Whether the press (or, with no button down, the pointer) is on a `pick` comment or on the
|
||||
/// selected one's resize handles.
|
||||
fn grabs(ui: &egui::Ui, cx: &PageCx<'_>, view: &DocView, pick: impl Fn(&Annotation) -> bool) -> bool {
|
||||
fn grabs(cx: &PageCx<'_>, view: &DocView, pick: impl Fn(&Annotation) -> bool) -> bool {
|
||||
if cx.hidden {
|
||||
return false;
|
||||
}
|
||||
@@ -1124,15 +1124,14 @@ fn grabs(ui: &egui::Ui, cx: &PageCx<'_>, view: &DocView, pick: impl Fn(&Annotati
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let Some(p) = press_point(ui, cx) else { return false };
|
||||
let Some(p) = press_point(cx) else { return false };
|
||||
on_handle(cx, view, p, &pick) || cx.hit(p).is_some_and(pick)
|
||||
}
|
||||
|
||||
/// Where a gesture on this page is decided: the press while the button is down, otherwise the
|
||||
/// pointer (egui clears the press origin on release). `None` off the page.
|
||||
fn press_point(ui: &egui::Ui, cx: &PageCx<'_>) -> Option<Pos2> {
|
||||
let down = ui.input(|i| i.pointer.any_down());
|
||||
(if down { cx.ptr.origin } else { cx.ptr.pos }).filter(|p| cx.xf.rect.contains(*p))
|
||||
fn press_point(cx: &PageCx<'_>) -> Option<Pos2> {
|
||||
(if cx.ptr.any_down { cx.ptr.origin } else { cx.ptr.pos }).filter(|p| cx.xf.rect.contains(*p))
|
||||
}
|
||||
|
||||
/// Whether `p` is on a resize handle of the selected comment, when it is a resizable `pick`.
|
||||
|
||||
@@ -340,7 +340,7 @@ pub(crate) fn page_input(
|
||||
&& gp == page
|
||||
{
|
||||
consumed = true;
|
||||
if resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down()) {
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
cv.grab = None;
|
||||
let end = pointer.unwrap_or(start);
|
||||
if let Some((sp, si)) = cv.selected
|
||||
|
||||
@@ -560,7 +560,7 @@ fn crop_input(
|
||||
max.x = max.x.clamp(min.x + min_w, b.max.x);
|
||||
max.y = max.y.clamp(min.y + min_h, b.max.y);
|
||||
mode.keep = user_box(xf, info, page, Rect::from_min_max(min, max));
|
||||
if resp.drag_stopped() || !ui.input(|inp| inp.pointer.primary_down()) {
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
mode.drag = None;
|
||||
}
|
||||
}
|
||||
@@ -674,7 +674,7 @@ pub(crate) fn image_owns_pointer(ui: &egui::Ui, xf: &PageXform, ptr: &PagePointe
|
||||
let Some(image) = view.edit_images.get(&page).and_then(|(_, images)| images.get(selection.index)) else { return false };
|
||||
let Some(rect) = image.visible_rect else { return false };
|
||||
let b = xf.user_rect(info, page, rect.map(|v| v as f32));
|
||||
let p = if ui.input(|i| i.pointer.primary_down()) { ptr.origin } else { ptr.pos };
|
||||
let p = if ptr.primary_down { ptr.origin } else { ptr.pos };
|
||||
let knob = rotate_knob(b, ui.clip_rect()).0;
|
||||
p.is_some_and(|p| [b.left_top(), b.right_top(), b.left_bottom(), b.right_bottom(), knob].iter().any(|c| c.distance(p) < 8.0))
|
||||
}
|
||||
@@ -944,7 +944,7 @@ pub(crate) fn image_input(
|
||||
painter.rect(label, CornerRadius::same(3), Color32::WHITE, Stroke::new(1.0, ACCENT), egui::StrokeKind::Middle);
|
||||
painter.galley(label.center() - galley.size() / 2.0, galley, ACCENT);
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
if resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down()) {
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = None;
|
||||
s.turning = false;
|
||||
@@ -1002,7 +1002,7 @@ pub(crate) fn image_input(
|
||||
painter.rect_stroke(visible, CornerRadius::ZERO, Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside);
|
||||
handles(preview);
|
||||
}
|
||||
if resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down()) {
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = None;
|
||||
let norm = |r: Rect| {
|
||||
@@ -1272,7 +1272,7 @@ pub(crate) fn page_input(
|
||||
draw_handles(preview);
|
||||
}
|
||||
ui.ctx().set_cursor_icon(if resizing { egui::CursorIcon::ResizeHorizontal } else { egui::CursorIcon::Move });
|
||||
if resp.drag_stopped() || !ui.input(|i| i.pointer.any_down()) {
|
||||
if resp.drag_stopped() || !ptr.any_down {
|
||||
view.block_drag = None;
|
||||
let (from, to) = (user_box(xf, info, page, b), user_box(xf, info, page, preview));
|
||||
let edit = if resizing {
|
||||
|
||||
@@ -203,7 +203,7 @@ pub(crate) fn page_input(
|
||||
}
|
||||
// A text field opens on the press rather than the click, so a drag that starts on it
|
||||
// selects its text (#2213).
|
||||
let pressed = f.kind == FormFieldKind::Text && usable && resp.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_pressed());
|
||||
let pressed = f.kind == FormFieldKind::Text && usable && resp.is_pointer_button_down_on() && ptr.pressed().is_some();
|
||||
if pressed && view.forms.focus.as_ref().is_none_or(|x| x.name != f.name || x.widget != wi) {
|
||||
commit(view, form);
|
||||
view.forms.focus = Some(Focus { press: Some(p), ..open_focus(f, wi) });
|
||||
@@ -235,7 +235,7 @@ pub(crate) fn page_input(
|
||||
// The press already committed the previous field and opened this one (above), unless
|
||||
// press and release came in one frame. Committing again would re-run a validation that
|
||||
// refused the value on the press.
|
||||
FormFieldKind::Text if !ui.input(|i| i.pointer.primary_pressed()) => {
|
||||
FormFieldKind::Text if ptr.pressed().is_none() => {
|
||||
// An editor opened by the press cannot be hit until a frame has laid it out: the
|
||||
// release lands on the page and would take its focus away.
|
||||
if let Some(x) = view.forms.focus.as_mut().filter(|x| x.name == f.name && x.widget == wi) {
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(crate) fn page_input(
|
||||
if let Some((dp, start)) = lv.drag
|
||||
&& dp == page
|
||||
{
|
||||
if resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down()) {
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
lv.drag = None;
|
||||
let end = pointer.unwrap_or(start);
|
||||
let r = Rect::from_two_pos(start, end).intersect(xf.rect);
|
||||
|
||||
@@ -171,11 +171,10 @@ pub(crate) fn page_input(
|
||||
}
|
||||
if view.content.draft.is_some() || view.line_editor.as_ref().is_some_and(|e| e.has_changes()) {
|
||||
let blocked = view.objects.blocked_page == Some(page);
|
||||
let modifier_press =
|
||||
ui.input(|i| i.pointer.primary_pressed()) && modified(ui) && ptr.pos.is_some_and(|p| xf.rect.contains(p) && ui.clip_rect().contains(p));
|
||||
let modifier_press = ptr.pressed().is_some() && modified(ui) && ptr.pos.is_some_and(|p| xf.rect.contains(p) && ui.clip_rect().contains(p));
|
||||
if blocked || modifier_press {
|
||||
view.objects.hold_editor = true;
|
||||
view.objects.blocked_page = ui.input(|i| i.pointer.primary_down()).then_some(page);
|
||||
view.objects.blocked_page = ptr.primary_down.then_some(page);
|
||||
if modifier_press {
|
||||
view.objects.notice = Some(tl!("Finish or discard the text being edited before selecting several objects.").into());
|
||||
}
|
||||
@@ -201,7 +200,7 @@ fn input(
|
||||
view: &mut DocView,
|
||||
) -> bool {
|
||||
let pointer = ptr.pos;
|
||||
let (pressed, down) = ui.input(|i| (i.pointer.primary_pressed(), i.pointer.primary_down()));
|
||||
let (pressed, down) = (ptr.pressed().is_some(), ptr.primary_down);
|
||||
let info = &doc.info;
|
||||
if s.page == Some(page)
|
||||
&& let Some(mut gesture) = s.gesture
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! chrome over a page scrolled under it) and keeps a press with the widget it began on until
|
||||
//! release. egui's raw pointer state knows neither, and its hover position is gone on the frame
|
||||
//! a touch or a Windows pen lifts: egui-winit sends `PointerGone` with every touch release, and
|
||||
//! winit reports Windows pen input as touch. So page modules read [`PagePointer`], never
|
||||
//! `hover_pos`, `latest_pos` or `interact_pos`.
|
||||
//! winit reports Windows pen input as touch. So page modules read [`PagePointer`], never egui's
|
||||
//! pointer positions or button states; a test below keeps it that way.
|
||||
|
||||
use egui::{Event, PointerButton, Pos2, Rect};
|
||||
|
||||
@@ -33,12 +33,17 @@ pub(crate) struct PagePointer {
|
||||
pub origin: Option<Pos2>,
|
||||
/// This frame's primary presses and releases of a press the pages hold, in order.
|
||||
pub edges: Vec<Edge>,
|
||||
/// The primary button is down, wherever it was pressed: a gesture ends once it is up.
|
||||
pub primary_down: bool,
|
||||
/// Some button is down, wherever it was pressed.
|
||||
pub any_down: bool,
|
||||
}
|
||||
|
||||
impl PagePointer {
|
||||
/// The pointer for `resp`, the content widget that holds every page, allocated in `ui`.
|
||||
pub(crate) fn read(ui: &egui::Ui, resp: &egui::Response) -> Self {
|
||||
let (at, origin, edges) = ui.input(|i| (i.pointer.interact_pos(), i.pointer.press_origin(), edges(&i.events)));
|
||||
let (at, origin, edges, primary_down, any_down) =
|
||||
ui.input(|i| (i.pointer.interact_pos(), i.pointer.press_origin(), edges(&i.events), i.pointer.primary_down(), i.pointer.any_down()));
|
||||
// egui's own answer to "do the pages hold a press?": from the press on until the frame
|
||||
// of its click, release or drag stop. A drag can outlive egui's press while the
|
||||
// pointer is outside the window.
|
||||
@@ -49,9 +54,9 @@ impl PagePointer {
|
||||
let on_top = |p: &Pos2| resp.contains_pointer() && ui.ctx().layer_id_at(*p).is_none_or(|l| l == layer);
|
||||
let pos = at.filter(|p| held || on_top(p));
|
||||
if !held {
|
||||
return Self { pos, origin: None, edges: Vec::new() };
|
||||
return Self { pos, origin: None, edges: Vec::new(), primary_down, any_down };
|
||||
}
|
||||
Self { pos, origin: origin.or_else(|| first_press(&edges)), edges }
|
||||
Self { pos, origin: origin.or_else(|| first_press(&edges)), edges, primary_down, any_down }
|
||||
}
|
||||
|
||||
/// Where the pointer is, when it is inside `r`.
|
||||
@@ -164,10 +169,10 @@ mod tests {
|
||||
}
|
||||
// Off the pages altogether.
|
||||
assert_eq!(frame(&ctx, vec![Event::PointerMoved(pos2(300.0, 50.0))], true).pos, None);
|
||||
// A press on the Area is not the pages' press.
|
||||
// A press on the Area is not the pages' press, though the button is down all the same.
|
||||
let p = pos2(160.0, 160.0);
|
||||
let press = frame(&ctx, vec![button(p, PointerButton::Primary, true)], true);
|
||||
assert_eq!(press, PagePointer::default());
|
||||
assert_eq!(press, PagePointer { primary_down: true, any_down: true, ..PagePointer::default() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -180,14 +185,24 @@ mod tests {
|
||||
// Dragged over the Area and off the pages: still theirs.
|
||||
for p in [pos2(160.0, 160.0), pos2(300.0, 50.0)] {
|
||||
let held = frame(&ctx, vec![Event::PointerMoved(p)], true);
|
||||
assert_eq!((held.pos, held.origin), (Some(p), Some(start)), "{p:?}");
|
||||
assert_eq!((held.pos, held.origin, held.primary_down), (Some(p), Some(start), true), "{p:?}");
|
||||
}
|
||||
let end = pos2(300.0, 60.0);
|
||||
let up = frame(&ctx, vec![Event::PointerMoved(end), button(end, PointerButton::Primary, false)], true);
|
||||
assert_eq!((up.pos, up.edges.as_slice()), (Some(end), [Edge::Release(end)].as_slice()), "the release is the pages' too");
|
||||
assert!(!up.primary_down && !up.any_down, "and the button is up");
|
||||
assert_eq!(frame(&ctx, vec![], true).pos, None, "then off the pages, the pointer is not theirs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_secondary_press_is_some_button_down_but_no_primary_edge() {
|
||||
let ctx = Context::default();
|
||||
let p = pos2(50.0, 60.0);
|
||||
frame(&ctx, vec![Event::PointerMoved(p)], false);
|
||||
let right = frame(&ctx, vec![button(p, PointerButton::Secondary, true)], false);
|
||||
assert_eq!((right.pos, right.any_down, right.primary_down, right.edges.len()), (Some(p), true, false, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edges_are_primary_finite_in_order_and_capped() {
|
||||
let p = pos2(1.0, 2.0);
|
||||
@@ -204,4 +219,149 @@ mod tests {
|
||||
assert_eq!(edges(&many).len(), MAX_EDGES);
|
||||
assert_eq!(first_press(&[Edge::Release(q), Edge::Press(p), Edge::Press(q)]), Some(p));
|
||||
}
|
||||
|
||||
/// The page modules: the per-page chain in `canvas::document_area` and what it calls.
|
||||
const PAGE_MODULES: [&str; 18] = [
|
||||
"canvas.rs",
|
||||
"comments.rs",
|
||||
"forms_ui.rs",
|
||||
"prepare.rs",
|
||||
"fill_sign.rs",
|
||||
"sign_ui.rs",
|
||||
"crop.rs",
|
||||
"zoom_snap.rs",
|
||||
"camera_ui.rs",
|
||||
"measure_ui.rs",
|
||||
"redact_ui.rs",
|
||||
"link_ui.rs",
|
||||
"content_ui.rs",
|
||||
"edit_text_ui.rs",
|
||||
"object_ui.rs",
|
||||
"image_pick.rs",
|
||||
"signature_drag.rs",
|
||||
"calc_order_ui.rs",
|
||||
];
|
||||
|
||||
/// egui's raw pointer state: where the pointer is, and which buttons are down or went down.
|
||||
const RAW_READS: [&str; 15] = [
|
||||
"hover_pos()",
|
||||
"interact_pos()",
|
||||
"latest_pos()",
|
||||
"press_origin()",
|
||||
"pointer_hover_pos()",
|
||||
"pointer_interact_pos()",
|
||||
"pointer_latest_pos()",
|
||||
"primary_down()",
|
||||
"primary_pressed()",
|
||||
"primary_released()",
|
||||
"primary_clicked()",
|
||||
"any_down()",
|
||||
"any_pressed()",
|
||||
"any_released()",
|
||||
"any_click()",
|
||||
];
|
||||
|
||||
/// 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] = [
|
||||
("canvas.rs", "zoom_anchor", "a pinch zooms the whole view, and is read before the pages' widget exists"),
|
||||
("canvas.rs", "drawing_edge_pointer", "scrolls the view at its edges, read before the pages' widget exists"),
|
||||
("canvas.rs", "glide_interrupted", "a press anywhere stops the touchpad's glide"),
|
||||
("canvas.rs", "presentation_click", "full screen's click to turn the page, read before the pages' widget exists"),
|
||||
("canvas.rs", "page_size_hint", "the page size hint sits in the view's corner, not on a page"),
|
||||
("canvas.rs", "organize_grid", "Organize Pages shows thumbnails, not pages"),
|
||||
("comments.rs", "keys", "keyboard handling after the pages: a drag cancelled with Escape ends once no button is down"),
|
||||
("forms_ui.rs", "overlay", "the field editor is an Area of its own over the page"),
|
||||
("content_ui.rs", "editor", "the added-text editor is an Area of its own over the page"),
|
||||
("edit_text_ui.rs", "overlay", "the paragraph editor is an Area of its own over the page"),
|
||||
("fill_sign.rs", "scan_tab", "the signature dialog's Scan tab"),
|
||||
];
|
||||
|
||||
/// The function a `fn` line declares, laid out as rustfmt does (`pub`, `const` and the like
|
||||
/// before `fn`).
|
||||
fn declares(line: &str) -> Option<&str> {
|
||||
let mut rest = line;
|
||||
loop {
|
||||
let before = rest;
|
||||
for prefix in ["pub(crate) ", "pub(super) ", "pub ", "const ", "async ", "unsafe "] {
|
||||
rest = rest.strip_prefix(prefix).unwrap_or(rest);
|
||||
}
|
||||
if rest == before {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let rest = rest.strip_prefix("fn ")?;
|
||||
rest.find(|c: char| !(c.is_alphanumeric() || c == '_')).map(|end| &rest[..end])
|
||||
}
|
||||
|
||||
/// Whether `line` calls `read`, and not a longer name that ends in it.
|
||||
fn reads(line: &str, read: &str) -> bool {
|
||||
line.match_indices(read).any(|(at, _)| !line[..at].ends_with(|c: char| c.is_alphanumeric() || c == '_'))
|
||||
}
|
||||
|
||||
/// Page modules read the pointer only through `PagePointer`. egui's raw state misses what is
|
||||
/// drawn over the pages and where a touch lifted, so a raw read in a page module brings back
|
||||
/// the bugs `PagePointer` fixed: a cursor or a press through a dialog, a tap that does nothing.
|
||||
#[test]
|
||||
fn page_modules_read_the_pointer_only_through_page_pointer() {
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut problems = Vec::new();
|
||||
let mut needed = vec![false; NOT_PAGE_INPUT.len()];
|
||||
for file in PAGE_MODULES {
|
||||
let source = std::fs::read_to_string(src.join(file)).expect("page module").replace("\r\n", "\n");
|
||||
// Tests drive egui's input themselves.
|
||||
let code = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default();
|
||||
// The top-level function, or method of an `impl`, that each line is in.
|
||||
let (mut function, mut in_impl) = (None, false);
|
||||
for (n, line) in code.lines().enumerate() {
|
||||
if line.starts_with("impl") {
|
||||
(function, in_impl) = (None, true);
|
||||
} else if line == "}" {
|
||||
(function, in_impl) = (None, false);
|
||||
} else if let Some(name) = declares(line).or_else(|| line.strip_prefix(" ").filter(|_| in_impl).and_then(declares)) {
|
||||
function = Some(name);
|
||||
}
|
||||
if line.trim_start().starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
for read in RAW_READS.into_iter().filter(|read| reads(line, read)) {
|
||||
match NOT_PAGE_INPUT.iter().position(|(f, name, _)| *f == file && function == Some(*name)) {
|
||||
Some(k) => needed[k] = true,
|
||||
None => problems.push(format!(
|
||||
"src/{file}:{}: `{read}` reads egui's pointer state in a page module. Read `PagePointer` \
|
||||
(src/page_pointer.rs) instead: it knows what is drawn over the pages and where a touch \
|
||||
lifted. Not page input? Add the function to NOT_PAGE_INPUT there, with why.",
|
||||
n + 1
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for ((file, name, _), needed) in NOT_PAGE_INPUT.iter().zip(needed) {
|
||||
if !needed {
|
||||
problems.push(format!("src/{file}: fn {name} reads no pointer state any more: remove it from NOT_PAGE_INPUT."));
|
||||
}
|
||||
}
|
||||
// A module that takes the page pointer is a page module, so it is checked too.
|
||||
let mut dirs = vec![src.clone()];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("source directory") {
|
||||
let path = entry.expect("source entry").path();
|
||||
if path.is_dir() {
|
||||
dirs.push(path);
|
||||
continue;
|
||||
}
|
||||
if path.extension().is_none_or(|ext| ext != "rs") {
|
||||
continue;
|
||||
}
|
||||
let file =
|
||||
path.strip_prefix(&src).expect("under src").components().map(|c| c.as_os_str().to_string_lossy()).collect::<Vec<_>>().join("/");
|
||||
let source = std::fs::read_to_string(&path).expect("source file");
|
||||
if file != "page_pointer.rs" && source.contains("PagePointer") && !PAGE_MODULES.contains(&file.as_str()) {
|
||||
problems.push(format!("src/{file} takes the page pointer: add it to PAGE_MODULES."));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ pub(crate) fn page_input(
|
||||
return out;
|
||||
}
|
||||
if prep.ignore_release {
|
||||
if !ui.input(|i| i.pointer.primary_down()) {
|
||||
if !ptr.primary_down {
|
||||
prep.ignore_release = false;
|
||||
}
|
||||
out.consumed = true;
|
||||
@@ -273,7 +273,7 @@ pub(crate) fn page_input(
|
||||
&& gp == page
|
||||
{
|
||||
out.consumed = true;
|
||||
if resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down()) {
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
prep.grab = None;
|
||||
let end = pointer.unwrap_or(start);
|
||||
match grab {
|
||||
|
||||
@@ -250,7 +250,7 @@ pub(crate) fn page_input(
|
||||
if let Some((dp, start)) = view.redact_drag
|
||||
&& dp == page
|
||||
{
|
||||
if resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down()) {
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
view.redact_drag = None;
|
||||
let end = pointer.unwrap_or(start);
|
||||
let r = Rect::from_two_pos(start, end).intersect(xf.rect);
|
||||
|
||||
Reference in New Issue
Block a user