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