Resize drag: spend Esc on the cancelled drag

Esc during a header-boundary resize was only peeked at, so the grid's
own Esc handling saw it as well and dropped a marked copy and Format
Painter. The cancel now removes the Esc presses from the frame's input,
with any modifiers, as before, cancelling the drag. The regression test
covers a pending copy and sticky Format Painter for rows and columns,
with the release in a later frame or the same frame, and Shift+Esc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-11-08 13:01:31 +01:00
co-authored by Claude Opus 5.5
parent 3e64ddbd80
commit 7cd0a6cc72
2 changed files with 53 additions and 2 deletions
+12 -2
View File
@@ -634,8 +634,18 @@ fn handle_dragging(app: &mut SheetApp, input: &InteractionInput<'_>) {
let (geo, sh, wb, sel, pos) = (input.geo, input.sheet, input.workbook, &input.selection, input.position);
let resp = input.response;
// Esc during a header-boundary drag cancels it: undo the preview, ignore the release. egui
// aborts the drag on Esc, so `resp.dragged()` is already false in this frame.
if matches!(app.grid.drag, Drag::ResizeCol { .. } | Drag::ResizeRow { .. }) && input.context.input(|i| i.key_pressed(egui::Key::Escape)) {
// aborts the drag on Esc, so `resp.dragged()` is already false in this frame. The key is
// spent on the drag, so the grid's Esc doesn't also drop a pending copy or Format Painter.
if matches!(app.grid.drag, Drag::ResizeCol { .. } | Drag::ResizeRow { .. })
&& input.context.input_mut(|i| {
let pressed = i.key_pressed(egui::Key::Escape);
if pressed {
// With any modifiers, as before: the drag is cancelled and every Esc press spent.
i.events.retain(|e| !matches!(e, egui::Event::Key { key: egui::Key::Escape, pressed: true, .. }));
}
pressed
})
{
// Undo back to where the drag began, but never past it into an earlier edit.
let steps = resize_steps(app);
if steps > 0 {
+41
View File
@@ -374,3 +374,44 @@ fn escaping_a_resize_drag_leaves_redo_history_as_it_was() {
}
}
}
#[test]
fn escaping_a_resize_drag_keeps_a_pending_copy_and_format_painter() {
// Esc cancels the drag only: the marked copy and Format Painter stay, whether the release comes
// in a later frame or in the same frame as Esc.
for rows in [false, true] {
for (same_frame, modifiers) in [(false, Modifiers::NONE), (true, Modifiers::NONE), (false, Modifiers::SHIFT)] {
let mut h = harness(if rows { "1:3" } else { "A:C" });
h.state_mut().session.execute("edit.copy", json!({"range": "A1:B2"})).unwrap();
h.state_mut().session.execute("edit.formatPainter", json!({"sticky": true})).unwrap();
h.run_steps(2);
assert!(h.state().session.clipboard.is_some() && h.state().session.format_painter.is_some());
let before = sizes(&h, rows);
let (start, _) = edge(&h, rows, 1);
let delta = if rows { vec2(0.0, 30.0) } else { vec2(30.0, 0.0) };
button(&mut h, start, true);
h.input_mut().events.push(Event::PointerMoved(start + delta / 2.0));
h.step();
h.input_mut().events.push(Event::PointerMoved(start + delta));
h.step();
assert_ne!(sizes(&h, rows), before, "the drag previews");
let pos = start + delta;
let esc = Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers };
if same_frame {
h.input_mut().events.extend([esc, Event::PointerButton { pos, button: PointerButton::Primary, pressed: false, modifiers }]);
} else {
h.input_mut().events.push(esc);
h.step();
h.input_mut()
.events
.extend([Event::PointerMoved(pos), Event::PointerButton { pos, button: PointerButton::Primary, pressed: false, modifiers }]);
}
h.step();
h.run_steps(2);
let what = format!("rows: {rows}, same frame: {same_frame}, modifiers: {modifiers:?}");
assert_eq!(sizes(&h, rows), before, "the drag is cancelled ({what})");
assert!(h.state().session.clipboard.is_some(), "the copy is still marked ({what})");
assert!(h.state().session.format_painter.is_some(), "Format Painter is still on ({what})");
}
}
}