Skip to main content

agg_gui/widgets/tree_view/
row.rs

1//! Compositional row widgets for `TreeView`:
2//! `ExpandToggle`, `NodeIconWidget`, and `TreeRow`.
3//!
4//! These widgets are intended to be composed into a `FlexRow` (or positioned
5//! manually) by the `TreeView` when building visible rows.
6
7use std::sync::Arc;
8
9use crate::color::Color;
10use crate::event::{Event, EventResult};
11use crate::geometry::{Rect, Size};
12use crate::draw_ctx::DrawCtx;
13use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
14use crate::text::Font;
15use crate::widget::Widget;
16use crate::widgets::label::Label;
17use crate::widgets::primitives::SizedBox;
18
19use super::node::NodeIcon;
20
21// ---------------------------------------------------------------------------
22// Constants (moved from mod.rs so drag.rs and row.rs share one source)
23// ---------------------------------------------------------------------------
24
25pub const EXPAND_W: f64 = 18.0; // space reserved for expand arrow
26pub const ICON_W:   f64 = 14.0;
27pub const ICON_GAP: f64 = 4.0;
28
29// ---------------------------------------------------------------------------
30// icon_color helper
31// ---------------------------------------------------------------------------
32
33/// Return the fill colour for a given node icon type.
34pub fn icon_color(icon: NodeIcon) -> Color {
35    match icon {
36        NodeIcon::Folder  => Color::rgb(0.90, 0.72, 0.20),
37        NodeIcon::File    => Color::rgb(0.55, 0.78, 0.95),
38        NodeIcon::Package => Color::rgb(0.70, 0.60, 0.88),
39    }
40}
41
42// ---------------------------------------------------------------------------
43// ExpandToggle
44// ---------------------------------------------------------------------------
45
46/// Draws the ▶/▼ expand arrow. **Display-only** — returns `Ignored` for all events.
47///
48/// Interaction is handled centrally by `TreeView::on_event()`, which uses the
49/// `RowMeta::toggle_rect` field (populated from `TreeRow::toggle_local_bounds` during
50/// layout) to detect clicks on the toggle area and toggle `TreeNode::is_expanded` directly.
51pub struct ExpandToggle {
52    bounds: Rect,
53    pub has_children: bool,
54    pub is_expanded: bool,
55    children: Vec<Box<dyn Widget>>,
56    base: WidgetBase,
57}
58
59impl ExpandToggle {
60    pub fn new(has_children: bool, is_expanded: bool) -> Self {
61        Self {
62            bounds: Rect::default(),
63            has_children,
64            is_expanded,
65            children: Vec::new(),
66            base: WidgetBase::new(),
67        }
68    }
69}
70
71impl Widget for ExpandToggle {
72    fn type_name(&self) -> &'static str { "ExpandToggle" }
73    fn bounds(&self) -> Rect { self.bounds }
74    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
75    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
76    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
77
78    fn margin(&self)   -> Insets  { self.base.margin }
79    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
80    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
81    fn min_size(&self) -> Size    { self.base.min_size }
82    fn max_size(&self) -> Size    { self.base.max_size }
83
84    fn layout(&mut self, available: Size) -> Size {
85        Size::new(EXPAND_W, available.height)
86    }
87
88    // The framework has already translated `ctx` to this widget's bottom-left origin.
89    // All drawing coordinates are widget-local (0,0 = bottom-left of this widget).
90    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
91        if !self.has_children { return; }
92
93        let w = self.bounds.width;
94        let h = self.bounds.height;
95        let cx = w * 0.5;
96        let cy = h * 0.5;
97
98        ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, 0.45));
99        ctx.begin_path();
100        if self.is_expanded {
101            // Down-pointing ▼
102            ctx.move_to(cx - 4.5, cy + 2.0);
103            ctx.line_to(cx + 4.5, cy + 2.0);
104            ctx.line_to(cx, cy - 3.0);
105            ctx.close_path();
106        } else {
107            // Right-pointing ▶
108            ctx.move_to(cx - 2.5, cy - 4.5);
109            ctx.line_to(cx - 2.5, cy + 4.5);
110            ctx.line_to(cx + 3.5, cy);
111            ctx.close_path();
112        }
113        ctx.fill();
114    }
115
116    fn on_event(&mut self, _: &Event) -> EventResult { EventResult::Ignored }
117}
118
119// ---------------------------------------------------------------------------
120// NodeIconWidget
121// ---------------------------------------------------------------------------
122
123/// Draws the coloured icon glyph for a node.
124/// Width is `ICON_W + ICON_GAP`; height fills the row.
125pub struct NodeIconWidget {
126    bounds: Rect,
127    pub icon: NodeIcon,
128    children: Vec<Box<dyn Widget>>,
129    base: WidgetBase,
130}
131
132impl NodeIconWidget {
133    pub fn new(icon: NodeIcon) -> Self {
134        Self {
135            bounds: Rect::default(),
136            icon,
137            children: Vec::new(),
138            base: WidgetBase::new(),
139        }
140    }
141}
142
143impl Widget for NodeIconWidget {
144    fn type_name(&self) -> &'static str { "NodeIconWidget" }
145    fn bounds(&self) -> Rect { self.bounds }
146    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
147    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
148    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
149
150    fn margin(&self)   -> Insets  { self.base.margin }
151    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
152    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
153    fn min_size(&self) -> Size    { self.base.min_size }
154    fn max_size(&self) -> Size    { self.base.max_size }
155
156    fn layout(&mut self, available: Size) -> Size {
157        Size::new(ICON_W + ICON_GAP, available.height)
158    }
159
160    // The framework has already translated `ctx` to this widget's bottom-left origin.
161    // All drawing coordinates are widget-local (0,0 = bottom-left of this widget).
162    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
163        let h = self.bounds.height;
164        let iy = (h - ICON_W) * 0.5;
165
166        ctx.set_fill_color(icon_color(self.icon));
167        ctx.begin_path();
168        ctx.rounded_rect(0.0, iy, ICON_W, ICON_W, 2.0);
169        ctx.fill();
170
171        if matches!(self.icon, NodeIcon::Folder) {
172            // Folder tab nub
173            ctx.begin_path();
174            ctx.rounded_rect(0.0, iy + ICON_W * 0.55, ICON_W * 0.45, ICON_W * 0.5, 1.0);
175            ctx.fill();
176        }
177    }
178
179    fn on_event(&mut self, _: &Event) -> EventResult { EventResult::Ignored }
180}
181
182// ---------------------------------------------------------------------------
183// TreeRow
184// ---------------------------------------------------------------------------
185
186/// Compositional row: `SizedBox` (indent) | `ExpandToggle` | `NodeIconWidget` | `Label`.
187///
188/// **Event-routing note:** `TreeRow` and its children all return `EventResult::Ignored`.
189/// The containing `TreeView` handles all events (selection, expand/collapse) using its
190/// `row_metas: Vec<RowMeta>` which records each row's node_idx and toggle bounds.
191pub struct TreeRow {
192    bounds: Rect,
193    pub node_idx: usize,
194    /// Bounds of the `ExpandToggle` in row-local coordinates (set in `layout()`).
195    /// For leaf nodes (`has_children = false`), this field is `Rect::default()` (all zeros)
196    /// and is never read — `TreeView` uses `None` for the corresponding `RowMeta::toggle_rect`.
197    pub toggle_local_bounds: Rect,
198    is_selected: bool,
199    is_hovered: bool,
200    focused: bool,
201    children: Vec<Box<dyn Widget>>,
202    base: WidgetBase,
203}
204
205impl TreeRow {
206    #[allow(clippy::too_many_arguments)]
207    pub fn new(
208        node_idx: usize,
209        depth: u32,
210        has_children: bool,
211        is_expanded: bool,
212        is_selected: bool,
213        is_hovered: bool,
214        focused: bool,
215        icon: NodeIcon,
216        label: impl Into<String>,
217        font: Arc<Font>,
218        font_size: f64,
219        indent_width: f64,
220        row_height: f64,
221    ) -> Self {
222        let indent_px = depth as f64 * indent_width;
223        let mut children: Vec<Box<dyn Widget>> = Vec::with_capacity(4);
224        children.push(Box::new(SizedBox::fixed(indent_px, row_height)));
225        children.push(Box::new(ExpandToggle::new(has_children, is_expanded)));
226        children.push(Box::new(NodeIconWidget::new(icon)));
227        children.push(Box::new(Label::new(label, font).with_font_size(font_size)));
228
229        Self {
230            bounds: Rect::default(),
231            node_idx,
232            toggle_local_bounds: Rect::default(),
233            is_selected,
234            is_hovered,
235            focused,
236            children,
237            base: WidgetBase::new(),
238        }
239    }
240}
241
242impl Widget for TreeRow {
243    fn type_name(&self) -> &'static str { "TreeRow" }
244    fn bounds(&self) -> Rect { self.bounds }
245    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
246    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
247    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
248
249    fn margin(&self)   -> Insets  { self.base.margin }
250    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
251    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
252    fn min_size(&self) -> Size    { self.base.min_size }
253    fn max_size(&self) -> Size    { self.base.max_size }
254
255    fn layout(&mut self, available: Size) -> Size {
256        let h = available.height;
257        let total_w = available.width;
258
259        // Children 0, 1, 2 get their natural width.
260        // Child 3 (Label) gets the remaining width.
261        let mut x = 0.0;
262
263        // Child 0: SizedBox (indent)
264        let s0 = self.children[0].layout(Size::new(total_w, h));
265        self.children[0].set_bounds(Rect::new(x, 0.0, s0.width, h));
266        x += s0.width;
267
268        // Child 1: ExpandToggle — cache its x for toggle hit-testing
269        let s1 = self.children[1].layout(Size::new(total_w - x, h));
270        self.children[1].set_bounds(Rect::new(x, 0.0, s1.width, h));
271        self.toggle_local_bounds = Rect::new(x, 0.0, s1.width, h);
272        x += s1.width;
273
274        // Child 2: NodeIconWidget
275        let s2 = self.children[2].layout(Size::new(total_w - x, h));
276        self.children[2].set_bounds(Rect::new(x, 0.0, s2.width, h));
277        x += s2.width;
278
279        // Child 3: Label — remaining width
280        let label_w = (total_w - x).max(0.0);
281        let s3 = self.children[3].layout(Size::new(label_w, h));
282        self.children[3].set_bounds(Rect::new(x, 0.0, s3.width, h));
283
284        Size::new(total_w, h)
285    }
286
287    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
288        let w = self.bounds.width;
289        let h = self.bounds.height;
290        let v = ctx.visuals();
291
292        if self.is_selected {
293            let c = if self.focused {
294                // Accent-tinted overlay — same colour in both themes so the
295                // selection reads as "selected" regardless of palette.
296                Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.25)
297            } else {
298                // Theme-neutral dim overlay: subtle tint of the text color.
299                Color::rgba(v.text_color.r, v.text_color.g, v.text_color.b, 0.12)
300            };
301            ctx.set_fill_color(c);
302            ctx.begin_path();
303            ctx.rect(0.0, 0.0, w, h);
304            ctx.fill();
305        } else if self.is_hovered {
306            ctx.set_fill_color(Color::rgba(
307                v.text_color.r, v.text_color.g, v.text_color.b, 0.08,
308            ));
309            ctx.begin_path();
310            ctx.rect(0.0, 0.0, w, h);
311            ctx.fill();
312        }
313    }
314
315    fn on_event(&mut self, _: &Event) -> EventResult { EventResult::Ignored }
316}