mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 04:06:52 -06:00
Grid: an armed Format Painter paints whole rows and columns from their headers (refs #1211)
This commit is contained in:
@@ -1557,6 +1557,63 @@ fn format_painter(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let Some(src_sheet) = wb.sheet(sheet) else { return Ok(()) };
|
||||
let (h, w) = (src.height(), src.width());
|
||||
let dest = if dest.is_single() { RangeRef::new(dest.start, dest.start.offset_clamped(h as i64 - 1, w as i64 - 1)) } else { dest };
|
||||
// Whole rows or columns take a line format, as the ribbon's formatting commands give
|
||||
// them, instead of a style on each of their cells (a column alone has a million). The
|
||||
// line format comes from the first cell of the source pattern across the line; cells
|
||||
// already stored follow the pattern. Row heights and column widths stay as they are.
|
||||
let rows = dest.is_full_rows() && !dest.is_full_cols();
|
||||
let cols = dest.is_full_cols() && !dest.is_full_rows();
|
||||
let src_full = (rows && src.is_full_rows()) || (cols && src.is_full_cols());
|
||||
if rows || cols {
|
||||
let (first, src_first, n) = if rows { (dest.start.row, src.start.row, h) } else { (dest.start.col, src.start.col, w) };
|
||||
let lines = if rows { dest.start.row..=dest.end.row } else { dest.start.col..=dest.end.col };
|
||||
for line in lines {
|
||||
let sl = src_first + (line - first) % n;
|
||||
// The source cell for position `k` along the line.
|
||||
let at = |k: u32| if rows { CellRef::new(sl, src.start.col + k % w) } else { CellRef::new(src.start.row + k % h, sl) };
|
||||
let line_style = if src_full {
|
||||
if rows { src_sheet.rows.get(&sl) } else { src_sheet.cols.get(&sl) }.and_then(|i| i.style)
|
||||
} else {
|
||||
Some(src_sheet.style_id(at(0)))
|
||||
};
|
||||
let line_style = line_style.map(|st| if same { st } else { cx.wb.styles.intern(wb.styles.get(st).clone()) });
|
||||
let (dest_line, src_line) = if rows {
|
||||
(RangeRef::rows(line, line), RangeRef::rows(sl, sl))
|
||||
} else {
|
||||
(RangeRef::cols(line, line), RangeRef::cols(sl, sl))
|
||||
};
|
||||
let mut cells: Vec<CellRef> =
|
||||
cx.wb.sheet(dest_sheet).map(|sh| sh.cells.iter_range(dest_line).map(|(c, _)| c).collect()).unwrap_or_default();
|
||||
if src_full {
|
||||
cells.extend(
|
||||
src_sheet
|
||||
.cells
|
||||
.iter_range(src_line)
|
||||
.map(|(c, _)| if rows { CellRef::new(line, c.col) } else { CellRef::new(c.row, line) }),
|
||||
);
|
||||
}
|
||||
if cols {
|
||||
// A row format outranks a column format: the column's cells in formatted
|
||||
// rows need a style of their own.
|
||||
cells.extend(
|
||||
cx.wb
|
||||
.sheet(dest_sheet)
|
||||
.into_iter()
|
||||
.flat_map(|sh| sh.rows.iter().filter(|(_, i)| i.style.is_some()).map(|(r, _)| CellRef::new(*r, line))),
|
||||
);
|
||||
}
|
||||
let sh = cx.sheet_mut(dest_sheet)?;
|
||||
(if rows { &mut sh.rows } else { &mut sh.cols }).entry(line).or_default().style = line_style;
|
||||
for c in cells {
|
||||
let st = src_sheet.style_id(at(if rows { c.col } else { c.row }));
|
||||
let st = if same { st } else { cx.wb.styles.intern(wb.styles.get(st).clone()) };
|
||||
cx.sheet_mut(dest_sheet)?.set_style(c, st);
|
||||
cx.touch(dest_sheet, c);
|
||||
}
|
||||
}
|
||||
*cx.sel = crate::Selection::range(dest);
|
||||
return Ok(());
|
||||
}
|
||||
for c in dest.iter().take(2_000_000) {
|
||||
let sc = CellRef::new(src.start.row + (c.row - dest.start.row) % h, src.start.col + (c.col - dest.start.col) % w);
|
||||
let st = src_sheet.style_id(sc);
|
||||
|
||||
+18
-10
@@ -2019,8 +2019,10 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
||||
if point_header(app, wb, sh, RangeRef::cols(c, c)) {
|
||||
return;
|
||||
}
|
||||
// An armed Format Painter paints the columns dragged over instead of moving them.
|
||||
if resp.drag_started()
|
||||
&& !mods.any()
|
||||
&& app.session.format_painter.is_none()
|
||||
&& sel.ranges.len() == 1
|
||||
&& sel.current().is_full_cols()
|
||||
&& sel.current().contains(CellRef::new(0, c))
|
||||
@@ -2264,17 +2266,10 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
||||
{
|
||||
select_to_pointer(app, geo, sh, wb, p);
|
||||
}
|
||||
// An armed Format Painter pastes onto the cells just clicked or dragged over
|
||||
// (not while pointing a formula reference, which also drags a Select).
|
||||
if app.session.format_painter.is_some() && !app.editor.as_ref().is_some_and(|ed| ed.can_point()) {
|
||||
let dest = app.session.active().map(|d| d.selection.current().a1());
|
||||
if let Some(dest) = dest
|
||||
&& let Err(e) = app.run("edit.formatPainter", json!({"apply": dest}))
|
||||
{
|
||||
app.message = Some(("GridCraft".into(), crate::clean_error(&e)));
|
||||
}
|
||||
}
|
||||
format_painter_apply(app);
|
||||
}
|
||||
// Whole rows or columns clicked or dragged over by their headers.
|
||||
Drag::Rows(_) | Drag::Cols(_) => format_painter_apply(app),
|
||||
Drag::Fill { target: Some(t) } => {
|
||||
let src = sel.current();
|
||||
if t != src {
|
||||
@@ -2342,6 +2337,19 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
||||
keyboard(app, &ctx, resp, geo, sh);
|
||||
}
|
||||
|
||||
/// An armed Format Painter pastes onto the cells just clicked or dragged over (not while
|
||||
/// pointing a formula reference, which also drags a Select).
|
||||
fn format_painter_apply(app: &mut SheetApp) {
|
||||
if app.session.format_painter.is_some() && !app.editor.as_ref().is_some_and(|ed| ed.can_point()) {
|
||||
let dest = app.session.active().map(|d| d.selection.current().a1());
|
||||
if let Some(dest) = dest
|
||||
&& let Err(e) = app.run("edit.formatPainter", json!({"apply": dest}))
|
||||
{
|
||||
app.message = Some(("GridCraft".into(), crate::clean_error(&e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Point mode on a header: inserts the whole-column or whole-row reference instead of selecting.
|
||||
fn point_header(app: &mut SheetApp, wb: &Workbook, sh: &Sheet, range: RangeRef) -> bool {
|
||||
let Some(ed) = app.editor.as_mut().filter(|ed| ed.can_point()) else { return false };
|
||||
|
||||
@@ -76,3 +76,121 @@ fn ribbon_button_picks_up_the_format_and_a_click_on_a_cell_pastes_it() {
|
||||
assert!(h.state().session.format_painter.is_none(), "a single-click painter disarms after one use");
|
||||
assert!(!pressed(&h));
|
||||
}
|
||||
|
||||
fn header_point(h: &Harness, header: &str) -> Pos2 {
|
||||
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);
|
||||
match header.parse::<u32>() {
|
||||
Ok(row) => {
|
||||
let cell = geo.cell_rect(sheet, CellRef::new(row - 1, 0)).center();
|
||||
Pos2::new(geo.cells.left() - geo.header_w * 0.5, cell.y)
|
||||
}
|
||||
Err(_) => {
|
||||
let cell = geo.cell_rect(sheet, CellRef::parse(&format!("{header}1")).unwrap()).center();
|
||||
Pos2::new(cell.x, geo.cells.top() - geo.header_h * 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drag(h: &mut Harness, from: Pos2, to: Pos2) {
|
||||
for event in [
|
||||
Event::PointerMoved(from),
|
||||
Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE },
|
||||
Event::PointerMoved(to),
|
||||
Event::PointerMoved(to + egui::vec2(1.0, 0.0)),
|
||||
Event::PointerButton { pos: to + egui::vec2(1.0, 0.0), button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE },
|
||||
] {
|
||||
h.input_mut().events.push(event);
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn stored_cells(h: &Harness) -> usize {
|
||||
h.state().session.active().unwrap().wb.active().unwrap().cells.len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_on_a_row_header_pastes_the_format_on_the_whole_row() {
|
||||
let mut h = harness();
|
||||
let source = style(&h, "A1");
|
||||
h.get_by_label("Format Painter").click();
|
||||
h.run_steps(2);
|
||||
|
||||
let at = header_point(&h, "5");
|
||||
drag(&mut h, at, at);
|
||||
assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "5:5");
|
||||
for a1 in ["A5", "K5", "XFD5"] {
|
||||
assert_eq!(style(&h, a1), source, "{a1} must take A1's format");
|
||||
}
|
||||
assert_ne!(style(&h, "A6"), source, "row 6 was not painted");
|
||||
// The row takes a row format: its 16384 cells are not stored one by one.
|
||||
assert_eq!(stored_cells(&h), 1);
|
||||
assert!(h.state().session.format_painter.is_none(), "a single-click painter disarms after one use");
|
||||
assert!(!pressed(&h));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_over_column_headers_with_a_kept_painter_formats_every_column() {
|
||||
let mut h = harness();
|
||||
let source = style(&h, "A1");
|
||||
h.state_mut().session.execute("edit.formatPainter", json!({"sticky": true})).unwrap();
|
||||
h.run_steps(2);
|
||||
|
||||
let (from, to) = (header_point(&h, "C"), header_point(&h, "E"));
|
||||
drag(&mut h, from, to);
|
||||
assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "C:E");
|
||||
for a1 in ["C2", "D500", "E1048576"] {
|
||||
assert_eq!(style(&h, a1), source, "{a1} must take A1's format");
|
||||
}
|
||||
for a1 in ["B2", "F2"] {
|
||||
assert_ne!(style(&h, a1), source, "{a1} is outside the dragged headers");
|
||||
}
|
||||
assert_eq!(stored_cells(&h), 1, "whole columns take a column format, not a million cell styles");
|
||||
assert!(h.state().session.format_painter.is_some(), "a double-click painter stays armed");
|
||||
|
||||
let at = header_point(&h, "8");
|
||||
drag(&mut h, at, at);
|
||||
assert_eq!(style(&h, "Z8"), source, "the kept painter paints the next header clicked too");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_from_the_header_of_the_picked_up_column_paints_instead_of_moving_it() {
|
||||
let mut h = harness();
|
||||
let session = &mut h.state_mut().session;
|
||||
session.execute("range.setValues", json!({"range": "C1", "values": [["kept"]]})).unwrap();
|
||||
session.execute("home.italic", json!({"range": "C:C", "on": true})).unwrap();
|
||||
session.execute("selection.set", json!({"range": "C:C"})).unwrap();
|
||||
session.execute("edit.formatPainter", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
let source = style(&h, "C2");
|
||||
|
||||
let (from, to) = (header_point(&h, "C"), header_point(&h, "E"));
|
||||
drag(&mut h, from, to);
|
||||
let sheet = h.state().session.active().unwrap().wb.active().unwrap();
|
||||
assert_eq!(sheet.value(CellRef::parse("C1").unwrap()), "kept".into(), "column C must stay where it is");
|
||||
assert_eq!(style(&h, "E2"), source, "E must take C's format");
|
||||
assert!(h.state().session.format_painter.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_two_cell_source_on_a_column_header_paints_a_column_format() {
|
||||
let mut h = harness();
|
||||
let source = style(&h, "A1");
|
||||
let session = &mut h.state_mut().session;
|
||||
session.execute("home.italic", json!({"range": "A2", "on": true})).unwrap();
|
||||
session.execute("home.fillColor", json!({"range": "7:7", "color": "#00FF00"})).unwrap();
|
||||
session.execute("selection.set", json!({"range": "A1:A2"})).unwrap();
|
||||
session.execute("edit.formatPainter", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(stored_cells(&h), 2);
|
||||
|
||||
let at = header_point(&h, "C");
|
||||
drag(&mut h, at, at);
|
||||
assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "C:C");
|
||||
assert_eq!(style(&h, "C1048575"), source, "the column takes the pattern's first cell as its format");
|
||||
assert_eq!(style(&h, "C7"), source, "row 7's own format must not hide the painted column");
|
||||
// Only C7 is stored on top of A1:A2: not a million cells, one per pattern repeat.
|
||||
assert_eq!(stored_cells(&h), 3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user