Skip to main content

tui_treelistview/
state.rs

1use std::hash::Hash;
2use std::ops::Deref;
3
4use ratatui::buffer::Buffer;
5use ratatui::layout::Rect;
6use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11use crate::context::TreeMarkState;
12use crate::model::TreeRevision;
13use crate::projection::{ProjectedNode, TreeProjection};
14
15pub use hit::{TreeHit, TreeHitRegion};
16
17mod actions;
18pub mod hit;
19mod marks;
20mod navigation;
21mod visibility;
22
23#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
24struct ExpansionPath<Id> {
25    parent: Option<Id>,
26    id: Id,
27}
28
29impl<Id> ExpansionPath<Id> {
30    const fn new(parent: Option<Id>, id: Id) -> Self {
31        Self { parent, id }
32    }
33}
34
35struct RevisionedSet<T> {
36    values: FxHashSet<T>,
37    revision: TreeRevision,
38}
39
40impl<T: Eq + Hash> RevisionedSet<T> {
41    fn with_capacity(capacity: usize) -> Self {
42        Self {
43            values: FxHashSet::with_capacity_and_hasher(capacity, FxBuildHasher),
44            revision: TreeRevision::INITIAL,
45        }
46    }
47
48    const fn revision(&self) -> TreeRevision {
49        self.revision
50    }
51
52    fn mutate(&mut self, mutation: impl FnOnce(&mut FxHashSet<T>) -> bool) -> bool {
53        let changed = mutation(&mut self.values);
54        if changed {
55            self.revision.advance();
56        }
57        changed
58    }
59
60    fn set_membership(&mut self, value: T, present: bool) -> bool {
61        self.mutate(|values| {
62            if present {
63                values.insert(value)
64            } else {
65                values.remove(&value)
66            }
67        })
68    }
69
70    fn clear(&mut self) -> bool {
71        self.mutate(|values| {
72            if values.is_empty() {
73                false
74            } else {
75                values.clear();
76                true
77            }
78        })
79    }
80
81    fn retain(&mut self, mut keep: impl FnMut(&T) -> bool) -> bool {
82        self.mutate(|values| {
83            let old_len = values.len();
84            values.retain(&mut keep);
85            values.len() != old_len
86        })
87    }
88
89    fn replace(&mut self, values: FxHashSet<T>) -> bool {
90        self.mutate(|current| {
91            if *current == values {
92                false
93            } else {
94                *current = values;
95                true
96            }
97        })
98    }
99}
100
101impl<T> Deref for RevisionedSet<T> {
102    type Target = FxHashSet<T>;
103
104    fn deref(&self) -> &Self::Target {
105        &self.values
106    }
107}
108
109/// Persistent view state and its derived caches.
110pub struct TreeListViewState<Id> {
111    projection: TreeProjection<Id>,
112    selected: Option<Id>,
113    selected_row: Option<usize>,
114    selection_needs_visibility: bool,
115    offset: usize,
116    selected_column: Option<usize>,
117    column_needs_visibility: bool,
118    horizontal_offset: u16,
119    expanded: RevisionedSet<ExpansionPath<Id>>,
120    manual_marked: RevisionedSet<Id>,
121    mark_states: FxHashMap<Id, TreeMarkState>,
122    mark_stamp: Option<(TreeRevision, TreeRevision)>,
123    draw_lines: bool,
124    pub(crate) hit_map: hit::TreeHitMap,
125    pub(crate) render_buffer: Buffer,
126    #[cfg(feature = "keymap")]
127    keymap: crate::keymap::TreeKeyBindings,
128}
129
130impl<Id: Copy + Eq + Hash> TreeListViewState<Id> {
131    /// Creates empty view state.
132    #[must_use]
133    pub fn new() -> Self {
134        Self::with_capacity(0)
135    }
136
137    /// Creates view state with preallocated projection storage.
138    #[must_use]
139    pub fn with_capacity(capacity: usize) -> Self {
140        Self {
141            projection: TreeProjection::with_capacity(capacity),
142            selected: None,
143            selected_row: None,
144            selection_needs_visibility: false,
145            offset: 0,
146            selected_column: None,
147            column_needs_visibility: false,
148            horizontal_offset: 0,
149            expanded: RevisionedSet::with_capacity(capacity),
150            manual_marked: RevisionedSet::with_capacity(capacity),
151            mark_states: FxHashMap::with_capacity_and_hasher(capacity, FxBuildHasher),
152            mark_stamp: None,
153            draw_lines: true,
154            hit_map: hit::TreeHitMap::default(),
155            render_buffer: Buffer::empty(Rect::ZERO),
156            #[cfg(feature = "keymap")]
157            keymap: crate::keymap::TreeKeyBindings::new(),
158        }
159    }
160
161    /// Restores state from a snapshot.
162    #[must_use]
163    pub fn from_snapshot(snapshot: TreeListViewSnapshot<Id>) -> Self {
164        let mut state = Self::new();
165        state.restore(snapshot);
166        state
167    }
168
169    /// Returns the current projection. Before reading it, the application must call
170    /// [`TreeListViewState::ensure_projection`] or render the widget.
171    #[must_use]
172    pub const fn projection(&self) -> &TreeProjection<Id> {
173        &self.projection
174    }
175
176    /// Captures the persistent part of the state.
177    #[must_use]
178    pub fn snapshot(&self) -> TreeListViewSnapshot<Id> {
179        TreeListViewSnapshot {
180            expanded: self
181                .expanded
182                .iter()
183                .map(|path| (path.parent, path.id))
184                .collect(),
185            manual_marked: self.manual_marked.iter().copied().collect(),
186            selected: self.selected,
187            selected_column: self.selected_column,
188            offset: self.offset,
189            horizontal_offset: self.horizontal_offset,
190            draw_lines: self.draw_lines,
191        }
192    }
193
194    /// Restores persistent state and resets derived caches.
195    pub fn restore(&mut self, snapshot: TreeListViewSnapshot<Id>) {
196        self.expanded.replace(
197            snapshot
198                .expanded
199                .into_iter()
200                .map(|(parent, id)| ExpansionPath::new(parent, id))
201                .collect(),
202        );
203        self.manual_marked
204            .replace(snapshot.manual_marked.into_iter().collect());
205        self.selected = snapshot.selected;
206        self.selected_row = None;
207        self.selection_needs_visibility = self.selected.is_some();
208        self.selected_column = snapshot.selected_column;
209        self.column_needs_visibility = self.selected_column.is_some();
210        self.offset = snapshot.offset;
211        self.horizontal_offset = snapshot.horizontal_offset;
212        self.draw_lines = snapshot.draw_lines;
213    }
214
215    #[must_use]
216    pub const fn draw_lines(&self) -> bool {
217        self.draw_lines
218    }
219
220    pub const fn set_draw_lines(&mut self, draw: bool) {
221        self.draw_lines = draw;
222    }
223
224    pub(crate) fn is_expanded(&self, parent: Option<Id>, id: Id) -> bool {
225        self.expanded.contains(&ExpansionPath::new(parent, id))
226    }
227
228    pub(crate) fn mark_state_cached(&self, id: Id) -> TreeMarkState {
229        self.mark_states.get(&id).copied().unwrap_or_default()
230    }
231
232    pub(crate) fn selected_node(&self) -> Option<ProjectedNode<Id>> {
233        let selected = self.selected?;
234        self.selected_row
235            .and_then(|index| self.projection.nodes().get(index))
236            .copied()
237            .filter(|node| node.id() == selected)
238    }
239
240    #[cfg(feature = "keymap")]
241    /// Returns the mutable key bindings.
242    pub const fn keymap_mut(&mut self) -> &mut crate::keymap::TreeKeyBindings {
243        &mut self.keymap
244    }
245}
246
247impl<Id: Copy + Eq + Hash> Default for TreeListViewState<Id> {
248    fn default() -> Self {
249        Self::new()
250    }
251}
252
253/// The serializable persistent part of view state.
254#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
255#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct TreeListViewSnapshot<Id> {
257    pub expanded: Vec<(Option<Id>, Id)>,
258    pub manual_marked: Vec<Id>,
259    pub selected: Option<Id>,
260    pub selected_column: Option<usize>,
261    pub offset: usize,
262    pub horizontal_offset: u16,
263    pub draw_lines: bool,
264}