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    /// Returns the selected sheet index
26    pub fn get_selected_sheet(&self) -> u32 {
27        if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
28            view.sheet
29        } else {
30            0
31        }
32    }
33
34    /// Returns the selected cell
35    pub fn get_selected_cell(&self) -> (u32, i32, i32) {
36        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
37            view.sheet
38        } else {
39            0
40        };
41        if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
42            if let Some(view) = worksheet.views.get(&self.model.view_id) {
43                return (sheet, view.row, view.column);
44            }
45        }
46        // return a safe default
47        (0, 1, 1)
48    }
49
50    /// Returns selected view
51    pub fn get_selected_view(&self) -> SelectedView {
52        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
53            view.sheet
54        } else {
55            0
56        };
57        if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
58            if let Some(view) = worksheet.views.get(&self.model.view_id) {
59                return SelectedView {
60                    sheet,
61                    row: view.row,
62                    column: view.column,
63                    range: view.range,
64                    top_row: view.top_row,
65                    left_column: view.left_column,
66                };
67            }
68        }
69        // return a safe default
70        SelectedView {
71            sheet: 0,
72            row: 1,
73            column: 1,
74            range: [1, 1, 1, 1],
75            top_row: 1,
76            left_column: 1,
77        }
78    }
79
80    /// Sets the the selected sheet
81    pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
82        if self.model.workbook.worksheet(sheet).is_err() {
83            return Err(format!("Invalid worksheet index {sheet}"));
84        }
85        if let Some(view) = self.model.workbook.views.get_mut(&0) {
86            view.sheet = sheet;
87        }
88        Ok(())
89    }
90
91    /// Sets the selected cell for the current view. Note that this also sets the selected range
92    pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
93        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
94            view.sheet
95        } else {
96            0
97        };
98        if !is_valid_column_number(column) {
99            return Err(format!("Invalid column: '{column}'"));
100        }
101        if !is_valid_row(row) {
102            return Err(format!("Invalid row: '{row}'"));
103        }
104        if self.model.workbook.worksheet(sheet).is_err() {
105            return Err(format!("Invalid worksheet index {sheet}"));
106        }
107        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
108            if let Some(view) = worksheet.views.get_mut(&0) {
109                view.row = row;
110                view.column = column;
111                view.range = [row, column, row, column];
112            }
113        }
114        Ok(())
115    }
116
117    /// Sets the selected range. Note that the selected cell must be in the selected range.
118    pub fn set_selected_range(
119        &mut self,
120        start_row: i32,
121        start_column: i32,
122        end_row: i32,
123        end_column: i32,
124    ) -> Result<(), String> {
125        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
126            view.sheet
127        } else {
128            0
129        };
130
131        if !is_valid_column_number(start_column) {
132            return Err(format!("Invalid column: '{start_column}'"));
133        }
134        if !is_valid_row(start_row) {
135            return Err(format!("Invalid row: '{start_row}'"));
136        }
137
138        if !is_valid_column_number(end_column) {
139            return Err(format!("Invalid column: '{end_column}'"));
140        }
141        if !is_valid_row(end_row) {
142            return Err(format!("Invalid row: '{end_row}'"));
143        }
144        if self.model.workbook.worksheet(sheet).is_err() {
145            return Err(format!("Invalid worksheet index {sheet}"));
146        }
147        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
148            if let Some(view) = worksheet.views.get_mut(&0) {
149                let selected_row = view.row;
150                let selected_column = view.column;
151                if start_row == 1 && end_row == LAST_ROW {
152                    // full row selected. The cell must be at the top or the bottom of the range
153                    if selected_column != start_column && selected_column != end_column {
154                        return Err(format!(
155                            "The selected cell is not the column edge. Column '{selected_column}' and column range '({start_column}, {end_column})'"
156                        ));
157                    }
158                } else if start_column == 1 && end_column == LAST_COLUMN {
159                    // full column selected. The cell must be at the left or the right of the range
160                    if selected_row != start_row && selected_row != end_row {
161                        return Err(format!(
162                            "The selected cell is not in the row edge. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
163                        ));
164                    }
165                } else {
166                    // The selected cell must be on one of the corners of the selected range:
167                    if selected_row != start_row && selected_row != end_row {
168                        return Err(format!(
169                        "The selected cell is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
170                    ));
171                    }
172                    if selected_column != start_column && selected_column != end_column {
173                        return Err(format!(
174                        "The selected cell is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'"
175                    ));
176                    }
177                }
178                view.range = [start_row, start_column, end_row, end_column];
179            }
180        }
181        Ok(())
182    }
183
184    /// The selected range is expanded with the keyboard
185    pub fn on_expand_selected_range(&mut self, key: &str) -> Result<(), String> {
186        let (sheet, window_width, window_height) =
187            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
188                (
189                    view.sheet,
190                    view.window_width as f64,
191                    view.window_height as f64,
192                )
193            } else {
194                return Ok(());
195            };
196        let (selected_row, selected_column, range, top_row, left_column) =
197            if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
198                if let Some(view) = worksheet.views.get(&self.model.view_id) {
199                    (
200                        view.row,
201                        view.column,
202                        view.range,
203                        view.top_row,
204                        view.left_column,
205                    )
206                } else {
207                    return Ok(());
208                }
209            } else {
210                return Ok(());
211            };
212        let [row_start, column_start, row_end, column_end] = range;
213        if ["ArrowUp", "ArrowDown"].contains(&key) && row_start == 1 && row_end == LAST_ROW {
214            // full column selected, nothing to do
215            return Ok(());
216        }
217        if ["ArrowRight", "ArrowLeft"].contains(&key)
218            && column_start == 1
219            && column_end == LAST_COLUMN
220        {
221            // full row selected, nothing to do
222            return Ok(());
223        }
224        let worksheet = self.model.workbook.worksheet(sheet)?;
225
226        match key {
227            "ArrowRight" => {
228                if selected_column > column_start {
229                    let mut new_column = column_start + 1;
230                    while new_column < LAST_COLUMN && worksheet.is_column_hidden(new_column)? {
231                        new_column += 1;
232                    }
233                    if !(is_valid_column_number(new_column)) {
234                        return Ok(());
235                    }
236                    self.set_selected_range(row_start, new_column, row_end, column_end)?;
237                } else {
238                    let mut new_column = column_end + 1;
239                    while new_column < LAST_COLUMN && worksheet.is_column_hidden(new_column)? {
240                        new_column += 1;
241                    }
242                    if !is_valid_column_number(new_column) {
243                        return Ok(());
244                    }
245                    // if the column is not fully visible we 'scroll' right until it is
246                    let mut width = 0.0;
247                    let mut c = left_column;
248                    while c <= new_column {
249                        width += self.model.get_column_width(sheet, c)?;
250                        c += 1;
251                    }
252                    if width > window_width {
253                        self.set_top_left_visible_cell(top_row, left_column + 1)?;
254                    }
255                    self.set_selected_range(row_start, column_start, row_end, new_column)?;
256                }
257            }
258            "ArrowLeft" => {
259                if selected_column < column_end {
260                    let mut new_column = column_end - 1;
261                    while new_column > 1 && worksheet.is_column_hidden(new_column)? {
262                        new_column -= 1;
263                    }
264                    if !is_valid_column_number(new_column) {
265                        return Ok(());
266                    }
267                    if new_column < left_column {
268                        self.set_top_left_visible_cell(top_row, new_column)?;
269                    }
270                    self.set_selected_range(row_start, column_start, row_end, new_column)?;
271                } else {
272                    let mut new_column = column_start - 1;
273                    while new_column > 1 && worksheet.is_column_hidden(new_column)? {
274                        new_column -= 1;
275                    }
276                    if !is_valid_column_number(new_column) {
277                        return Ok(());
278                    }
279                    if new_column < left_column {
280                        self.set_top_left_visible_cell(top_row, new_column)?;
281                    }
282                    self.set_selected_range(row_start, new_column, row_end, column_end)?;
283                }
284            }
285            "ArrowUp" => {
286                if selected_row < row_end {
287                    let mut new_row = row_end - 1;
288                    while new_row > 1 && worksheet.is_row_hidden(new_row)? {
289                        new_row -= 1;
290                    }
291                    if !is_valid_row(new_row) {
292                        return Ok(());
293                    }
294                    self.set_selected_range(row_start, column_start, new_row, column_end)?;
295                } else {
296                    let mut new_row = row_start - 1;
297                    while new_row > 1 && worksheet.is_row_hidden(new_row)? {
298                        new_row -= 1;
299                    }
300                    if !is_valid_row(new_row) {
301                        return Ok(());
302                    }
303                    if new_row < top_row {
304                        self.set_top_left_visible_cell(new_row, left_column)?;
305                    }
306                    self.set_selected_range(new_row, column_start, row_end, column_end)?;
307                }
308            }
309            "ArrowDown" => {
310                if selected_row > row_start {
311                    let mut new_row = row_start + 1;
312                    while new_row < LAST_ROW && worksheet.is_row_hidden(new_row)? {
313                        new_row += 1;
314                    }
315                    if !is_valid_row(new_row) {
316                        return Ok(());
317                    }
318                    self.set_selected_range(new_row, column_start, row_end, column_end)?;
319                } else {
320                    let mut new_row = row_end + 1;
321                    while new_row < LAST_ROW && worksheet.is_row_hidden(new_row)? {
322                        new_row += 1;
323                    }
324                    if !is_valid_row(new_row) {
325                        return Ok(());
326                    }
327                    let mut height = 0.0;
328                    let mut r = top_row;
329                    while r <= new_row + 1 {
330                        height += self.model.get_row_height(sheet, r)?;
331                        r += 1;
332                    }
333                    if height >= window_height {
334                        self.set_top_left_visible_cell(top_row + 1, left_column)?;
335                    }
336                    self.set_selected_range(row_start, column_start, new_row, column_end)?;
337                }
338            }
339            _ => {}
340        }
341
342        Ok(())
343    }
344
345    /// Sets the value of the first visible cell
346    pub fn set_top_left_visible_cell(
347        &mut self,
348        top_row: i32,
349        left_column: i32,
350    ) -> Result<(), String> {
351        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
352            view.sheet
353        } else {
354            0
355        };
356
357        if !is_valid_column_number(left_column) {
358            return Err(format!("Invalid column: '{left_column}'"));
359        }
360        if !is_valid_row(top_row) {
361            return Err(format!("Invalid row: '{top_row}'"));
362        }
363        if self.model.workbook.worksheet(sheet).is_err() {
364            return Err(format!("Invalid worksheet index {sheet}"));
365        }
366        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
367            if let Some(view) = worksheet.views.get_mut(&0) {
368                view.top_row = top_row;
369                view.left_column = left_column;
370            }
371        }
372        Ok(())
373    }
374
375    /// Sets the width of the window
376    pub fn set_window_width(&mut self, window_width: f64) {
377        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
378            view.window_width = window_width as i64;
379        };
380    }
381
382    /// Gets the width of the window
383    pub fn get_window_width(&mut self) -> Result<i64, String> {
384        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
385            return Ok(view.window_width);
386        };
387        Err("View not found".to_string())
388    }
389
390    /// Sets the height of the window
391    pub fn set_window_height(&mut self, window_height: f64) {
392        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
393            view.window_height = window_height as i64;
394        };
395    }
396
397    /// Gets the height of the window
398    pub fn get_window_height(&mut self) -> Result<i64, String> {
399        if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
400            return Ok(view.window_height);
401        };
402        Err("View not found".to_string())
403    }
404
405    /// User presses right arrow
406    pub fn on_arrow_right(&mut self) -> Result<(), String> {
407        let (sheet, window_width) =
408            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
409                (view.sheet, view.window_width)
410            } else {
411                return Err("View not found".to_string());
412            };
413        let worksheet = match self.model.workbook.worksheet(sheet) {
414            Ok(s) => s,
415            Err(_) => return Err("Worksheet not found".to_string()),
416        };
417        let view = match worksheet.views.get(&self.model.view_id) {
418            Some(s) => s,
419            None => return Err("View not found".to_string()),
420        };
421        let mut new_column = view.column + 1;
422        while new_column <= LAST_COLUMN
423            && self
424                .model
425                .workbook
426                .worksheet(sheet)?
427                .is_column_hidden(new_column)?
428        {
429            new_column += 1;
430        }
431        if !is_valid_column_number(new_column) {
432            return Ok(());
433        }
434        // if the column is not fully visible we 'scroll' right until it is
435        let mut width = 0.0;
436        let mut column = view.left_column;
437        while column <= new_column {
438            width += self.model.get_column_width(sheet, column)?;
439            column += 1;
440        }
441        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
442            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
443                view.column = new_column;
444                view.range = [view.row, new_column, view.row, new_column];
445                if width > window_width as f64 {
446                    view.left_column += 1;
447                }
448            }
449        }
450        Ok(())
451    }
452
453    /// User presses left arrow
454    pub fn on_arrow_left(&mut self) -> Result<(), String> {
455        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
456            view.sheet
457        } else {
458            return Err("View not found".to_string());
459        };
460        let worksheet = match self.model.workbook.worksheet(sheet) {
461            Ok(s) => s,
462            Err(_) => return Err("Worksheet not found".to_string()),
463        };
464        let view = match worksheet.views.get(&self.model.view_id) {
465            Some(s) => s,
466            None => return Err("View not found".to_string()),
467        };
468        let mut new_column = view.column - 1;
469        while new_column >= 1
470            && self
471                .model
472                .workbook
473                .worksheet(sheet)?
474                .is_column_hidden(new_column)?
475        {
476            new_column -= 1;
477        }
478        if !is_valid_column_number(new_column) {
479            return Ok(());
480        }
481        // if the column is not fully visible we 'scroll' right until it is
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.column = new_column;
485                view.range = [view.row, new_column, view.row, new_column];
486                if new_column < view.left_column {
487                    view.left_column = new_column;
488                }
489            }
490        }
491        Ok(())
492    }
493
494    /// User presses up arrow key
495    pub fn on_arrow_up(&mut self) -> Result<(), String> {
496        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
497            view.sheet
498        } else {
499            return Err("View not found".to_string());
500        };
501        let worksheet = match self.model.workbook.worksheet(sheet) {
502            Ok(s) => s,
503            Err(_) => return Err("Worksheet not found".to_string()),
504        };
505        let view = match worksheet.views.get(&self.model.view_id) {
506            Some(s) => s,
507            None => return Err("View not found".to_string()),
508        };
509        let mut new_row = view.row - 1;
510        while new_row >= 1
511            && self
512                .model
513                .workbook
514                .worksheet(sheet)?
515                .is_row_hidden(new_row)?
516        {
517            new_row -= 1;
518        }
519        if !is_valid_row(new_row) {
520            return Ok(());
521        }
522        // if the column is not fully visible we 'scroll' right until it is
523        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
524            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
525                view.row = new_row;
526                view.range = [new_row, view.column, new_row, view.column];
527                if new_row < view.top_row {
528                    view.top_row = new_row;
529                }
530            }
531        }
532        Ok(())
533    }
534
535    /// User presses down arrow key
536    pub fn on_arrow_down(&mut self) -> Result<(), String> {
537        let (sheet, window_height) =
538            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
539                (view.sheet, view.window_height)
540            } else {
541                return Err("View not found".to_string());
542            };
543        let worksheet = match self.model.workbook.worksheet(sheet) {
544            Ok(s) => s,
545            Err(_) => return Err("Worksheet not found".to_string()),
546        };
547        let view = match worksheet.views.get(&self.model.view_id) {
548            Some(s) => s,
549            None => return Err("View not found".to_string()),
550        };
551        let mut new_row = view.row + 1;
552        while new_row <= LAST_ROW
553            && self
554                .model
555                .workbook
556                .worksheet(sheet)?
557                .is_row_hidden(new_row)?
558        {
559            new_row += 1;
560        }
561        if !is_valid_row(new_row) {
562            return Ok(());
563        }
564        // if the row is not fully visible we 'scroll' down until it is
565        let mut height = 0.0;
566        let mut row = view.top_row;
567        while row <= new_row + 1 && row <= LAST_ROW {
568            height += self.model.get_row_height(sheet, row)?;
569            row += 1;
570        }
571        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
572            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
573                view.row = new_row;
574                view.range = [new_row, view.column, new_row, view.column];
575                if height > window_height as f64 {
576                    view.top_row += 1;
577                }
578            }
579        }
580        Ok(())
581    }
582
583    // TODO: This function should be memoized
584    /// Returns the x-coordinate of the cell in the top left corner
585    pub fn get_scroll_x(&self) -> Result<f64, String> {
586        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
587            view.sheet
588        } else {
589            return Err("View not found".to_string());
590        };
591        let worksheet = match self.model.workbook.worksheet(sheet) {
592            Ok(s) => s,
593            Err(_) => return Err("Worksheet not found".to_string()),
594        };
595        let view = match worksheet.views.get(&self.model.view_id) {
596            Some(s) => s,
597            None => return Err("View not found".to_string()),
598        };
599        let mut scroll_x = 0.0;
600        for column in 1..view.left_column {
601            scroll_x += self.model.get_column_width(sheet, column)?;
602        }
603        Ok(scroll_x)
604    }
605
606    // TODO: This function should be memoized
607    /// Returns the y-coordinate of the cell in the top left corner
608    pub fn get_scroll_y(&self) -> Result<f64, String> {
609        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
610            view.sheet
611        } else {
612            return Err("View not found".to_string());
613        };
614        let worksheet = match self.model.workbook.worksheet(sheet) {
615            Ok(s) => s,
616            Err(_) => return Err("Worksheet not found".to_string()),
617        };
618        let view = match worksheet.views.get(&self.model.view_id) {
619            Some(s) => s,
620            None => return Err("View not found".to_string()),
621        };
622        let mut scroll_y = 0.0;
623        for row in 1..view.top_row {
624            scroll_y += self.model.get_row_height(sheet, row)?;
625        }
626        Ok(scroll_y)
627    }
628
629    /// User presses page down.
630    /// The `top_row` is now the first row that is not fully visible
631    pub fn on_page_down(&mut self) -> Result<(), String> {
632        let (sheet, window_height) =
633            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
634                (view.sheet, view.window_height)
635            } else {
636                return Err("View not found".to_string());
637            };
638        let worksheet = match self.model.workbook.worksheet(sheet) {
639            Ok(s) => s,
640            Err(_) => return Err("Worksheet not found".to_string()),
641        };
642        let view = match worksheet.views.get(&self.model.view_id) {
643            Some(s) => s,
644            None => return Err("View not found".to_string()),
645        };
646        let mut last_row = view.top_row;
647        let mut height = self.model.get_row_height(sheet, last_row)?;
648        while height <= window_height as f64 {
649            last_row += 1;
650            height += self.model.get_row_height(sheet, last_row)?;
651        }
652        if !is_valid_row(last_row) {
653            return Ok(());
654        }
655        let row_delta = view.row - view.top_row;
656        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
657            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
658                view.top_row = last_row;
659                view.row = view.top_row + row_delta;
660                view.range = [view.row, view.column, view.row, view.column];
661            }
662        }
663        Ok(())
664    }
665
666    /// On page up. tis needs to be the inverse of page down
667    pub fn on_page_up(&mut self) -> Result<(), String> {
668        let (sheet, window_height) =
669            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
670                (view.sheet, view.window_height as f64)
671            } else {
672                return Err("View not found".to_string());
673            };
674        let worksheet = match self.model.workbook.worksheet(sheet) {
675            Ok(s) => s,
676            Err(_) => return Err("Worksheet not found".to_string()),
677        };
678        let view = match worksheet.views.get(&self.model.view_id) {
679            Some(s) => s,
680            None => return Err("View not found".to_string()),
681        };
682
683        let mut first_row = view.top_row;
684        let mut height = self.model.get_row_height(sheet, first_row)?;
685        while height <= window_height && first_row > 1 {
686            first_row -= 1;
687            height += self.model.get_row_height(sheet, first_row)?;
688        }
689
690        let row_delta = view.row - view.top_row;
691        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
692            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
693                view.top_row = first_row;
694                view.row = view.top_row + row_delta;
695                view.range = [view.row, view.column, view.row, view.column];
696            }
697        }
698        Ok(())
699    }
700
701    /// We extend the selection to cell (target_row, target_column)
702    pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<(), String> {
703        let (sheet, window_width, window_height) =
704            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
705                (
706                    view.sheet,
707                    view.window_width as f64,
708                    view.window_height as f64,
709                )
710            } else {
711                return Ok(());
712            };
713        let (selected_row, selected_column, range, top_row, left_column) =
714            if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
715                if let Some(view) = worksheet.views.get(&self.model.view_id) {
716                    (
717                        view.row,
718                        view.column,
719                        view.range,
720                        view.top_row,
721                        view.left_column,
722                    )
723                } else {
724                    return Ok(());
725                }
726            } else {
727                return Ok(());
728            };
729        let [row_start, column_start, _row_end, _column_end] = range;
730
731        let mut new_left_column = left_column;
732        if target_column >= selected_column {
733            let mut width = 0.0;
734            let mut column = left_column;
735            while column <= target_column {
736                width += self.model.get_column_width(sheet, column)?;
737                column += 1;
738            }
739
740            while width > window_width {
741                width -= self.model.get_column_width(sheet, new_left_column)?;
742                new_left_column += 1;
743            }
744        } else if target_column < new_left_column {
745            new_left_column = target_column;
746        }
747        let mut new_top_row = top_row;
748        if target_row >= selected_row {
749            let mut height = 0.0;
750            let mut row = top_row;
751            while row <= target_row {
752                height += self.model.get_row_height(sheet, row)?;
753                row += 1;
754            }
755            while height > window_height {
756                height -= self.model.get_row_height(sheet, new_top_row)?;
757                new_top_row += 1;
758            }
759        } else if target_row < new_top_row {
760            new_top_row = target_row;
761        }
762
763        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
764            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
765                view.range = [row_start, column_start, target_row, target_column];
766                if new_top_row != top_row {
767                    view.top_row = new_top_row;
768                }
769                if new_left_column != left_column {
770                    view.left_column = new_left_column;
771                }
772            }
773        }
774
775        Ok(())
776    }
777
778    /// User navigates to the edge in the given direction
779    pub fn on_navigate_to_edge_in_direction(
780        &mut self,
781        direction: NavigationDirection,
782    ) -> Result<(), String> {
783        let (sheet, window_height, window_width) =
784            if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
785                (view.sheet, view.window_height, view.window_width)
786            } else {
787                return Err("View not found".to_string());
788            };
789        let worksheet = match self.model.workbook.worksheet(sheet) {
790            Ok(s) => s,
791            Err(_) => return Err("Worksheet not found".to_string()),
792        };
793        let view = match worksheet.views.get(&self.model.view_id) {
794            Some(s) => s,
795            None => return Err("View not found".to_string()),
796        };
797        let row = view.row;
798        let column = view.column;
799        if !is_valid_row(row) || !is_valid_column_number(column) {
800            return Err("Invalid row or column".to_string());
801        }
802        let (new_row, new_column) =
803            worksheet.navigate_to_edge_in_direction(row, column, direction)?;
804        if !is_valid_row(new_row) || !is_valid_column_number(new_column) {
805            return Err("Invalid row or column after navigation".to_string());
806        }
807        if new_row == row && new_column == column {
808            return Ok(()); // No change in selection
809        }
810
811        let mut top_row = view.top_row;
812        let mut left_column = view.left_column;
813
814        match direction {
815            NavigationDirection::Left | NavigationDirection::Right => {
816                // If the new column is not fully visible we 'scroll' until it is
817                // We need to check two conditions:
818                // 1. new_column > view.left_column
819                // 2. right_column < new_column
820                if new_column < view.left_column {
821                    left_column = new_column;
822                } else {
823                    let mut c = new_column;
824                    let mut width = self.model.get_column_width(sheet, c)?;
825                    while c > 1 && width <= window_width as f64 {
826                        c -= 1;
827                        width += self.model.get_column_width(sheet, c)?;
828                    }
829                    if c > view.left_column {
830                        left_column = c;
831                    }
832                }
833            }
834            NavigationDirection::Up | NavigationDirection::Down => {
835                // If the new row is not fully visible we 'scroll' until it is
836                // We need to check two conditions:
837                // 1. new_row > view.top_row
838                // 2. bottom_row < new_row
839                if new_row < view.top_row {
840                    top_row = new_row;
841                } else {
842                    let mut r = new_row;
843                    let mut height = self.model.get_row_height(sheet, r)?;
844                    while r > 1 && height <= window_height as f64 {
845                        r -= 1;
846                        height += self.model.get_row_height(sheet, r)?;
847                    }
848                    if r > view.top_row {
849                        top_row = r;
850                    }
851                }
852            }
853        }
854
855        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
856            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
857                view.row = new_row;
858                view.column = new_column;
859                view.range = [new_row, new_column, new_row, new_column];
860
861                view.top_row = top_row;
862                view.left_column = left_column;
863            }
864        }
865        Ok(())
866    }
867}