1use 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#[derive(Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub enum InspectMode {
17 Widget,
19 WidgetAndDescendants,
23 #[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#[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#[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#[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 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#[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#[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}