mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-12 17:02:08 -06:00
Keep Esc on a resize drag from undoing an earlier size edit
A resize drag shares its undo label with an earlier width/height edit, so coalescing removed the drag's own entry and Esc undid the earlier edit too. Track the workbook the drag started from and merge/cancel only entries from there. Esc is also handled when egui has already ended the drag. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
5d440c82ac
commit
96aece4e57
@@ -180,6 +180,10 @@ impl Trace {
|
||||
#[derive(Default)]
|
||||
pub struct GridState {
|
||||
pub drag: Drag,
|
||||
/// A header-boundary resize drag: the workbook as it was when the drag began. The drag's own
|
||||
/// undo entries are the ones from the first entry that snapshots it; earlier edits with the
|
||||
/// same label are not the drag's to merge or cancel.
|
||||
pub resize_base: Option<Arc<Workbook>>,
|
||||
pub ensure_visible: bool,
|
||||
/// With `ensure_visible`: scroll to the moving end of the selection instead of the active cell
|
||||
/// (Shift/Ctrl+Shift+arrows keep the active cell on the anchor while the far corner moves).
|
||||
|
||||
@@ -300,6 +300,7 @@ fn handle_column_header_press(app: &mut SheetApp, input: &InteractionInput<'_>,
|
||||
let in_col_header = input.in_column_header;
|
||||
if in_col_header {
|
||||
if let Some((c, x)) = col_edge(geo, sh, p.x) {
|
||||
app.grid.resize_base = app.session.active().map(|d| d.wb.clone());
|
||||
app.grid.drag = Drag::ResizeCol { col: c, start: p.x, orig: sh.col_width(c).max(0.0) * 0.0 + (x - geo.x(sh, c)) / geo.z, to: None };
|
||||
return;
|
||||
}
|
||||
@@ -335,6 +336,7 @@ fn handle_row_header_press(app: &mut SheetApp, input: &InteractionInput<'_>, p:
|
||||
let in_row_header = input.in_row_header;
|
||||
if in_row_header {
|
||||
if let Some((r, y)) = row_edge(geo, sh, p.y) {
|
||||
app.grid.resize_base = app.session.active().map(|d| d.wb.clone());
|
||||
app.grid.drag = Drag::ResizeRow { row: r, start: p.y, orig: (y - geo.y(sh, r)) / geo.z, to: None };
|
||||
return;
|
||||
}
|
||||
@@ -576,14 +578,16 @@ fn handle_context_menu_and_keyboard(app: &mut SheetApp, input: &InteractionInput
|
||||
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.
|
||||
if resp.dragged() && input.context.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
if let Drag::ResizeCol { to: Some(_), .. } | Drag::ResizeRow { to: Some(_), .. } = app.grid.drag {
|
||||
let _ = app.session.run("edit.undo", json!({}));
|
||||
}
|
||||
if matches!(app.grid.drag, Drag::ResizeCol { .. } | Drag::ResizeRow { .. }) {
|
||||
app.grid.drag = Drag::None;
|
||||
// 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)) {
|
||||
// Undo back to where the drag began, but never past it into an earlier edit.
|
||||
let steps = resize_steps(app);
|
||||
if steps > 0 {
|
||||
let _ = app.session.run("edit.undo", json!({"steps": steps}));
|
||||
}
|
||||
app.grid.drag = Drag::None;
|
||||
app.grid.resize_base = None;
|
||||
}
|
||||
if resp.dragged()
|
||||
&& let Some(p) = pos
|
||||
@@ -638,14 +642,14 @@ fn handle_dragging(app: &mut SheetApp, input: &InteractionInput<'_>) {
|
||||
Some((format!("Width: {:.2} ({} pixels)", gridcraft_engine::cmd::format::points_to_chars(w as f64), w.round()), crate::now_ms()));
|
||||
// Preview on the dragged column only; the release applies it to the selection.
|
||||
let _ = app.session.run("home.columnWidth", json!({"cols": RangeRef::cols(col, col).a1(), "width": w.round()}));
|
||||
coalesce_last_undo(app);
|
||||
coalesce_resize_undo(app);
|
||||
app.grid.drag = Drag::ResizeCol { col, start, orig, to: Some(w) };
|
||||
}
|
||||
Drag::ResizeRow { row, start, orig, .. } => {
|
||||
let h = nan_safe_size(orig + (p.y - start) / geo.z);
|
||||
app.toast = Some((format!("Height: {:.2} ({} pixels)", h * 0.75, h.round()), crate::now_ms()));
|
||||
let _ = app.session.run("home.rowHeight", json!({"rows": format!("{}:{}", row + 1, row + 1), "height": h.round()}));
|
||||
coalesce_last_undo(app);
|
||||
coalesce_resize_undo(app);
|
||||
app.grid.drag = Drag::ResizeRow { row, start, orig, to: Some(h) };
|
||||
}
|
||||
Drag::Freeze { cols, .. } => {
|
||||
@@ -741,14 +745,14 @@ fn handle_release(app: &mut SheetApp, input: &InteractionInput<'_>) {
|
||||
let cols = resize_targets(&sel.ranges, col, true);
|
||||
if cols != RangeRef::cols(col, col).a1() {
|
||||
let _ = app.session.run("home.columnWidth", json!({"cols": cols, "width": w.round()}));
|
||||
coalesce_last_undo(app);
|
||||
coalesce_resize_undo(app);
|
||||
}
|
||||
}
|
||||
Drag::ResizeRow { row, to: Some(h), .. } => {
|
||||
let rows = resize_targets(&sel.ranges, row, false);
|
||||
if rows != format!("{}:{}", row + 1, row + 1) {
|
||||
let _ = app.session.run("home.rowHeight", json!({"rows": rows, "height": h.round()}));
|
||||
coalesce_last_undo(app);
|
||||
coalesce_resize_undo(app);
|
||||
}
|
||||
}
|
||||
Drag::MoveCols { source, before: Some(before) } => {
|
||||
@@ -884,6 +888,23 @@ fn coalesce_last_undo(app: &mut SheetApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo entries a header resize drag has pushed so far: those from the first one that snapshots
|
||||
/// the workbook the drag started from (`resize_base`). A failed command pushes none.
|
||||
fn resize_steps(app: &SheetApp) -> usize {
|
||||
let (Some(base), Some(d)) = (&app.grid.resize_base, app.session.active()) else { return 0 };
|
||||
d.undo.iter().position(|e| std::sync::Arc::ptr_eq(&e.wb, base)).map_or(0, |first| d.undo.len() - first)
|
||||
}
|
||||
|
||||
/// Like [`coalesce_last_undo`] for a resize drag: merges only the drag's own entries, so an
|
||||
/// earlier edit with the same label ("Column width") stays a separate undo step.
|
||||
fn coalesce_resize_undo(app: &mut SheetApp) {
|
||||
if resize_steps(app) >= 2
|
||||
&& let Some(d) = app.session.active_mut()
|
||||
{
|
||||
d.undo.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_by(app: &mut SheetApp, d: egui::Vec2) {
|
||||
if d != egui::Vec2::ZERO
|
||||
&& let Some(v) = app.view_mut()
|
||||
|
||||
@@ -201,6 +201,62 @@ fn a_multi_selection_resize_previews_on_the_dragged_line_and_applies_on_release(
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaping_a_resize_drag_keeps_an_earlier_size_edit() {
|
||||
// A resize drag right after another resize shares its undo label; Esc must cancel only the drag.
|
||||
for (rows, selection) in [(false, "B:B"), (true, "2:2"), (false, "A:C"), (true, "1:3")] {
|
||||
let mut h = harness(selection);
|
||||
let key = if rows { ("home.rowHeight", "rows", "2:2", "height") } else { ("home.columnWidth", "cols", "B:B", "width") };
|
||||
h.state_mut().session.execute(key.0, json!({key.1: key.2, key.3: 80})).unwrap();
|
||||
h.run_steps(2);
|
||||
let earlier = sizes(&h, rows);
|
||||
let undo_len = h.state().session.active().unwrap().undo.len();
|
||||
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), earlier, "the drag previews");
|
||||
h.input_mut().events.push(Event::Key {
|
||||
key: egui::Key::Escape,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: Modifiers::NONE,
|
||||
});
|
||||
h.step();
|
||||
button(&mut h, start + delta, false);
|
||||
h.run_steps(2);
|
||||
assert_eq!(sizes(&h, rows), earlier, "Esc cancels only the drag (rows: {rows}, selection {selection})");
|
||||
assert_eq!(h.state().session.active().unwrap().undo.len(), undo_len, "the earlier edit stays on the undo stack");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_drag_after_another_resize_is_its_own_undo_step() {
|
||||
for (rows, selection) in [(false, "B:B"), (true, "2:2"), (false, "A:C"), (true, "1:3")] {
|
||||
let mut h = harness(selection);
|
||||
let key = if rows { ("home.rowHeight", "rows", "2:2", "height") } else { ("home.columnWidth", "cols", "B:B", "width") };
|
||||
h.state_mut().session.execute(key.0, json!({key.1: key.2, key.3: 80})).unwrap();
|
||||
h.run_steps(2);
|
||||
let earlier = 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();
|
||||
button(&mut h, start + delta, false);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().session.active().unwrap().undo.len(), 2, "selection {selection}: two undo steps");
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(sizes(&h, rows), earlier, "selection {selection}: one undo reverts only the drag");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_filter_autofit_works_through_command_and_header_double_click() {
|
||||
for zoom in [50, 100, 200] {
|
||||
|
||||
Reference in New Issue
Block a user