Skip to main content

tui_treelistview/state/
actions.rs

1use std::hash::Hash;
2
3#[cfg(feature = "keymap")]
4use crossterm::event::KeyEvent;
5
6use crate::action::{
7    TreeAction, TreeEditAction, TreeEditRequest, TreeEvent, TreeIntent, TreeViewAction,
8};
9use crate::columns::TreeColumns;
10use crate::context::TreeExpansionState;
11use crate::edit::{TreeChangeSet, TreeEditCommand, TreeEditor, TreeSelectionUpdate};
12use crate::model::{TreeFilter, TreeModel, TreeQuery, TreeSort};
13
14use super::TreeListViewState;
15
16#[derive(Clone, Copy)]
17enum ExpansionAction {
18    Expand,
19    Toggle,
20}
21
22impl<Id: Copy + Eq + Hash> TreeListViewState<Id> {
23    /// Handles an action against the current projection.
24    pub fn handle_action<T, F, S, C, Custom>(
25        &mut self,
26        model: &T,
27        query: &TreeQuery<F, S>,
28        columns: &C,
29        action: TreeAction<Custom>,
30    ) -> TreeEvent<Id, Custom>
31    where
32        T: TreeModel<Id = Id>,
33        F: TreeFilter<T>,
34        S: TreeSort<T>,
35        C: TreeColumns<T>,
36    {
37        self.ensure_projection(model, query);
38        let event = match action {
39            TreeAction::View(action) => {
40                self.handle_view_action(model, columns.column_count(), action)
41            }
42            TreeAction::Edit(action) => self.handle_edit_intent(action),
43            TreeAction::Custom(custom) => TreeEvent::Intent(TreeIntent::Custom(custom)),
44        };
45        if matches!(event, TreeEvent::Changed) {
46            self.ensure_projection(model, query);
47        }
48        event
49    }
50
51    /// Applies a command through the model, reconciles persistent state, and rebuilds the projection.
52    ///
53    /// # Errors
54    ///
55    /// Returns the model-specific error from [`TreeEditor::apply`] without changing view state.
56    pub fn apply_edit<T, F, S>(
57        &mut self,
58        model: &mut T,
59        query: &TreeQuery<F, S>,
60        command: TreeEditCommand<Id>,
61    ) -> Result<TreeChangeSet<Id>, T::Error>
62    where
63        T: TreeEditor<Id = Id>,
64        F: TreeFilter<T>,
65        S: TreeSort<T>,
66    {
67        let changes = model.apply(command)?;
68        self.reconcile_changes(&changes);
69        if let TreeSelectionUpdate::Select(id) = changes.selection {
70            self.expand_to(model, id);
71        }
72        self.ensure_projection(model, query);
73        Ok(changes)
74    }
75
76    /// Reconciles marks, expansion, and selection with an exact model change set.
77    pub fn reconcile_changes(&mut self, changes: &TreeChangeSet<Id>) {
78        self.expanded.retain(|path| {
79            !changes.removed.contains(&path.id)
80                && !path
81                    .parent
82                    .is_some_and(|parent| changes.removed.contains(&parent))
83                && !changes.moved.contains(&path.id)
84        });
85
86        self.manual_marked
87            .retain(|id| !changes.removed.contains(id));
88
89        match changes.selection {
90            TreeSelectionUpdate::Keep => {}
91            TreeSelectionUpdate::Select(id) => {
92                self.selected = Some(id);
93                self.selected_row = None;
94                self.selection_needs_visibility = true;
95            }
96            TreeSelectionUpdate::Clear => {
97                self.selected = None;
98                self.selected_row = None;
99                self.selection_needs_visibility = false;
100            }
101        }
102    }
103
104    fn handle_view_action<T, C>(
105        &mut self,
106        model: &T,
107        column_count: usize,
108        action: TreeViewAction,
109    ) -> TreeEvent<Id, C>
110    where
111        T: TreeModel<Id = Id>,
112    {
113        let changed = match action {
114            TreeViewAction::SelectPrev => self.select_prev(),
115            TreeViewAction::SelectNext => self.select_next(),
116            TreeViewAction::SelectParent => self.select_parent(),
117            TreeViewAction::SelectFirstChild => self.select_first_child(),
118            TreeViewAction::Expand => {
119                return self.change_selected_expansion(ExpansionAction::Expand);
120            }
121            TreeViewAction::Collapse => self.collapse_selected(),
122            TreeViewAction::ExpandOrSelectFirstChild => {
123                return self.expand_or_select_first_child();
124            }
125            TreeViewAction::CollapseOrSelectParent => {
126                if self.collapse_selected() {
127                    true
128                } else {
129                    self.select_parent()
130                }
131            }
132            TreeViewAction::ToggleNode => {
133                return self.change_selected_expansion(ExpansionAction::Toggle);
134            }
135            TreeViewAction::ToggleRecursive => return self.toggle_selected_recursive(model),
136            TreeViewAction::ExpandAll => self.expand_all(model),
137            TreeViewAction::CollapseAll => self.collapse_all(),
138            TreeViewAction::ToggleGuides => {
139                self.draw_lines = !self.draw_lines;
140                true
141            }
142            TreeViewAction::ToggleMark => self
143                .selected
144                .is_some_and(|selected| self.toggle_marked(selected)),
145            TreeViewAction::SelectFirst => self.select_first(),
146            TreeViewAction::SelectLast => self.select_last(),
147            TreeViewAction::SelectColumnLeft => self.select_column_left(column_count),
148            TreeViewAction::SelectColumnRight => self.select_column_right(column_count),
149            TreeViewAction::SelectFirstColumn => {
150                self.select_column((column_count > 0).then_some(0), column_count)
151            }
152            TreeViewAction::SelectLastColumn => self.select_column(
153                (column_count > 0).then_some(column_count.saturating_sub(1)),
154                column_count,
155            ),
156            TreeViewAction::ScrollViewUp => self.scroll_view_by(-1),
157            TreeViewAction::ScrollViewDown => self.scroll_view_by(1),
158            TreeViewAction::ScrollLeft => self.scroll_horizontal_by(-1),
159            TreeViewAction::ScrollRight => self.scroll_horizontal_by(1),
160        };
161        changed_event(changed)
162    }
163
164    fn change_selected_expansion<C>(&mut self, action: ExpansionAction) -> TreeEvent<Id, C> {
165        let Some(node) = self.selected_node() else {
166            return TreeEvent::Unchanged;
167        };
168        match node.expansion() {
169            TreeExpansionState::Collapsed => {
170                self.set_expanded(node.id(), node.parent(), true);
171                TreeEvent::Changed
172            }
173            TreeExpansionState::Expanded if matches!(action, ExpansionAction::Toggle) => {
174                self.set_expanded(node.id(), node.parent(), false);
175                TreeEvent::Changed
176            }
177            TreeExpansionState::Unloaded => TreeEvent::Intent(TreeIntent::LoadChildren(node.id())),
178            TreeExpansionState::Leaf
179            | TreeExpansionState::Expanded
180            | TreeExpansionState::ForcedByFilter
181            | TreeExpansionState::Loading => TreeEvent::Unchanged,
182        }
183    }
184
185    fn collapse_selected(&mut self) -> bool {
186        let Some(node) = self.selected_node() else {
187            return false;
188        };
189        matches!(node.expansion(), TreeExpansionState::Expanded)
190            && self.set_expanded(node.id(), node.parent(), false)
191    }
192
193    fn expand_or_select_first_child<C>(&mut self) -> TreeEvent<Id, C> {
194        let event = self.change_selected_expansion(ExpansionAction::Expand);
195        match event {
196            TreeEvent::Unchanged => changed_event(self.select_first_child()),
197            TreeEvent::Changed | TreeEvent::Intent(_) => event,
198        }
199    }
200
201    fn toggle_selected_recursive<T, C>(&mut self, model: &T) -> TreeEvent<Id, C>
202    where
203        T: TreeModel<Id = Id>,
204    {
205        let Some(node) = self.selected_node() else {
206            return TreeEvent::Unchanged;
207        };
208        match node.expansion() {
209            TreeExpansionState::Collapsed | TreeExpansionState::Expanded => {
210                let expand = matches!(node.expansion(), TreeExpansionState::Collapsed);
211                changed_event(self.set_expanded_recursive(model, node.id(), node.parent(), expand))
212            }
213            TreeExpansionState::Unloaded => TreeEvent::Intent(TreeIntent::LoadChildren(node.id())),
214            TreeExpansionState::Leaf
215            | TreeExpansionState::ForcedByFilter
216            | TreeExpansionState::Loading => TreeEvent::Unchanged,
217        }
218    }
219
220    fn handle_edit_intent<C>(&self, action: TreeEditAction) -> TreeEvent<Id, C> {
221        let Some(node) = self.selected_node() else {
222            return TreeEvent::Unchanged;
223        };
224        let request = match action {
225            TreeEditAction::ReorderUp => {
226                let Some(parent) = node.parent() else {
227                    return TreeEvent::Unchanged;
228                };
229                TreeEditRequest::ReorderUp {
230                    node: node.id(),
231                    parent,
232                }
233            }
234            TreeEditAction::ReorderDown => {
235                let Some(parent) = node.parent() else {
236                    return TreeEvent::Unchanged;
237                };
238                TreeEditRequest::ReorderDown {
239                    node: node.id(),
240                    parent,
241                }
242            }
243            TreeEditAction::AddChild => TreeEditRequest::AddChild { parent: node.id() },
244            TreeEditAction::Rename => TreeEditRequest::Rename { node: node.id() },
245            TreeEditAction::Detach => {
246                let Some(parent) = node.parent() else {
247                    return TreeEvent::Unchanged;
248                };
249                TreeEditRequest::Detach {
250                    node: node.id(),
251                    parent,
252                }
253            }
254            TreeEditAction::Delete => TreeEditRequest::Delete { node: node.id() },
255            TreeEditAction::Yank => TreeEditRequest::Yank { node: node.id() },
256            TreeEditAction::Paste => TreeEditRequest::Paste { parent: node.id() },
257        };
258        TreeEvent::Intent(TreeIntent::Edit(request))
259    }
260
261    #[cfg(feature = "keymap")]
262    /// Resolves a crossterm event into an action and handles it.
263    pub fn handle_key<T, F, S, C>(
264        &mut self,
265        model: &T,
266        query: &TreeQuery<F, S>,
267        columns: &C,
268        key: KeyEvent,
269    ) -> TreeEvent<Id>
270    where
271        T: TreeModel<Id = Id>,
272        F: TreeFilter<T>,
273        S: TreeSort<T>,
274        C: TreeColumns<T>,
275    {
276        self.handle_key_with(model, query, columns, key, |_| None::<()>)
277    }
278
279    #[cfg(feature = "keymap")]
280    /// A version of [`handle_key`](Self::handle_key) with custom mapping.
281    pub fn handle_key_with<T, F, S, C, Custom, R>(
282        &mut self,
283        model: &T,
284        query: &TreeQuery<F, S>,
285        columns: &C,
286        key: KeyEvent,
287        custom: R,
288    ) -> TreeEvent<Id, Custom>
289    where
290        T: TreeModel<Id = Id>,
291        F: TreeFilter<T>,
292        S: TreeSort<T>,
293        C: TreeColumns<T>,
294        R: Fn(KeyEvent) -> Option<Custom>,
295    {
296        let Some(action) = self.keymap.resolve_with(key, custom) else {
297            return TreeEvent::Unchanged;
298        };
299        self.handle_action(model, query, columns, action)
300    }
301}
302const fn changed_event<Id, Custom>(changed: bool) -> TreeEvent<Id, Custom> {
303    if changed {
304        TreeEvent::Changed
305    } else {
306        TreeEvent::Unchanged
307    }
308}