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
34/// Node set on the window to inspect.
35pub 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    // persist config, at least across instances of the Inspector.
43    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    /// Message send to ourselves as an `INSPECT_CMD` param.
58    enum InspectorUpdateOnly {
59        /// Pump `inspected_tree.update`
60        Info,
61        /// Pump `inspected_tree.update_render`
62        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                    // pump state
90                    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                    // can't inspect inspector window, redirect command to inspected
98                    INSPECT_CMD.scoped(inspected).notify();
99                } else {
100                    // focus or open the inspector window
101                    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
133/// Node in the inspected window, draws adorners around widgets selected on the inspector window.
134fn 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
180// node in the inspected window, handles selection on click.
181fn select_on_click(child: impl UiNode, hit_select: impl Var<HitSelect>) -> impl UiNode {
182    // when `pending` we need to block interaction with window content, as if a modal
183    // overlay was opened, but we can't rebuild info, and we actually want the click target,
184    // so we only manually block common pointer events.
185
186    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                    // set cursor to Crosshair and lock it in by resetting on a hook.
202                    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}