zng_wgt_input/
state.rs

1use std::{collections::HashSet, time::Duration};
2
3use zng_app::timer::TIMERS;
4use zng_ext_input::{
5    gesture::{CLICK_EVENT, GESTURES},
6    mouse::{ClickMode, MOUSE_HOVERED_EVENT, MOUSE_INPUT_EVENT, WidgetInfoMouseExt as _},
7    pointer_capture::POINTER_CAPTURE_EVENT,
8    touch::TOUCHED_EVENT,
9};
10use zng_view_api::{mouse::ButtonState, touch::TouchPhase};
11use zng_wgt::prelude::*;
12
13/// If the mouse pointer is over the widget or a descendant and the widget is disabled.
14#[property(EVENT)]
15pub fn is_hovered_disabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
16    event_state(child, state, false, MOUSE_HOVERED_EVENT, |args| {
17        if args.is_mouse_enter_disabled() {
18            Some(true)
19        } else if args.is_mouse_leave_disabled() {
20            Some(false)
21        } else {
22            None
23        }
24    })
25}
26
27/// If the mouse pointer is over the widget or a descendant and the widget is enabled.
28///
29/// This state property does not consider pointer capture, if the pointer is captured by the widget
30/// but is not actually over the widget this is `false`, use [`is_cap_hovered`] to include the captured state.
31///
32/// The value is always `false` when the widget is not [`ENABLED`], use [`is_hovered_disabled`] to implement *disabled hovered* visuals.
33///
34/// [`is_cap_hovered`]: fn@is_cap_hovered
35/// [`ENABLED`]: Interactivity::ENABLED
36/// [`is_hovered_disabled`]: fn@is_hovered_disabled
37#[property(EVENT)]
38pub fn is_hovered(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
39    event_state(child, state, false, MOUSE_HOVERED_EVENT, |args| {
40        if args.is_mouse_enter_enabled() {
41            Some(true)
42        } else if args.is_mouse_leave_enabled() {
43            Some(false)
44        } else {
45            None
46        }
47    })
48}
49
50/// If the mouse pointer is over the widget, or a descendant, or is captured by the it.
51///
52/// The value is always `false` when the widget is not [`ENABLED`].
53///
54/// [`ENABLED`]: Interactivity::ENABLED
55#[property(EVENT)]
56pub fn is_cap_hovered(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
57    event_state2(
58        child,
59        state,
60        false,
61        MOUSE_HOVERED_EVENT,
62        false,
63        |hovered_args| {
64            if hovered_args.is_mouse_enter_enabled() {
65                Some(true)
66            } else if hovered_args.is_mouse_leave_enabled() {
67                Some(false)
68            } else {
69                None
70            }
71        },
72        POINTER_CAPTURE_EVENT,
73        false,
74        |cap_args| {
75            if cap_args.is_got(WIDGET.id()) {
76                Some(true)
77            } else if cap_args.is_lost(WIDGET.id()) {
78                Some(false)
79            } else {
80                None
81            }
82        },
83        |hovered, captured| Some(hovered || captured),
84    )
85}
86
87/// If the mouse pointer is pressed in the widget and it is enabled.
88///
89/// This is `true` when the mouse primary button started pressing in the widget
90/// and the pointer is over the widget and the primary button is still pressed and
91/// the widget is fully [`ENABLED`].
92///
93/// This state property only considers pointer capture for repeat [click modes](ClickMode), if the pointer is captured by a widget
94/// with [`ClickMode::repeat`] `false` and the pointer is not actually over the widget the state is `false`,
95/// use [`is_cap_mouse_pressed`] to always include the captured state.
96///
97/// [`ENABLED`]: Interactivity::ENABLED
98/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
99/// [`ClickMode::repeat`]: zng_ext_input::mouse::ClickMode::repeat
100#[property(EVENT)]
101pub fn is_mouse_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
102    event_state3(
103        child,
104        state,
105        false,
106        MOUSE_HOVERED_EVENT,
107        false,
108        |hovered_args| {
109            if hovered_args.is_mouse_enter_enabled() {
110                Some(true)
111            } else if hovered_args.is_mouse_leave_enabled() {
112                Some(false)
113            } else {
114                None
115            }
116        },
117        MOUSE_INPUT_EVENT,
118        false,
119        |input_args| {
120            if input_args.is_primary() {
121                match input_args.state {
122                    ButtonState::Pressed => {
123                        if input_args.capture_allows() {
124                            return Some(input_args.is_enabled(WIDGET.id()));
125                        }
126                    }
127                    ButtonState::Released => return Some(false),
128                }
129            }
130            None
131        },
132        POINTER_CAPTURE_EVENT,
133        false,
134        |cap_args| {
135            if cap_args.is_got(WIDGET.id()) {
136                Some(true)
137            } else if cap_args.is_lost(WIDGET.id()) {
138                Some(false)
139            } else {
140                None
141            }
142        },
143        {
144            let mut info_gen = 0;
145            let mut mode = ClickMode::default();
146
147            move |hovered, is_down, is_captured| {
148                // cache mode
149                let tree = WINDOW.info();
150                if info_gen != tree.stats().generation {
151                    mode = tree.get(WIDGET.id()).unwrap().click_mode();
152                    info_gen = tree.stats().generation;
153                }
154
155                if mode.repeat {
156                    Some(is_down || is_captured)
157                } else {
158                    Some(hovered && is_down)
159                }
160            }
161        },
162    )
163}
164
165/// If the mouse pointer is pressed or captured by the widget and it is enabled.
166///
167/// [`ENABLED`]: Interactivity::ENABLED
168#[property(EVENT)]
169pub fn is_cap_mouse_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
170    event_state2(
171        child,
172        state,
173        false,
174        MOUSE_INPUT_EVENT,
175        false,
176        |input_args| {
177            if input_args.is_primary() {
178                match input_args.state {
179                    ButtonState::Pressed => {
180                        if input_args.capture_allows() {
181                            return Some(input_args.is_enabled(WIDGET.id()));
182                        }
183                    }
184                    ButtonState::Released => return Some(false),
185                }
186            }
187            None
188        },
189        POINTER_CAPTURE_EVENT,
190        false,
191        |cap_args| {
192            if cap_args.is_got(WIDGET.id()) {
193                Some(true)
194            } else if cap_args.is_lost(WIDGET.id()) {
195                Some(false)
196            } else {
197                None
198            }
199        },
200        |is_down, is_captured| Some(is_down || is_captured),
201    )
202}
203
204/// If the widget was clicked by shortcut or accessibility event and the [`shortcut_pressed_duration`] has not elapsed.
205///
206/// [`shortcut_pressed_duration`]: GESTURES::shortcut_pressed_duration
207#[property(EVENT)]
208pub fn is_shortcut_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
209    let state = state.into_var();
210    let mut shortcut_press = None;
211
212    match_node(child, move |child, op| match op {
213        UiNodeOp::Init => {
214            let _ = state.set(false);
215            WIDGET.sub_event(&CLICK_EVENT);
216        }
217        UiNodeOp::Deinit => {
218            let _ = state.set(false);
219        }
220        UiNodeOp::Event { update } => {
221            if let Some(args) = CLICK_EVENT.on(update) {
222                if (args.is_from_keyboard() || args.is_from_access()) && args.is_enabled(WIDGET.id()) {
223                    // if a shortcut click happened, we show pressed for the duration of `shortcut_pressed_duration`
224                    // unless we where already doing that, then we just stop showing pressed, this causes
225                    // a flickering effect when rapid clicks are happening.
226                    if shortcut_press.take().is_none() {
227                        let duration = GESTURES.shortcut_pressed_duration().get();
228                        if duration != Duration::default() {
229                            let dl = TIMERS.deadline(duration);
230                            dl.subscribe(UpdateOp::Update, WIDGET.id()).perm();
231                            shortcut_press = Some(dl);
232                            let _ = state.set(true);
233                        }
234                    } else {
235                        let _ = state.set(false);
236                    }
237                }
238            }
239        }
240        UiNodeOp::Update { updates } => {
241            child.update(updates);
242
243            if let Some(timer) = &shortcut_press {
244                if timer.is_new() {
245                    shortcut_press = None;
246                    let _ = state.set(false);
247                }
248            }
249        }
250        _ => {}
251    })
252}
253
254/// If a touch contact point is over the widget or a descendant and the it is enabled.
255///
256/// This state property does not consider pointer capture, if the pointer is captured by the widget
257/// but is not actually over the widget this is `false`, use [`is_cap_touched`] to include the captured state.
258///
259/// This state property also does not consider where the touch started, if it started in a different widget
260/// and is not over this widget the widget is touched, use [`is_touched_from_start`] to ignore touched that move in.
261///
262/// The value is always `false` when the widget is not [`ENABLED`].
263///
264/// [`is_cap_touched`]: fn@is_cap_touched
265/// [`is_touched_from_start`]: fn@is_touched_from_start
266/// [`ENABLED`]: Interactivity::ENABLED
267#[property(EVENT)]
268pub fn is_touched(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
269    event_state(child, state, false, TOUCHED_EVENT, |args| {
270        if args.is_touch_enter_enabled() {
271            Some(true)
272        } else if args.is_touch_leave_enabled() {
273            Some(false)
274        } else {
275            None
276        }
277    })
278}
279
280/// If a touch contact that started over the widget is over it and it is enabled.
281///
282/// This state property does not consider pointer capture, if the pointer is captured by the widget
283/// but is not actually over the widget this is `false`, use [`is_cap_touched_from_start`] to include the captured state.
284///
285/// The value is always `false` when the widget is not [`ENABLED`].
286///
287/// [`ENABLED`]: Interactivity::ENABLED
288/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
289#[property(EVENT)]
290pub fn is_touched_from_start(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
291    #[expect(clippy::mutable_key_type)] // EventPropagationHandle compares pointers, not value
292    let mut touches_started = HashSet::new();
293    event_state(child, state, false, TOUCHED_EVENT, move |args| {
294        if args.is_touch_enter_enabled() {
295            match args.phase {
296                TouchPhase::Start => {
297                    touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
298                    touches_started.insert(args.touch_propagation.clone());
299                    Some(true)
300                }
301                TouchPhase::Move => Some(touches_started.contains(&args.touch_propagation)),
302                TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
303            }
304        } else if args.is_touch_leave_enabled() {
305            if let TouchPhase::End | TouchPhase::Cancel = args.phase {
306                touches_started.remove(&args.touch_propagation);
307            }
308            Some(false)
309        } else {
310            None
311        }
312    })
313}
314
315/// If a touch contact point is over the widget, or is over a descendant, or is captured by it.
316///
317/// The value is always `false` when the widget is not [`ENABLED`].
318///
319/// [`ENABLED`]: Interactivity::ENABLED
320#[property(EVENT)]
321pub fn is_cap_touched(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
322    event_state2(
323        child,
324        state,
325        false,
326        TOUCHED_EVENT,
327        false,
328        |hovered_args| {
329            if hovered_args.is_touch_enter_enabled() {
330                Some(true)
331            } else if hovered_args.is_touch_leave_enabled() {
332                Some(false)
333            } else {
334                None
335            }
336        },
337        POINTER_CAPTURE_EVENT,
338        false,
339        |cap_args| {
340            if cap_args.is_got(WIDGET.id()) {
341                Some(true)
342            } else if cap_args.is_lost(WIDGET.id()) {
343                Some(false)
344            } else {
345                None
346            }
347        },
348        |hovered, captured| Some(hovered || captured),
349    )
350}
351
352/// If a touch contact point is over the widget, or is over a descendant, or is captured by it.
353///
354/// The value is always `false` when the widget is not [`ENABLED`].
355///
356/// [`ENABLED`]: Interactivity::ENABLED
357#[property(EVENT)]
358pub fn is_cap_touched_from_start(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
359    #[expect(clippy::mutable_key_type)] // EventPropagationHandle compares pointers, not value
360    let mut touches_started = HashSet::new();
361    event_state2(
362        child,
363        state,
364        false,
365        TOUCHED_EVENT,
366        false,
367        move |hovered_args| {
368            if hovered_args.is_touch_enter_enabled() {
369                match hovered_args.phase {
370                    TouchPhase::Start => {
371                        touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
372                        touches_started.insert(hovered_args.touch_propagation.clone());
373                        Some(true)
374                    }
375                    TouchPhase::Move => Some(touches_started.contains(&hovered_args.touch_propagation)),
376                    TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
377                }
378            } else if hovered_args.is_touch_leave_enabled() {
379                if let TouchPhase::End | TouchPhase::Cancel = hovered_args.phase {
380                    touches_started.remove(&hovered_args.touch_propagation);
381                }
382                Some(false)
383            } else {
384                None
385            }
386        },
387        POINTER_CAPTURE_EVENT,
388        false,
389        |cap_args| {
390            if cap_args.is_got(WIDGET.id()) {
391                Some(true)
392            } else if cap_args.is_lost(WIDGET.id()) {
393                Some(false)
394            } else {
395                None
396            }
397        },
398        |hovered, captured| Some(hovered || captured),
399    )
400}
401
402/// If [`is_mouse_pressed`] or [`is_touched_from_start`].
403///
404/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pointer_pressed`] to
405/// include the captured state.
406///
407/// [`is_mouse_pressed`]: fn@is_mouse_pressed
408/// [`is_touched_from_start`]: fn@is_touched_from_start
409/// [`is_cap_pointer_pressed`]: fn@is_cap_pointer_pressed
410#[property(EVENT)]
411pub fn is_pointer_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
412    let pressed = state_var();
413    let child = is_mouse_pressed(child, pressed.clone());
414
415    let touched = state_var();
416    let child = is_touched_from_start(child, touched.clone());
417
418    bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
419}
420
421/// If [`is_mouse_pressed`], [`is_touched_from_start`] or [`is_shortcut_pressed`].
422///
423/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pressed`] to
424/// include the captured state.
425///
426/// [`is_mouse_pressed`]: fn@is_mouse_pressed
427/// [`is_touched_from_start`]: fn@is_touched_from_start
428/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
429/// [`is_cap_pressed`]: fn@is_cap_pressed
430#[property(EVENT)]
431pub fn is_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
432    let pressed = state_var();
433    let child = is_mouse_pressed(child, pressed.clone());
434
435    let touched = state_var();
436    let child = is_touched_from_start(child, touched.clone());
437
438    let shortcut_pressed = state_var();
439    let child = is_shortcut_pressed(child, shortcut_pressed.clone());
440
441    bind_state(
442        child,
443        merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
444        state,
445    )
446}
447
448/// If [`is_cap_mouse_pressed`] or [`is_cap_touched_from_start`].
449///
450/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
451/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
452#[property(EVENT)]
453pub fn is_cap_pointer_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
454    let pressed = state_var();
455    let child = is_cap_mouse_pressed(child, pressed.clone());
456
457    let touched = state_var();
458    let child = is_cap_touched_from_start(child, touched.clone());
459
460    bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
461}
462
463/// If [`is_cap_mouse_pressed`], [`is_cap_touched_from_start`] or [`is_shortcut_pressed`].
464///
465/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
466/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
467/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
468#[property(EVENT)]
469pub fn is_cap_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
470    let pressed = state_var();
471    let child = is_cap_mouse_pressed(child, pressed.clone());
472
473    let touched = state_var();
474    let child = is_cap_touched_from_start(child, touched.clone());
475
476    let shortcut_pressed = state_var();
477    let child = is_shortcut_pressed(child, pressed.clone());
478
479    bind_state(
480        child,
481        merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
482        state,
483    )
484}