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::draw_ctx::DrawCtx;
11use crate::event::{Event, EventResult};
12use crate::geometry::{Rect, Size};
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 {
73        "ExpandToggle"
74    }
75    fn bounds(&self) -> Rect {
76        self.bounds
77    }
78    fn set_bounds(&mut self, b: Rect) {
79        self.bounds = b;
80    }
81    fn children(&self) -> &[Box<dyn Widget>] {
82        &self.children
83    }
84    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
85        &mut self.children
86    }
87
88    fn margin(&self) -> Insets {
89        self.base.margin
90    }
91    fn h_anchor(&self) -> HAnchor {
92        self.base.h_anchor
93    }
94    fn v_anchor(&self) -> VAnchor {
95        self.base.v_anchor
96    }
97    fn min_size(&self) -> Size {
98        self.base.min_size
99    }
100    fn max_size(&self) -> Size {
101        self.base.max_size
102    }
103
104    fn layout(&mut self, available: Size) -> Size {
105        Size::new(EXPAND_W, available.height)
106    }
107
108    // The framework has already translated `ctx` to this widget's bottom-left origin.
109    // All drawing coordinates are widget-local (0,0 = bottom-left of this widget).
110    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
111        if !self.has_children {
112            return;
113        }
114
115        let w = self.bounds.width;
116        let h = self.bounds.height;
117        let cx = w * 0.5;
118        let cy = h * 0.5;
119
120        let v = ctx.visuals();
121        ctx.set_fill_color(Color::rgba(
122            v.text_color.r,
123            v.text_color.g,
124            v.text_color.b,
125            0.55,
126        ));
127        ctx.begin_path();
128        if self.is_expanded {
129            // Down-pointing ▼
130            ctx.move_to(cx - 4.5, cy + 2.0);
131            ctx.line_to(cx + 4.5, cy + 2.0);
132            ctx.line_to(cx, cy - 3.0);
133            ctx.close_path();
134        } else {
135            // Right-pointing ▶
136            ctx.move_to(cx - 2.5, cy - 4.5);
137            ctx.line_to(cx - 2.5, cy + 4.5);
138            ctx.line_to(cx + 3.5, cy);
139            ctx.close_path();
140        }
141        ctx.fill();
142    }
143
144    fn on_event(&mut self, _: &Event) -> EventResult {
145        EventResult::Ignored
146    }
147}
148
149// ---------------------------------------------------------------------------
150// NodeIconWidget
151// ---------------------------------------------------------------------------
152
153/// Draws the coloured icon glyph for a node.
154/// Width is `ICON_W + ICON_GAP`; height fills the row.
155pub struct NodeIconWidget {
156    bounds: Rect,
157    pub icon: NodeIcon,
158    children: Vec<Box<dyn Widget>>,
159    base: WidgetBase,
160}
161
162impl NodeIconWidget {
163    pub fn new(icon: NodeIcon) -> Self {
164        Self {
165            bounds: Rect::default(),
166            icon,
167            children: Vec::new(),
168            base: WidgetBase::new(),
169        }
170    }
171}
172
173impl Widget for NodeIconWidget {
174    fn type_name(&self) -> &'static str {
175        "NodeIconWidget"
176    }
177    fn bounds(&self) -> Rect {
178        self.bounds
179    }
180    fn set_bounds(&mut self, b: Rect) {
181        self.bounds = b;
182    }
183    fn children(&self) -> &[Box<dyn Widget>] {
184        &self.children
185    }
186    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
187        &mut self.children
188    }
189
190    fn margin(&self) -> Insets {
191        self.base.margin
192    }
193    fn h_anchor(&self) -> HAnchor {
194        self.base.h_anchor
195    }
196    fn v_anchor(&self) -> VAnchor {
197        self.base.v_anchor
198    }
199    fn min_size(&self) -> Size {
200        self.base.min_size
201    }
202    fn max_size(&self) -> Size {
203        self.base.max_size
204    }
205
206    fn layout(&mut self, available: Size) -> Size {
207        Size::new(ICON_W + ICON_GAP, available.height)
208    }
209
210    // The framework has already translated `ctx` to this widget's bottom-left origin.
211    // All drawing coordinates are widget-local (0,0 = bottom-left of this widget).
212    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
213        let h = self.bounds.height;
214        let iy = (h - ICON_W) * 0.5;
215
216        ctx.set_fill_color(icon_color(self.icon));
217        ctx.begin_path();
218        ctx.rounded_rect(0.0, iy, ICON_W, ICON_W, 2.0);
219        ctx.fill();
220
221        if matches!(self.icon, NodeIcon::Folder) {
222            // Folder tab nub
223            ctx.begin_path();
224            ctx.rounded_rect(0.0, iy + ICON_W * 0.55, ICON_W * 0.45, ICON_W * 0.5, 1.0);
225            ctx.fill();
226        }
227    }
228
229    fn on_event(&mut self, _: &Event) -> EventResult {
230        EventResult::Ignored
231    }
232}
233
234// ---------------------------------------------------------------------------
235// TreeRow
236// ---------------------------------------------------------------------------
237
238/// Compositional row: `SizedBox` (indent) | `ExpandToggle` | `NodeIconWidget` | `Label`.
239///
240/// **Event-routing note:** `TreeRow` and its children all return `EventResult::Ignored`.
241/// The containing `TreeView` handles all events (selection, expand/collapse) using its
242/// `row_metas: Vec<RowMeta>` which records each row's node_idx and toggle bounds.
243pub struct TreeRow {
244    bounds: Rect,
245    pub node_idx: usize,
246    /// Bounds of the `ExpandToggle` in row-local coordinates (set in `layout()`).
247    /// For leaf nodes (`has_children = false`), this field is `Rect::default()` (all zeros)
248    /// and is never read — `TreeView` uses `None` for the corresponding `RowMeta::toggle_rect`.
249    pub toggle_local_bounds: Rect,
250    is_selected: bool,
251    is_hovered: bool,
252    focused: bool,
253    children: Vec<Box<dyn Widget>>,
254    base: WidgetBase,
255}
256
257impl TreeRow {
258    #[allow(clippy::too_many_arguments)]
259    pub fn new(
260        node_idx: usize,
261        depth: u32,
262        has_children: bool,
263        is_expanded: bool,
264        is_selected: bool,
265        is_hovered: bool,
266        focused: bool,
267        icon: NodeIcon,
268        label: impl Into<String>,
269        font: Arc<Font>,
270        font_size: f64,
271        indent_width: f64,
272        row_height: f64,
273    ) -> Self {
274        let indent_px = depth as f64 * indent_width;
275        let mut children: Vec<Box<dyn Widget>> = Vec::with_capacity(4);
276        children.push(Box::new(SizedBox::fixed(indent_px, row_height)));
277        children.push(Box::new(ExpandToggle::new(has_children, is_expanded)));
278        children.push(Box::new(NodeIconWidget::new(icon)));
279        children.push(Box::new(Label::new(label, font).with_font_size(font_size)));
280
281        Self {
282            bounds: Rect::default(),
283            node_idx,
284            toggle_local_bounds: Rect::default(),
285            is_selected,
286            is_hovered,
287            focused,
288            children,
289            base: WidgetBase::new(),
290        }
291    }
292}
293
294impl Widget for TreeRow {
295    fn type_name(&self) -> &'static str {
296        "TreeRow"
297    }
298    fn bounds(&self) -> Rect {
299        self.bounds
300    }
301    fn set_bounds(&mut self, b: Rect) {
302        self.bounds = b;
303    }
304    fn children(&self) -> &[Box<dyn Widget>] {
305        &self.children
306    }
307    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
308        &mut self.children
309    }
310
311    fn margin(&self) -> Insets {
312        self.base.margin
313    }
314    fn h_anchor(&self) -> HAnchor {
315        self.base.h_anchor
316    }
317    fn v_anchor(&self) -> VAnchor {
318        self.base.v_anchor
319    }
320    fn min_size(&self) -> Size {
321        self.base.min_size
322    }
323    fn max_size(&self) -> Size {
324        self.base.max_size
325    }
326
327    fn layout(&mut self, available: Size) -> Size {
328        let h = available.height;
329        let total_w = available.width;
330
331        // Children 0, 1, 2 get their natural width.
332        // Child 3 (Label) gets the remaining width.
333        let mut x = 0.0;
334
335        // Child 0: SizedBox (indent)
336        let s0 = self.children[0].layout(Size::new(total_w, h));
337        self.children[0].set_bounds(Rect::new(x, 0.0, s0.width, h));
338        x += s0.width;
339
340        // Child 1: ExpandToggle — cache its x for toggle hit-testing
341        let s1 = self.children[1].layout(Size::new(total_w - x, h));
342        self.children[1].set_bounds(Rect::new(x, 0.0, s1.width, h));
343        self.toggle_local_bounds = Rect::new(x, 0.0, s1.width, h);
344        x += s1.width;
345
346        // Child 2: NodeIconWidget
347        let s2 = self.children[2].layout(Size::new(total_w - x, h));
348        self.children[2].set_bounds(Rect::new(x, 0.0, s2.width, h));
349        x += s2.width;
350
351        // Child 3: Label — remaining width
352        let label_w = (total_w - x).max(0.0);
353        let s3 = self.children[3].layout(Size::new(label_w, h));
354        self.children[3].set_bounds(Rect::new(x, 0.0, s3.width, h));
355
356        Size::new(total_w, h)
357    }
358
359    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
360        let w = self.bounds.width;
361        let h = self.bounds.height;
362        let v = ctx.visuals();
363
364        if self.is_selected {
365            let c = if self.focused {
366                // Accent-tinted overlay — same colour in both themes so the
367                // selection reads as "selected" regardless of palette.
368                Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.25)
369            } else {
370                // Theme-neutral dim overlay: subtle tint of the text color.
371                Color::rgba(v.text_color.r, v.text_color.g, v.text_color.b, 0.12)
372            };
373            ctx.set_fill_color(c);
374            ctx.begin_path();
375            ctx.rect(0.0, 0.0, w, h);
376            ctx.fill();
377        } else if self.is_hovered {
378            ctx.set_fill_color(Color::rgba(
379                v.text_color.r,
380                v.text_color.g,
381                v.text_color.b,
382                0.08,
383            ));
384            ctx.begin_path();
385            ctx.rect(0.0, 0.0, w, h);
386            ctx.fill();
387        }
388    }
389
390    fn on_event(&mut self, _: &Event) -> EventResult {
391        EventResult::Ignored
392    }
393}