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.
255///
256/// **Hover painting** — the hover background is drawn by `TreeView::paint` from
257/// the parent's `hovered_row` state.  Keeping it out of `TreeRow` means a hover
258/// flip doesn't need to invalidate / rebuild the row's child label cache: only
259/// the `TreeView` body re-rasterises, and the framework re-uses each label's
260/// existing backbuffer.
261pub struct TreeRow {
262    bounds: Rect,
263    pub node_idx: usize,
264    /// Bounds of the `ExpandToggle` in row-local coordinates (set in `layout()`).
265    /// For leaf nodes (`has_children = false`), this field is `Rect::default()` (all zeros)
266    /// and is never read — `TreeView` uses `None` for the corresponding `RowMeta::toggle_rect`.
267    pub toggle_local_bounds: Rect,
268    is_selected: bool,
269    focused: bool,
270    children: Vec<Box<dyn Widget>>,
271    base: WidgetBase,
272}
273
274impl TreeRow {
275    #[allow(clippy::too_many_arguments)]
276    pub fn new(
277        node_idx: usize,
278        depth: u32,
279        has_children: bool,
280        is_expanded: bool,
281        is_selected: bool,
282        focused: bool,
283        icon: NodeIcon,
284        label: impl Into<String>,
285        font: Arc<Font>,
286        font_size: f64,
287        indent_width: f64,
288        row_height: f64,
289    ) -> Self {
290        let indent_px = depth as f64 * indent_width;
291        let mut children: Vec<Box<dyn Widget>> = Vec::with_capacity(4);
292        children.push(Box::new(SizedBox::fixed(indent_px, row_height)));
293        children.push(Box::new(ExpandToggle::new(has_children, is_expanded)));
294        children.push(Box::new(NodeIconWidget::new(icon)));
295        children.push(Box::new(Label::new(label, font).with_font_size(font_size)));
296
297        Self {
298            bounds: Rect::default(),
299            node_idx,
300            toggle_local_bounds: Rect::default(),
301            is_selected,
302            focused,
303            children,
304            base: WidgetBase::new(),
305        }
306    }
307}
308
309impl Widget for TreeRow {
310    fn type_name(&self) -> &'static str {
311        "TreeRow"
312    }
313    fn bounds(&self) -> Rect {
314        self.bounds
315    }
316    fn set_bounds(&mut self, b: Rect) {
317        self.bounds = b;
318    }
319    fn children(&self) -> &[Box<dyn Widget>] {
320        &self.children
321    }
322    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
323        &mut self.children
324    }
325
326    fn margin(&self) -> Insets {
327        self.base.margin
328    }
329    fn widget_base(&self) -> Option<&WidgetBase> {
330        Some(&self.base)
331    }
332    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
333        Some(&mut self.base)
334    }
335    fn h_anchor(&self) -> HAnchor {
336        self.base.h_anchor
337    }
338    fn v_anchor(&self) -> VAnchor {
339        self.base.v_anchor
340    }
341    fn min_size(&self) -> Size {
342        self.base.min_size
343    }
344    fn max_size(&self) -> Size {
345        self.base.max_size
346    }
347
348    fn layout(&mut self, available: Size) -> Size {
349        let h = available.height;
350        let total_w = available.width;
351
352        // Children 0, 1, 2 get their natural width.
353        // Child 3 (Label) gets the remaining width.
354        let mut x = 0.0;
355
356        // Child 0: SizedBox (indent)
357        let s0 = self.children[0].layout(Size::new(total_w, h));
358        self.children[0].set_bounds(Rect::new(x, 0.0, s0.width, h));
359        x += s0.width;
360
361        // Child 1: ExpandToggle — cache its x for toggle hit-testing
362        let s1 = self.children[1].layout(Size::new(total_w - x, h));
363        self.children[1].set_bounds(Rect::new(x, 0.0, s1.width, h));
364        self.toggle_local_bounds = Rect::new(x, 0.0, s1.width, h);
365        x += s1.width;
366
367        // Child 2: NodeIconWidget
368        let s2 = self.children[2].layout(Size::new(total_w - x, h));
369        self.children[2].set_bounds(Rect::new(x, 0.0, s2.width, h));
370        x += s2.width;
371
372        // Child 3: Label — remaining width
373        let label_w = (total_w - x).max(0.0);
374        let s3 = self.children[3].layout(Size::new(label_w, h));
375        self.children[3].set_bounds(Rect::new(x, 0.0, s3.width, h));
376
377        Size::new(total_w, h)
378    }
379
380    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
381        let w = self.bounds.width;
382        let h = self.bounds.height;
383        let v = ctx.visuals();
384
385        if self.is_selected {
386            let c = if self.focused {
387                // Accent-tinted overlay — same colour in both themes so the
388                // selection reads as "selected" regardless of palette.
389                Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.25)
390            } else {
391                // Theme-neutral dim overlay: subtle tint of the text color.
392                Color::rgba(v.text_color.r, v.text_color.g, v.text_color.b, 0.12)
393            };
394            ctx.set_fill_color(c);
395            ctx.begin_path();
396            ctx.rect(0.0, 0.0, w, h);
397            ctx.fill();
398        }
399    }
400
401    fn on_event(&mut self, _: &Event) -> EventResult {
402        EventResult::Ignored
403    }
404}