zng_wgt_inspector/
debug.rs

1//! Debug inspection properties.
2
3use std::{cell::RefCell, fmt, rc::Rc};
4
5use zng_ext_input::{
6    focus::WidgetInfoFocusExt as _,
7    mouse::{MOUSE_HOVERED_EVENT, MOUSE_MOVE_EVENT},
8};
9use zng_ext_window::WINDOW_Ext as _;
10use zng_layout::unit::Orientation2D;
11use zng_view_api::display_list::FrameValue;
12use zng_wgt::prelude::*;
13
14/// Target of inspection properties.
15#[derive(Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub enum InspectMode {
17    /// Just the widget where the inspector property is set.
18    Widget,
19    /// The widget where the inspector property is set and all descendants.
20    ///
21    /// The `true` value converts to this.
22    WidgetAndDescendants,
23    /// Disable inspection.
24    ///
25    /// The `false` value converts to this.
26    #[default]
27    Disabled,
28}
29impl fmt::Debug for InspectMode {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        if f.alternate() {
32            write!(f, "InspectMode::")?;
33        }
34        match self {
35            Self::Widget => write!(f, "Widget"),
36            Self::WidgetAndDescendants => write!(f, "WidgetAndDescendants"),
37            Self::Disabled => write!(f, "Disabled"),
38        }
39    }
40}
41impl_from_and_into_var! {
42    fn from(widget_and_descendants: bool) -> InspectMode {
43        if widget_and_descendants {
44            InspectMode::WidgetAndDescendants
45        } else {
46            InspectMode::Disabled
47        }
48    }
49}
50
51/// Draws a debug dot in target widget's [center point].
52///
53/// [center point]: zng_wgt::prelude::WidgetInfo::center
54#[property(CONTEXT, default(false))]
55pub fn show_center_points(child: impl IntoUiNode, mode: impl IntoVar<InspectMode>) -> UiNode {
56    show_widget_tree(
57        child,
58        |_, wgt, frame| {
59            frame.push_debug_dot(wgt.center(), colors::GREEN);
60        },
61        mode,
62    )
63}
64
65/// Draws a border for every target widget's outer and inner bounds.
66///
67/// The outer bounds is drawn dotted and in pink, the inner bounds is drawn solid and in blue.
68#[property(CONTEXT, default(false))]
69pub fn show_bounds(child: impl IntoUiNode, mode: impl IntoVar<InspectMode>) -> UiNode {
70    show_widget_tree(
71        child,
72        |_, wgt, frame| {
73            let p = Dip::new(1).to_px(frame.scale_factor());
74
75            let outer_bounds = wgt.outer_bounds();
76            let inner_bounds = wgt.inner_bounds();
77
78            if outer_bounds != inner_bounds && !outer_bounds.is_empty() {
79                frame.push_border(
80                    wgt.outer_bounds(),
81                    PxSideOffsets::new_all_same(p),
82                    BorderSides::dotted(web_colors::PINK),
83                    PxCornerRadius::zero(),
84                );
85            }
86
87            if !inner_bounds.size.is_empty() {
88                frame.push_border(
89                    inner_bounds,
90                    PxSideOffsets::new_all_same(p),
91                    BorderSides::solid(web_colors::ROYAL_BLUE),
92                    PxCornerRadius::zero(),
93                );
94            }
95        },
96        mode,
97    )
98}
99
100/// Draws a border over every inlined widget row in the window.
101#[property(CONTEXT, default(false))]
102pub fn show_rows(child: impl IntoUiNode, mode: impl IntoVar<InspectMode>) -> UiNode {
103    let spatial_id = SpatialFrameId::new_unique();
104    show_widget_tree(
105        child,
106        move |i, wgt, frame| {
107            let p = Dip::new(1).to_px(frame.scale_factor());
108
109            let wgt = wgt.bounds_info();
110            let transform = wgt.inner_transform();
111            if let Some(inline) = wgt.inline() {
112                frame.push_reference_frame((spatial_id, i as u32).into(), FrameValue::Value(transform), false, false, |frame| {
113                    for row in &inline.rows {
114                        if !row.size.is_empty() {
115                            frame.push_border(
116                                *row,
117                                PxSideOffsets::new_all_same(p),
118                                BorderSides::dotted(web_colors::LIGHT_SALMON),
119                                PxCornerRadius::zero(),
120                            );
121                        }
122                    }
123                })
124            };
125        },
126        mode,
127    )
128}
129
130fn show_widget_tree(
131    child: impl IntoUiNode,
132    mut render: impl FnMut(usize, WidgetInfo, &mut FrameBuilder) + Send + 'static,
133    mode: impl IntoVar<InspectMode>,
134) -> UiNode {
135    let mode = mode.into_var();
136    let cancel_space = SpatialFrameId::new_unique();
137    match_node(child, move |child, op| match op {
138        UiNodeOp::Init => {
139            WIDGET.sub_var_render(&mode);
140        }
141        UiNodeOp::Render { frame } => {
142            child.render(frame);
143
144            let mut r = |render: &mut dyn FnMut(WidgetInfo, &mut FrameBuilder)| {
145                let tree = WINDOW.info();
146                if let Some(wgt) = tree.get(WIDGET.id()) {
147                    if WIDGET.parent_id().is_none() {
148                        render(wgt, frame);
149                    } else if let Some(t) = frame.transform().inverse() {
150                        // cancel current transform
151                        frame.push_reference_frame(cancel_space.into(), t.into(), false, false, |frame| {
152                            render(wgt, frame);
153                        })
154                    } else {
155                        tracing::error!("cannot inspect from `{:?}`, non-invertible transform", WIDGET.id())
156                    }
157                }
158            };
159
160            match mode.get() {
161                InspectMode::Widget => {
162                    r(&mut |wgt, frame| {
163                        render(0, wgt, frame);
164                    });
165                }
166                InspectMode::WidgetAndDescendants => {
167                    r(&mut |wgt, frame| {
168                        for (i, wgt) in wgt.self_and_descendants().enumerate() {
169                            render(i, wgt, frame);
170                        }
171                    });
172                }
173                InspectMode::Disabled => {}
174            }
175        }
176        _ => {}
177    })
178}
179
180/// Draws the inner bounds that where tested for the mouse point.
181///
182/// # Window Only
183///
184/// This property only works if set in a window, if set in another widget it will log an error and not render anything.
185#[property(CONTEXT, default(false))]
186pub fn show_hit_test(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
187    let enabled = enabled.into_var();
188    let mut handles = VarHandles::default();
189    let mut valid = false;
190    let mut fails = vec![];
191    let mut hits = vec![];
192
193    match_node(child, move |child, op| match op {
194        UiNodeOp::Init => {
195            valid = WIDGET.parent_id().is_none();
196            if valid {
197                WIDGET.sub_var(&enabled);
198
199                if enabled.get() {
200                    let id = WIDGET.id();
201                    handles = [
202                        MOUSE_MOVE_EVENT.subscribe(UpdateOp::Update, id),
203                        MOUSE_HOVERED_EVENT.subscribe(UpdateOp::Update, id),
204                    ]
205                    .into();
206                } else {
207                    handles.clear();
208                }
209            } else {
210                tracing::error!("property `show_hit_test` is only valid in a window");
211            }
212        }
213        UiNodeOp::Deinit => {
214            handles.clear();
215        }
216        UiNodeOp::Update { .. } => {
217            if let Some(enabled) = enabled.get_new() {
218                if enabled && valid {
219                    let id = WIDGET.id();
220                    handles = [
221                        MOUSE_MOVE_EVENT.subscribe(UpdateOp::Update, id),
222                        MOUSE_HOVERED_EVENT.subscribe(UpdateOp::Update, id),
223                    ]
224                    .into();
225                } else {
226                    handles.clear();
227                }
228                WIDGET.render();
229            }
230
231            MOUSE_MOVE_EVENT.each_update(true, |args| {
232                if valid && enabled.get() {
233                    let factor = WINDOW.vars().scale_factor().get();
234                    let pt = args.position.to_px(factor);
235
236                    let new_fails = Rc::new(RefCell::new(vec![]));
237                    let new_hits = Rc::new(RefCell::new(vec![]));
238
239                    let tree = WINDOW.info();
240                    let _ = tree
241                        .root()
242                        .spatial_iter(clmv!(new_fails, new_hits, |w| {
243                            let bounds = w.inner_bounds();
244                            let hit = bounds.contains(pt);
245                            if hit {
246                                new_hits.borrow_mut().push(bounds);
247                            } else {
248                                new_fails.borrow_mut().push(bounds);
249                            }
250                            hit
251                        }))
252                        .count();
253
254                    let new_fails = Rc::try_unwrap(new_fails).unwrap().into_inner();
255                    let new_hits = Rc::try_unwrap(new_hits).unwrap().into_inner();
256
257                    if fails != new_fails || hits != new_hits {
258                        fails = new_fails;
259                        hits = new_hits;
260
261                        WIDGET.render();
262                    }
263                }
264            });
265            MOUSE_HOVERED_EVENT.each_update(true, |args| {
266                if args.target.is_none() && !fails.is_empty() && !hits.is_empty() {
267                    fails.clear();
268                    hits.clear();
269
270                    WIDGET.render();
271                }
272            });
273        }
274        UiNodeOp::Render { frame } => {
275            child.render(frame);
276
277            if valid && enabled.get() {
278                let widths = PxSideOffsets::new_all_same(Px(1));
279                let fail_sides = BorderSides::solid(colors::RED);
280                let hits_sides = BorderSides::solid(web_colors::LIME_GREEN);
281
282                frame.with_hit_tests_disabled(|frame| {
283                    for fail in &fails {
284                        if !fail.size.is_empty() {
285                            frame.push_border(*fail, widths, fail_sides, PxCornerRadius::zero());
286                        }
287                    }
288
289                    for hit in &hits {
290                        if !hit.size.is_empty() {
291                            frame.push_border(*hit, widths, hits_sides, PxCornerRadius::zero());
292                        }
293                    }
294                });
295            }
296        }
297        _ => {}
298    })
299}
300
301/// Draw the directional query for closest sibling of the hovered focusable widget.
302///
303/// # Window Only
304///
305/// This property only works if set in a window, if set in another widget it will log an error and not render anything.
306#[property(CONTEXT, default(None))]
307pub fn show_directional_query(child: impl IntoUiNode, orientation: impl IntoVar<Option<Orientation2D>>) -> UiNode {
308    let orientation = orientation.into_var();
309    let mut valid = false;
310    let mut search_quads = vec![];
311    let mut _mouse_hovered_handle = None;
312
313    match_node(child, move |child, op| match op {
314        UiNodeOp::Init => {
315            valid = WIDGET.parent_id().is_none();
316            if valid {
317                WIDGET.sub_var(&orientation);
318                if orientation.get().is_some() {
319                    _mouse_hovered_handle = Some(MOUSE_HOVERED_EVENT.subscribe(UpdateOp::Update, WIDGET.id()));
320                }
321            } else {
322                tracing::error!("property `show_directional_query` is only valid in a window");
323            }
324        }
325        UiNodeOp::Deinit => {
326            _mouse_hovered_handle = None;
327        }
328        UiNodeOp::Update { .. } => {
329            if !valid {
330                return;
331            }
332            if let Some(ori) = orientation.get_new() {
333                search_quads.clear();
334
335                if ori.is_some() {
336                    _mouse_hovered_handle = Some(MOUSE_HOVERED_EVENT.subscribe(UpdateOp::Update, WIDGET.id()));
337                } else {
338                    _mouse_hovered_handle = None;
339                }
340
341                WIDGET.render();
342            }
343            MOUSE_HOVERED_EVENT.each_update(true, |args| {
344                if let Some(orientation) = orientation.get() {
345                    let mut none = true;
346                    if let Some(target) = &args.target {
347                        let tree = WINDOW.info();
348                        for w_id in target.widgets_path().iter().rev() {
349                            if let Some(w) = tree.get(*w_id)
350                                && let Some(w) = w.into_focusable(true, true)
351                            {
352                                let sq: Vec<_> = orientation
353                                    .search_bounds(w.info().center(), Px::MAX, tree.spatial_bounds().to_box2d())
354                                    .map(|q| q.to_rect())
355                                    .collect();
356
357                                if search_quads != sq {
358                                    search_quads = sq;
359                                    WIDGET.render();
360                                }
361
362                                none = false;
363                                break;
364                            }
365                        }
366                    }
367
368                    if none && !search_quads.is_empty() {
369                        search_quads.clear();
370                        WIDGET.render();
371                    }
372                }
373            });
374        }
375        UiNodeOp::Render { frame } => {
376            child.render(frame);
377
378            if valid && orientation.get().is_some() {
379                let widths = PxSideOffsets::new_all_same(Px(1));
380                let quad_sides = BorderSides::solid(colors::YELLOW);
381
382                frame.with_hit_tests_disabled(|frame| {
383                    for quad in &search_quads {
384                        if !quad.size.is_empty() {
385                            frame.push_border(*quad, widths, quad_sides, PxCornerRadius::zero());
386                        }
387                    }
388                });
389            }
390        }
391        _ => {}
392    })
393}