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.hovered_row == Some(i),
137                self.focused,
138                node.icon,
139                node.label.clone(),
140                Arc::clone(&self.font),
141                font_size,
142                ind,
143                rh,
144            );
145
146            tree_row.layout(Size::new(w, rh));
147            tree_row.set_bounds(Rect::new(0.0, y_bot, w, rh));
148
149            let toggle_rect = if flat.has_children {
150                let tlb = tree_row.toggle_local_bounds;
151                Some(Rect::new(tlb.x, y_bot + tlb.y, tlb.width, tlb.height))
152            } else {
153                None
154            };
155
156            self.row_metas.push(RowMeta {
157                node_idx: flat.node_idx,
158                toggle_rect,
159            });
160            self.row_widgets.push(Box::new(tree_row));
161        }
162        self.last_row_content_sig = Some(new_sig);
163
164        available
165    }
166
167    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
168        let h = self.bounds.height;
169        let w = self.bounds.width;
170        let content_w = w - SCROLLBAR_W;
171        let v = ctx.visuals().clone();
172
173        // Background — follow the theme's window fill rather than hard-coded white.
174        ctx.set_fill_color(v.window_fill);
175        ctx.begin_path();
176        ctx.rect(0.0, 0.0, w, h);
177        ctx.fill();
178
179        // Scrollbar — theme-aware track and thumb.
180        let sb_x = self.scrollbar_x();
181        if self.content_height > h {
182            ctx.set_fill_color(v.scroll_track);
183            ctx.begin_path();
184            ctx.rect(sb_x, 0.0, SCROLLBAR_W, h);
185            ctx.fill();
186            if let Some((thumb_y, thumb_h)) = self.thumb_metrics() {
187                let thumb_color = if self.dragging_scrollbar {
188                    v.scroll_thumb_dragging
189                } else if self.hovered_scrollbar {
190                    v.scroll_thumb_hovered
191                } else {
192                    v.scroll_thumb
193                };
194                ctx.set_fill_color(thumb_color);
195                ctx.begin_path();
196                ctx.rounded_rect(sb_x + 2.0, thumb_y, SCROLLBAR_W - 4.0, thumb_h, 3.0);
197                ctx.fill();
198            }
199        }
200
201        // Content clip — rows must not bleed into the scrollbar strip.
202        // This clip is active during framework recursion into row_widgets (after paint() returns).
203        ctx.clip_rect(0.0, 0.0, content_w, h);
204
205        // Drop indicator and ghost (drag feedback)
206        let rows = flatten_visible(&self.nodes);
207        if let Some(drop_target) = self.drop_target {
208            if self.drag.as_ref().map_or(false, |d| d.live) {
209                let rh = self.row_height;
210                let off = self.scroll_offset;
211                let ind = self.indent_width;
212                let ref_node = match drop_target {
213                    DropPosition::Before(ni)
214                    | DropPosition::After(ni)
215                    | DropPosition::AsChild(ni) => ni,
216                };
217                if let Some(ri) = rows.iter().position(|r| r.node_idx == ref_node) {
218                    let y_bot = h - (ri as f64 + 1.0) * rh + off;
219                    let indent = rows[ri].depth as f64 * ind + EXPAND_W;
220                    match drop_target {
221                        DropPosition::Before(_) => {
222                            paint_drop_line(ctx, indent, y_bot + rh, content_w - indent)
223                        }
224                        DropPosition::After(_) => {
225                            paint_drop_line(ctx, indent, y_bot, content_w - indent)
226                        }
227                        DropPosition::AsChild(_) => {
228                            paint_drop_child_highlight(ctx, y_bot, content_w, rh)
229                        }
230                    }
231                }
232            }
233        }
234        if let Some(drag) = &self.drag {
235            if drag.live {
236                let label = self.nodes[drag.node_idx].label.clone();
237                let ic = icon_color(self.nodes[drag.node_idx].icon);
238                let pos = drag.current_pos;
239                let rh = self.row_height;
240                let font = Arc::clone(&self.font);
241                let fs = self.font_size;
242                paint_ghost(ctx, &label, pos, content_w, rh, &font, fs, ic);
243            }
244        }
245    }
246
247    fn on_event(&mut self, event: &Event) -> EventResult {
248        // Every consumed event in a tree view mutates some visible state —
249        // selection, expansion, scroll offset, hover row, focus ring.  Wrap
250        // the dispatch so a `Consumed` result translates to a repaint
251        // request.  Events that bubble away as `Ignored` do NOT tick,
252        // honouring the "only repaint on real change" contract.
253        let result = match event {
254            Event::FocusGained => {
255                self.focused = true;
256                EventResult::Consumed
257            }
258            Event::FocusLost => {
259                self.focused = false;
260                EventResult::Consumed
261            }
262
263            Event::MouseWheel { delta_y, .. } => {
264                // Convention: delta_y > 0 = user scrolled DOWN (wants to see content below).
265                // Increasing scroll_offset shifts content UP → reveals lower rows. ✓
266                self.scroll_offset =
267                    (self.scroll_offset + delta_y * 40.0).clamp(0.0, self.max_scroll());
268                self.hovered_row = None;
269                EventResult::Consumed
270            }
271
272            Event::MouseMove { pos } => self.handle_mouse_move(*pos),
273            Event::MouseDown {
274                pos,
275                button: MouseButton::Left,
276                modifiers,
277            } => self.handle_mouse_down(*pos, *modifiers),
278            Event::MouseUp {
279                button: MouseButton::Left,
280                pos,
281                ..
282            } => self.handle_mouse_up(*pos),
283            Event::KeyDown { key, modifiers } => self.handle_key_down(key, *modifiers),
284            _ => EventResult::Ignored,
285        };
286        if result == EventResult::Consumed {
287            crate::animation::request_draw();
288        }
289        result
290    }
291}