Skip to main content

tui_treelistview/state/
visibility.rs

1use std::hash::Hash;
2
3use rustc_hash::{FxBuildHasher, FxHashMap};
4use smallvec::SmallVec;
5
6use crate::context::TreeExpansionState;
7use crate::model::{
8    TreeChildren, TreeFilter, TreeModel, TreeQuery, TreeSelectionFallback, TreeSort,
9};
10use crate::projection::{OccurrencePath, ProjectedNode};
11use crate::traversal::TreeWalk;
12
13use super::{ExpansionPath, TreeListViewState};
14
15impl<Id: Copy + Eq + Hash> TreeListViewState<Id> {
16    /// Synchronizes the projection with model, query, and expansion revisions.
17    ///
18    /// Returns `true` when the projection was rebuilt.
19    pub fn ensure_projection<T, F, S>(&mut self, model: &T, query: &TreeQuery<F, S>) -> bool
20    where
21        T: TreeModel<Id = Id>,
22        F: TreeFilter<T>,
23        S: TreeSort<T>,
24    {
25        let expansion_revision = self.expanded.revision();
26        if self.projection.is_current(model, query, expansion_revision) {
27            return false;
28        }
29
30        let old_index = self.selected_row;
31        let old_path = old_index.and_then(|index| self.projection.occurrence_path(index));
32        let expanded = &self.expanded;
33        self.projection
34            .rebuild(model, query, expansion_revision, |parent, id| {
35                expanded.contains(&ExpansionPath::new(parent, id))
36            });
37        self.restore_selection_after_rebuild(
38            old_index,
39            old_path.as_ref(),
40            query.selection_fallback(),
41        );
42        self.selection_needs_visibility = self.selected.is_some();
43        self.clamp_offsets();
44        true
45    }
46
47    /// Expands the path to a node and selects it when it is present in the projection.
48    pub fn select_by_id<T, F, S>(&mut self, model: &T, query: &TreeQuery<F, S>, id: Id) -> bool
49    where
50        T: TreeModel<Id = Id>,
51        F: TreeFilter<T>,
52        S: TreeSort<T>,
53    {
54        if !self.expand_to(model, id) {
55            return false;
56        }
57        self.ensure_projection(model, query);
58        if let Some(index) = self.projection.index_of(id) {
59            self.selected = Some(id);
60            self.selected_row = Some(index);
61            self.selection_needs_visibility = true;
62            true
63        } else {
64            false
65        }
66    }
67
68    /// Expands every loaded ancestor of a node.
69    pub fn expand_to<T: TreeModel<Id = Id>>(&mut self, model: &T, target: Id) -> bool {
70        let hint = model.size_hint();
71        let mut parents = FxHashMap::with_capacity_and_hasher(hint, FxBuildHasher);
72        let mut found = false;
73        for node in TreeWalk::forest(model) {
74            parents.insert(node.id, (node.parent, node.children.is_branch()));
75            if node.id == target {
76                found = true;
77                break;
78            }
79        }
80        if !found {
81            return false;
82        }
83
84        let mut path = SmallVec::<[Id; 16]>::new();
85        let mut cursor = Some(target);
86        while let Some(id) = cursor {
87            path.push(id);
88            cursor = parents.get(&id).and_then(|(parent, _)| *parent);
89        }
90        path.reverse();
91
92        self.expanded.mutate(|expanded| {
93            let mut changed = false;
94            for window in path.windows(2) {
95                let (parent, is_branch) = parents[&window[0]];
96                if is_branch {
97                    changed |= expanded.insert(ExpansionPath::new(parent, window[0]));
98                }
99            }
100            changed
101        });
102        true
103    }
104
105    /// Expands every loaded branch in the forest.
106    pub fn expand_all<T: TreeModel<Id = Id>>(&mut self, model: &T) -> bool {
107        self.expanded.mutate(|expanded| {
108            let mut changed = false;
109            for node in TreeWalk::forest(model) {
110                if let TreeChildren::Loaded(children) = node.children
111                    && !children.is_empty()
112                {
113                    changed |= expanded.insert(ExpansionPath::new(node.parent, node.id));
114                }
115            }
116            changed
117        })
118    }
119
120    /// Collapses every branch.
121    pub fn collapse_all(&mut self) -> bool {
122        self.expanded.clear()
123    }
124
125    /// Sets the expansion state of a specific path.
126    pub fn set_expanded(&mut self, id: Id, parent: Option<Id>, expanded: bool) -> bool {
127        let path = ExpansionPath::new(parent, id);
128        self.expanded.set_membership(path, expanded)
129    }
130
131    /// Returns persisted expansion state rather than filter-forced state.
132    #[must_use]
133    pub fn node_is_expanded(&self, id: Id, parent: Option<Id>) -> bool {
134        self.is_expanded(parent, id)
135    }
136
137    /// Returns the effective expansion state of a visible node.
138    #[must_use]
139    pub fn effective_expansion(&self, id: Id) -> Option<TreeExpansionState> {
140        self.projection.get_by_id(id).map(ProjectedNode::expansion)
141    }
142
143    /// Iterates over persisted expanded paths in unspecified order.
144    pub fn expanded_paths(&self) -> impl Iterator<Item = (Option<Id>, Id)> + '_ {
145        self.expanded.iter().map(|path| (path.parent, path.id))
146    }
147
148    pub(crate) fn set_expanded_recursive<T: TreeModel<Id = Id>>(
149        &mut self,
150        model: &T,
151        root: Id,
152        parent: Option<Id>,
153        expand: bool,
154    ) -> bool {
155        self.expanded.mutate(|expanded| {
156            let mut changed = false;
157            for node in TreeWalk::subtree(model, parent, root) {
158                let path = ExpansionPath::new(node.parent, node.id);
159                if expand {
160                    if matches!(node.children, TreeChildren::Loaded(children) if !children.is_empty())
161                    {
162                        changed |= expanded.insert(path);
163                    }
164                } else {
165                    changed |= expanded.remove(&path);
166                }
167            }
168            changed
169        })
170    }
171
172    fn restore_selection_after_rebuild(
173        &mut self,
174        old_index: Option<usize>,
175        old_path: Option<&OccurrencePath<Id>>,
176        fallback: TreeSelectionFallback,
177    ) {
178        if let Some(path) = old_path {
179            if let Some(index) = self.projection.index_of_path(path) {
180                self.select_rebuilt_row(Some(index));
181                return;
182            }
183
184            if let Some(index) = self
185                .selected
186                .and_then(|selected| self.projection.index_of(selected))
187            {
188                self.select_rebuilt_row(Some(index));
189                return;
190            }
191
192            if matches!(fallback, TreeSelectionFallback::ParentThenNearest) {
193                for end in (1..path.len()).rev() {
194                    if let Some(index) = self.projection.index_of_path_prefix(path, end) {
195                        self.select_rebuilt_row(Some(index));
196                        return;
197                    }
198                }
199            }
200        } else if let Some(index) = self
201            .selected
202            .and_then(|selected| self.projection.index_of(selected))
203        {
204            self.select_rebuilt_row(Some(index));
205            return;
206        }
207
208        let selected_row = match fallback {
209            TreeSelectionFallback::Clear => None,
210            TreeSelectionFallback::Nearest | TreeSelectionFallback::ParentThenNearest => old_index
211                .and_then(|index| {
212                    let index = index.min(self.projection.len().saturating_sub(1));
213                    self.projection.nodes().get(index).map(|_| index)
214                }),
215        };
216        self.select_rebuilt_row(selected_row);
217    }
218
219    fn select_rebuilt_row(&mut self, selected_row: Option<usize>) {
220        self.selected = selected_row
221            .and_then(|index| self.projection.nodes().get(index))
222            .map(|node| node.id());
223        self.selected_row = selected_row;
224    }
225
226    fn clamp_offsets(&mut self) {
227        self.offset = self.offset.min(self.projection.len().saturating_sub(1));
228        if self.projection.is_empty() {
229            self.offset = 0;
230        }
231    }
232}