Files
gridcraft/crates/engine/src/selection.rs
T
nihilist-narwhal 1b0b2009aa Current region: grow past 10,000 rows, in linear time
current_region (Ctrl+A, and the range that sort, filter, AutoSum,
tables and Get Data use when one cell is selected) grew the region one
ring of cells per step and stopped after 10,000 steps, rescanning every
cell of the ring each time. With 20,000 rows of data and one cell
selected, data.sortDescending sorted only the first 10,001 rows and
left the rest as they were; a 100,000-row region took 30 s and was cut
at row 10,001.

It now grows one side at a time while the line next to that side has
data (diagonals included), checking only the cells of that line, with
no step limit.
2026-10-10 03:17:10 -05:00

258 lines
8.9 KiB
Rust

//! Cell selection: one or more ranges with an active cell.
use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
use gridcraft_model::Sheet;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Selection {
/// The active (white) cell.
pub active: CellRef,
/// Where a Shift-extension is anchored.
pub anchor: CellRef,
/// Selected areas; the last one contains the active cell. Never empty.
pub ranges: Vec<RangeRef>,
}
impl Default for Selection {
fn default() -> Self {
Selection::at(CellRef::default())
}
}
impl Selection {
pub fn at(c: CellRef) -> Selection {
Selection { active: c, anchor: c, ranges: vec![RangeRef::cell(c)] }
}
pub fn range(r: RangeRef) -> Selection {
Selection { active: r.start, anchor: r.start, ranges: vec![r] }
}
/// The current (last) area.
pub fn current(&self) -> RangeRef {
self.ranges.last().copied().unwrap_or(RangeRef::cell(self.active))
}
/// Bounding range of all areas.
pub fn bounds(&self) -> RangeRef {
let mut it = self.ranges.iter();
let first = it.next().copied().unwrap_or(RangeRef::cell(self.active));
it.fold(first, |a, b| a.union(b))
}
pub fn contains(&self, c: CellRef) -> bool {
self.ranges.iter().any(|r| r.contains(c))
}
pub fn is_single_cell(&self) -> bool {
self.ranges.len() == 1 && self.current().is_single()
}
/// Extends the current area from the anchor to `to` (Shift+click / Shift+arrows).
pub fn extend_to(&mut self, to: CellRef) {
let r = RangeRef::new(self.anchor, to);
if let Some(last) = self.ranges.last_mut() {
*last = r;
} else {
self.ranges.push(r);
}
}
/// The A1 text of all areas (`A1:B2,D4`).
pub fn a1(&self) -> String {
self.ranges.iter().map(|r| r.a1()).collect::<Vec<_>>().join(",")
}
/// Expands areas so merged cells are included whole.
pub fn expand_merges(&mut self, sheet: &Sheet) {
for r in self.ranges.iter_mut() {
let mut cur = *r;
loop {
let mut grown = cur;
for m in &sheet.merges {
if m.intersects(&grown) {
grown = grown.union(m);
}
}
if grown == cur {
break;
}
cur = grown;
}
*r = cur;
}
}
/// Moves the active cell within the selection (Enter/Tab inside a multi-cell selection).
pub fn cycle(&mut self, forward: bool, by_row: bool) {
let r = self.current();
if r.is_single() && self.ranges.len() == 1 {
return;
}
let a = self.active;
let (mut row, mut col) = (a.row as i64, a.col as i64);
let step: i64 = if forward { 1 } else { -1 };
if by_row {
col += step;
if col > r.end.col as i64 {
col = r.start.col as i64;
row += 1;
} else if col < r.start.col as i64 {
col = r.end.col as i64;
row -= 1;
}
if row > r.end.row as i64 {
row = r.start.row as i64;
} else if row < r.start.row as i64 {
row = r.end.row as i64;
}
} else {
row += step;
if row > r.end.row as i64 {
row = r.start.row as i64;
col += 1;
} else if row < r.start.row as i64 {
row = r.end.row as i64;
col -= 1;
}
if col > r.end.col as i64 {
col = r.start.col as i64;
} else if col < r.start.col as i64 {
col = r.end.col as i64;
}
}
self.active = CellRef::new(row.clamp(0, MAX_ROWS as i64 - 1) as u32, col.clamp(0, MAX_COLS as i64 - 1) as u32);
}
}
/// Ctrl+arrow: jump to the edge of the current data region (Excel's End mode).
pub fn jump(sheet: &Sheet, from: CellRef, dr: i64, dc: i64) -> CellRef {
let filled = |c: CellRef| sheet.value_ref(c).is_some_and(|v| !v.is_empty());
let Some(mut cur) = from.offset(dr, dc) else { return from };
let here = filled(from);
let next = filled(cur);
if here && next {
// Move to the last filled cell of the run.
while let Some(n) = cur.offset(dr, dc) {
if !filled(n) {
break;
}
cur = n;
}
return cur;
}
// Move to the next filled cell, or the sheet edge. Use the used range to avoid walking a
// million empty cells.
let used = sheet.used_range();
loop {
if filled(cur) {
return cur;
}
let beyond = match used {
Some(u) => {
(dr > 0 && cur.row > u.end.row)
|| (dc > 0 && cur.col > u.end.col)
|| (dr < 0 && cur.row < u.start.row)
|| (dc < 0 && cur.col < u.start.col)
}
None => true,
};
if beyond {
let r = if dr > 0 {
MAX_ROWS - 1
} else if dr < 0 {
0
} else {
cur.row
};
let c = if dc > 0 {
MAX_COLS - 1
} else if dc < 0 {
0
} else {
cur.col
};
return CellRef::new(r, c);
}
match cur.offset(dr, dc) {
Some(n) => cur = n,
None => return cur,
}
}
}
/// The contiguous data region around `c` (Ctrl+A / Ctrl+Shift+8, AutoSum, sort, tables).
pub fn current_region(sheet: &Sheet, c: CellRef) -> RangeRef {
let filled = |c: CellRef| sheet.cell(c).is_some_and(|x| !x.value.is_empty() || x.formula.is_some()) || sheet.spill.contains_key(&c);
// Whether a row (or column) has a filled cell between two columns (rows), inclusive.
let row_has = |row: u32, a: u32, b: u32| (a..=b).any(|col| filled(CellRef::new(row, col)));
let col_has = |col: u32, a: u32, b: u32| (a..=b).any(|row| filled(CellRef::new(row, col)));
let (mut r0, mut r1, mut c0, mut c1) = (c.row, c.row, c.col, c.col);
// Grow one side at a time while the line next to it (diagonals included) has data. Each
// pass scans only the lines next to the region, so tall regions take linear time.
loop {
let before = (r0, r1, c0, c1);
let (lo_c, hi_c) = (c0.saturating_sub(1), (c1 + 1).min(MAX_COLS - 1));
while r0 > 0 && row_has(r0 - 1, lo_c, hi_c) {
r0 -= 1;
}
while r1 + 1 < MAX_ROWS && row_has(r1 + 1, lo_c, hi_c) {
r1 += 1;
}
let (lo_r, hi_r) = (r0.saturating_sub(1), (r1 + 1).min(MAX_ROWS - 1));
while c0 > 0 && col_has(c0 - 1, lo_r, hi_r) {
c0 -= 1;
}
while c1 + 1 < MAX_COLS && col_has(c1 + 1, lo_r, hi_r) {
c1 += 1;
}
if (r0, r1, c0, c1) == before {
break;
}
}
RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1))
}
#[cfg(test)]
mod tests {
use super::*;
use gridcraft_core::Value;
#[test]
fn jumps() {
let mut s = Sheet::new("S");
for r in 0..5 {
s.set_value(CellRef::new(r, 0), Value::Number(1.0));
}
s.set_value(CellRef::new(10, 0), Value::Number(1.0));
assert_eq!(jump(&s, CellRef::new(0, 0), 1, 0), CellRef::new(4, 0));
assert_eq!(jump(&s, CellRef::new(4, 0), 1, 0), CellRef::new(10, 0));
assert_eq!(jump(&s, CellRef::new(10, 0), 1, 0), CellRef::new(MAX_ROWS - 1, 0));
assert_eq!(jump(&s, CellRef::new(0, 0), 0, 1), CellRef::new(0, MAX_COLS - 1));
assert_eq!(jump(&s, CellRef::new(0, 0), -1, 0), CellRef::new(0, 0));
}
#[test]
fn regions() {
let mut s = Sheet::new("S");
for r in 2..5 {
for c in 1..4 {
s.set_value(CellRef::new(r, c), Value::Number(1.0));
}
}
assert_eq!(current_region(&s, CellRef::new(3, 2)).a1(), "B3:D5");
assert_eq!(current_region(&s, CellRef::new(5, 4)).a1(), "B3:E6");
assert_eq!(current_region(&s, CellRef::new(20, 20)).a1(), "U21");
// Taller than 10,000 rows: it used to stop growing after 10,000 steps.
let mut s = Sheet::new("S");
for r in 0..20_000 {
s.set_value(CellRef::new(r, 0), Value::Number(r as f64));
s.set_value(CellRef::new(r, 1), Value::Number(1.0));
}
assert_eq!(current_region(&s, CellRef::new(5, 0)).a1(), "A1:B20000");
}
#[test]
fn cycling() {
let mut sel = Selection::range(RangeRef::parse("A1:B2").unwrap());
sel.cycle(true, false);
assert_eq!(sel.active.a1(), "A2");
sel.cycle(true, false);
assert_eq!(sel.active.a1(), "B1");
sel.cycle(true, true);
assert_eq!(sel.active.a1(), "A2");
}
}