Objects: a resize handle under the headers leaves the press to them

A selected object's handles reach a few points outside it, and a press
was matched against them before any check of where it landed, so a press
on the column or row header beside an object at the sheet's edge resized
the object instead of selecting the column or row. The cursor already
ignored handles outside the cells.

`chartview::selected_handle` finds the selected object's handle under a
point only over the cell area, as `hit` does, and not while objects are
hidden; the press and the cursor both use it.

Test: presses within a handle's reach over the column header and the row
header neither move nor resize the object.
This commit is contained in:
prxnvt committed 2026-10-31 06:02:30 -04:00
1 parent de8f1854f1
commit 028ce2b417
4 files changed
+39 -9

No files matched your search

+13
View File
@@ -505,6 +505,19 @@ pub fn object_rect(geo: &Geo, sh: &Sheet, o: ObjRef) -> Option<Rect> {
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.
+1 -5
View File
@@ -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));
+1 -4
View File
@@ -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))
{
+24
View File
@@ -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");
}