Skip to main content

ironcalc_base/user_model/
ui.rs

1#![deny(missing_docs)]
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    constants::{LAST_COLUMN, LAST_ROW},
7    expressions::utils::{is_valid_column_number, is_valid_row},
8    worksheet::NavigationDirection,
9};
10
11use super::common::UserModel;
12
13#[derive(Serialize, Deserialize)]
14#[cfg_attr(test, derive(PartialEq, Debug))]
15pub struct SelectedView {
16    pub sheet: u32,
17    pub row: i32,
18    pub column: i32,
19    pub range: [i32; 4],
20    pub top_row: i32,
21    pub left_column: i32,
22}
23
24impl<'a> UserModel<'a> {
25    // The UI renders every row and column at a whole number of pixels
26    // (the canvas rounds each size before drawing), so all the scroll and
27    // visibility arithmetic in this module must accumulate the rounded
28    // sizes: summing the raw values drifts away from the rendered geometry
29    // as the rounding errors pile up.
30    fn ui_row_height(&self, sheet: u32, row: i32) -> Result<f64, String> {
31        self.model.get_row_height(sheet, row).map(f64::round)
32    }
33
34    fn ui_column_width(&self, sheet: u32, column: i32) -> Result<f64, String> {
35        self.model.get_column_width(sheet, column).map(f64::round)
36    }
37
38    // Returns the anchor of the merged cell containing (row, column), or the
39    // cell itself if it is not merged.
40    fn merge_anchor(&self, sheet: u32, row: i32, column: i32) -> Result<(i32, i32), String> {
41        Ok(self
42            .model
43            .workbook
44            .worksheet(sheet)?
45            .merge_anchor(row, column))
46    }
47
48    // Returns the selection range of a single selected cell: the whole merged
49    // range when the cell is merged, the cell itself otherwise.
50    fn single_cell_range(&self, sheet: u32, row: i32, column: i32) -> Result<[i32; 4], String> {
51        let worksheet = self.model.workbook.worksheet(sheet)?;
52        Ok(match worksheet.merged_cell_containing(row, column) {
53            Some(m) => [m.row, m.column, m.last_row(), m.last_column()],
54            None => [row, column, row, column],
55        })
56    }
57
58    // The selected range for a selection anchored at (anchor_row, anchor_column)
59    // whose moving corner (focus) is at (focus_row, focus_column): the bounding
60    // box of both cells — each taken as its whole merged cell when inside one —
61    // grown so it never covers a merged cell partially. Recomputing the range
62    // from anchor and focus (instead of stretching the previous, already grown,
63    // range) is what lets a selection shrink back over a merged cell.
64    fn selection_range(
65        &self,
66        sheet: u32,
67        anchor_row: i32,
68        anchor_column: i32,
69        focus_row: i32,
70        focus_column: i32,
71    ) -> Result<[i32; 4], String> {
72        let worksheet = self.model.workbook.worksheet(sheet)?;
73        let cell_rect = |row: i32, column: i32| match worksheet.merged_cell_containing(row, column)
74        {
75            Some(m) => (m.row, m.column, m.last_row(), m.last_column()),
76            None => (row, column, row, column),
77        };
78        let (anchor_first_row, anchor_first_column, anchor_last_row, anchor_last_column) =
79            cell_rect(anchor_row, anchor_column);
80        let (focus_first_row, focus_first_column, focus_last_row, focus_last_column) =
81            cell_rect(focus_row, focus_column);
82        self.grow_range_over_merged_cells(
83            sheet,
84            [
85                anchor_first_row.min(focus_first_row),
86                anchor_first_column.min(focus_first_column),
87                anchor_last_row.max(focus_last_row),
88                anchor_last_column.max(focus_last_column),
89            ],
90        )
91    }
92
93    // Grows the range until it fully contains every merged cell it touches
94    // (growing to swallow one merge can graze another, hence the fixpoint
95    // loop). The orientation of the range is preserved: the start corner stays
96    // on the same side it was.
97    fn grow_range_over_merged_cells(
98        &self,
99        sheet: u32,
100        range: [i32; 4],
101    ) -> Result<[i32; 4], String> {
102        let [start_row, start_column, end_row, end_column] = range;
103        let mut min_row = start_row.min(end_row);
104        let mut max_row = start_row.max(end_row);
105        let mut min_column = start_column.min(end_column);
106        let mut max_column = start_column.max(end_column);
107        let worksheet = self.model.workbook.worksheet(sheet)?;
108        loop {
109            let mut changed = false;
110            for m in &worksheet.merged_cells {
111                if m.intersects(
112                    min_row,
113                    min_column,
114                    max_column - min_column + 1,
115                    max_row - min_row + 1,
116                ) {
117                    if m.row < min_row {
118                        min_row = m.row;
119                        changed = true;
120                    }
121                    if m.last_row() > max_row {
122                        max_row = m.last_row();
123                        changed = true;
124                    }
125                    if m.column < min_column {
126                        min_column = m.column;
127                        changed = true;
128                    }
129                    if m.last_column() > max_column {
130                        max_column = m.last_column();
131                        changed = true;
132                    }
133                }
134            }
135            if !changed {
136                break;
137            }
138        }
139        let (new_start_row, new_end_row) = if start_row <= end_row {
140            (min_row, max_row)
141        } else {
142            (max_row, min_row)
143        };
144        let (new_start_column, new_end_column) = if start_column <= end_column {
145            (min_column, max_column)
146        } else {
147            (max_column, min_column)
148        };
149        Ok([new_start_row, new_start_column, new_end_row, new_end_column])
150    }
151
152    /// Returns the selected sheet index
153    pub fn get_selected_sheet(&self) -> u32 {
154        if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
155            view.sheet
156        } else {
157            0
158        }
159    }
160
161    /// Returns the selected cell
162    pub fn get_selected_cell(&self) -> (u32, i32, i32) {
163        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
164            view.sheet
165        } else {
166            0
167        };
168        if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
169            if let Some(view) = worksheet.views.get(&self.model.view_id) {
170                return (sheet, view.row, view.column);
171            }
172        }
173        // return a safe default
174        (0, 1, 1)
175    }
176
177    /// Returns selected view
178    pub fn get_selected_view(&self) -> SelectedView {
179        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
180            view.sheet
181        } else {
182            0
183        };
184        if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
185            if let Some(view) = worksheet.views.get(&self.model.view_id) {
186                return SelectedView {
187                    sheet,
188                    row: view.row,
189                    column: view.column,
190                    range: view.range,
191                    top_row: view.top_row,
192                    left_column: view.left_column,
193                };
194            }
195        }
196        // return a safe default
197        SelectedView {
198            sheet: 0,
199            row: 1,
200            column: 1,
201            range: [1, 1, 1, 1],
202            top_row: 1,
203            left_column: 1,
204        }
205    }
206
207    /// Sets the the selected sheet
208    pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
209        if self.model.workbook.worksheet(sheet).is_err() {
210            return Err(format!("Invalid worksheet index {sheet}"));
211        }
212        if let Some(view) = self.model.workbook.views.get_mut(&0) {
213            view.sheet = sheet;
214        }
215        Ok(())
216    }
217
218    /// Sets the selected cell for the current view. Note that this also sets the selected range
219    pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
220        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
221            view.sheet
222        } else {
223            0
224        };
225        if !is_valid_column_number(column) {
226            return Err(format!("Invalid column: '{column}'"));
227        }
228        if !is_valid_row(row) {
229            return Err(format!("Invalid row: '{row}'"));
230        }
231        if self.model.workbook.worksheet(sheet).is_err() {
232            return Err(format!("Invalid worksheet index {sheet}"));
233        }
234        // A covered cell can never be selected: snap to the anchor and select
235        // the whole merged range.
236        let (row, column) = self.merge_anchor(sheet, row, column)?;
237        let range = self.single_cell_range(sheet, row, column)?;
238        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
239            if let Some(view) = worksheet.views.get_mut(&0) {
240                view.row = row;
241                view.column = column;
242                view.range = range;
243                view.focus_row = row;
244                view.focus_column = column;
245            }
246        }
247        Ok(())
248    }
249
250    /// Sets the selected range. Note that the selected cell must be in the selected range.
251    pub fn set_selected_range(
252        &mut self,
253        start_row: i32,
254        start_column: i32,
255        end_row: i32,
256        end_column: i32,
257    ) -> Result<(), String> {
258        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
259            view.sheet
260        } else {
261            0
262        };
263
264        if !is_valid_column_number(start_column) {
265            return Err(format!("Invalid column: '{start_column}'"));
266        }
267        if !is_valid_row(start_row) {
268            return Err(format!("Invalid row: '{start_row}'"));
269        }
270
271        if !is_valid_column_number(end_column) {
272            return Err(format!("Invalid column: '{end_column}'"));
273        }
274        if !is_valid_row(end_row) {
275            return Err(format!("Invalid row: '{end_row}'"));
276        }
277        if self.model.workbook.worksheet(sheet).is_err() {
278            return Err(format!("Invalid worksheet index {sheet}"));
279        }
280        let (selected_row, selected_column) = {
281            let worksheet = self.model.workbook.worksheet(sheet)?;
282            match worksheet.views.get(&self.model.view_id) {
283                Some(view) => (view.row, view.column),
284                None => return Ok(()),
285            }
286        };
287        // The selected cell must be inside the range. It is not necessarily on
288        // a corner: extending a selection over merged cells can grow the range
289        // past the selected cell on any side.
290        let min_row = start_row.min(end_row);
291        let max_row = start_row.max(end_row);
292        let min_column = start_column.min(end_column);
293        let max_column = start_column.max(end_column);
294        if selected_row < min_row || selected_row > max_row {
295            return Err(format!(
296                "The selected cell is not inside the range. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
297            ));
298        }
299        if selected_column < min_column || selected_column > max_column {
300            return Err(format!(
301                "The selected cell is not inside the range. Column '{selected_column}' and column range '({start_column}, {end_column})'"
302            ));
303        }
304        // The focus is the moving corner of the selection: the corner of the
305        // requested range opposite the selected cell on each axis.
306        let focus_row = if selected_row == end_row {
307            start_row
308        } else {
309            end_row
310        };
311        let focus_column = if selected_column == end_column {
312            start_column
313        } else {
314            end_column
315        };
316        // A selection can never cover part of a merged cell — except full-row
317        // and full-column selections which may slice through
318        // merged ranges without dragging their other rows/columns in
319        let full_columns = start_row == 1 && end_row == LAST_ROW;
320        let full_rows = start_column == 1 && end_column == LAST_COLUMN;
321        let range = if full_columns || full_rows {
322            [min_row, min_column, max_row, max_column]
323        } else {
324            self.grow_range_over_merged_cells(sheet, [min_row, min_column, max_row, max_column])?
325        };
326        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
327            if let Some(view) = worksheet.views.get_mut(&0) {
328                view.range = range;
329                view.focus_row = focus_row;
330                view.focus_column = focus_column;
331            }
332        }
333        Ok(())
334    }
335
336    /// The selected range is expanded with the keyboard: the focus (the moving
337    /// corner of the selection) steps one cell in the key's direction — from
338    /// the far edge of its merged cell when it is inside one — skipping hidden
339    /// rows and columns, and the selected range is recomputed from the anchor
340    /// and the new focus.
341    pub fn on_expand_selected_range(&mut self, key: &str) -> Result<(), String> {
342        let (sheet, window_width, window_height) =
343            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
344                (
345                    view.sheet,
346                    view.window_width as f64,
347                    view.window_height as f64,
348                )
349            } else {
350                return Ok(());
351            };
352        let (selected_row, selected_column, focus_row, focus_column, range, top_row, left_column) =
353            if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
354                if let Some(view) = worksheet.views.get(&self.model.view_id) {
355                    (
356                        view.row,
357                        view.column,
358                        view.focus_row,
359                        view.focus_column,
360                        view.range,
361                        view.top_row,
362                        view.left_column,
363                    )
364                } else {
365                    return Ok(());
366                }
367            } else {
368                return Ok(());
369            };
370        let [row_start, column_start, row_end, column_end] = range;
371        if ["ArrowUp", "ArrowDown"].contains(&key) && row_start == 1 && row_end == LAST_ROW {
372            // full column selected, nothing to do
373            return Ok(());
374        }
375        if ["ArrowRight", "ArrowLeft"].contains(&key)
376            && column_start == 1
377            && column_end == LAST_COLUMN
378        {
379            // full row selected, nothing to do
380            return Ok(());
381        }
382        let frozen_rows = self.model.get_frozen_rows_count(sheet)?;
383        let frozen_columns = self.model.get_frozen_columns_count(sheet)?;
384        let worksheet = self.model.workbook.worksheet(sheet)?;
385        // Stepping starts at the far edge of the focus' merged cell, so a
386        // single keystroke crosses the whole merged range
387        let focus_merge = worksheet
388            .merged_cell_containing(focus_row, focus_column)
389            .cloned();
390
391        let mut new_focus_row = focus_row;
392        let mut new_focus_column = focus_column;
393        match key {
394            "ArrowRight" => {
395                let edge = focus_merge.map_or(focus_column, |m| m.last_column());
396                let mut new_column = edge + 1;
397                while new_column < LAST_COLUMN && worksheet.is_column_hidden(new_column)? {
398                    new_column += 1;
399                }
400                if !is_valid_column_number(new_column) {
401                    return Ok(());
402                }
403                if new_column > selected_column {
404                    // extending right: if the column is not fully visible we
405                    // 'scroll' right until it is
406                    let mut width = 0.0;
407                    let mut c = left_column;
408                    while c <= new_column {
409                        width += self.ui_column_width(sheet, c)?;
410                        c += 1;
411                    }
412                    if width > window_width {
413                        self.set_top_left_visible_cell(top_row, left_column + 1)?;
414                    }
415                }
416                new_focus_column = new_column;
417            }
418            "ArrowLeft" => {
419                let edge = focus_merge.map_or(focus_column, |m| m.column);
420                let mut new_column = edge - 1;
421                while new_column > 1 && worksheet.is_column_hidden(new_column)? {
422                    new_column -= 1;
423                }
424                if !is_valid_column_number(new_column) {
425                    return Ok(());
426                }
427                // Frozen columns are always visible: no scrolling needed there
428                if new_column < left_column && new_column > frozen_columns {
429                    self.set_top_left_visible_cell(top_row, new_column)?;
430                }
431                new_focus_column = new_column;
432            }
433            "ArrowUp" => {
434                let edge = focus_merge.map_or(focus_row, |m| m.row);
435                let mut new_row = edge - 1;
436                while new_row > 1 && worksheet.is_row_hidden(new_row)? {
437                    new_row -= 1;
438                }
439                if !is_valid_row(new_row) {
440                    return Ok(());
441                }
442                // Frozen rows are always visible: no scrolling needed there
443                if new_row < top_row && new_row > frozen_rows {
444                    self.set_top_left_visible_cell(new_row, left_column)?;
445                }
446                new_focus_row = new_row;
447            }
448            "ArrowDown" => {
449                let edge = focus_merge.map_or(focus_row, |m| m.last_row());
450                let mut new_row = edge + 1;
451                while new_row < LAST_ROW && worksheet.is_row_hidden(new_row)? {
452                    new_row += 1;
453                }
454                if !is_valid_row(new_row) {
455                    return Ok(());
456                }
457                if new_row > selected_row {
458                    // extending down: scroll one row if the new row is not
459                    // fully visible
460                    let mut height = 0.0;
461                    let mut r = top_row;
462                    while r <= new_row + 1 {
463                        height += self.ui_row_height(sheet, r)?;
464                        r += 1;
465                    }
466                    if height >= window_height {
467                        self.set_top_left_visible_cell(top_row + 1, left_column)?;
468                    }
469                }
470                new_focus_row = new_row;
471            }
472            _ => return Ok(()),
473        }
474
475        let new_range = self.selection_range(
476            sheet,
477            selected_row,
478            selected_column,
479            new_focus_row,
480            new_focus_column,
481        )?;
482        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
483            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
484                view.range = new_range;
485                view.focus_row = new_focus_row;
486                view.focus_column = new_focus_column;
487            }
488        }
489        Ok(())
490    }
491
492    /// Sets the value of the first visible cell
493    pub fn set_top_left_visible_cell(
494        &mut self,
495        top_row: i32,
496        left_column: i32,
497    ) -> Result<(), String> {
498        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
499            view.sheet
500        } else {
501            0
502        };
503
504        if !is_valid_column_number(left_column) {
505            return Err(format!("Invalid column: '{left_column}'"));
506        }
507        if !is_valid_row(top_row) {
508            return Err(format!("Invalid row: '{top_row}'"));
509        }
510        if self.model.workbook.worksheet(sheet).is_err() {
511            return Err(format!("Invalid worksheet index {sheet}"));
512        }
513        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
514            if let Some(view) = worksheet.views.get_mut(&0) {
515                view.top_row = top_row;
516                view.left_column = left_column;
517            }
518        }
519        Ok(())
520    }
521
522    /// Sets the width of the window
523    pub fn set_window_width(&mut self, window_width: f64) {
524        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
525            view.window_width = window_width as i64;
526        };
527    }
528
529    /// Gets the width of the window
530    pub fn get_window_width(&mut self) -> Result<i64, String> {
531        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
532            return Ok(view.window_width);
533        };
534        Err("View not found".to_string())
535    }
536
537    /// Sets the height of the window
538    pub fn set_window_height(&mut self, window_height: f64) {
539        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
540            view.window_height = window_height as i64;
541        };
542    }
543
544    /// Gets the height of the window
545    pub fn get_window_height(&mut self) -> Result<i64, String> {
546        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
547            return Ok(view.window_height);
548        };
549        Err("View not found".to_string())
550    }
551
552    /// User presses right arrow
553    pub fn on_arrow_right(&mut self) -> Result<(), String> {
554        let (sheet, window_width) =
555            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
556                (view.sheet, view.window_width)
557            } else {
558                return Err("View not found".to_string());
559            };
560        let worksheet = match self.model.workbook.worksheet(sheet) {
561            Ok(s) => s,
562            Err(_) => return Err("Worksheet not found".to_string()),
563        };
564        let view = match worksheet.views.get(&self.model.view_id) {
565            Some(s) => s,
566            None => return Err("View not found".to_string()),
567        };
568        // Leaving a merged cell starts past its last column
569        let row = view.row;
570        let mut new_column = match worksheet.merged_cell_containing(row, view.column) {
571            Some(m) => m.last_column() + 1,
572            None => view.column + 1,
573        };
574        while new_column <= LAST_COLUMN
575            && self
576                .model
577                .workbook
578                .worksheet(sheet)?
579                .is_column_hidden(new_column)?
580        {
581            new_column += 1;
582        }
583        if !is_valid_column_number(new_column) {
584            return Ok(());
585        }
586        // if the column is not fully visible we 'scroll' right until it is
587        let mut width = 0.0;
588        let mut column = view.left_column;
589        while column <= new_column {
590            width += self.ui_column_width(sheet, column)?;
591            column += 1;
592        }
593        // Landing inside a merged cell selects its anchor
594        let (new_row, new_column) = self.merge_anchor(sheet, row, new_column)?;
595        let new_range = self.single_cell_range(sheet, new_row, new_column)?;
596        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
597            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
598                view.row = new_row;
599                view.column = new_column;
600                view.range = new_range;
601                view.focus_row = new_row;
602                view.focus_column = new_column;
603                if width > window_width as f64 {
604                    view.left_column += 1;
605                }
606            }
607        }
608        Ok(())
609    }
610
611    /// User presses left arrow
612    pub fn on_arrow_left(&mut self) -> Result<(), String> {
613        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
614            view.sheet
615        } else {
616            return Err("View not found".to_string());
617        };
618        let worksheet = match self.model.workbook.worksheet(sheet) {
619            Ok(s) => s,
620            Err(_) => return Err("Worksheet not found".to_string()),
621        };
622        let view = match worksheet.views.get(&self.model.view_id) {
623            Some(s) => s,
624            None => return Err("View not found".to_string()),
625        };
626        // Leaving a merged cell starts before its first column
627        let row = view.row;
628        let mut new_column = match worksheet.merged_cell_containing(row, view.column) {
629            Some(m) => m.column - 1,
630            None => view.column - 1,
631        };
632        while new_column >= 1
633            && self
634                .model
635                .workbook
636                .worksheet(sheet)?
637                .is_column_hidden(new_column)?
638        {
639            new_column -= 1;
640        }
641        if !is_valid_column_number(new_column) {
642            return Ok(());
643        }
644        // Landing inside a merged cell selects its anchor
645        let (new_row, new_column) = self.merge_anchor(sheet, row, new_column)?;
646        let new_range = self.single_cell_range(sheet, new_row, new_column)?;
647        // if the column is not fully visible we 'scroll' right until it is
648        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
649            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
650                view.row = new_row;
651                view.column = new_column;
652                view.range = new_range;
653                view.focus_row = new_row;
654                view.focus_column = new_column;
655                if new_column < view.left_column {
656                    view.left_column = new_column;
657                }
658            }
659        }
660        Ok(())
661    }
662
663    /// User presses up arrow key
664    pub fn on_arrow_up(&mut self) -> Result<(), String> {
665        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
666            view.sheet
667        } else {
668            return Err("View not found".to_string());
669        };
670        let worksheet = match self.model.workbook.worksheet(sheet) {
671            Ok(s) => s,
672            Err(_) => return Err("Worksheet not found".to_string()),
673        };
674        let view = match worksheet.views.get(&self.model.view_id) {
675            Some(s) => s,
676            None => return Err("View not found".to_string()),
677        };
678        // Leaving a merged cell starts above its first row
679        let column = view.column;
680        let mut new_row = match worksheet.merged_cell_containing(view.row, column) {
681            Some(m) => m.row - 1,
682            None => view.row - 1,
683        };
684        while new_row >= 1
685            && self
686                .model
687                .workbook
688                .worksheet(sheet)?
689                .is_row_hidden(new_row)?
690        {
691            new_row -= 1;
692        }
693        if !is_valid_row(new_row) {
694            return Ok(());
695        }
696        // Landing inside a merged cell selects its anchor
697        let (new_row, new_column) = self.merge_anchor(sheet, new_row, column)?;
698        let new_range = self.single_cell_range(sheet, new_row, new_column)?;
699        // if the column is not fully visible we 'scroll' right until it is
700        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
701            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
702                view.row = new_row;
703                view.column = new_column;
704                view.range = new_range;
705                view.focus_row = new_row;
706                view.focus_column = new_column;
707                if new_row < view.top_row {
708                    view.top_row = new_row;
709                }
710            }
711        }
712        Ok(())
713    }
714
715    /// User presses down arrow key
716    pub fn on_arrow_down(&mut self) -> Result<(), String> {
717        let (sheet, window_height) =
718            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
719                (view.sheet, view.window_height)
720            } else {
721                return Err("View not found".to_string());
722            };
723        let worksheet = match self.model.workbook.worksheet(sheet) {
724            Ok(s) => s,
725            Err(_) => return Err("Worksheet not found".to_string()),
726        };
727        let view = match worksheet.views.get(&self.model.view_id) {
728            Some(s) => s,
729            None => return Err("View not found".to_string()),
730        };
731        // Leaving a merged cell starts below its last row
732        let column = view.column;
733        let mut new_row = match worksheet.merged_cell_containing(view.row, column) {
734            Some(m) => m.last_row() + 1,
735            None => view.row + 1,
736        };
737        while new_row <= LAST_ROW
738            && self
739                .model
740                .workbook
741                .worksheet(sheet)?
742                .is_row_hidden(new_row)?
743        {
744            new_row += 1;
745        }
746        if !is_valid_row(new_row) {
747            return Ok(());
748        }
749        // if the row is not fully visible we 'scroll' down until it is
750        let mut height = 0.0;
751        let mut row = view.top_row;
752        while row <= new_row + 1 && row <= LAST_ROW {
753            height += self.ui_row_height(sheet, row)?;
754            row += 1;
755        }
756        // Landing inside a merged cell selects its anchor
757        let (new_row, new_column) = self.merge_anchor(sheet, new_row, column)?;
758        let new_range = self.single_cell_range(sheet, new_row, new_column)?;
759        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
760            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
761                view.row = new_row;
762                view.column = new_column;
763                view.range = new_range;
764                view.focus_row = new_row;
765                view.focus_column = new_column;
766                if height > window_height as f64 {
767                    view.top_row += 1;
768                }
769            }
770        }
771        Ok(())
772    }
773
774    // TODO: This function should be memoized
775    /// Returns the x-coordinate of the cell in the top left corner
776    pub fn get_scroll_x(&self) -> Result<f64, String> {
777        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
778            view.sheet
779        } else {
780            return Err("View not found".to_string());
781        };
782        let worksheet = match self.model.workbook.worksheet(sheet) {
783            Ok(s) => s,
784            Err(_) => return Err("Worksheet not found".to_string()),
785        };
786        let view = match worksheet.views.get(&self.model.view_id) {
787            Some(s) => s,
788            None => return Err("View not found".to_string()),
789        };
790        let mut scroll_x = 0.0;
791        for column in 1..view.left_column {
792            scroll_x += self.ui_column_width(sheet, column)?;
793        }
794        Ok(scroll_x)
795    }
796
797    // TODO: This function should be memoized
798    /// Returns the y-coordinate of the cell in the top left corner
799    pub fn get_scroll_y(&self) -> Result<f64, String> {
800        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
801            view.sheet
802        } else {
803            return Err("View not found".to_string());
804        };
805        let worksheet = match self.model.workbook.worksheet(sheet) {
806            Ok(s) => s,
807            Err(_) => return Err("Worksheet not found".to_string()),
808        };
809        let view = match worksheet.views.get(&self.model.view_id) {
810            Some(s) => s,
811            None => return Err("View not found".to_string()),
812        };
813        let mut scroll_y = 0.0;
814        for row in 1..view.top_row {
815            scroll_y += self.ui_row_height(sheet, row)?;
816        }
817        Ok(scroll_y)
818    }
819
820    /// User presses page down.
821    /// The `top_row` is now the first row that is not fully visible
822    pub fn on_page_down(&mut self) -> Result<(), String> {
823        let (sheet, window_height) =
824            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
825                (view.sheet, view.window_height)
826            } else {
827                return Err("View not found".to_string());
828            };
829        let worksheet = match self.model.workbook.worksheet(sheet) {
830            Ok(s) => s,
831            Err(_) => return Err("Worksheet not found".to_string()),
832        };
833        let view = match worksheet.views.get(&self.model.view_id) {
834            Some(s) => s,
835            None => return Err("View not found".to_string()),
836        };
837        let mut last_row = view.top_row;
838        let mut height = self.ui_row_height(sheet, last_row)?;
839        while height <= window_height as f64 {
840            last_row += 1;
841            height += self.ui_row_height(sheet, last_row)?;
842        }
843        if !is_valid_row(last_row) {
844            return Ok(());
845        }
846        let row_delta = view.row - view.top_row;
847        // Landing inside a merged cell selects its anchor
848        let (new_row, new_column) = self.merge_anchor(sheet, last_row + row_delta, view.column)?;
849        let new_range = self.single_cell_range(sheet, new_row, new_column)?;
850        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
851            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
852                view.top_row = last_row;
853                view.row = new_row;
854                view.column = new_column;
855                view.range = new_range;
856                view.focus_row = new_row;
857                view.focus_column = new_column;
858            }
859        }
860        Ok(())
861    }
862
863    /// On page up. tis needs to be the inverse of page down
864    pub fn on_page_up(&mut self) -> Result<(), String> {
865        let (sheet, window_height) =
866            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
867                (view.sheet, view.window_height as f64)
868            } else {
869                return Err("View not found".to_string());
870            };
871        let worksheet = match self.model.workbook.worksheet(sheet) {
872            Ok(s) => s,
873            Err(_) => return Err("Worksheet not found".to_string()),
874        };
875        let view = match worksheet.views.get(&self.model.view_id) {
876            Some(s) => s,
877            None => return Err("View not found".to_string()),
878        };
879
880        let mut first_row = view.top_row;
881        let mut height = self.ui_row_height(sheet, first_row)?;
882        while height <= window_height && first_row > 1 {
883            first_row -= 1;
884            height += self.ui_row_height(sheet, first_row)?;
885        }
886
887        let row_delta = view.row - view.top_row;
888        // Landing inside a merged cell selects its anchor
889        let (new_row, new_column) = self.merge_anchor(sheet, first_row + row_delta, view.column)?;
890        let new_range = self.single_cell_range(sheet, new_row, new_column)?;
891        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
892            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
893                view.top_row = first_row;
894                view.row = new_row;
895                view.column = new_column;
896                view.range = new_range;
897                view.focus_row = new_row;
898                view.focus_column = new_column;
899            }
900        }
901        Ok(())
902    }
903
904    /// We extend the selection to cell (target_row, target_column)
905    pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<(), String> {
906        let (sheet, window_width, window_height) =
907            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
908                (
909                    view.sheet,
910                    view.window_width as f64,
911                    view.window_height as f64,
912                )
913            } else {
914                return Ok(());
915            };
916        let (selected_row, selected_column, top_row, left_column) =
917            if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
918                if let Some(view) = worksheet.views.get(&self.model.view_id) {
919                    (view.row, view.column, view.top_row, view.left_column)
920                } else {
921                    return Ok(());
922                }
923            } else {
924                return Ok(());
925            };
926        let frozen_rows = self.model.get_frozen_rows_count(sheet)?;
927        let frozen_columns = self.model.get_frozen_columns_count(sheet)?;
928
929        let mut new_left_column = left_column;
930        if target_column <= frozen_columns {
931            // The target is in the frozen pane, which is always visible:
932            // no scrolling
933        } else if target_column >= selected_column {
934            let mut width = 0.0;
935            let mut column = left_column;
936            while column <= target_column {
937                width += self.ui_column_width(sheet, column)?;
938                column += 1;
939            }
940
941            while width > window_width {
942                width -= self.ui_column_width(sheet, new_left_column)?;
943                new_left_column += 1;
944            }
945        } else if target_column < new_left_column {
946            new_left_column = target_column;
947        }
948        let mut new_top_row = top_row;
949        if target_row <= frozen_rows {
950            // The target is in the frozen pane, which is always visible:
951            // no scrolling
952        } else if target_row >= selected_row {
953            let mut height = 0.0;
954            let mut row = top_row;
955            while row <= target_row {
956                height += self.ui_row_height(sheet, row)?;
957                row += 1;
958            }
959            while height > window_height {
960                height -= self.ui_row_height(sheet, new_top_row)?;
961                new_top_row += 1;
962            }
963        } else if target_row < new_top_row {
964            new_top_row = target_row;
965        }
966
967        // The target is the new focus; the range is recomputed from the anchor
968        // (never stretched from the previous, already grown, range: that would
969        // keep merged cells the pointer has left)
970        let range = self.selection_range(
971            sheet,
972            selected_row,
973            selected_column,
974            target_row,
975            target_column,
976        )?;
977        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
978            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
979                view.range = range;
980                view.focus_row = target_row;
981                view.focus_column = target_column;
982                if new_top_row != top_row {
983                    view.top_row = new_top_row;
984                }
985                if new_left_column != left_column {
986                    view.left_column = new_left_column;
987                }
988            }
989        }
990
991        Ok(())
992    }
993
994    /// User navigates to the edge in the given direction
995    pub fn on_navigate_to_edge_in_direction(
996        &mut self,
997        direction: NavigationDirection,
998    ) -> Result<(), String> {
999        let (sheet, window_height, window_width) =
1000            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
1001                (view.sheet, view.window_height, view.window_width)
1002            } else {
1003                return Err("View not found".to_string());
1004            };
1005        let worksheet = match self.model.workbook.worksheet(sheet) {
1006            Ok(s) => s,
1007            Err(_) => return Err("Worksheet not found".to_string()),
1008        };
1009        let view = match worksheet.views.get(&self.model.view_id) {
1010            Some(s) => s,
1011            None => return Err("View not found".to_string()),
1012        };
1013        let row = view.row;
1014        let column = view.column;
1015        if !is_valid_row(row) || !is_valid_column_number(column) {
1016            return Err("Invalid row or column".to_string());
1017        }
1018        let (new_row, new_column) =
1019            worksheet.navigate_to_edge_in_direction(row, column, direction)?;
1020        if !is_valid_row(new_row) || !is_valid_column_number(new_column) {
1021            return Err("Invalid row or column after navigation".to_string());
1022        }
1023        if new_row == row && new_column == column {
1024            return Ok(()); // No change in selection
1025        }
1026
1027        let mut top_row = view.top_row;
1028        let mut left_column = view.left_column;
1029
1030        match direction {
1031            NavigationDirection::Left | NavigationDirection::Right => {
1032                // If the new column is not fully visible we 'scroll' until it is
1033                // We need to check two conditions:
1034                // 1. new_column > view.left_column
1035                // 2. right_column < new_column
1036                if new_column < view.left_column {
1037                    left_column = new_column;
1038                } else {
1039                    let mut c = new_column;
1040                    let mut width = self.ui_column_width(sheet, c)?;
1041                    while c > 1 && width <= window_width as f64 {
1042                        c -= 1;
1043                        width += self.ui_column_width(sheet, c)?;
1044                    }
1045                    if c > view.left_column {
1046                        left_column = c;
1047                    }
1048                }
1049            }
1050            NavigationDirection::Up | NavigationDirection::Down => {
1051                // If the new row is not fully visible we 'scroll' until it is
1052                // We need to check two conditions:
1053                // 1. new_row > view.top_row
1054                // 2. bottom_row < new_row
1055                if new_row < view.top_row {
1056                    top_row = new_row;
1057                } else {
1058                    let mut r = new_row;
1059                    let mut height = self.ui_row_height(sheet, r)?;
1060                    while r > 1 && height <= window_height as f64 {
1061                        r -= 1;
1062                        height += self.ui_row_height(sheet, r)?;
1063                    }
1064                    if r > view.top_row {
1065                        top_row = r;
1066                    }
1067                }
1068            }
1069        }
1070
1071        // Landing inside a merged cell selects its anchor
1072        let (new_row, new_column) = self.merge_anchor(sheet, new_row, new_column)?;
1073        let new_range = self.single_cell_range(sheet, new_row, new_column)?;
1074        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
1075            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
1076                view.row = new_row;
1077                view.column = new_column;
1078                view.range = new_range;
1079                view.focus_row = new_row;
1080                view.focus_column = new_column;
1081
1082                view.top_row = top_row;
1083                view.left_column = left_column;
1084            }
1085        }
1086        Ok(())
1087    }
1088}