Skip to main content

hefesto_widgets/popups/tree_popup/
mod.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Color, Style},
5    widgets::StatefulWidget,
6};
7
8use crate::popup::{Popup, PopupSize};
9use crate::popups::AutoSized;
10use crate::tree::{Tree, TreeNode, TreeState};
11use crate::{BadgeStack, BorderType, DotPattern};
12
13/// Popup que envuelve un `Tree` para mostrar una jerarquía expandible.
14///
15/// Compone un `Popup` (chrome) con un `Tree` (árbol).
16/// Hereda la configuración del `Tree` a través de builders delegados.
17#[derive(Clone)]
18pub struct TreePopup<'a> {
19    popup: Popup<'a>,
20    tree: Tree<'a>,
21}
22
23impl<'a> TreePopup<'a> {
24    pub fn new(nodes: Vec<TreeNode<'a>>) -> Self {
25        Self {
26            popup: Popup::new(Color::White)
27                .padding(0)
28                .border_type(BorderType::Rounded),
29            tree: Tree::new(nodes),
30        }
31    }
32
33    pub fn title(mut self, title: &'a str) -> Self {
34        self.popup = self.popup.title(title);
35        self
36    }
37
38    pub fn cursor_style(mut self, style: Style) -> Self {
39        self.tree = self.tree.highlight_style(style);
40        self
41    }
42
43    pub fn border_color(mut self, color: Color) -> Self {
44        self.popup = self.popup.border_color(color);
45        self
46    }
47
48    pub fn border_type(mut self, bt: BorderType) -> Self {
49        self.popup = self.popup.border_type(bt);
50        self
51    }
52
53    pub fn bg_color(mut self, color: Color) -> Self {
54        self.popup = self.popup.bg_color(color);
55        self
56    }
57
58    pub fn width(mut self, w: PopupSize) -> Self {
59        self.popup = self.popup.width(w);
60        self
61    }
62
63    pub fn height(mut self, h: PopupSize) -> Self {
64        self.popup = self.popup.height(h);
65        self
66    }
67
68    pub fn position(mut self, x: u16, y: u16) -> Self {
69        self.popup = self.popup.position(x, y);
70        self
71    }
72
73    pub fn origin(mut self, x: u16, y: u16) -> Self {
74        self.popup = self.popup.origin(x, y);
75        self
76    }
77
78    pub fn header(mut self) -> Self {
79        self.popup = self.popup.header();
80        self
81    }
82
83    pub fn padding(mut self, p: u16) -> Self {
84        self.popup = self.popup.padding(p);
85        self
86    }
87
88    // Tree config delegation
89
90
91    // Tree config delegation
92
93    pub fn indent(mut self, n: u16) -> Self {
94        self.tree = self.tree.indent(n);
95        self
96    }
97
98    pub fn expand_icon(mut self, icon: &'a str) -> Self {
99        self.tree = self.tree.expand_icon(icon);
100        self
101    }
102
103    pub fn collapse_icon(mut self, icon: &'a str) -> Self {
104        self.tree = self.tree.collapse_icon(icon);
105        self
106    }
107
108    pub fn leaf_icon(mut self, icon: &'a str) -> Self {
109        self.tree = self.tree.leaf_icon(icon);
110        self
111    }
112
113    pub fn highlight_style(mut self, style: Style) -> Self {
114        self.tree = self.tree.highlight_style(style);
115        self
116    }
117
118    pub fn highlight_symbol(mut self, symbol: &'a str) -> Self {
119        self.tree = self.tree.highlight_symbol(symbol);
120        self
121    }
122
123    pub fn borders(mut self, borders: ratatui::widgets::Borders) -> Self {
124        self.tree = self.tree.borders(borders);
125        self
126    }
127
128    pub fn border_style(mut self, style: Style) -> Self {
129        self.tree = self.tree.border_style(style);
130        self
131    }
132
133    pub fn no_background(mut self) -> Self {
134        self.popup = self.popup.no_background();
135        self
136    }
137
138    pub fn background_dots(mut self, color: Color, symbol: &str, density: u16) -> Self {
139        self.popup = self.popup.background_dots(color, symbol, density);
140        self
141    }
142
143    pub fn background_pattern(mut self, pattern: DotPattern) -> Self {
144        self.popup = self.popup.background_pattern(pattern);
145        self
146    }
147
148    pub fn background_spacing(mut self, density_x: u16, density_y: u16) -> Self {
149        self.popup = self.popup.background_spacing(density_x, density_y);
150        self
151    }
152
153    pub fn badges(mut self, badges: BadgeStack<'a>) -> Self {
154        self.popup = self.popup.badges(badges);
155        self
156    }
157}
158
159impl AutoSized for TreePopup<'_> {
160    type State = TreeState;
161
162    fn auto_height(&self, state: &Self::State, _area: Rect) -> u16 {
163        let header_height: u16 = if self.popup.has_header() { 2 } else { 0 };
164        let tree_height = self.tree.visible_count(&state.expanded) as u16 + 2 + header_height;
165        tree_height.max(5)
166    }
167}
168
169impl<'a> TreePopup<'a> {
170    /// Map a screen row to the flat visible item index at that position.
171    /// Returns `None` if the row is in the border/header area or beyond the
172    /// last visible item.
173    pub fn item_at(&self, state: &TreeState, popup_rect: Rect, row: u16) -> Option<usize> {
174        let content_y = popup_rect.y + self.popup.content_offset();
175        if row < content_y {
176            return None;
177        }
178        let list_row = (row - content_y) as usize;
179        let idx = list_row + state.scroll_state.list_state.offset();
180        let total = self.tree.visible_count(&state.expanded);
181        if idx < total {
182            Some(idx)
183        } else {
184            None
185        }
186    }
187
188    /// Resolves the final `Rect` for this popup within the given `area`,
189    /// using the current tree state for Auto-height resolution.
190    ///
191    /// See [`AutoSized::auto_height`].
192    pub fn resolve_rect(&self, area: Rect, state: &TreeState) -> Rect {
193        if self.popup.get_height() == PopupSize::Auto {
194            self.popup
195                .clone()
196                .height(PopupSize::Fixed(self.auto_height(state, area)))
197                .resolve_rect(area)
198        } else {
199            self.popup.resolve_rect(area)
200        }
201    }
202}
203
204impl StatefulWidget for TreePopup<'_> {
205    type State = TreeState;
206
207    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
208        let auto_h = if self.popup.get_height() == PopupSize::Auto {
209            Some(self.auto_height(state, area))
210        } else {
211            None
212        };
213        let mut popup = self.popup;
214        if let Some(h) = auto_h {
215            popup = popup.height(PopupSize::Fixed(h));
216        }
217        let inner = popup.render_inner(area, buf);
218
219        self.tree.render(inner, buf, state);
220    }
221}
222
223#[cfg(test)]
224mod tests;