mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Grid: header drags resize every Ctrl-selected row or column (#341)
Dragging a header boundary inside a selection of whole columns (rows)
sized only the current area, so with B, D and F Ctrl-selected only the
last-selected one changed. The drag now targets every whole-column
(whole-row) area of the selection, as Excel does; lines outside the
selection, and partial-cell areas, keep their size.
home.columnWidth / home.rowHeight accept a comma-separated list of
areas ("B:B,D:F") so the drag stays one command and one undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -156,7 +156,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Row Height…",
|
||||
["Home", "Cells", "Format"],
|
||||
None,
|
||||
"{rows?: \"2:5\", height: 20 (points on screen)}",
|
||||
"{rows?: \"2:5\" | \"2:2,4:5\", height: 20 (points on screen)}",
|
||||
has_doc,
|
||||
row_height
|
||||
),
|
||||
@@ -165,7 +165,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Column Width…",
|
||||
["Home", "Cells", "Format"],
|
||||
None,
|
||||
"{cols?: \"B:D\", width: 64 (points) | chars: 8.43}",
|
||||
"{cols?: \"B:D\" | \"B:B,D:F\", width: 64 (points) | chars: 8.43}",
|
||||
has_doc,
|
||||
col_width
|
||||
),
|
||||
@@ -910,8 +910,14 @@ fn cell_style(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
fn line_targets(s: &Session, p: &Json, rows: bool) -> Result<Vec<(u32, u32)>> {
|
||||
let key = if rows { "rows" } else { "cols" };
|
||||
if let Some(t) = str_param(p, key) {
|
||||
let r = RangeRef::parse(t).ok_or_else(|| bad(key, format!("not a range: {t}")))?;
|
||||
return Ok(vec![if rows { (r.start.row, r.end.row) } else { (r.start.col, r.end.col) }]);
|
||||
// A comma-separated list ("B:B,D:F") targets several areas, like a multi-area selection.
|
||||
return t
|
||||
.split(',')
|
||||
.map(|part| {
|
||||
let r = RangeRef::parse(part).ok_or_else(|| bad(key, format!("not a range: {part}")))?;
|
||||
Ok(if rows { (r.start.row, r.end.row) } else { (r.start.col, r.end.col) })
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Ok(s.doc()?.selection.ranges.iter().map(|r| if rows { (r.start.row, r.end.row) } else { (r.start.col, r.end.col) }).collect())
|
||||
}
|
||||
|
||||
@@ -1633,3 +1633,27 @@ fn notes_comments_and_links_survive_a_json_save() {
|
||||
assert_eq!(after.hyperlinks, before.hyperlinks);
|
||||
assert_eq!(after.cells, before.cells);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_height_and_column_width_take_a_list_of_areas() {
|
||||
// Issue #341: header drags on a multi-area selection size every selected line in one step.
|
||||
let mut s = s();
|
||||
s.execute("home.columnWidth", json!({"cols": "B:B,D:E", "width": 120})).unwrap();
|
||||
s.execute("home.rowHeight", json!({"rows": "2:2,4:5", "height": 40})).unwrap();
|
||||
let widths: Vec<f32> = (0..6).map(|c| s.doc().unwrap().wb.active().unwrap().col_width(c)).collect();
|
||||
let heights: Vec<f32> = (0..6).map(|r| s.doc().unwrap().wb.active().unwrap().row_height(r)).collect();
|
||||
for c in [1, 3, 4] {
|
||||
assert_eq!(widths[c], 120.0, "column {c}");
|
||||
}
|
||||
for c in [0, 2, 5] {
|
||||
assert_ne!(widths[c], 120.0, "column {c}");
|
||||
}
|
||||
for r in [1, 3, 4] {
|
||||
assert_eq!(heights[r], 40.0, "row {r}");
|
||||
}
|
||||
for r in [0, 2, 5] {
|
||||
assert_ne!(heights[r], 40.0, "row {r}");
|
||||
}
|
||||
assert!(s.execute("home.columnWidth", json!({"cols": "B:B,nope", "width": 50})).is_err());
|
||||
assert_eq!(s.doc().unwrap().wb.active().unwrap().col_width(1), 120.0, "a bad list changes nothing");
|
||||
}
|
||||
|
||||
@@ -1359,8 +1359,10 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
||||
let w = (orig + (p.x - start) / geo.z).max(0.0);
|
||||
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.current().a1()
|
||||
sel.ranges.iter().filter(|r| r.is_full_cols()).map(|r| r.a1()).collect::<Vec<_>>().join(",")
|
||||
} else {
|
||||
RangeRef::cols(col, col).a1()
|
||||
};
|
||||
@@ -1371,7 +1373,12 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
||||
let h = (orig + (p.y - start) / geo.z).max(0.0);
|
||||
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) {
|
||||
format!("{}:{}", sel.current().start.row + 1, sel.current().end.row + 1)
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -114,3 +114,41 @@ fn dragging_a_header_boundary_still_resizes_normally() {
|
||||
assert!(!h.state().session.journal.iter().any(|(id, _)| id.starts_with("home.autofit")));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_boundary_inside_a_multi_area_selection_resizes_every_selected_line() {
|
||||
// Issue #341: Ctrl-selected rows/columns resize together; unselected lines keep their size.
|
||||
for (rows, selection, dragged, changed) in [
|
||||
(false, "A:A,C:C", 2, [true, false, true, false]),
|
||||
(false, "A:A,C:C", 0, [true, false, true, false]),
|
||||
(false, "A:A,C:D", 3, [true, false, true, true]),
|
||||
(false, "A:A,C:C", 1, [false, true, false, false]),
|
||||
(false, "A:A,C3", 0, [true, false, false, false]),
|
||||
(true, "1:1,3:3", 2, [true, false, true, false]),
|
||||
(true, "1:1,3:3", 0, [true, false, true, false]),
|
||||
(true, "1:1,3:4", 3, [true, false, true, true]),
|
||||
(true, "1:1,3:3", 1, [false, true, false, false]),
|
||||
(true, "1:1,C3", 0, [true, false, false, false]),
|
||||
] {
|
||||
let mut h = harness(selection);
|
||||
let before = sizes(&h, rows);
|
||||
let (start, zoom) = edge(&h, rows, dragged);
|
||||
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);
|
||||
let expected = (before[dragged as usize] + 30.0 / zoom).round();
|
||||
for (index, changed) in changed.iter().enumerate() {
|
||||
let want = if *changed { expected } else { before[index] };
|
||||
assert_eq!(after[index], want, "selection {selection}, dragged {dragged}, line {index}");
|
||||
}
|
||||
assert_eq!(h.state().session.active().unwrap().undo.len(), 1, "selection {selection}: one undo step");
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(sizes(&h, rows), before, "selection {selection}: undo restores every line");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user