Skip to main content

tui_treelistview/state/
navigation.rs

1use std::hash::Hash;
2
3use crate::projection::ProjectedNode;
4use crate::style::TreeScrollPolicy;
5
6use super::TreeListViewState;
7
8impl<Id: Copy + Eq + Hash> TreeListViewState<Id> {
9    /// Возвращает идентификатор выбранной строки.
10    #[must_use]
11    pub const fn selected_id(&self) -> Option<Id> {
12        self.selected
13    }
14
15    /// Возвращает индекс выбранного вхождения в текущей проекции.
16    #[must_use]
17    pub const fn selected_index(&self) -> Option<usize> {
18        self.selected_row
19    }
20
21    /// Выбирает первое видимое вхождение узла по идентификатору.
22    pub fn select_id(&mut self, selected: Option<Id>) -> bool {
23        let index = selected.and_then(|id| self.projection.index_of(id));
24        self.set_selection(index)
25    }
26
27    /// Selects a row in the current projection.
28    pub fn select_index(&mut self, index: Option<usize>) -> bool {
29        self.set_selection(index)
30    }
31
32    /// Selects the first row.
33    pub fn select_first(&mut self) -> bool {
34        self.select_index((!self.projection.is_empty()).then_some(0))
35    }
36
37    /// Selects the last row.
38    pub fn select_last(&mut self) -> bool {
39        self.select_index(
40            (!self.projection.is_empty()).then_some(self.projection.len().saturating_sub(1)),
41        )
42    }
43
44    /// Selects the previous row, starting at the last row when nothing is selected.
45    pub fn select_prev(&mut self) -> bool {
46        if self.projection.is_empty() {
47            return self.set_selection(None);
48        }
49        let index = self.selected_index().map_or_else(
50            || self.projection.len().saturating_sub(1),
51            |index| index.saturating_sub(1),
52        );
53        self.select_index(Some(index))
54    }
55
56    /// Selects the next row, starting at the first row when nothing is selected.
57    pub fn select_next(&mut self) -> bool {
58        if self.projection.is_empty() {
59            return self.set_selection(None);
60        }
61        let index = self.selected_index().map_or(0, |index| {
62            index.saturating_add(1).min(self.projection.len() - 1)
63        });
64        self.select_index(Some(index))
65    }
66
67    /// Selects the visible parent.
68    pub fn select_parent(&mut self) -> bool {
69        let parent = self.selected_node().and_then(ProjectedNode::parent_index);
70        parent.is_some() && self.set_selection(parent)
71    }
72
73    /// Selects the first visible direct child.
74    pub fn select_first_child(&mut self) -> bool {
75        let Some(index) = self.selected_index() else {
76            return false;
77        };
78        let Some(parent) = self.projection.nodes().get(index).copied() else {
79            return false;
80        };
81        let child = self
82            .projection
83            .nodes()
84            .get(index.saturating_add(1))
85            .filter(|candidate| candidate.level() == parent.level().saturating_add(1))
86            .map(|_| index.saturating_add(1));
87        child.is_some() && self.set_selection(child)
88    }
89
90    /// Returns the selected node's parent even when a synthetic parent is hidden.
91    #[must_use]
92    pub fn selected_parent_id(&self) -> Option<Id> {
93        self.selected_node().and_then(ProjectedNode::parent)
94    }
95
96    #[must_use]
97    pub fn selected_level(&self) -> Option<usize> {
98        self.selected_node().map(ProjectedNode::level)
99    }
100
101    #[must_use]
102    pub const fn visible_len(&self) -> usize {
103        self.projection.len()
104    }
105
106    #[must_use]
107    pub const fn is_empty(&self) -> bool {
108        self.projection.is_empty()
109    }
110
111    pub fn visible_ids(&self) -> impl Iterator<Item = Id> + '_ {
112        self.projection.nodes().iter().map(|node| node.id())
113    }
114
115    #[must_use]
116    pub fn visible_index_of(&self, id: Id) -> Option<usize> {
117        self.projection.index_of(id)
118    }
119
120    #[must_use]
121    pub fn visible_contains(&self, id: Id) -> bool {
122        self.projection.index_of(id).is_some()
123    }
124
125    /// Returns the index of the first viewport row.
126    #[must_use]
127    pub const fn offset(&self) -> usize {
128        self.offset
129    }
130
131    /// Sets the first viewport row independently of selection.
132    pub fn set_offset(&mut self, offset: usize) -> bool {
133        let offset = offset.min(self.projection.len().saturating_sub(1));
134        let changed = self.offset != offset;
135        self.offset = offset;
136        self.selection_needs_visibility = false;
137        changed
138    }
139
140    /// Scrolls the viewport without changing selection.
141    pub fn scroll_view_by(&mut self, amount: isize) -> bool {
142        let offset = if amount.is_negative() {
143            self.offset.saturating_sub(amount.unsigned_abs())
144        } else {
145            self.offset.saturating_add(amount.cast_unsigned())
146        };
147        self.set_offset(offset)
148    }
149
150    #[must_use]
151    pub const fn horizontal_offset(&self) -> u16 {
152        self.horizontal_offset
153    }
154
155    pub const fn set_horizontal_offset(&mut self, offset: u16) -> bool {
156        let changed = self.horizontal_offset != offset;
157        self.horizontal_offset = offset;
158        self.column_needs_visibility = false;
159        changed
160    }
161
162    pub const fn scroll_horizontal_by(&mut self, amount: i16) -> bool {
163        let offset = if amount.is_negative() {
164            self.horizontal_offset.saturating_sub(amount.unsigned_abs())
165        } else {
166            self.horizontal_offset
167                .saturating_add(amount.cast_unsigned())
168        };
169        self.set_horizontal_offset(offset)
170    }
171
172    pub(crate) fn clamp_horizontal_offset(&mut self, maximum: u16) {
173        self.horizontal_offset = self.horizontal_offset.min(maximum);
174    }
175
176    #[must_use]
177    pub const fn selected_column(&self) -> Option<usize> {
178        self.selected_column
179    }
180
181    pub fn select_column(&mut self, column: Option<usize>, column_count: usize) -> bool {
182        let column = column.filter(|column| *column < column_count);
183        let changed = self.selected_column != column;
184        self.selected_column = column;
185        if changed {
186            self.column_needs_visibility = column.is_some();
187        }
188        changed
189    }
190
191    pub fn select_column_left(&mut self, column_count: usize) -> bool {
192        if column_count == 0 {
193            return self.select_column(None, 0);
194        }
195        let column = self
196            .selected_column
197            .filter(|column| *column < column_count)
198            .map_or(column_count - 1, |column| column.saturating_sub(1));
199        self.select_column(Some(column), column_count)
200    }
201
202    pub fn select_column_right(&mut self, column_count: usize) -> bool {
203        if column_count == 0 {
204            return self.select_column(None, 0);
205        }
206        let column = self
207            .selected_column
208            .filter(|column| *column < column_count)
209            .map_or(0, |column| column.saturating_add(1).min(column_count - 1));
210        self.select_column(Some(column), column_count)
211    }
212
213    pub(crate) fn ensure_selection_visible(
214        &mut self,
215        viewport_height: usize,
216        policy: TreeScrollPolicy,
217    ) {
218        if !self.selection_needs_visibility {
219            return;
220        }
221        let Some(selected) = self.selected_index() else {
222            self.selection_needs_visibility = false;
223            return;
224        };
225        let height = viewport_height.max(1);
226        match policy {
227            TreeScrollPolicy::KeepInView => {
228                if selected < self.offset {
229                    self.offset = selected;
230                } else if selected >= self.offset.saturating_add(height) {
231                    self.offset = selected.saturating_add(1).saturating_sub(height);
232                }
233            }
234            TreeScrollPolicy::CenterOnSelect => {
235                self.offset = selected.saturating_sub(height / 2);
236            }
237        }
238        self.offset = self
239            .offset
240            .min(self.projection.len().saturating_sub(height));
241        self.selection_needs_visibility = false;
242    }
243
244    pub(crate) fn clamp_offset_to_viewport(&mut self, viewport_height: usize) {
245        let maximum = self.projection.len().saturating_sub(viewport_height.max(1));
246        self.offset = self.offset.min(maximum);
247    }
248
249    pub(crate) const fn ensure_column_visible(
250        &mut self,
251        start: u16,
252        width: u16,
253        viewport_width: u16,
254    ) {
255        if !self.column_needs_visibility || viewport_width == 0 {
256            return;
257        }
258        let end = start.saturating_add(width);
259        if start < self.horizontal_offset {
260            self.horizontal_offset = start;
261        } else if end > self.horizontal_offset.saturating_add(viewport_width) {
262            self.horizontal_offset = end.saturating_sub(viewport_width);
263        }
264        self.column_needs_visibility = false;
265    }
266
267    fn set_selection(&mut self, selected_row: Option<usize>) -> bool {
268        let selected_row = selected_row.filter(|&index| index < self.projection.len());
269        let selected = selected_row
270            .and_then(|index| self.projection.nodes().get(index))
271            .map(|node| node.id());
272        let changed = self.selected != selected || self.selected_row != selected_row;
273        self.selected = selected;
274        self.selected_row = selected_row;
275        if changed {
276            self.selection_needs_visibility = selected.is_some();
277        }
278        changed
279    }
280}