Skip to main content

ravenrs_sheet/engine/
selection.rs

1//! Cell selection: one or more ranges with an active cell.
2
3use crate::base::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
4use crate::model::Sheet;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Selection {
9    /// The active (white) cell.
10    pub active: CellRef,
11    /// Where a Shift-extension is anchored.
12    pub anchor: CellRef,
13    /// Selected areas; the last one contains the active cell. Never empty.
14    pub ranges: Vec<RangeRef>,
15}
16
17impl Default for Selection {
18    fn default() -> Self {
19        Selection::at(CellRef::default())
20    }
21}
22
23impl Selection {
24    pub fn at(c: CellRef) -> Selection {
25        Selection { active: c, anchor: c, ranges: vec![RangeRef::cell(c)] }
26    }
27    pub fn range(r: RangeRef) -> Selection {
28        Selection { active: r.start, anchor: r.start, ranges: vec![r] }
29    }
30    /// The current (last) area.
31    pub fn current(&self) -> RangeRef {
32        self.ranges.last().copied().unwrap_or(RangeRef::cell(self.active))
33    }
34    /// Bounding range of all areas.
35    pub fn bounds(&self) -> RangeRef {
36        let mut it = self.ranges.iter();
37        let first = it.next().copied().unwrap_or(RangeRef::cell(self.active));
38        it.fold(first, |a, b| a.union(b))
39    }
40    pub fn contains(&self, c: CellRef) -> bool {
41        self.ranges.iter().any(|r| r.contains(c))
42    }
43    pub fn is_single_cell(&self) -> bool {
44        self.ranges.len() == 1 && self.current().is_single()
45    }
46    /// The corner of the current area opposite the anchor: the end that Shift+arrows move.
47    pub fn moving_end(&self) -> CellRef {
48        let r = self.current();
49        let row = if self.anchor.row == r.start.row { r.end.row } else { r.start.row };
50        let col = if self.anchor.col == r.start.col { r.end.col } else { r.start.col };
51        CellRef::new(row, col)
52    }
53    /// Extends the current area from the anchor to `to` (Shift+click / Shift+arrows).
54    pub fn extend_to(&mut self, to: CellRef) {
55        let r = RangeRef::new(self.anchor, to);
56        if let Some(last) = self.ranges.last_mut() {
57            *last = r;
58        } else {
59            self.ranges.push(r);
60        }
61    }
62    /// The A1 text of all areas (`A1:B2,D4`).
63    pub fn a1(&self) -> String {
64        self.ranges.iter().map(|r| r.a1()).collect::<Vec<_>>().join(",")
65    }
66    /// Expands areas so merged cells are included whole. Whole columns and whole rows stay as
67    /// they are: clicking column B's header selects just B even when B1:C1 is merged, as in Excel.
68    pub fn expand_merges(&mut self, sheet: &Sheet) {
69        for r in self.ranges.iter_mut() {
70            if r.is_full_cols() || r.is_full_rows() {
71                continue;
72            }
73            let mut cur = *r;
74            loop {
75                let mut grown = cur;
76                for m in &sheet.merges {
77                    if m.intersects(&grown) {
78                        grown = grown.union(m);
79                    }
80                }
81                if grown == cur {
82                    break;
83                }
84                cur = grown;
85            }
86            *r = cur;
87        }
88    }
89    /// Moves the active cell within the selection (Enter/Tab inside a multi-cell selection).
90    pub fn cycle(&mut self, forward: bool, by_row: bool) {
91        let r = self.current();
92        if r.is_single() && self.ranges.len() == 1 {
93            return;
94        }
95        let a = self.active;
96        let (mut row, mut col) = (a.row as i64, a.col as i64);
97        let step: i64 = if forward { 1 } else { -1 };
98        if by_row {
99            col += step;
100            if col > r.end.col as i64 {
101                col = r.start.col as i64;
102                row += 1;
103            } else if col < r.start.col as i64 {
104                col = r.end.col as i64;
105                row -= 1;
106            }
107            if row > r.end.row as i64 {
108                row = r.start.row as i64;
109            } else if row < r.start.row as i64 {
110                row = r.end.row as i64;
111            }
112        } else {
113            row += step;
114            if row > r.end.row as i64 {
115                row = r.start.row as i64;
116                col += 1;
117            } else if row < r.start.row as i64 {
118                row = r.end.row as i64;
119                col -= 1;
120            }
121            if col > r.end.col as i64 {
122                col = r.start.col as i64;
123            } else if col < r.start.col as i64 {
124                col = r.end.col as i64;
125            }
126        }
127        self.active = CellRef::new(row.clamp(0, MAX_ROWS as i64 - 1) as u32, col.clamp(0, MAX_COLS as i64 - 1) as u32);
128    }
129}
130
131impl Selection {
132    /// Ctrl+. (Next Corner of Selection): the active cell moves clockwise to the next corner of
133    /// the current area (top-left, top-right, bottom-right, bottom-left) and becomes the anchor,
134    /// so Shift+arrows then move the opposite corner. The area stays selected. From a cell that
135    /// isn't a corner the next corner is the top-left one; a single cell stays where it is.
136    pub fn next_corner(&mut self) {
137        self.next_corner_by(Some);
138    }
139
140    /// [`Selection::next_corner`] where `stop` says where each corner takes the active cell: the
141    /// top-left cell of a merged cell the corner is in, or `None` for a corner that can't be
142    /// selected (it is skipped). Corners that end up on the active cell are one stop with it.
143    pub fn next_corner_by(&mut self, stop: impl Fn(CellRef) -> Option<CellRef>) {
144        let r = self.current();
145        let corners = [r.start, CellRef::new(r.start.row, r.end.col), r.end, CellRef::new(r.end.row, r.start.col)];
146        let next = match corners.iter().rposition(|c| stop(*c) == Some(self.active)) {
147            Some(i) => (1..=4).filter_map(|k| corners.get((i + k) % 4)).filter_map(|c| stop(*c)).find(|c| *c != self.active),
148            None => corners.iter().find_map(|c| stop(*c)),
149        };
150        if let Some(next) = next {
151            self.active = next;
152            self.anchor = next;
153        }
154    }
155}
156
157/// Ctrl+arrow: jump to the edge of the current data region (Excel's End mode).
158pub fn jump(sheet: &Sheet, from: CellRef, dr: i64, dc: i64) -> CellRef {
159    if dr == 0 && dc == 0 {
160        // No direction: nowhere to go (the search below would never advance).
161        return from;
162    }
163    let filled = |c: CellRef| sheet.value_ref(c).is_some_and(|v| !v.is_empty());
164    let Some(mut cur) = from.offset(dr, dc) else { return from };
165    let here = filled(from);
166    let next = filled(cur);
167    if here && next {
168        // Move to the last filled cell of the run.
169        while let Some(n) = cur.offset(dr, dc) {
170            if !filled(n) {
171                break;
172            }
173            cur = n;
174        }
175        return cur;
176    }
177    // Move to the next filled cell, or the sheet edge. Use the used range to avoid walking a
178    // million empty cells.
179    let used = sheet.used_range();
180    loop {
181        if filled(cur) {
182            return cur;
183        }
184        let beyond = match used {
185            Some(u) => {
186                (dr > 0 && cur.row > u.end.row)
187                    || (dc > 0 && cur.col > u.end.col)
188                    || (dr < 0 && cur.row < u.start.row)
189                    || (dc < 0 && cur.col < u.start.col)
190            }
191            None => true,
192        };
193        if beyond {
194            let r = if dr > 0 {
195                MAX_ROWS - 1
196            } else if dr < 0 {
197                0
198            } else {
199                cur.row
200            };
201            let c = if dc > 0 {
202                MAX_COLS - 1
203            } else if dc < 0 {
204                0
205            } else {
206                cur.col
207            };
208            return CellRef::new(r, c);
209        }
210        match cur.offset(dr, dc) {
211            Some(n) => cur = n,
212            None => return cur,
213        }
214    }
215}
216
217/// The contiguous data region around `c` (Ctrl+A / Ctrl+Shift+8, AutoSum, sort, tables).
218pub fn current_region(sheet: &Sheet, c: CellRef) -> RangeRef {
219    let filled = |c: CellRef| sheet.cell(c).is_some_and(crate::model::Cell::has_content) || sheet.spill.contains_key(&c);
220    // Whether a row (or column) has a filled cell between two columns (rows), inclusive.
221    let row_has = |row: u32, a: u32, b: u32| (a..=b).any(|col| filled(CellRef::new(row, col)));
222    let col_has = |col: u32, a: u32, b: u32| (a..=b).any(|row| filled(CellRef::new(row, col)));
223    let (mut r0, mut r1, mut c0, mut c1) = (c.row, c.row, c.col, c.col);
224    // Grow one side at a time while the line next to it (diagonals included) has data. Each
225    // pass scans only the lines next to the region, so tall regions take linear time.
226    loop {
227        let before = (r0, r1, c0, c1);
228        let (lo_c, hi_c) = (c0.saturating_sub(1), (c1 + 1).min(MAX_COLS - 1));
229        while r0 > 0 && row_has(r0 - 1, lo_c, hi_c) {
230            r0 -= 1;
231        }
232        while r1 + 1 < MAX_ROWS && row_has(r1 + 1, lo_c, hi_c) {
233            r1 += 1;
234        }
235        let (lo_r, hi_r) = (r0.saturating_sub(1), (r1 + 1).min(MAX_ROWS - 1));
236        while c0 > 0 && col_has(c0 - 1, lo_r, hi_r) {
237            c0 -= 1;
238        }
239        while c1 + 1 < MAX_COLS && col_has(c1 + 1, lo_r, hi_r) {
240            c1 += 1;
241        }
242        if (r0, r1, c0, c1) == before {
243            break;
244        }
245    }
246    RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1))
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::base::Value;
253
254    #[test]
255    fn jumps() {
256        let mut s = Sheet::new("S");
257        for r in 0..5 {
258            s.set_value(CellRef::new(r, 0), Value::Number(1.0));
259        }
260        s.set_value(CellRef::new(10, 0), Value::Number(1.0));
261        assert_eq!(jump(&s, CellRef::new(0, 0), 1, 0), CellRef::new(4, 0));
262        // Without a direction it stays put (it used to loop forever from a blank cell).
263        assert_eq!(jump(&s, CellRef::new(7, 0), 0, 0), CellRef::new(7, 0));
264        assert_eq!(jump(&s, CellRef::new(4, 0), 1, 0), CellRef::new(10, 0));
265        assert_eq!(jump(&s, CellRef::new(10, 0), 1, 0), CellRef::new(MAX_ROWS - 1, 0));
266        assert_eq!(jump(&s, CellRef::new(0, 0), 0, 1), CellRef::new(0, MAX_COLS - 1));
267        assert_eq!(jump(&s, CellRef::new(0, 0), -1, 0), CellRef::new(0, 0));
268    }
269
270    #[test]
271    fn regions() {
272        let mut s = Sheet::new("S");
273        for r in 2..5 {
274            for c in 1..4 {
275                s.set_value(CellRef::new(r, c), Value::Number(1.0));
276            }
277        }
278        assert_eq!(current_region(&s, CellRef::new(3, 2)).a1(), "B3:D5");
279        assert_eq!(current_region(&s, CellRef::new(5, 4)).a1(), "B3:E6");
280        assert_eq!(current_region(&s, CellRef::new(20, 20)).a1(), "U21");
281        // Taller than 10,000 rows: it used to stop growing after 10,000 steps.
282        let mut s = Sheet::new("S");
283        for r in 0..20_000 {
284            s.set_value(CellRef::new(r, 0), Value::Number(r as f64));
285            s.set_value(CellRef::new(r, 1), Value::Number(1.0));
286        }
287        assert_eq!(current_region(&s, CellRef::new(5, 0)).a1(), "A1:B20000");
288    }
289
290    #[test]
291    fn cycling() {
292        let mut sel = Selection::range(RangeRef::parse("A1:B2").unwrap());
293        sel.cycle(true, false);
294        assert_eq!(sel.active.a1(), "A2");
295        sel.cycle(true, false);
296        assert_eq!(sel.active.a1(), "B1");
297        sel.cycle(true, true);
298        assert_eq!(sel.active.a1(), "A2");
299    }
300}