mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-12 17:02:08 -06:00
Merge pull request #1135 from vangel76/fix/group-resize-on-release-1082
Grid: resizing several selected columns applies the width on release (#1082)
This commit is contained in:
@@ -198,6 +198,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
| 2026-10-13 | XLSX export writes a picture used on several sheets once, as one `xl/media` part every drawing points to (it wrote a copy per drawing: a 16-sheet workbook with one 112 KB logo grew from 0.4 to 1.9 MB) (#985) |
|
||||
| 2026-10-13 | XLSX: `[Content_Types].xml`, `_rels/.rels`, the workbook rels and `xl/workbook.xml` are the first zip entries (document properties last), so libmagic and PHP `finfo` detect the file type even with large properties (#1090) |
|
||||
| 2026-10-13 | Conditional formatting: a rule gathers range statistics only when it reads them (top/bottom, above average, duplicates, scales, bars, icons) and borrows them instead of copying them per cell, so the per-frame lookup of an expression or cell-value rule no longer visits its whole range (#1098, part 1) |
|
||||
| 2026-10-13 | Grid: resizing several selected columns or rows previews on the dragged one and applies the size to all on release; Esc cancels (#1082) |
|
||||
| 2026-10-12 | Next/previous sheet navigation stops at the first/last visible worksheet instead of activating a hidden boundary sheet; the current selection is retained |
|
||||
| 2026-10-12 | Insert › Text › WordArt (`insert.wordArt`, `wordArt.set`): floating decorative text with 10 original styles, outline and shadow, in-place editing, XLSX and print (#784) |
|
||||
| 2026-10-12 | Number formatting: one table of built-in formats (xlsx); numbers, dates and General render with one scratch buffer instead of per-token strings; output unchanged, checked against digests recorded from the old code (#817) |
|
||||
|
||||
@@ -112,15 +112,19 @@ pub enum Drag {
|
||||
source: RangeRef,
|
||||
before: Option<u32>,
|
||||
},
|
||||
/// `to`: the previewed size once the pointer moved. Only the dragged line changes while the
|
||||
/// button is held; the release applies the size to every selected line (Excel).
|
||||
ResizeCol {
|
||||
col: u32,
|
||||
start: f32,
|
||||
orig: f32,
|
||||
to: Option<f32>,
|
||||
},
|
||||
ResizeRow {
|
||||
row: u32,
|
||||
start: f32,
|
||||
orig: f32,
|
||||
to: Option<f32>,
|
||||
},
|
||||
/// Dragging the fill handle; `menu` when the right button drags it (the fill menu opens on release).
|
||||
Fill {
|
||||
@@ -244,6 +248,12 @@ pub struct GridState {
|
||||
/// active sheet's rules, kept the same way: they are read from that sheet's cells alone.
|
||||
pub(crate) cf: Option<(std::sync::Weak<Sheet>, CfCache)>,
|
||||
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>>,
|
||||
/// The redo stack when that drag began; Esc puts it back (the preview commands clear it).
|
||||
pub resize_redo: Vec<gridcraft_engine::HistoryEntry>,
|
||||
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).
|
||||
|
||||
@@ -325,7 +325,8 @@ 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.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 };
|
||||
begin_resize(app);
|
||||
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;
|
||||
}
|
||||
let c = geo.col_at(sh, p.x);
|
||||
@@ -362,7 +363,8 @@ 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.drag = Drag::ResizeRow { row: r, start: p.y, orig: (y - geo.y(sh, r)) / geo.z };
|
||||
begin_resize(app);
|
||||
app.grid.drag = Drag::ResizeRow { row: r, start: p.y, orig: (y - geo.y(sh, r)) / geo.z, to: None };
|
||||
return;
|
||||
}
|
||||
let r = geo.row_at(sh, p.y);
|
||||
@@ -631,6 +633,31 @@ 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. egui
|
||||
// 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 {
|
||||
let _ = app.session.run("edit.undo", json!({"steps": steps}));
|
||||
}
|
||||
// Undo pushes the cancelled preview onto the redo stack; put back what the drag began with.
|
||||
if let Some(d) = app.session.active_mut() {
|
||||
d.redo = std::mem::take(&mut app.grid.resize_redo);
|
||||
}
|
||||
app.grid.drag = Drag::None;
|
||||
app.grid.resize_base = None;
|
||||
}
|
||||
if resp.dragged()
|
||||
&& let Some(p) = pos
|
||||
{
|
||||
@@ -682,35 +709,22 @@ fn handle_dragging(app: &mut SheetApp, input: &InteractionInput<'_>) {
|
||||
app.grid.drag = Drag::MoveCols { source, before };
|
||||
scroll_by(app, vec2(auto.x, 0.0));
|
||||
}
|
||||
Drag::ResizeCol { col, start, orig } => {
|
||||
let w = (orig + (p.x - start) / geo.z).max(0.0);
|
||||
Drag::ResizeCol { col, start, orig, to } => {
|
||||
let w = nan_safe_size(orig + (p.x - start) / geo.z);
|
||||
app.toast =
|
||||
Some((format!("Width: {:.2} ({} pixels)", gridcraft_engine::cmd::format::points_to_chars(w as f64), w.round()), crate::now_ms()));
|
||||
// Inside a selection of whole columns, every selected column takes the new width
|
||||
// (Ctrl-selected areas included); otherwise only the dragged one.
|
||||
let cols = if sel.ranges.iter().any(|r| r.is_full_cols() && col >= r.start.col && col <= r.end.col) {
|
||||
sel.ranges.iter().filter(|r| r.is_full_cols()).map(|r| r.a1()).collect::<Vec<_>>().join(",")
|
||||
} else {
|
||||
RangeRef::cols(col, col).a1()
|
||||
};
|
||||
let _ = app.session.run("home.columnWidth", json!({"cols": cols, "width": w.round()}));
|
||||
coalesce_last_undo(app);
|
||||
// Preview on the dragged column only; the release applies it to the selection. A
|
||||
// rejected size leaves the last shown preview (and `to`) as it was.
|
||||
let ok = app.session.run("home.columnWidth", json!({"cols": RangeRef::cols(col, col).a1(), "width": w.round()})).is_ok();
|
||||
coalesce_resize_undo(app);
|
||||
app.grid.drag = Drag::ResizeCol { col, start, orig, to: if ok { Some(w) } else { to } };
|
||||
}
|
||||
Drag::ResizeRow { row, start, orig } => {
|
||||
let h = (orig + (p.y - start) / geo.z).max(0.0);
|
||||
Drag::ResizeRow { row, start, orig, to } => {
|
||||
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 rows = if sel.ranges.iter().any(|r| r.is_full_rows() && row >= r.start.row && row <= r.end.row) {
|
||||
sel.ranges
|
||||
.iter()
|
||||
.filter(|r| r.is_full_rows())
|
||||
.map(|r| format!("{}:{}", r.start.row + 1, r.end.row + 1))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
} else {
|
||||
format!("{}:{}", row + 1, row + 1)
|
||||
};
|
||||
let _ = app.session.run("home.rowHeight", json!({"rows": rows, "height": h.round()}));
|
||||
coalesce_last_undo(app);
|
||||
let ok = app.session.run("home.rowHeight", json!({"rows": format!("{}:{}", row + 1, row + 1), "height": h.round()})).is_ok();
|
||||
coalesce_resize_undo(app);
|
||||
app.grid.drag = Drag::ResizeRow { row, start, orig, to: if ok { Some(h) } else { to } };
|
||||
}
|
||||
Drag::Freeze { cols, .. } => {
|
||||
app.grid.drag = Drag::Freeze { cols, to: Some(freeze_boundary(geo, sh, p, cols)) };
|
||||
@@ -794,6 +808,21 @@ fn handle_release(app: &mut SheetApp, input: &InteractionInput<'_>) {
|
||||
crate::macro_ui::call(app, "GridCraft", "range.move", json!({"range": src.a1(), "to": to.a1()}));
|
||||
}
|
||||
}
|
||||
Drag::ResizeCol { col, to: Some(w), .. } => {
|
||||
// The preview already sized the dragged column; the other selected ones follow now.
|
||||
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_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_resize_undo(app);
|
||||
}
|
||||
}
|
||||
Drag::MoveCols { source, before: Some(before) } => {
|
||||
if let Err(e) = app.run("sheet.moveColumns", json!({"cols": source.a1(), "before": before})) {
|
||||
app.message = Some(("GridCraft".into(), crate::clean_error(&e)));
|
||||
@@ -936,6 +965,29 @@ fn select_to_pointer(app: &mut SheetApp, geo: &Geo, sh: &Sheet, wb: &Workbook, p
|
||||
}
|
||||
}
|
||||
|
||||
/// A drag size in points: NaN or negative becomes 0 (a hidden line).
|
||||
fn nan_safe_size(v: f32) -> f32 {
|
||||
if v.is_nan() { 0.0 } else { v.max(0.0) }
|
||||
}
|
||||
|
||||
/// The lines a header-boundary drag resizes on release: every selected whole line when the
|
||||
/// dragged one is part of the selection (Ctrl-selected areas included), otherwise only it.
|
||||
fn resize_targets(ranges: &[RangeRef], index: u32, cols: bool) -> String {
|
||||
let whole = |r: &RangeRef| if cols { r.is_full_cols() } else { r.is_full_rows() };
|
||||
let span = |r: &RangeRef| if cols { (r.start.col, r.end.col) } else { (r.start.row, r.end.row) };
|
||||
let name = |r: &RangeRef| {
|
||||
let (a, b) = span(r);
|
||||
if cols { RangeRef::cols(a, b).a1() } else { format!("{}:{}", a + 1, b + 1) }
|
||||
};
|
||||
if ranges.iter().any(|r| whole(r) && (span(r).0..=span(r).1).contains(&index)) {
|
||||
ranges.iter().filter(|r| whole(r)).map(name).collect::<Vec<_>>().join(",")
|
||||
} else if cols {
|
||||
RangeRef::cols(index, index).a1()
|
||||
} else {
|
||||
format!("{}:{}", index + 1, index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn coalesce_last_undo(app: &mut SheetApp) {
|
||||
// Merge consecutive drag steps into one undo entry.
|
||||
if let Some(d) = app.session.active_mut()
|
||||
@@ -948,6 +1000,29 @@ fn coalesce_last_undo(app: &mut SheetApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a header resize drag: remembers the workbook and the redo stack it began with.
|
||||
fn begin_resize(app: &mut SheetApp) {
|
||||
app.grid.resize_base = app.session.active().map(|d| d.wb.clone());
|
||||
app.grid.resize_redo = app.session.active().map(|d| d.redo.clone()).unwrap_or_default();
|
||||
}
|
||||
|
||||
/// 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()
|
||||
@@ -1161,6 +1236,28 @@ pub fn point_move(app: &mut SheetApp, ed: &mut EditState, dr: i64, dc: i64, exte
|
||||
app.grid.ensure_visible = true;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod resize_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resize_targets_follow_the_selection_only_when_the_dragged_line_is_in_it() {
|
||||
let sel = [RangeRef::cols(0, 0), RangeRef::cols(2, 3)];
|
||||
assert_eq!(resize_targets(&sel, 3, true), "A:A,C:D");
|
||||
assert_eq!(resize_targets(&sel, 1, true), "B:B");
|
||||
assert_eq!(resize_targets(&[RangeRef::rows(1, 2)], 2, false), "2:3");
|
||||
assert_eq!(resize_targets(&[RangeRef::rows(1, 2)], 5, false), "6:6");
|
||||
assert_eq!(resize_targets(&[], 0, true), "A:A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drag_sizes_are_nan_safe() {
|
||||
assert_eq!(nan_safe_size(f32::NAN), 0.0);
|
||||
assert_eq!(nan_safe_size(-4.0), 0.0);
|
||||
assert_eq!(nan_safe_size(12.5), 12.5);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -153,6 +153,110 @@ fn dragging_a_boundary_inside_a_multi_area_selection_resizes_every_selected_line
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_multi_selection_resize_previews_on_the_dragged_line_and_applies_on_release() {
|
||||
// Issue #1082: only the dragged line moves while the button is held; Esc cancels.
|
||||
for rows in [false, true] {
|
||||
let mut h = harness(if rows { "1:3" } else { "A:C" });
|
||||
let before = sizes(&h, rows);
|
||||
let (start, zoom) = 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();
|
||||
let held = sizes(&h, rows);
|
||||
assert_eq!(held[1], (before[1] + 30.0 / zoom).round(), "the dragged line previews");
|
||||
for index in [0, 2, 3] {
|
||||
assert_eq!(held[index], before[index], "line {index} waits for the release");
|
||||
}
|
||||
button(&mut h, start + delta, false);
|
||||
h.run_steps(2);
|
||||
let after = sizes(&h, rows);
|
||||
for (index, size) in after.iter().take(3).enumerate() {
|
||||
assert_eq!(*size, held[1], "line {index} takes the width on release");
|
||||
}
|
||||
assert_eq!(after[3], before[3]);
|
||||
assert_eq!(h.state().session.active().unwrap().undo.len(), 1);
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(sizes(&h, rows), before);
|
||||
|
||||
// Esc during the drag cancels everything.
|
||||
let mut h = harness(if rows { "1:3" } else { "A:C" });
|
||||
button(&mut h, start, true);
|
||||
h.input_mut().events.push(Event::PointerMoved(start + delta));
|
||||
h.step();
|
||||
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), before, "Esc restores the sizes");
|
||||
}
|
||||
}
|
||||
|
||||
#[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] {
|
||||
@@ -191,3 +295,123 @@ fn table_filter_autofit_works_through_command_and_header_double_click() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn press_escape(h: &mut egui_kittest::Harness<'_, SheetApp>) {
|
||||
h.input_mut().events.push(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rejected_final_preview_releases_with_the_size_last_shown() {
|
||||
// The engine refuses rows over 546 px and columns over 1800 px; the release must apply the last
|
||||
// accepted preview to the whole selection, not retry the refused size.
|
||||
for rows in [false, true] {
|
||||
let mut h = harness(if rows { "1:3" } else { "A:C" });
|
||||
let before = sizes(&h, rows);
|
||||
let (start, zoom) = edge(&h, rows, 1);
|
||||
let unit = |d: f32| if rows { vec2(0.0, d) } else { vec2(d, 0.0) };
|
||||
button(&mut h, start, true);
|
||||
h.input_mut().events.push(Event::PointerMoved(start + unit(15.0)));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerMoved(start + unit(30.0)));
|
||||
h.step();
|
||||
let shown = sizes(&h, rows)[1];
|
||||
assert_eq!(shown, (before[1] + 30.0 / zoom).round());
|
||||
h.input_mut().events.push(Event::PointerMoved(start + unit(4000.0 * zoom)));
|
||||
h.step();
|
||||
assert_eq!(sizes(&h, rows)[1], shown, "the refused size is not shown");
|
||||
button(&mut h, start + unit(4000.0 * zoom), false);
|
||||
h.run_steps(2);
|
||||
let after = sizes(&h, rows);
|
||||
for (index, size) in after.iter().take(3).enumerate() {
|
||||
assert_eq!(*size, shown, "line {index} takes the size that was shown (rows: {rows})");
|
||||
}
|
||||
assert_eq!(after[3], before[3]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaping_a_resize_drag_leaves_redo_history_as_it_was() {
|
||||
for rows in [false, true] {
|
||||
for with_redo in [false, true] {
|
||||
let mut h = harness(if rows { "1:3" } else { "A:C" });
|
||||
let cmd = if rows { ("home.rowHeight", "rows", "4:4", "height") } else { ("home.columnWidth", "cols", "D:D", "width") };
|
||||
let mut redo_len = 0;
|
||||
let mut d_size = 0.0;
|
||||
if with_redo {
|
||||
h.state_mut().session.execute(cmd.0, json!({cmd.1: cmd.2, cmd.3: 80})).unwrap();
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
redo_len = h.state().session.active().unwrap().redo.len();
|
||||
assert_eq!(redo_len, 1);
|
||||
d_size = sizes(&h, rows)[3];
|
||||
}
|
||||
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");
|
||||
press_escape(&mut h);
|
||||
button(&mut h, start + delta, false);
|
||||
h.run_steps(2);
|
||||
assert_eq!(sizes(&h, rows), before);
|
||||
assert_eq!(h.state().session.active().unwrap().redo.len(), redo_len, "Esc leaves the redo stack alone (rows: {rows}, redo: {with_redo})");
|
||||
// Ctrl+Y brings back only what was undone before the drag.
|
||||
let redone = h.state_mut().session.execute("edit.redo", json!({}));
|
||||
if with_redo {
|
||||
redone.unwrap();
|
||||
let redone = sizes(&h, rows);
|
||||
assert_eq!(redone[3], 80.0);
|
||||
assert_ne!(redone[3], d_size);
|
||||
assert_eq!(redone[..3], before[..3], "the cancelled drag does not come back");
|
||||
} else {
|
||||
assert!(redone.is_err(), "nothing to redo after a cancelled drag");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user