zng_wgt_inspector/
live.rs
1#![cfg(feature = "live")]
2
3use zng_app::access::ACCESS_CLICK_EVENT;
4use zng_ext_config::CONFIG;
5use zng_ext_input::{
6 gesture::CLICK_EVENT,
7 mouse::{MOUSE_HOVERED_EVENT, MOUSE_INPUT_EVENT, MOUSE_MOVE_EVENT, MOUSE_WHEEL_EVENT},
8 touch::{TOUCH_INPUT_EVENT, TOUCH_LONG_PRESS_EVENT, TOUCH_MOVE_EVENT, TOUCH_TAP_EVENT, TOUCH_TRANSFORM_EVENT, TOUCHED_EVENT},
9};
10use zng_ext_window::{WINDOW_Ext as _, WINDOWS};
11use zng_view_api::window::CursorIcon;
12use zng_wgt::prelude::*;
13use zng_wgt_input::CursorSource;
14
15use crate::INSPECT_CMD;
16
17mod data_model;
18mod inspector_window;
19
20#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
21struct Config {
22 adorn_selected: bool,
23 select_focused: bool,
24}
25impl Default for Config {
26 fn default() -> Self {
27 Self {
28 adorn_selected: true,
29 select_focused: false,
30 }
31 }
32}
33
34pub fn inspect_node(can_inspect: impl IntoVar<bool>) -> impl UiNode {
36 let mut inspected_tree = None::<data_model::InspectedTree>;
37 let inspector = WindowId::new_unique();
38
39 let selected_wgt = var(None);
40 let hit_select = var(HitSelect::Disabled);
41
42 let config = CONFIG.get::<Config>(
44 if WINDOW.id().name().is_empty() {
45 formatx!("window.sequential({}).inspector", WINDOW.id().sequential())
46 } else {
47 formatx!("window.{}.inspector", WINDOW.id().name())
48 },
49 Config::default(),
50 );
51 let adorn_selected = config.map_ref_bidi(|c| &c.adorn_selected, |c| &mut c.adorn_selected);
52 let select_focused = config.map_ref_bidi(|c| &c.select_focused, |c| &mut c.select_focused);
53
54 let can_inspect = can_inspect.into_var();
55 let mut cmd_handle = CommandHandle::dummy();
56
57 enum InspectorUpdateOnly {
59 Info,
61 Render,
63 }
64
65 let child = match_node_leaf(clmv!(selected_wgt, hit_select, adorn_selected, select_focused, |op| match op {
66 UiNodeOp::Init => {
67 WIDGET.sub_var(&can_inspect);
68 cmd_handle = INSPECT_CMD.scoped(WINDOW.id()).subscribe_wgt(can_inspect.get(), WIDGET.id());
69 }
70 UiNodeOp::Update { .. } => {
71 if let Some(e) = can_inspect.get_new() {
72 cmd_handle.set_enabled(e);
73 }
74 }
75 UiNodeOp::Info { .. } => {
76 if inspected_tree.is_some() {
77 if WINDOWS.is_open(inspector) {
78 INSPECT_CMD.scoped(WINDOW.id()).notify_param(InspectorUpdateOnly::Info);
79 } else if !WINDOWS.is_opening(inspector) {
80 inspected_tree = None;
81 }
82 }
83 }
84 UiNodeOp::Event { update } => {
85 if let Some(args) = INSPECT_CMD.scoped(WINDOW.id()).on_unhandled(update) {
86 args.propagation().stop();
87
88 if let Some(u) = args.param::<InspectorUpdateOnly>() {
89 if let Some(i) = &inspected_tree {
91 match u {
92 InspectorUpdateOnly::Info => i.update(WINDOW.info()),
93 InspectorUpdateOnly::Render => i.update_render(),
94 }
95 }
96 } else if let Some(inspected) = inspector_window::inspected() {
97 INSPECT_CMD.scoped(inspected).notify();
99 } else {
100 let inspected_tree = match &inspected_tree {
102 Some(i) => {
103 i.update(WINDOW.info());
104 i.clone()
105 }
106 None => {
107 let i = data_model::InspectedTree::new(WINDOW.info());
108 inspected_tree = Some(i.clone());
109 i
110 }
111 };
112
113 let inspected = WINDOW.id();
114 WINDOWS.focus_or_open(
115 inspector,
116 async_clmv!(inspected_tree, selected_wgt, hit_select, adorn_selected, select_focused, {
117 inspector_window::new(inspected, inspected_tree, selected_wgt, hit_select, adorn_selected, select_focused)
118 }),
119 );
120 }
121 }
122 }
123 UiNodeOp::Render { .. } | UiNodeOp::RenderUpdate { .. } => {
124 INSPECT_CMD.scoped(WINDOW.id()).notify_param(InspectorUpdateOnly::Render);
125 }
126 _ => {}
127 }));
128
129 let child = self::adorn_selected(child, selected_wgt, adorn_selected);
130 select_on_click(child, hit_select)
131}
132
133fn adorn_selected(child: impl UiNode, selected_wgt: impl Var<Option<data_model::InspectedWidget>>, enabled: impl Var<bool>) -> impl UiNode {
135 use inspector_window::SELECTED_BORDER_VAR;
136
137 let selected_info = selected_wgt.flat_map(|s| {
138 if let Some(s) = s {
139 s.info().map(|i| Some(i.clone())).boxed()
140 } else {
141 var(None).boxed()
142 }
143 });
144 let transform_id = SpatialFrameId::new_unique();
145 match_node(child, move |c, op| match op {
146 UiNodeOp::Init => {
147 WIDGET
148 .sub_var_render(&selected_info)
149 .sub_var_render(&enabled)
150 .sub_var_render(&SELECTED_BORDER_VAR);
151 }
152 UiNodeOp::Render { frame } => {
153 c.render(frame);
154
155 if !enabled.get() {
156 return;
157 }
158 selected_info.with(|w| {
159 if let Some(w) = w {
160 let bounds = w.bounds_info();
161 let transform = bounds.inner_transform();
162 let size = bounds.inner_size();
163
164 frame.push_reference_frame(transform_id.into(), transform.into(), false, false, |frame| {
165 let widths = Dip::new(3).to_px(frame.scale_factor());
166 frame.push_border(
167 PxRect::from_size(size).inflate(widths, widths),
168 PxSideOffsets::new_all_same(widths),
169 SELECTED_BORDER_VAR.get().into(),
170 PxCornerRadius::default(),
171 );
172 });
173 }
174 });
175 }
176 _ => {}
177 })
178}
179
180fn select_on_click(child: impl UiNode, hit_select: impl Var<HitSelect>) -> impl UiNode {
182 let mut click_handle = EventHandles::dummy();
187 let mut _cursor_handle = VarHandle::dummy();
188 match_node(child, move |c, op| match op {
189 UiNodeOp::Init => {
190 WIDGET.sub_var(&hit_select);
191 }
192 UiNodeOp::Deinit => {
193 _cursor_handle = VarHandle::dummy();
194 click_handle.clear();
195 }
196 UiNodeOp::Update { .. } => {
197 if let Some(h) = hit_select.get_new() {
198 if matches!(h, HitSelect::Enabled) {
199 let cursor = WINDOW.vars().cursor();
200
201 let locked_cur = CursorSource::Icon(CursorIcon::Crosshair);
203 cursor.set(locked_cur.clone());
204 let weak_cursor = cursor.downgrade();
205 _cursor_handle = cursor.hook(move |a| {
206 let icon = a.value();
207 if icon != &locked_cur {
208 let cursor = weak_cursor.upgrade().unwrap();
209 cursor.set(locked_cur.clone());
210 }
211 true
212 });
213
214 click_handle.push(MOUSE_INPUT_EVENT.subscribe(WIDGET.id()));
215 click_handle.push(TOUCH_INPUT_EVENT.subscribe(WIDGET.id()));
216 } else {
217 WINDOW.vars().cursor().set(CursorIcon::Default);
218 _cursor_handle = VarHandle::dummy();
219
220 click_handle.clear();
221 }
222 }
223 }
224 UiNodeOp::Event { update } => {
225 if matches!(hit_select.get(), HitSelect::Enabled) {
226 let mut select = None;
227
228 if let Some(args) = MOUSE_MOVE_EVENT.on(update) {
229 args.propagation().stop();
230 c.delegated();
231 } else if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
232 args.propagation().stop();
233 c.delegated();
234 select = Some(args.target.widget_id());
235 } else if let Some(args) = MOUSE_HOVERED_EVENT.on(update) {
236 args.propagation().stop();
237 c.delegated();
238 } else if let Some(args) = MOUSE_WHEEL_EVENT.on(update) {
239 args.propagation().stop();
240 c.delegated();
241 } else if let Some(args) = CLICK_EVENT.on(update) {
242 args.propagation().stop();
243 c.delegated();
244 } else if let Some(args) = ACCESS_CLICK_EVENT.on(update) {
245 args.propagation().stop();
246 c.delegated();
247 } else if let Some(args) = TOUCH_INPUT_EVENT.on(update) {
248 args.propagation().stop();
249 c.delegated();
250 select = Some(args.target.widget_id());
251 } else if let Some(args) = TOUCHED_EVENT.on(update) {
252 args.propagation().stop();
253 c.delegated();
254 } else if let Some(args) = TOUCH_MOVE_EVENT.on(update) {
255 args.propagation().stop();
256 c.delegated();
257 } else if let Some(args) = TOUCH_TAP_EVENT.on(update) {
258 args.propagation().stop();
259 c.delegated();
260 } else if let Some(args) = TOUCH_TRANSFORM_EVENT.on(update) {
261 args.propagation().stop();
262 c.delegated();
263 } else if let Some(args) = TOUCH_LONG_PRESS_EVENT.on(update) {
264 args.propagation().stop();
265 c.delegated();
266 }
267
268 if let Some(id) = select {
269 let _ = hit_select.set(HitSelect::Select(id));
270 }
271 }
272 }
273 _ => {}
274 })
275}
276
277#[derive(Debug, Clone, Copy, PartialEq)]
278enum HitSelect {
279 Disabled,
280 Enabled,
281 Select(WidgetId),
282}