diff --git a/crates/ui-egui/src/chartview.rs b/crates/ui-egui/src/chartview.rs index 36604c5b7..efa19d3d7 100644 --- a/crates/ui-egui/src/chartview.rs +++ b/crates/ui-egui/src/chartview.rs @@ -505,6 +505,19 @@ pub fn object_rect(geo: &Geo, sh: &Sheet, o: ObjRef) -> Option { sh.object_anchor(o).map(|a| anchor_rect(geo, sh, &a)) } +/// The resize handle of the selected object under `p`, with the object and its rect. Handles +/// reach a little outside their object, so they win over [`hit`], but like it only over the cell +/// area: a handle that lies under the headers, the formula bar or a scroll bar leaves the press +/// to them. Nothing while objects are hidden. +pub fn selected_handle(app: &SheetApp, geo: &Geo, sh: &Sheet, p: Pos2) -> Option<(ObjRef, Rect, crate::objects::Handle)> { + if !geo.cells.contains(p) || app.session.active().is_some_and(|d| objects_hidden(&d.wb)) { + return None; + } + let o = app.selected_object?; + let r = object_rect(geo, sh, o)?; + crate::objects::handle_at(r, p).map(|h| (o, r, h)) +} + /// The topmost object under `p` and its rect: slicers paint on top, so they are hit first, then /// charts, pictures and shapes, each from the top of its stack. Nothing is hit outside the cell /// area or while objects are hidden. diff --git a/crates/ui-egui/src/grid/cursor.rs b/crates/ui-egui/src/grid/cursor.rs index e15c4800a..f93e59cb6 100644 --- a/crates/ui-egui/src/grid/cursor.rs +++ b/crates/ui-egui/src/grid/cursor.rs @@ -78,11 +78,7 @@ fn hover_cursor(app: &SheetApp, ctx: &egui::Context, resp: &egui::Response, poin let on_cur_border = on_border(cur_rect, p) && border_panes.iter().any(|r| r.contains(p)); // The selected object's handles resize it; any object's body moves it. let idle = app.grid.drag == Drag::None && app.editor.is_none(); - let object_handle = if idle && in_cells { - app.selected_object.and_then(|o| crate::chartview::object_rect(geo, sh, o)).and_then(|r| crate::objects::handle_at(r, p)) - } else { - None - }; + let object_handle = if idle { crate::chartview::selected_handle(app, geo, sh, p).map(|(.., h)| h) } else { None }; let on_object = idle && crate::chartview::hit(app, geo, sh, p).is_some(); if let Some(h) = object_handle { ctx.set_cursor_icon(h.cursor(geo.rtl)); diff --git a/crates/ui-egui/src/grid/interact.rs b/crates/ui-egui/src/grid/interact.rs index 5b8eade31..84d8dbc33 100644 --- a/crates/ui-egui/src/grid/interact.rs +++ b/crates/ui-egui/src/grid/interact.rs @@ -124,10 +124,7 @@ pub(super) fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Respo // A drag is recognised only after the pointer has moved, so grab the object where the press began. let at = if primary_drag { pointer.press_origin().map(|p| geo.mirror_pos(p)).or(pos) } else { pos }; // The selected object's handles reach a little outside it, so they win over hit-testing. - let on_handle = app.selected_object.and_then(|o| { - let r = crate::chartview::object_rect(geo, sh, o)?; - at.and_then(|p| crate::objects::handle_at(r, p)).map(|_| (o, r)) - }); + let on_handle = at.and_then(|p| crate::chartview::selected_handle(app, geo, sh, p)).map(|(o, r, _)| (o, r)); if let Some(p) = at && let Some((obj, rect)) = on_handle.or_else(|| crate::chartview::hit(app, geo, sh, p)) { diff --git a/crates/ui-egui/tests/it/objects.rs b/crates/ui-egui/tests/it/objects.rs index 733a1427a..b9912559c 100644 --- a/crates/ui-egui/tests/it/objects.rs +++ b/crates/ui-egui/tests/it/objects.rs @@ -300,3 +300,27 @@ fn on_a_right_to_left_sheet_the_mirrored_handles_resize() { assert!((after.left() - (before.left() - 20.0)).abs() < 1.0 && (after.right() - before.right()).abs() < 1.0, "{before:?} {after:?}"); assert!((after.bottom() - (before.bottom() + 16.0)).abs() < 1.0 && (after.top() - before.top()).abs() < 1.0, "{before:?} {after:?}"); } + +#[test] +fn a_handle_under_the_headers_does_not_take_the_press() { + let (mut h, o) = fixture(); + h.state_mut().run("object.move", json!({"kind": "shape", "id": o.shape.id, "at": "A1", "dx": 0, "dy": 0})).unwrap(); + h.run_steps(2); + let before = rect(&h, o.shape); + click(&mut h, before.center()); + assert_eq!(h.state().selected_object, Some(o.shape)); + let cells = h.state().grid.cells_rect.unwrap(); + assert_eq!(before.left_top(), cells.left_top(), "the shape's top-left handle sits on the header corner"); + // Within the top-left handle's reach, but over the column header: the press is the header's. + let grab = Handle::TopLeft.pos(before) + vec2(4.0, -4.0); + assert!(grab.y < cells.top()); + drag(&mut h, grab, grab + vec2(30.0, 40.0)); + assert_eq!(rect(&h, o.shape), before, "the shape is neither moved nor resized"); + assert_eq!(h.state().selected_object, None, "the press went to the column header"); + // Within the left handle's reach, but over the row header. + let grab = Handle::Left.pos(before) + vec2(-4.0, 0.0); + assert!(grab.x < cells.left()); + h.state_mut().selected_object = Some(o.shape); + drag(&mut h, grab, grab + vec2(40.0, 0.0)); + assert_eq!(rect(&h, o.shape), before, "the shape is neither moved nor resized"); +}