Skip to main content

ravenrs_sheet/engine/cmd/edit/
selection.rs

1use super::{
2    CellRef, EngineError, Json, RangeRef, Result, Session, bool_param, cell_param, current_region, json, jump, split_sheet, str_param, target_ranges,
3    target_sheet,
4};
5
6// ---------------------------------------------------------------- selection
7
8/// Whether the cell may be selected: a protected sheet can forbid selecting locked and/or unlocked cells.
9fn cell_selectable(wb: &crate::model::Workbook, sheet: &crate::model::Sheet, c: CellRef) -> bool {
10    let Some(p) = sheet.protection.as_ref() else { return true };
11    if wb.styles.get(sheet.style_id(c)).protection.locked { p.select_locked } else { p.select_unlocked }
12}
13
14pub(super) fn selection_set(s: &mut Session, p: &Json) -> Result<Json> {
15    if p.get("sheet").is_some() {
16        let i = target_sheet(s, p)?;
17        s.doc_mut()?.wb_switch_sheet(i);
18    } else if let Some(r) = str_param(p, "range").or_else(|| str_param(p, "cell"))
19        && let (Some(_), _) = split_sheet(r)
20    {
21        let i = target_sheet(s, p)?;
22        s.doc_mut()?.wb_switch_sheet(i);
23    }
24    let ranges = target_ranges(s, p)?;
25    let active = cell_param(s, p, "active")?;
26    let d = s.doc_mut()?;
27    let active = active.or_else(|| ranges.last().map(|r| r.start)).unwrap_or_default();
28    let mut sel = crate::engine::Selection { active, anchor: ranges.last().map(|r| r.start).unwrap_or(active), ranges };
29    if let Some(sh) = d.wb.active() {
30        sel.expand_merges(sh);
31        if !cell_selectable(&d.wb, sh, sel.active) {
32            return Err(EngineError::Other("The cell you are trying to change is on a protected sheet that does not allow selecting it.".into()));
33        }
34    }
35    d.selection = sel;
36    Ok(json!({"selection": d.selection.a1(), "active": d.selection.active.a1()}))
37}
38
39impl crate::engine::DocState {
40    /// Switches the active sheet, remembering each sheet's selection.
41    pub fn wb_switch_sheet(&mut self, i: usize) {
42        if i >= self.wb.sheets.len() || i == self.wb.active_sheet() {
43            return;
44        }
45        let cur = self.wb.active_sheet();
46        if self.sheet_selections.len() < self.wb.sheets.len() {
47            self.sheet_selections.resize(self.wb.sheets.len(), crate::engine::Selection::default());
48        }
49        if let Some(slot) = self.sheet_selections.get_mut(cur) {
50            *slot = self.selection.clone();
51        }
52        // View state: set in place, so the workbook is neither copied nor made dirty.
53        self.wb.set_active_sheet(i);
54        self.selection = self.sheet_selections.get(i).cloned().unwrap_or_default();
55    }
56}
57
58pub(super) fn selection_move(s: &mut Session, p: &Json) -> Result<Json> {
59    let dr = p.get("dr").and_then(Json::as_i64).unwrap_or(0);
60    let dc = p.get("dc").and_then(Json::as_i64).unwrap_or(0);
61    let extend = bool_param(p, "extend").unwrap_or(false);
62    let jmp = bool_param(p, "jump").unwrap_or(false);
63    let d = s.doc_mut()?;
64    let Some(sheet) = d.wb.active() else { return Err(EngineError::NoDocument) };
65    let sel = &mut d.selection;
66    // Extension moves the far corner, not the active cell.
67    let from = if extend { sel.moving_end() } else { sel.active };
68    // Skip hidden rows/columns and step over merged areas.
69    let mut to = if jmp && extend && (dr == 0) != (dc == 0) {
70        // Excel: Ctrl+Shift+arrow searches the active cell's column (or row) from the moving
71        // edge, so a header row selected with Ctrl+Shift+Right extends down to the end of the
72        // active column's data.
73        let probe = if dr != 0 { CellRef::new(from.row, sel.active.col) } else { CellRef::new(sel.active.row, from.col) };
74        let edge = jump(sheet, probe, dr.signum(), dc.signum());
75        if dr != 0 { CellRef::new(edge.row, from.col) } else { CellRef::new(from.row, edge.col) }
76    } else if jmp {
77        jump(sheet, from, dr.signum(), dc.signum())
78    } else {
79        let mut t = from.offset_clamped(dr, dc);
80        let mut guard = 0;
81        while (sheet.is_row_hidden(t.row) || sheet.is_col_hidden(t.col)) && guard < 100_000 {
82            let n = t.offset_clamped(dr.signum(), dc.signum());
83            if n == t {
84                break;
85            }
86            t = n;
87            guard += 1;
88        }
89        if !extend && let Some(m) = sheet.merge_at(from) {
90            // Leaving a merged cell moves from its edge.
91            if dr > 0 {
92                t = CellRef::new(m.end.offset_clamped(dr, 0).row, from.col);
93            } else if dc > 0 {
94                t = CellRef::new(from.row, m.end.offset_clamped(0, dc).col);
95            }
96        }
97        t
98    };
99    if let Some(m) = sheet.merge_at(to)
100        && !extend
101    {
102        to = m.start;
103    }
104    if !extend && !cell_selectable(&d.wb, sheet, to) {
105        // Step on to the next selectable cell in the same direction; stay put if there is none.
106        let (sr, sc) = (dr.signum(), dc.signum());
107        let mut t = to;
108        let mut found = false;
109        if !jmp && (sr, sc) != (0, 0) {
110            for _ in 0..10_000 {
111                let n = t.offset_clamped(sr, sc);
112                if n == t {
113                    break;
114                }
115                t = n;
116                if !sheet.is_row_hidden(t.row) && !sheet.is_col_hidden(t.col) && cell_selectable(&d.wb, sheet, t) {
117                    found = true;
118                    break;
119                }
120            }
121        }
122        if !found {
123            return Ok(json!({"selection": sel.a1(), "active": sel.active.a1()}));
124        }
125        to = t;
126    }
127    if extend {
128        sel.extend_to(to);
129        sel.expand_merges(sheet);
130    } else {
131        *sel = crate::engine::Selection::at(to);
132        if let Some(m) = sheet.merge_at(to) {
133            sel.ranges = vec![m];
134        }
135    }
136    Ok(json!({"selection": sel.a1(), "active": sel.active.a1()}))
137}
138
139pub(super) fn selection_next(s: &mut Session, p: &Json) -> Result<Json> {
140    let fwd = bool_param(p, "forward").unwrap_or(true);
141    let by_row = bool_param(p, "byRow").unwrap_or(false);
142    let d = s.doc_mut()?;
143    d.selection.cycle(fwd, by_row);
144    Ok(json!({"active": d.selection.active.a1()}))
145}
146
147pub(super) fn selection_next_corner(s: &mut Session, _: &Json) -> Result<Json> {
148    let d = s.doc_mut()?;
149    let Some(sheet) = d.wb.active() else { return Err(EngineError::NoDocument) };
150    // A corner in a merged cell lands on its top-left cell. One in a hidden row or column, one
151    // the sheet's protection doesn't let you select, and a merged cell that starts outside the
152    // selected area are skipped.
153    let area = d.selection.current();
154    let stop = |c: CellRef| {
155        Some(sheet.merge_at(c).map_or(c, |m| m.start))
156            .filter(|c| area.contains(*c) && !sheet.is_row_hidden(c.row) && !sheet.is_col_hidden(c.col) && cell_selectable(&d.wb, sheet, *c))
157    };
158    let mut next = d.selection.clone();
159    next.next_corner_by(stop);
160    d.selection = next;
161    Ok(json!({"active": d.selection.active.a1(), "selection": d.selection.a1()}))
162}
163
164pub(super) fn select_all(s: &mut Session, _: &Json) -> Result<Json> {
165    let d = s.doc_mut()?;
166    let Some(sheet) = d.wb.active() else { return Err(EngineError::NoDocument) };
167    let region = current_region(sheet, d.selection.active);
168    let all = RangeRef::all();
169    let r = if region.is_single() || d.selection.current() == region { all } else { region };
170    let active = d.selection.active;
171    d.selection = crate::engine::Selection { active, anchor: r.start, ranges: vec![r] };
172    Ok(json!({"selection": d.selection.a1()}))
173}
174
175pub(super) fn select_region(s: &mut Session, _: &Json) -> Result<Json> {
176    let d = s.doc_mut()?;
177    let Some(sheet) = d.wb.active() else { return Err(EngineError::NoDocument) };
178    let r = current_region(sheet, d.selection.active);
179    let active = d.selection.active;
180    d.selection = crate::engine::Selection { active, anchor: r.start, ranges: vec![r] };
181    Ok(json!({"selection": d.selection.a1()}))
182}
183
184pub(super) fn select_row(s: &mut Session, _: &Json) -> Result<Json> {
185    let d = s.doc_mut()?;
186    let r = d.selection.current();
187    let active = d.selection.active;
188    d.selection = crate::engine::Selection { active, anchor: CellRef::new(r.start.row, 0), ranges: vec![RangeRef::rows(r.start.row, r.end.row)] };
189    Ok(json!({"selection": d.selection.a1()}))
190}
191
192pub(super) fn select_col(s: &mut Session, _: &Json) -> Result<Json> {
193    let d = s.doc_mut()?;
194    let r = d.selection.current();
195    let active = d.selection.active;
196    d.selection = crate::engine::Selection { active, anchor: CellRef::new(0, r.start.col), ranges: vec![RangeRef::cols(r.start.col, r.end.col)] };
197    Ok(json!({"selection": d.selection.a1()}))
198}