Fix header double-click AutoFit for selected ranges (#42)

This commit is contained in:
harrisonoconnorhover
2026-10-09 10:23:09 -04:00
parent 51b531be3e
commit 38c11f6fb9
2 changed files with 155 additions and 20 deletions
+39 -20
View File
@@ -1080,6 +1080,32 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
}
}
// A double-click is also a click: handle header AutoFit before resize setup returns.
if resp.double_clicked()
&& let Some(p) = pos
{
let autofit = if in_col_header {
col_edge(geo, sh, p.x).map(|(col, _)| {
let selected = sel.ranges.iter().any(|r| r.is_full_cols() && col >= r.start.col && col <= r.end.col);
("home.autofitColumnWidth", if selected { json!({}) } else { json!({"cols": RangeRef::cols(col, col).a1()}) })
})
} else if in_row_header {
row_edge(geo, sh, p.y).map(|(row, _)| {
let selected = sel.ranges.iter().any(|r| r.is_full_rows() && row >= r.start.row && row <= r.end.row);
("home.autofitRowHeight", if selected { json!({}) } else { json!({"rows": RangeRef::rows(row, row).a1()}) })
})
} else {
None
};
if let Some((command, params)) = autofit {
app.grid.drag = Drag::None;
app.grid.drag_select = false;
app.run_or_alert(command, params);
resp.request_focus();
return;
}
}
// Press: decide the drag mode.
if resp.drag_started() || (resp.clicked() && app.grid.drag == Drag::None) {
// Where the press began (a drag is only recognised after the pointer has moved).
@@ -1334,29 +1360,22 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
app.grid.drag_select = false;
}
// Double-click on header edges autofits.
// Double-click the fill handle.
if resp.double_clicked()
&& let Some(p) = pos
&& handle.contains(p)
{
if in_col_header && let Some((c, _)) = col_edge(geo, sh, p.x) {
let _ = app.run("home.autofitColumnWidth", json!({"cols": RangeRef::cols(c, c).a1()}));
} else if in_row_header && let Some((r, _)) = row_edge(geo, sh, p.y) {
let _ = app.run("home.autofitRowHeight", json!({"rows": format!("{}:{}", r + 1, r + 1)}));
} else if handle.contains(p) {
// Double-click the fill handle: fill down as far as the neighbouring column goes.
let src = sel.current();
let neighbour = if src.start.col > 0 { src.start.col - 1 } else { src.end.col + 1 };
let mut last = src.end.row;
while last + 1 < MAX_ROWS
&& sh.value_ref(CellRef::new(last + 1, neighbour)).is_some_and(|v| !v.is_empty())
&& last - src.end.row < 1_000_000
{
last += 1;
}
if last > src.end.row {
let target = RangeRef::new(src.start, CellRef::new(last, src.end.col));
let _ = app.run("edit.autoFill", json!({"source": src.a1(), "target": target.a1()}));
}
// Fill down as far as the neighbouring column goes.
let src = sel.current();
let neighbour = if src.start.col > 0 { src.start.col - 1 } else { src.end.col + 1 };
let mut last = src.end.row;
while last + 1 < MAX_ROWS && sh.value_ref(CellRef::new(last + 1, neighbour)).is_some_and(|v| !v.is_empty()) && last - src.end.row < 1_000_000
{
last += 1;
}
if last > src.end.row {
let target = RangeRef::new(src.start, CellRef::new(last, src.end.col));
let _ = app.run("edit.autoFill", json!({"source": src.a1(), "target": target.a1()}));
}
}
+116
View File
@@ -0,0 +1,116 @@
//! Header AutoFit and resize gestures, driven through real egui pointer events.
use egui::{Event, Modifiers, PointerButton, Pos2, pos2, vec2};
use gridcraft_engine::Session;
use gridcraft_ui_egui::{SheetApp, grid::Geo};
use serde_json::json;
fn harness(selection: &str) -> egui_kittest::Harness<'static, SheetApp> {
let mut session = Session::new();
session.new_workbook();
session
.execute(
"range.setValues",
json!({"range": "A1", "values": [
["Short", "This column has considerably longer content", "Third", "Unchanged"],
["Two\nlines", "B", "C", "D"],
["Three\ntext\nlines", "B", "C", "D"]
]}),
)
.unwrap();
session.execute("home.columnWidth", json!({"cols": "A:D", "width": 100})).unwrap();
session.execute("home.rowHeight", json!({"rows": "1:4", "height": 60})).unwrap();
session.execute("selection.set", json!({"range": selection})).unwrap();
let mut h = egui_kittest::Harness::builder().with_step_dt(1.0 / 60.0).with_size(vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SheetApp| {
app.logic(ui.ctx());
app.ui(ui);
},
SheetApp::new(session, Default::default()),
);
h.run_steps(4);
h.state_mut().session.active_mut().unwrap().undo.clear();
h.state_mut().session.journal.clear();
h
}
fn edge(h: &egui_kittest::Harness<'_, SheetApp>, rows: bool, index: u32) -> (Pos2, f32) {
let app = h.state();
let sheet = app.session.active().unwrap().wb.active().unwrap();
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
let pos = if rows {
pos2(geo.rect.left() + geo.header_w / 2.0, geo.y(sheet, index + 1))
} else {
pos2(geo.x(sheet, index + 1), geo.rect.top() + geo.header_h / 2.0)
};
(pos, geo.z)
}
fn button(h: &mut egui_kittest::Harness<'_, SheetApp>, pos: Pos2, pressed: bool) {
h.input_mut()
.events
.extend([Event::PointerMoved(pos), Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
h.step();
}
fn sizes(h: &egui_kittest::Harness<'_, SheetApp>, rows: bool) -> Vec<f32> {
let sheet = h.state().session.active().unwrap().wb.active().unwrap();
(0..4).map(|index| if rows { sheet.row_height(index) } else { sheet.col_width(index) }).collect()
}
#[test]
fn header_double_click_autofits_selected_areas_or_only_the_clicked_line() {
for (rows, selection, clicked, changed) in [
(false, "B:B", 1, [false, true, false, false]),
(false, "A:C", 1, [true, true, true, false]),
(false, "A:A,C:C", 0, [true, false, true, false]),
(false, "A:B", 2, [false, false, true, false]),
(true, "1:3", 1, [true, true, true, false]),
(true, "1:1,3:3", 0, [true, false, true, false]),
(true, "1:2", 2, [false, false, true, false]),
] {
let mut h = harness(selection);
let before = sizes(&h, rows);
let selection_before = h.state().session.active().unwrap().selection.clone();
let (pos, _) = edge(&h, rows, clicked);
button(&mut h, pos, true);
button(&mut h, pos, false);
h.step(); // Let the first click's resize state settle before the second click.
button(&mut h, pos, true);
button(&mut h, pos, false);
h.run_steps(2);
for (index, ((old, new), changed)) in before.iter().zip(sizes(&h, rows)).zip(changed).enumerate() {
assert_eq!(*old != new, changed, "selection {selection}, clicked {clicked}, line {index}: {old} → {new}");
}
assert_eq!(h.state().session.active().unwrap().selection, selection_before);
let command = if rows { "home.autofitRowHeight" } else { "home.autofitColumnWidth" };
assert_eq!(h.state().session.journal.iter().filter(|(id, _)| id == command).count(), 1);
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, "AutoFit should undo as a single operation");
}
}
#[test]
fn dragging_a_header_boundary_still_resizes_normally() {
for rows in [false, true] {
let mut h = harness(if rows { "2:2" } else { "B:B" });
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();
button(&mut h, start + delta, false);
h.run_steps(2);
let after = sizes(&h, rows);
assert_eq!(after[1], (before[1] + 30.0 / zoom).round());
for index in [0, 2, 3] {
assert_eq!(before[index], after[index]);
}
assert!(!h.state().session.journal.iter().any(|(id, _)| id.starts_with("home.autofit")));
}
}