Skip to main content

agg_gui/widgets/inspector/
widget_impl.rs

1//! `Widget` impl for `InspectorPanel` — extracted from `mod.rs` to keep
2//! the parent file under the project's 800-line cap.  All InspectorPanel
3//! state and helpers still live in the parent module; this file only
4//! routes the trait methods (layout / paint / event dispatch) into them.
5
6use std::sync::Arc;
7
8use crate::color::Color;
9use crate::draw_ctx::DrawCtx;
10use crate::event::{Event, EventResult, MouseButton};
11use crate::geometry::{Rect, Size};
12use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
13use crate::widget::{InspectorOverlay, Widget};
14use crate::widgets::tree_view::{NodeIcon, TreeNode};
15
16use super::{
17    c_border, c_dim_text, c_header_bg, c_panel_bg, c_props_bg, c_split_bg, c_text, InspectorPanel,
18    HEADER_H, MIN_PROPS_H, MIN_TREE_H,
19};
20
21impl Widget for InspectorPanel {
22    fn type_name(&self) -> &'static str {
23        "InspectorPanel"
24    }
25    fn bounds(&self) -> Rect {
26        self.bounds
27    }
28    fn set_bounds(&mut self, b: Rect) {
29        self.bounds = b;
30    }
31    fn children(&self) -> &[Box<dyn Widget>] {
32        &self._children
33    }
34    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
35        &mut self._children
36    }
37
38    fn margin(&self) -> Insets {
39        self.base.margin
40    }
41    fn widget_base(&self) -> Option<&WidgetBase> {
42        Some(&self.base)
43    }
44    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
45        Some(&mut self.base)
46    }
47    fn h_anchor(&self) -> HAnchor {
48        self.base.h_anchor
49    }
50    fn v_anchor(&self) -> VAnchor {
51        self.base.v_anchor
52    }
53    fn min_size(&self) -> Size {
54        self.base.min_size
55    }
56    fn max_size(&self) -> Size {
57        self.base.max_size
58    }
59
60    fn layout(&mut self, available: Size) -> Size {
61        self.bounds.width = available.width;
62        self.bounds.height = available.height;
63
64        let nodes = self.nodes.borrow();
65        // Fingerprint of the inspector_nodes Vec.  When the harness skips
66        // a snapshot pass (e.g. during a window-resize drag) the Vec is
67        // reused, so the data ptr stays the same — we then skip the
68        // tree_view.nodes rebuild here.  Combined with TreeView's row
69        // caching, this is what makes inspector window resizing cheap.
70        let nodes_fingerprint = (nodes.as_ptr() as usize, nodes.len());
71        let pending_state = self.pending_expanded.is_some() || self.pending_selected.is_some();
72        let nodes_unchanged = !pending_state
73            && self.last_inspector_nodes_fingerprint == Some(nodes_fingerprint)
74            && !self.tree_view.nodes.is_empty();
75
76        if !nodes_unchanged {
77            // Preserve expansion/selection state by index before rebuilding.
78            let mut old_expanded: Vec<bool> =
79                self.tree_view.nodes.iter().map(|n| n.is_expanded).collect();
80            let mut old_selected: Vec<bool> =
81                self.tree_view.nodes.iter().map(|n| n.is_selected).collect();
82            if let Some(pe) = self.pending_expanded.take() {
83                old_expanded = pe;
84            }
85            if let Some(ps) = self.pending_selected.take() {
86                old_selected = vec![false; old_expanded.len().max(ps.map(|i| i + 1).unwrap_or(0))];
87                if let Some(i) = ps {
88                    if i < old_selected.len() {
89                        old_selected[i] = true;
90                    }
91                }
92            }
93
94            self.tree_view.nodes.clear();
95
96            // Convert flat InspectorNode list (with depths) to parent-child
97            // TreeNode structure via a depth stack.
98            let mut depth_stack: Vec<usize> = Vec::new();
99            let mut per_parent_counts: std::collections::HashMap<Option<usize>, u32> =
100                std::collections::HashMap::new();
101
102            for (orig_idx, node) in nodes.iter().enumerate() {
103                let parent = if node.depth == 0 {
104                    None
105                } else {
106                    depth_stack.get(node.depth.saturating_sub(1)).copied()
107                };
108                let order = {
109                    let cnt = per_parent_counts.entry(parent).or_insert(0);
110                    let o = *cnt;
111                    *cnt += 1;
112                    o
113                };
114                let b = &node.screen_bounds;
115                let label = format!("{}  {:.0}×{:.0}", node.type_name, b.width, b.height);
116                let tv_idx = self.tree_view.nodes.len();
117                self.tree_view
118                    .nodes
119                    .push(TreeNode::new(label, NodeIcon::Package, parent, order));
120                self.tree_view.nodes[tv_idx].is_expanded =
121                    old_expanded.get(orig_idx).copied().unwrap_or(true);
122                self.tree_view.nodes[tv_idx].is_selected =
123                    old_selected.get(orig_idx).copied().unwrap_or(false);
124                if depth_stack.len() <= node.depth {
125                    depth_stack.resize(node.depth + 1, 0);
126                }
127                depth_stack[node.depth] = tv_idx;
128            }
129            self.last_inspector_nodes_fingerprint = Some(nodes_fingerprint);
130        }
131
132        self.selected = self.tree_view.nodes.iter().position(|n| n.is_selected);
133
134        *self.hovered_bounds.borrow_mut() = self
135            .tree_view
136            .hovered_node_idx()
137            .and_then(|i| nodes.get(i))
138            .map(|n| InspectorOverlay {
139                bounds: n.screen_bounds,
140                margin: n.margin,
141                padding: n.padding,
142            });
143
144        let tree_w = available.width;
145        let tree_bot = self.tree_origin_y();
146        let tree_top = self.list_area_h();
147        let tree_h = (tree_top - tree_bot).max(0.0);
148        self.tree_view
149            .set_bounds(Rect::new(0.0, tree_bot, tree_w, tree_h));
150        self.tree_view.layout(Size::new(tree_w, tree_h));
151
152        // Keep the presence node's bounds in sync with the real TreeView so
153        // the inspector displays accurate bounds for this proxy entry.
154        self._children[0].set_bounds(self.tree_view.bounds());
155
156        if let Some(cell) = &self.snapshot_out {
157            *cell.borrow_mut() = Some(self.saved_state());
158        }
159
160        available
161    }
162
163    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
164        let w = self.bounds.width;
165        let h = self.bounds.height;
166        let sy = self.split_y();
167        let hdr_y = h - HEADER_H;
168        let v = ctx.visuals().clone();
169
170        // Panel background
171        ctx.set_fill_color(c_panel_bg(&v));
172        ctx.begin_path();
173        ctx.rect(0.0, 0.0, w, h);
174        ctx.fill();
175
176        ctx.set_stroke_color(c_border(&v));
177        ctx.set_line_width(1.0);
178        ctx.begin_path();
179        ctx.move_to(0.0, 0.0);
180        ctx.line_to(0.0, h);
181        ctx.stroke();
182
183        // Header
184        ctx.set_fill_color(c_header_bg(&v));
185        ctx.begin_path();
186        ctx.rect(0.0, hdr_y, w, HEADER_H);
187        ctx.fill();
188
189        ctx.set_stroke_color(c_border(&v));
190        ctx.set_line_width(1.0);
191        ctx.begin_path();
192        ctx.move_to(0.0, hdr_y);
193        ctx.line_to(w, hdr_y);
194        ctx.stroke();
195
196        ctx.set_font(Arc::clone(&self.font));
197        ctx.set_font_size(13.0);
198        ctx.set_fill_color(c_text(&v));
199        let title = "Widget Inspector";
200        if let Some(m) = ctx.measure_text(title) {
201            ctx.fill_text(
202                title,
203                12.0,
204                hdr_y + (HEADER_H - m.ascent - m.descent) * 0.5 + m.descent,
205            );
206        }
207
208        let count_txt = format!("{} widgets", self.nodes.borrow().len());
209        ctx.set_font_size(11.0);
210        ctx.set_fill_color(c_dim_text(&v));
211        if let Some(m) = ctx.measure_text(&count_txt) {
212            ctx.fill_text(
213                &count_txt,
214                w - m.width - 10.0,
215                hdr_y + (HEADER_H - m.ascent - m.descent) * 0.5 + m.descent,
216            );
217        }
218
219        // Properties pane
220        ctx.set_fill_color(c_props_bg(&v));
221        ctx.begin_path();
222        ctx.rect(0.0, 0.0, w, sy - 2.0);
223        ctx.fill();
224        self.paint_properties(ctx, sy - 2.0);
225
226        // Split handle
227        ctx.set_fill_color(c_split_bg(&v));
228        ctx.begin_path();
229        ctx.rect(0.0, sy - 2.0, w, 4.0);
230        ctx.fill();
231        ctx.set_stroke_color(c_border(&v));
232        ctx.set_line_width(1.0);
233        ctx.begin_path();
234        ctx.move_to(0.0, sy);
235        ctx.line_to(w, sy);
236        ctx.stroke();
237
238        // Tree area
239        let tree_bot = self.tree_origin_y();
240        let tree_top = self.list_area_h();
241        let tree_h = (tree_top - tree_bot).max(0.0);
242        if tree_h > 0.0 {
243            ctx.save();
244            ctx.translate(0.0, tree_bot);
245            ctx.clip_rect(0.0, 0.0, w, tree_h);
246            crate::widget::paint_subtree(&mut self.tree_view, ctx);
247            ctx.restore();
248        }
249    }
250
251    /// Chrome F12-style three-band highlight (margin / bounds / padding)
252    /// for the currently-hovered widget.  Runs from `paint_global_overlays`
253    /// AFTER the whole tree has painted, so it sits above any window/panel
254    /// the hovered widget lives inside.
255    ///
256    /// `hovered_bounds` is in logical root-space coordinates (Y-up); the
257    /// CTM at this point is local to the InspectorPanel, so we translate
258    /// the local origin back to root coords and offset the draw rect by
259    /// the inverse.  Strips the device-scale factor so logical inputs
260    /// stay logical under HiDPI.
261    fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
262        let Some(overlay) = *self.hovered_bounds.borrow() else {
263            return;
264        };
265
266        let mut ox = 0.0;
267        let mut oy = 0.0;
268        ctx.root_transform().transform(&mut ox, &mut oy);
269        let scale = crate::device_scale::device_scale().max(1e-6);
270        let ox = ox / scale;
271        let oy = oy / scale;
272
273        paint_inspector_overlay(ctx, overlay, ox, oy);
274    }
275
276    fn on_event(&mut self, event: &Event) -> EventResult {
277        match event {
278            Event::MouseDown {
279                pos,
280                button: MouseButton::Left,
281                ..
282            } => {
283                if pos.y < self.split_y() - 2.0 && self.try_emit_base_edit_from_click(*pos) {
284                    return EventResult::Consumed;
285                }
286                #[cfg(feature = "reflect")]
287                if pos.y < self.split_y() - 2.0 && self.try_emit_edit_from_click(*pos) {
288                    return EventResult::Consumed;
289                }
290                if self.on_split_handle(*pos) {
291                    self.split_dragging = true;
292                    return EventResult::Consumed;
293                }
294                if self.pos_in_tree_area(*pos) {
295                    return self.forward_to_tree(event);
296                }
297                EventResult::Ignored
298            }
299            Event::MouseMove { pos } => {
300                if self.split_dragging {
301                    self.props_h = pos.y.clamp(
302                        MIN_PROPS_H,
303                        (self.list_area_h() - MIN_TREE_H).max(MIN_PROPS_H),
304                    );
305                    crate::animation::request_draw();
306                    return EventResult::Consumed;
307                }
308                if self.pos_in_tree_area(*pos) {
309                    let _ = self.forward_to_tree(event);
310                } else {
311                    // Mouse left the tree area — clear the tree's own
312                    // hover state too so the previously-hovered row's
313                    // background goes away on the next frame.  Without
314                    // this the cached parent backbuffer would keep
315                    // showing the stale hover.
316                    self.tree_view.clear_hover();
317                }
318                self.update_hovered_bounds_from_tree();
319                EventResult::Ignored
320            }
321            Event::MouseUp {
322                button: MouseButton::Left,
323                pos,
324                ..
325            } => {
326                if self.split_dragging {
327                    self.split_dragging = false;
328                    crate::animation::request_draw();
329                    return EventResult::Consumed;
330                }
331                if self.pos_in_tree_area(*pos) {
332                    return self.forward_to_tree(event);
333                }
334                EventResult::Ignored
335            }
336            Event::MouseWheel { pos, .. } if self.pos_in_tree_area(*pos) => {
337                self.forward_to_tree(event)
338            }
339            _ => EventResult::Ignored,
340        }
341    }
342}
343
344/// Draw a Chrome-DevTools-style three-band overlay (margin / content / padding)
345/// plus a thin outline.  Inputs are in logical root coordinates; `(ox, oy)`
346/// is the position of `ctx`'s local origin in that same root frame, so the
347/// helper subtracts it from every coordinate to land at the right pixels.
348fn paint_inspector_overlay(ctx: &mut dyn DrawCtx, overlay: InspectorOverlay, ox: f64, oy: f64) {
349    let b = overlay.bounds;
350    let m = overlay.margin;
351    let p = overlay.padding;
352
353    let cx = b.x - ox;
354    let cy = b.y - oy;
355    let cw = b.width;
356    let ch = b.height;
357
358    // Margin band (orange-amber, painted as a frame outside the content rect)
359    let m_total = m.left + m.right + m.top + m.bottom;
360    if m_total > 0.0 {
361        let mx = cx - m.left;
362        let my = cy - m.bottom;
363        let mw = cw + m.left + m.right;
364        let mh = ch + m.top + m.bottom;
365        ctx.set_fill_color(Color::rgba(0.99, 0.61, 0.20, 0.30));
366        ctx.begin_path();
367        ctx.rect(mx, my, mw, mh);
368        ctx.fill();
369    }
370
371    // Content / bounds band (soft blue)
372    ctx.set_fill_color(Color::rgba(0.42, 0.66, 1.0, 0.30));
373    ctx.begin_path();
374    ctx.rect(cx, cy, cw, ch);
375    ctx.fill();
376
377    // Padding band (soft green, inset from the content rect)
378    let p_total = p.left + p.right + p.top + p.bottom;
379    if p_total > 0.0 {
380        let px = cx + p.left;
381        let py = cy + p.bottom;
382        let pw = (cw - p.left - p.right).max(0.0);
383        let ph = (ch - p.top - p.bottom).max(0.0);
384        if pw > 0.0 && ph > 0.0 {
385            ctx.set_fill_color(Color::rgba(0.55, 0.86, 0.55, 0.35));
386            ctx.begin_path();
387            ctx.rect(px, py, pw, ph);
388            ctx.fill();
389        }
390    }
391
392    // Crisp outline around the content rect — keeps the highlight legible
393    // when the widget sits over a busy background.
394    ctx.set_stroke_color(Color::rgba(0.10, 0.45, 0.95, 0.90));
395    ctx.set_line_width(1.0);
396    ctx.begin_path();
397    ctx.rect(cx, cy, cw, ch);
398    ctx.stroke();
399}