Skip to main content

agg_gui/widgets/tree_view/
widget_impl.rs

1//! `Widget` impl for `TreeView` — extracted from `mod.rs` to keep the
2//! main file under the project's 800-line cap.  All TreeView logic
3//! still lives in `mod.rs`; this submodule only routes the trait
4//! methods (layout / paint / event dispatch / focus / hit-test) into
5//! the helpers TreeView already exposes.
6
7use std::sync::Arc;
8
9use crate::draw_ctx::DrawCtx;
10use crate::event::{Event, EventResult, MouseButton};
11use crate::geometry::{Point, Rect, Size};
12use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
13use crate::widget::Widget;
14
15use super::drag::{paint_drop_child_highlight, paint_drop_line, paint_ghost};
16use super::node::{flatten_visible, DropPosition, FlatRow};
17use super::row::{icon_color, TreeRow, EXPAND_W};
18use super::{RowMeta, TreeView, SCROLLBAR_W};
19
20impl Widget for TreeView {
21    fn type_name(&self) -> &'static str {
22        "TreeView"
23    }
24    fn bounds(&self) -> Rect {
25        self.bounds
26    }
27    fn set_bounds(&mut self, b: Rect) {
28        self.bounds = b;
29    }
30    fn children(&self) -> &[Box<dyn Widget>] {
31        &self.row_widgets
32    }
33    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
34        &mut self.row_widgets
35    }
36    fn is_focusable(&self) -> bool {
37        true
38    }
39
40    fn margin(&self) -> Insets {
41        self.base.margin
42    }
43    fn widget_base(&self) -> Option<&WidgetBase> {
44        Some(&self.base)
45    }
46    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
47        Some(&mut self.base)
48    }
49    fn h_anchor(&self) -> HAnchor {
50        self.base.h_anchor
51    }
52    fn v_anchor(&self) -> VAnchor {
53        self.base.v_anchor
54    }
55    fn min_size(&self) -> Size {
56        self.base.min_size
57    }
58    fn max_size(&self) -> Size {
59        self.base.max_size
60    }
61
62    fn hit_test(&self, local_pos: Point) -> bool {
63        // Capture all events during drags even if cursor leaves bounds.
64        if self.drag.is_some() || self.dragging_scrollbar {
65            return true;
66        }
67        let b = self.bounds();
68        local_pos.x >= 0.0
69            && local_pos.x <= b.width
70            && local_pos.y >= 0.0
71            && local_pos.y <= b.height
72    }
73
74    fn layout(&mut self, available: Size) -> Size {
75        let rows = flatten_visible(&self.nodes);
76        self.content_height = rows.len() as f64 * self.row_height;
77        self.scroll_offset = self.scroll_offset.clamp(0.0, self.max_scroll());
78
79        let h = available.height;
80        let w = available.width - SCROLLBAR_W;
81        let rh = self.row_height;
82        let ind = self.indent_width;
83        let font_size = self.font_size;
84
85        // Reuse cached rows when the row content is unchanged from the
86        // previous layout — happens every frame of a window-resize drag.
87        // We only reposition the existing TreeRow widgets and refresh the
88        // toggle_rects, preserving each TreeRow's child Label backbuffers.
89        // Without this, resizing a window with the inspector open
90        // re-rasterised every label every frame.
91        let visible_rows: Vec<&FlatRow> = rows
92            .iter()
93            .filter(|flat| {
94                !self
95                    .drag
96                    .as_ref()
97                    .map_or(false, |d| d.live && d.node_idx == flat.node_idx)
98            })
99            .collect();
100        let new_sig = self.row_content_signature();
101        let can_reuse = self.last_row_content_sig == Some(new_sig)
102            && self.row_widgets.len() == visible_rows.len()
103            && !self.row_widgets.is_empty();
104
105        if can_reuse {
106            // Reposition existing rows in place — no allocations, no
107            // text re-rasterisation.
108            for (i, flat) in visible_rows.iter().enumerate() {
109                let y_bot = h - (i as f64 + 1.0) * rh + self.scroll_offset;
110                let row = &mut self.row_widgets[i];
111                row.layout(Size::new(w, rh));
112                row.set_bounds(Rect::new(0.0, y_bot, w, rh));
113                if let Some(meta) = self.row_metas.get_mut(i) {
114                    debug_assert_eq!(meta.node_idx, flat.node_idx);
115                    if let Some(ref mut tr) = meta.toggle_rect {
116                        tr.y = y_bot + (rh - tr.height) * 0.5;
117                    }
118                }
119            }
120            return available;
121        }
122
123        // Full rebuild path — content has changed since the last layout.
124        self.row_widgets.clear();
125        self.row_metas.clear();
126
127        for (i, flat) in visible_rows.iter().enumerate() {
128            let node = &self.nodes[flat.node_idx];
129            let y_bot = h - (i as f64 + 1.0) * rh + self.scroll_offset;
130            let mut tree_row = TreeRow::new(
131                flat.node_idx,
132                flat.depth,
133                flat.has_children,
134                node.is_expanded,
135                node.is_selected,
136                self.focused,
137                node.icon,
138                node.label.clone(),
139                Arc::clone(&self.font),
140                font_size,
141                ind,
142                rh,
143            );
144
145            tree_row.layout(Size::new(w, rh));
146            tree_row.set_bounds(Rect::new(0.0, y_bot, w, rh));
147
148            let toggle_rect = if flat.has_children {
149                let tlb = tree_row.toggle_local_bounds;
150                Some(Rect::new(tlb.x, y_bot + tlb.y, tlb.width, tlb.height))
151            } else {
152                None
153            };
154
155            self.row_metas.push(RowMeta {
156                node_idx: flat.node_idx,
157                toggle_rect,
158            });
159            self.row_widgets.push(Box::new(tree_row));
160        }
161        self.last_row_content_sig = Some(new_sig);
162
163        available
164    }
165
166    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
167        let h = self.bounds.height;
168        let w = self.bounds.width;
169        let content_w = w - SCROLLBAR_W;
170        let v = ctx.visuals().clone();
171
172        // Background — follow the theme's window fill rather than hard-coded white.
173        ctx.set_fill_color(v.window_fill);
174        ctx.begin_path();
175        ctx.rect(0.0, 0.0, w, h);
176        ctx.fill();
177
178        // Scrollbar — theme-aware track and thumb.
179        let sb_x = self.scrollbar_x();
180        if self.content_height > h {
181            ctx.set_fill_color(v.scroll_track);
182            ctx.begin_path();
183            ctx.rect(sb_x, 0.0, SCROLLBAR_W, h);
184            ctx.fill();
185            if let Some((thumb_y, thumb_h)) = self.thumb_metrics() {
186                let thumb_color = if self.dragging_scrollbar {
187                    v.scroll_thumb_dragging
188                } else if self.hovered_scrollbar {
189                    v.scroll_thumb_hovered
190                } else {
191                    v.scroll_thumb
192                };
193                ctx.set_fill_color(thumb_color);
194                ctx.begin_path();
195                ctx.rounded_rect(sb_x + 2.0, thumb_y, SCROLLBAR_W - 4.0, thumb_h, 3.0);
196                ctx.fill();
197            }
198        }
199
200        // Content clip — rows must not bleed into the scrollbar strip.
201        // This clip is active during framework recursion into row_widgets (after paint() returns).
202        ctx.clip_rect(0.0, 0.0, content_w, h);
203
204        // Hover background — painted here (not on the individual `TreeRow`
205        // widgets) so a hover flip doesn't have to invalidate the row's
206        // cached label backbuffers.  Framework recursion paints each row's
207        // content on top of this band.  Skip when the row is also
208        // selected (the selection tint already conveys focus).
209        if let Some(hi) = self.hovered_row {
210            if let (Some(meta), Some(row_widget)) =
211                (self.row_metas.get(hi), self.row_widgets.get(hi))
212            {
213                let is_sel = self
214                    .nodes
215                    .get(meta.node_idx)
216                    .map(|n| n.is_selected)
217                    .unwrap_or(false);
218                if !is_sel {
219                    let rb = row_widget.bounds();
220                    ctx.set_fill_color(crate::color::Color::rgba(
221                        v.text_color.r,
222                        v.text_color.g,
223                        v.text_color.b,
224                        0.08,
225                    ));
226                    ctx.begin_path();
227                    ctx.rect(rb.x, rb.y, rb.width, rb.height);
228                    ctx.fill();
229                }
230            }
231        }
232
233        // Drop indicator and ghost (drag feedback)
234        let rows = flatten_visible(&self.nodes);
235        if let Some(drop_target) = self.drop_target {
236            if self.drag.as_ref().map_or(false, |d| d.live) {
237                let rh = self.row_height;
238                let off = self.scroll_offset;
239                let ind = self.indent_width;
240                let ref_node = match drop_target {
241                    DropPosition::Before(ni)
242                    | DropPosition::After(ni)
243                    | DropPosition::AsChild(ni) => ni,
244                };
245                if let Some(ri) = rows.iter().position(|r| r.node_idx == ref_node) {
246                    let y_bot = h - (ri as f64 + 1.0) * rh + off;
247                    let indent = rows[ri].depth as f64 * ind + EXPAND_W;
248                    match drop_target {
249                        DropPosition::Before(_) => {
250                            paint_drop_line(ctx, indent, y_bot + rh, content_w - indent)
251                        }
252                        DropPosition::After(_) => {
253                            paint_drop_line(ctx, indent, y_bot, content_w - indent)
254                        }
255                        DropPosition::AsChild(_) => {
256                            paint_drop_child_highlight(ctx, y_bot, content_w, rh)
257                        }
258                    }
259                }
260            }
261        }
262        if let Some(drag) = &self.drag {
263            if drag.live {
264                let label = self.nodes[drag.node_idx].label.clone();
265                let ic = icon_color(self.nodes[drag.node_idx].icon);
266                let pos = drag.current_pos;
267                let rh = self.row_height;
268                let font = Arc::clone(&self.font);
269                let fs = self.font_size;
270                paint_ghost(ctx, &label, pos, content_w, rh, &font, fs, ic);
271            }
272        }
273    }
274
275    fn on_event(&mut self, event: &Event) -> EventResult {
276        // Every consumed event in a tree view mutates some visible state —
277        // selection, expansion, scroll offset, hover row, focus ring.  Wrap
278        // the dispatch so a `Consumed` result translates to a repaint
279        // request.  Events that bubble away as `Ignored` do NOT tick,
280        // honouring the "only repaint on real change" contract.
281        let result = match event {
282            Event::FocusGained => {
283                self.focused = true;
284                EventResult::Consumed
285            }
286            Event::FocusLost => {
287                self.focused = false;
288                EventResult::Consumed
289            }
290
291            Event::MouseWheel { delta_y, .. } => {
292                // Convention (matches winit / WheelEvent after OS
293                // natural-scroll): positive delta_y = user wants to
294                // see content ABOVE = DECREASE scroll_offset.
295                self.scroll_offset =
296                    (self.scroll_offset - delta_y * 40.0).clamp(0.0, self.max_scroll());
297                self.hovered_row = None;
298                EventResult::Consumed
299            }
300
301            Event::MouseMove { pos } => self.handle_mouse_move(*pos),
302            Event::MouseDown {
303                pos,
304                button: MouseButton::Left,
305                modifiers,
306            } => self.handle_mouse_down(*pos, *modifiers),
307            Event::MouseUp {
308                button: MouseButton::Left,
309                pos,
310                ..
311            } => self.handle_mouse_up(*pos),
312            Event::KeyDown { key, modifiers } => self.handle_key_down(key, *modifiers),
313            _ => EventResult::Ignored,
314        };
315        if result == EventResult::Consumed {
316            crate::animation::request_draw();
317        }
318        result
319    }
320}