Skip to main content

teksilo_widgets/table_view/
row_navigator.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Trait abstracting per-row navigation for `TableView` and `TreeTableView`.
5//!
6//! Flat tables answer arrow-up/down by stepping a contiguous index;
7//! tree tables need to ask their `TreeSlice` for the next visible
8//! flat-index. The shared keyboard handler in `keyboard.rs` is generic
9//! over this trait so both widgets re-use the same key-matrix logic.
10
11use std::rc::Rc;
12
13/// Per-row navigator. Implementations live alongside the consumer
14/// widget — `FlatNavigator` here for `TableView`; `TreeNavigator` in
15/// `tree_table_view.rs` for `TreeTableView`.
16pub(crate) trait RowNavigator {
17    fn row_count(&self) -> usize;
18
19    fn next_row(&self, current: usize) -> Option<usize> {
20        let n = self.row_count();
21        if n == 0 {
22            return None;
23        }
24        let next = current + 1;
25        if next < n { Some(next) } else { None }
26    }
27
28    fn prev_row(&self, current: usize) -> Option<usize> {
29        if current == 0 {
30            None
31        } else {
32            Some(current - 1)
33        }
34    }
35
36    fn first_row(&self) -> Option<usize> {
37        if self.row_count() == 0 { None } else { Some(0) }
38    }
39
40    fn last_row(&self) -> Option<usize> {
41        let n = self.row_count();
42        if n == 0 { None } else { Some(n - 1) }
43    }
44
45    /// Tree-only — depth of the row in the hierarchy. `None` for flat tables,
46    /// which is what makes [`parent_row`](Self::parent_row) inert there.
47    fn depth(&self, _row: usize) -> Option<usize> {
48        None
49    }
50
51    /// The row's parent in the current flattening, or `None` at the root (and
52    /// always, for a flat table).
53    ///
54    /// Derived by scanning back for the nearest shallower row rather than
55    /// asked of the source, because the flattening is the only shape both
56    /// navigators expose — a parent is by construction the closest preceding
57    /// row at a smaller depth. Bounded by the distance to the parent, on a
58    /// keypress.
59    fn parent_row(&self, row: usize) -> Option<usize> {
60        let depth = self.depth(row)?;
61        if depth == 0 {
62            return None;
63        }
64        (0..row)
65            .rev()
66            .find(|&i| self.depth(i).is_some_and(|d| d < depth))
67    }
68
69    fn has_children(&self, _row: usize) -> bool {
70        false
71    }
72
73    fn is_expanded(&self, _row: usize) -> bool {
74        false
75    }
76
77    fn toggle_expanded(&self, _row: usize) {}
78
79    /// Tree-only — whether the row is one cell across every column (a
80    /// `TreeTableView` full-width row). The cursor's column is kept on such a
81    /// row but means nothing there: Home and End stay on it, the
82    /// expand/collapse arrows act as on the tree column whatever the column
83    /// (← on a row with nothing to collapse moves up to its parent), and no
84    /// column of it opens an editor.
85    fn spans_all_columns(&self, _row: usize) -> bool {
86        false
87    }
88
89    /// The scroll offset that reveals `row`, for a view that covers the top of
90    /// its own viewport and so cannot use the plain row arithmetic; `None`
91    /// (the default) for one that covers nothing. `TreeTableView` answers it
92    /// while it pins ancestor rows there.
93    fn reveal_scroll(
94        &self,
95        _row: usize,
96        _scroll: f32,
97        _viewport: f32,
98        _max_scroll: f32,
99    ) -> Option<f32> {
100        None
101    }
102}
103
104/// Trivial navigator over a length-providing closure. Used by
105/// `TableView` directly.
106pub(crate) struct FlatNavigator {
107    pub(crate) len_fn: Rc<dyn Fn() -> usize>,
108}
109
110impl FlatNavigator {
111    pub(crate) fn new(len_fn: Rc<dyn Fn() -> usize>) -> Self {
112        Self { len_fn }
113    }
114}
115
116impl RowNavigator for FlatNavigator {
117    fn row_count(&self) -> usize {
118        (self.len_fn)()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use std::cell::Cell;
126
127    #[test]
128    fn flat_navigator_steps_through_rows() {
129        let count = Rc::new(Cell::new(5));
130        let c = count.clone();
131        let nav = FlatNavigator::new(Rc::new(move || c.get()));
132        assert_eq!(nav.row_count(), 5);
133        assert_eq!(nav.first_row(), Some(0));
134        assert_eq!(nav.last_row(), Some(4));
135        assert_eq!(nav.next_row(0), Some(1));
136        assert_eq!(nav.next_row(4), None);
137        assert_eq!(nav.prev_row(0), None);
138        assert_eq!(nav.prev_row(2), Some(1));
139    }
140
141    #[test]
142    fn empty_navigator_returns_none_for_first_last() {
143        let nav = FlatNavigator::new(Rc::new(|| 0));
144        assert_eq!(nav.first_row(), None);
145        assert_eq!(nav.last_row(), None);
146        assert_eq!(nav.next_row(0), None);
147        assert_eq!(nav.prev_row(0), None);
148    }
149
150    #[test]
151    fn flat_default_tree_methods_are_noops() {
152        let nav = FlatNavigator::new(Rc::new(|| 5));
153        assert_eq!(nav.depth(0), None);
154        assert!(!nav.has_children(0));
155        assert!(!nav.is_expanded(0));
156        nav.toggle_expanded(0); // no-op
157    }
158}