Skip to main content

tui_lipan/widgets/tree/
types.rs

1//! Tree widget types.
2
3use crate::callback::{Callback, KeyHandler};
4use crate::core::event::{KeyCode, KeyEvent, KeyMods};
5use crate::style::{Length, Style, StyleSlot};
6use crate::utils::gradient::ColorGradient;
7use crate::widgets::ScrollKeymap;
8use crate::widgets::{FocusAccordion, ListItem};
9use std::sync::Arc;
10
11/// A path to a tree node (index-based).
12#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub struct TreePath(pub(crate) Arc<[usize]>);
14
15impl TreePath {
16    /// Access the path segments.
17    pub fn segments(&self) -> &[usize] {
18        &self.0
19    }
20}
21
22impl From<Vec<usize>> for TreePath {
23    fn from(value: Vec<usize>) -> Self {
24        Self(value.into())
25    }
26}
27
28impl AsRef<[usize]> for TreePath {
29    fn as_ref(&self) -> &[usize] {
30        &self.0
31    }
32}
33
34/// Tree selection event.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct TreeEvent {
37    /// Visible row index.
38    pub index: usize,
39    /// Path to the selected node.
40    pub path: TreePath,
41}
42
43/// Tree expand/collapse event.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct TreeToggleEvent {
46    /// Visible row index.
47    pub index: usize,
48    /// Path to the toggled node.
49    pub path: TreePath,
50    /// New expanded state.
51    pub expanded: bool,
52}
53
54/// Keyboard shortcuts for tree navigation.
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct TreeKeymap(u8);
57
58impl TreeKeymap {
59    /// Disable key handling.
60    pub const NONE: Self = Self(0);
61    /// Arrow keys (Left/Right).
62    pub const ARROWS: Self = Self(1 << 0);
63    /// Vim-style h/l.
64    pub const VIM: Self = Self(1 << 1);
65    /// Toggle with Space.
66    pub const TOGGLE: Self = Self(1 << 2);
67    /// Default key set.
68    pub const DEFAULT: Self = Self(Self::ARROWS.0 | Self::VIM.0 | Self::TOGGLE.0);
69
70    /// Check if this keymap includes another set.
71    pub fn contains(self, other: Self) -> bool {
72        (self.0 & other.0) == other.0
73    }
74}
75
76impl std::ops::BitOr for TreeKeymap {
77    type Output = Self;
78
79    fn bitor(self, rhs: Self) -> Self {
80        Self(self.0 | rhs.0)
81    }
82}
83
84impl std::ops::BitOrAssign for TreeKeymap {
85    fn bitor_assign(&mut self, rhs: Self) {
86        self.0 |= rhs.0;
87    }
88}
89
90impl std::ops::BitAnd for TreeKeymap {
91    type Output = Self;
92
93    fn bitand(self, rhs: Self) -> Self {
94        Self(self.0 & rhs.0)
95    }
96}
97
98impl std::ops::BitAndAssign for TreeKeymap {
99    fn bitand_assign(&mut self, rhs: Self) {
100        self.0 &= rhs.0;
101    }
102}
103
104impl std::ops::Not for TreeKeymap {
105    type Output = Self;
106
107    fn not(self) -> Self {
108        Self(!self.0)
109    }
110}
111
112impl Default for TreeKeymap {
113    fn default() -> Self {
114        Self::DEFAULT
115    }
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub(crate) enum TreeAction {
120    Expand,
121    Collapse,
122    Toggle,
123}
124
125pub(crate) fn tree_action_from_key(key: &KeyEvent, keymap: TreeKeymap) -> Option<TreeAction> {
126    if key.mods != KeyMods::NONE {
127        return None;
128    }
129    match key.code {
130        KeyCode::Left if keymap.contains(TreeKeymap::ARROWS) => Some(TreeAction::Collapse),
131        KeyCode::Right if keymap.contains(TreeKeymap::ARROWS) => Some(TreeAction::Expand),
132        KeyCode::Char('h') if keymap.contains(TreeKeymap::VIM) => Some(TreeAction::Collapse),
133        KeyCode::Char('l') if keymap.contains(TreeKeymap::VIM) => Some(TreeAction::Expand),
134        KeyCode::Char(' ') if keymap.contains(TreeKeymap::TOGGLE) => Some(TreeAction::Toggle),
135        _ => None,
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn key(code: KeyCode, mods: KeyMods) -> KeyEvent {
144        KeyEvent { code, mods }
145    }
146
147    #[test]
148    fn tree_navigation_ignores_modified_keys() {
149        let keymap = TreeKeymap::DEFAULT;
150        assert_eq!(
151            tree_action_from_key(&key(KeyCode::Left, KeyMods::NONE), keymap),
152            Some(TreeAction::Collapse)
153        );
154        assert_eq!(
155            tree_action_from_key(
156                &key(
157                    KeyCode::Left,
158                    KeyMods {
159                        ctrl: true,
160                        ..KeyMods::NONE
161                    }
162                ),
163                keymap
164            ),
165            None
166        );
167    }
168}
169
170/// A node in a tree.
171#[derive(Clone, Debug, PartialEq, Eq)]
172pub struct TreeNode {
173    pub(crate) item: ListItem,
174    pub(crate) children: Vec<TreeNode>,
175    pub(crate) expanded: bool,
176    pub(crate) expandable: bool,
177    pub(crate) indent: u16,
178    pub(crate) leading_guide_fill_cells: u16,
179}
180
181impl TreeNode {
182    /// Create a new tree node.
183    pub fn new(item: impl Into<ListItem>) -> Self {
184        Self {
185            item: item.into(),
186            children: Vec::new(),
187            expanded: false,
188            expandable: false,
189            indent: 2,
190            leading_guide_fill_cells: 0,
191        }
192    }
193
194    /// Add a child node.
195    pub fn child(mut self, child: TreeNode) -> Self {
196        self.children.push(child);
197        self
198    }
199
200    /// Replace all children, discarding anything already added with
201    /// [`child`](Self::child). Call `child` repeatedly to append instead.
202    pub fn children(mut self, children: impl IntoIterator<Item = TreeNode>) -> Self {
203        self.children = children.into_iter().collect();
204        self
205    }
206
207    /// Set expanded state (initial).
208    pub fn expanded(mut self, expanded: bool) -> Self {
209        self.expanded = expanded;
210        self
211    }
212
213    /// Set whether this node can expand even when it currently has no children.
214    pub fn expandable(mut self, expandable: bool) -> Self {
215        self.expandable = expandable;
216        self
217    }
218
219    pub(crate) fn is_expandable(&self) -> bool {
220        self.expandable || !self.children.is_empty()
221    }
222
223    /// Set indentation per level (default 2).
224    pub fn indent(mut self, indent: u16) -> Self {
225        self.indent = indent;
226        self
227    }
228
229    pub(crate) fn leading_guide_fill_cells(mut self, cells: u16) -> Self {
230        self.leading_guide_fill_cells = cells;
231        self
232    }
233}
234
235/// Style of indentation guides.
236#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
237pub enum IndentStyle {
238    /// No guides.
239    #[default]
240    None,
241    /// Vertical lines only (│).
242    Line,
243    /// Short branch connectors (├, └).
244    Short,
245    /// Short branch connectors with rounded terminal elbows (├, ╰).
246    ShortRounded,
247    /// Long branch connectors (├─, └─).
248    Long,
249    /// Long branch connectors with rounded terminal elbows (├─, ╰─).
250    LongRounded,
251}
252
253#[derive(Clone, PartialEq)]
254pub(crate) struct TreeProps {
255    pub root: TreeNode,
256    pub selected: Option<usize>,
257    pub clear_selection: bool,
258    pub force_scroll_to_selected: bool,
259    pub gap: u16,
260    pub icon_gap: u16,
261    pub show_icons: bool,
262    pub expanded_icon: Arc<str>,
263    pub collapsed_icon: Arc<str>,
264    pub leaf_icon: Option<Arc<str>>,
265    pub icon_style: Style,
266    pub width: Length,
267    pub height: Length,
268    pub style: Style,
269    pub hover_style: StyleSlot,
270    pub item_hover_style: StyleSlot,
271    pub selection_style: StyleSlot,
272    pub unfocused_selection_style: StyleSlot,
273    pub selection_symbol: Option<Arc<str>>,
274    pub selection_symbol_style: Option<Style>,
275    pub unfocused_selection_symbol_style: Option<Style>,
276    pub scrollbar: bool,
277    pub scrollbar_config: crate::style::ScrollbarConfig,
278    pub scroll_keys: ScrollKeymap,
279    pub scroll_wheel: bool,
280    pub show_scroll_indicators: bool,
281    pub scroll_indicator_style: Style,
282    pub empty_text: Option<Arc<str>>,
283    pub empty_text_style: Style,
284    pub activate_on_click: bool,
285    pub focusable: bool,
286    pub tab_stop: bool,
287    pub on_focus: Option<Callback<()>>,
288    pub on_blur: Option<Callback<()>>,
289    pub on_select: Option<Callback<TreeEvent>>,
290    pub on_activate: Option<Callback<TreeEvent>>,
291    pub on_toggle: Option<Callback<TreeToggleEvent>>,
292    pub keymap: TreeKeymap,
293    pub focus_policy: Option<FocusAccordion>,
294    pub focus_key: Option<Arc<str>>,
295    pub indent_style: IndentStyle,
296    pub indent_guide_style: Style,
297    pub indent_gradient: Option<ColorGradient>,
298    pub(crate) solid_indent_connector_gap: bool,
299    pub selection_full_width: bool,
300    pub unselected_symbol: Option<Arc<str>>,
301    pub key_interceptor: Option<KeyHandler>,
302    pub indent_guide_start_depth: usize,
303}