fix(guided): the loupe's press is layer-aware, not just inside its rect

`primary_pressed` plus `resp.rect.contains` reads the raw pointer, so
anything floating above the photo — the context menu, a dropdown, a
dialog — also started a guide gesture under it. The gate now asks
`resp.contains_pointer()`, which the hit test keeps honest about
covering widgets; the navigator check stays. Two tests press on a menu
over a line and over an endpoint: nothing is selected, moved or drawn.
This commit is contained in:
mahmatias
2026-10-21 14:13:18 -03:00
parent b1c607d7ac
commit f2e97d39fe
2 changed files with 70 additions and 1 deletions
+5 -1
View File
@@ -1877,10 +1877,14 @@ fn guided_overlay(
// threshold would have been crossed): on an endpoint it drags that endpoint (the guide is
// selected when it lands), on a line it selects the guide (a press on the selected one
// deselects), anywhere else it starts a new guide from the press point. The primary button
// only, and never on the navigator — it pans the photo, it draws no guide.
// only, never on the navigator — it pans the photo, it draws no guide — and only where the
// loupe itself takes the press: `contains_pointer` is layer-aware (the raw pointer is not),
// so anything floating above the photo — a context menu, a dropdown, a dialog — owns the
// press and the guide under it stays untouched.
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())
&& resp.contains_pointer()
&& resp.rect.contains(q)
&& !nav.is_some_and(|n| n.expand(5.0).contains(q))
{
+65
View File
@@ -241,3 +241,68 @@ fn a_press_on_the_navigator_draws_no_guide() {
assert_eq!(guides_len(&h), 2, "the press on the navigator drew no guide");
assert_eq!(h.app.ui.selected_guide, None, "and selected nothing");
}
/// The context menu (anything floating above the photo) owns its own press: a left press on the
/// menu that sits over a guide line selects nothing under it and starts no guide gesture — the
/// canvas has to actually receive the press, not merely lie under the pointer.
#[test]
fn a_press_on_a_menu_over_a_guide_selects_nothing() {
let mut h = guided();
let id = h.app.session.active().expect("a photo");
let undo = h.app.session.undo.len();
let line = guide_at(&h, 0);
// right-click 30px up-left of that point: the context menu opens with its top-left corner
// at the pointer (gap 0), so its frame covers the guide from 30px inside every edge
let r = h.request("ui.click", json!({"x": line.x - 30.0, "y": line.y - 30.0, "button": "right"}), T);
assert_eq!(r["ok"], true, "{r}");
h.settle(SETTLE);
assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");
let img = h.app.image_rect.expect("the photo on screen");
let (lx, ly) = ((line.x - img.left()) / img.width(), (line.y - img.top()) / img.height());
let r = h.request("ui.pointer", json!({"events": [{"kind": "down", "x": lx, "y": ly}, {"kind": "up", "x": lx, "y": ly}]}), T);
assert_eq!(r["ok"], true, "{r}");
h.settle(SETTLE);
assert_eq!(h.app.ui.selected_guide, None, "the press under the menu selects nothing");
assert!(h.app.gesture.is_none(), "and starts no gesture");
assert_eq!(guides_len(&h), 2, "and draws nothing");
assert_eq!(h.app.session.undo.len(), undo, "no edit was written");
let d = h.app.session.catalog.photo(id).expect("the photo");
let (a, b) = d.develop.geometry.guides[0];
assert!(
(a.x - 0.2).abs() < 1e-6 && (a.y - 0.3).abs() < 1e-6 && (b.x - 0.8).abs() < 1e-6 && (b.y - 0.3).abs() < 1e-6,
"the guide is untouched: {a:?}–{b:?}"
);
}
/// Same for an endpoint under the menu: a press-drag on it moves nothing and selects nothing —
/// the endpoint-drag gesture never starts because the menu, not the canvas, took the press.
#[test]
fn a_press_on_a_menu_over_an_endpoint_moves_nothing() {
let mut h = guided();
let id = h.app.session.active().expect("a photo");
let undo = h.app.session.undo.len();
let end = guide_rect(&h, 0).left_top();
let r = h.request("ui.click", json!({"x": end.x - 30.0, "y": end.y - 30.0, "button": "right"}), T);
assert_eq!(r["ok"], true, "{r}");
h.settle(SETTLE);
assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");
let img = h.app.image_rect.expect("the photo on screen");
let (ex, ey) = ((end.x - img.left()) / img.width(), (end.y - img.top()) / img.height());
let r = h.request(
"ui.pointer",
json!({"events": [{"kind": "down", "x": ex, "y": ey}, {"kind": "drag", "x": ex + 0.05, "y": ey + 0.08}, {"kind": "up", "x": ex + 0.05, "y": ey + 0.08}]}),
T,
);
assert_eq!(r["ok"], true, "{r}");
h.settle(SETTLE);
assert_eq!(h.app.ui.selected_guide, None, "the endpoint's press under the menu selects nothing");
assert!(h.app.gesture.is_none(), "and starts no gesture");
assert_eq!(guides_len(&h), 2, "and draws nothing");
assert_eq!(h.app.session.undo.len(), undo, "no edit was written");
let d = h.app.session.catalog.photo(id).expect("the photo");
let (a, b) = d.develop.geometry.guides[0];
assert!(
(a.x - 0.2).abs() < 1e-6 && (a.y - 0.3).abs() < 1e-6 && (b.x - 0.8).abs() < 1e-6 && (b.y - 0.3).abs() < 1e-6,
"the endpoint under the menu did not move: {a:?}–{b:?}"
);
}