diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 4f13bf9c6..2b6e50b7f 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -156,7 +156,7 @@ pub fn specs() -> Vec { "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 { "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 { fn line_targets(s: &Session, p: &Json, rows: bool) -> Result> { 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()) } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 6bd03d55e..3c08ee085 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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 = (0..6).map(|c| s.doc().unwrap().wb.active().unwrap().col_width(c)).collect(); + let heights: Vec = (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"); +} diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 72d33e2dc..d228e515d 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -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::>().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::>() + .join(",") } else { format!("{}:{}", row + 1, row + 1) }; diff --git a/crates/ui-egui/tests/header_autofit.rs b/crates/ui-egui/tests/header_autofit.rs index 8a6673855..7d1bbbc0f 100644 --- a/crates/ui-egui/tests/header_autofit.rs +++ b/crates/ui-egui/tests/header_autofit.rs @@ -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"); + } +}