fix(guided): the press position survives press+release in one frame

egui clears `press_origin` on the release, so a slow frame that swallows
press, move and release into one batch reports `primary_pressed` with no
origin: the gesture fell back to the release position and a real drag
wrote nothing (its length check saw a zero-length stub). The gate now
reads the primary press back from the frame's own events before that
fallback — the press decides what it means, wherever the batch ended.
This commit is contained in:
mahmatias
2026-10-21 14:13:18 -03:00
parent c19265dee0
commit 77b0204331
2 changed files with 38 additions and 1 deletions
+13 -1
View File
@@ -1883,9 +1883,21 @@ fn guided_overlay(
// a menu can receive the press and the pointer can leave it within the same frame, so the
// press itself must belong to the loupe's layer. Anything floating above the photo — a
// context menu, a dropdown, a dialog — owns its own press; the guide under it stays put.
// A slow frame can also swallow the whole press-move-release into one batch, and egui
// clears `press_origin` on the release: then the press position is read back from the
// frame's own events, so the gesture still starts where the pointer went down.
let nav = app.widgets.iter().rev().find(|(w, _)| w == "canvas:navigator").map(|(_, r)| *r);
if ui.input(|i| i.pointer.primary_pressed())
&& let Some(q) = ui.input(|i| i.pointer.press_origin()).or(resp.interact_pointer_pos())
&& let Some(q) = ui
.input(|i| {
i.pointer.press_origin().or_else(|| {
i.events.iter().rev().find_map(|e| match e {
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, .. } => Some(*pos),
_ => None,
})
})
})
.or(resp.interact_pointer_pos())
&& resp.contains_pointer()
&& ui.ctx().layer_id_at(q) == Some(resp.layer_id)
&& resp.rect.contains(q)
+25
View File
@@ -207,6 +207,31 @@ fn a_new_guide_starts_at_the_press_point() {
assert!(rect.right_top().distance(p2) < 2.0, "and ends at the release point: {rect:?} vs {p2:?}");
}
/// A slow frame can swallow press, move and release into a single batch: egui then reports
/// `primary_pressed` with `press_origin` already cleared by the release, and the gesture must
/// still start at the press point — the guide spans the drag, not a zero-length stub at the
/// release. The control channel spreads pointer events one per frame, so the batch is pushed
/// raw into one frame here.
#[test]
fn a_drag_whose_press_and_release_share_one_frame_still_draws() {
let mut h = guided();
assert_eq!(guides_len(&h), 2);
let undo = h.app.session.undo.len();
let img = h.app.image_rect.expect("the photo on screen");
let p1 = egui::pos2(img.left() + 0.35 * img.width(), img.top() + 0.55 * img.height());
let p2 = egui::pos2(img.left() + 0.6 * img.width(), img.top() + 0.55 * img.height());
h.events.push(egui::Event::PointerMoved(p1));
h.events.push(egui::Event::PointerButton { pos: p1, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });
h.events.push(egui::Event::PointerMoved(p2));
h.events.push(egui::Event::PointerButton { pos: p2, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
h.settle(SETTLE);
assert_eq!(guides_len(&h), 3, "the drag that shared one frame still draws its guide");
assert_eq!(h.app.session.undo.len(), undo + 1, "one edit was written");
let rect = guide_rect(&h, 2);
assert!(rect.left_top().distance(p1) < 2.0, "the guide starts at the press point: {rect:?} vs {p1:?}");
assert!(rect.right_top().distance(p2) < 2.0, "and ends at the release point: {rect:?} vs {p2:?}");
}
/// The right button is the pan / context menu's, never the guides': on a guide it selects
/// nothing and draws nothing.
#[test]