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, MOUSE_MOVE_EVENT, MOUSE_WHEEL_EVENT, WidgetInfoMouseExt as _},
7    pointer_capture::POINTER_CAPTURE_EVENT,
8    touch::{TOUCH_TAP_EVENT, TOUCHED_EVENT},
9};
10use zng_view_api::{mouse::ButtonState, touch::TouchPhase};
11use zng_wgt::{node::validate_getter_var, 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 IntoUiNode, state: impl IntoVar<bool>) -> 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 IntoUiNode, state: impl IntoVar<bool>) -> 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 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 IntoUiNode, state: impl IntoVar<bool>) -> 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 IntoUiNode, state: impl IntoVar<bool>) -> 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.target.contains_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#[property(EVENT)]
167pub fn is_cap_mouse_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
168    event_state2(
169        child,
170        state,
171        false,
172        MOUSE_INPUT_EVENT,
173        false,
174        |input_args| {
175            if input_args.is_primary() {
176                match input_args.state {
177                    ButtonState::Pressed => {
178                        if input_args.capture_allows() {
179                            return Some(input_args.target.contains_enabled(WIDGET.id()));
180                        }
181                    }
182                    ButtonState::Released => return Some(false),
183                }
184            }
185            None
186        },
187        POINTER_CAPTURE_EVENT,
188        false,
189        |cap_args| {
190            if cap_args.is_got(WIDGET.id()) {
191                Some(true)
192            } else if cap_args.is_lost(WIDGET.id()) {
193                Some(false)
194            } else {
195                None
196            }
197        },
198        |is_down, is_captured| Some(is_down || is_captured),
199    )
200}
201
202/// If the widget was clicked by shortcut or accessibility event and the [`shortcut_pressed_duration`] has not elapsed.
203///
204/// [`shortcut_pressed_duration`]: GESTURES::shortcut_pressed_duration
205#[property(EVENT)]
206pub fn is_shortcut_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
207    let state = state.into_var();
208    let mut shortcut_press = None;
209
210    match_node(child, move |child, op| match op {
211        UiNodeOp::Init => {
212            state.set(false);
213            WIDGET.sub_event(&CLICK_EVENT);
214        }
215        UiNodeOp::Deinit => {
216            state.set(false);
217        }
218        UiNodeOp::Event { update } => {
219            if let Some(args) = CLICK_EVENT.on(update)
220                && (args.is_from_keyboard() || args.is_from_access())
221                && args.target.contains_enabled(WIDGET.id())
222            {
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                        state.set(true);
233                    }
234                } else {
235                    state.set(false);
236                }
237            }
238        }
239        UiNodeOp::Update { updates } => {
240            child.update(updates);
241
242            if let Some(timer) = &shortcut_press
243                && timer.is_new()
244            {
245                shortcut_press = None;
246                state.set(false);
247            }
248        }
249        _ => {}
250    })
251}
252
253/// If a touch contact point is over the widget or a descendant and the it is enabled.
254///
255/// This state property does not consider pointer capture, if the pointer is captured by the widget
256/// but is not actually over the widget this is `false`, use [`is_cap_touched`] to include the captured state.
257///
258/// This state property also does not consider where the touch started, if it started in a different widget
259/// and is not over this widget the widget is touched, use [`is_touched_from_start`] to ignore touched that move in.
260///
261/// The value is always `false` when the widget is not [`ENABLED`].
262///
263/// [`is_cap_touched`]: fn@is_cap_touched
264/// [`is_touched_from_start`]: fn@is_touched_from_start
265/// [`ENABLED`]: Interactivity::ENABLED
266#[property(EVENT)]
267pub fn is_touched(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
268    event_state(child, state, false, TOUCHED_EVENT, |args| {
269        if args.is_touch_enter_enabled() {
270            Some(true)
271        } else if args.is_touch_leave_enabled() {
272            Some(false)
273        } else {
274            None
275        }
276    })
277}
278
279/// If a touch contact that started over the widget is over it and it is enabled.
280///
281/// This state property does not consider pointer capture, if the pointer is captured by the widget
282/// but is not actually over the widget this is `false`, use [`is_cap_touched_from_start`] to include the captured state.
283///
284/// The value is always `false` when the widget is not [`ENABLED`].
285///
286/// [`ENABLED`]: Interactivity::ENABLED
287/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
288#[property(EVENT)]
289pub fn is_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
290    #[expect(clippy::mutable_key_type)] // EventPropagationHandle compares pointers, not value
291    let mut touches_started = HashSet::new();
292    event_state(child, state, false, TOUCHED_EVENT, move |args| {
293        if args.is_touch_enter_enabled() {
294            match args.phase {
295                TouchPhase::Start => {
296                    touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
297                    touches_started.insert(args.touch_propagation.clone());
298                    Some(true)
299                }
300                TouchPhase::Move => Some(touches_started.contains(&args.touch_propagation)),
301                TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
302            }
303        } else if args.is_touch_leave_enabled() {
304            if let TouchPhase::End | TouchPhase::Cancel = args.phase {
305                touches_started.remove(&args.touch_propagation);
306            }
307            Some(false)
308        } else {
309            None
310        }
311    })
312}
313
314/// If a touch contact point is over the widget, or is over a descendant, or is captured by it.
315///
316/// The value is always `false` when the widget is not [`ENABLED`].
317///
318/// [`ENABLED`]: Interactivity::ENABLED
319#[property(EVENT)]
320pub fn is_cap_touched(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
321    event_state2(
322        child,
323        state,
324        false,
325        TOUCHED_EVENT,
326        false,
327        |hovered_args| {
328            if hovered_args.is_touch_enter_enabled() {
329                Some(true)
330            } else if hovered_args.is_touch_leave_enabled() {
331                Some(false)
332            } else {
333                None
334            }
335        },
336        POINTER_CAPTURE_EVENT,
337        false,
338        |cap_args| {
339            if cap_args.is_got(WIDGET.id()) {
340                Some(true)
341            } else if cap_args.is_lost(WIDGET.id()) {
342                Some(false)
343            } else {
344                None
345            }
346        },
347        |hovered, captured| Some(hovered || captured),
348    )
349}
350
351/// If a touch contact point is over the widget, or is over a descendant, or is captured by it.
352///
353/// The value is always `false` when the widget is not [`ENABLED`].
354///
355/// [`ENABLED`]: Interactivity::ENABLED
356#[property(EVENT)]
357pub fn is_cap_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
358    #[expect(clippy::mutable_key_type)] // EventPropagationHandle compares pointers, not value
359    let mut touches_started = HashSet::new();
360    event_state2(
361        child,
362        state,
363        false,
364        TOUCHED_EVENT,
365        false,
366        move |hovered_args| {
367            if hovered_args.is_touch_enter_enabled() {
368                match hovered_args.phase {
369                    TouchPhase::Start => {
370                        touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
371                        touches_started.insert(hovered_args.touch_propagation.clone());
372                        Some(true)
373                    }
374                    TouchPhase::Move => Some(touches_started.contains(&hovered_args.touch_propagation)),
375                    TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
376                }
377            } else if hovered_args.is_touch_leave_enabled() {
378                if let TouchPhase::End | TouchPhase::Cancel = hovered_args.phase {
379                    touches_started.remove(&hovered_args.touch_propagation);
380                }
381                Some(false)
382            } else {
383                None
384            }
385        },
386        POINTER_CAPTURE_EVENT,
387        false,
388        |cap_args| {
389            if cap_args.is_got(WIDGET.id()) {
390                Some(true)
391            } else if cap_args.is_lost(WIDGET.id()) {
392                Some(false)
393            } else {
394                None
395            }
396        },
397        |hovered, captured| Some(hovered || captured),
398    )
399}
400
401/// If [`is_mouse_pressed`] or [`is_touched_from_start`].
402///
403/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pointer_pressed`] to
404/// include the captured state.
405///
406/// [`is_mouse_pressed`]: fn@is_mouse_pressed
407/// [`is_touched_from_start`]: fn@is_touched_from_start
408/// [`is_cap_pointer_pressed`]: fn@is_cap_pointer_pressed
409#[property(EVENT)]
410pub fn is_pointer_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
411    let pressed = var_state();
412    let child = is_mouse_pressed(child, pressed.clone());
413
414    let touched = var_state();
415    let child = is_touched_from_start(child, touched.clone());
416
417    bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
418}
419
420/// If [`is_mouse_pressed`], [`is_touched_from_start`] or [`is_shortcut_pressed`].
421///
422/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pressed`] to
423/// include the captured state.
424///
425/// [`is_mouse_pressed`]: fn@is_mouse_pressed
426/// [`is_touched_from_start`]: fn@is_touched_from_start
427/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
428/// [`is_cap_pressed`]: fn@is_cap_pressed
429#[property(EVENT)]
430pub fn is_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
431    let pressed = var_state();
432    let child = is_mouse_pressed(child, pressed.clone());
433
434    let touched = var_state();
435    let child = is_touched_from_start(child, touched.clone());
436
437    let shortcut_pressed = var_state();
438    let child = is_shortcut_pressed(child, shortcut_pressed.clone());
439
440    bind_state(
441        child,
442        merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
443        state,
444    )
445}
446
447/// If [`is_cap_mouse_pressed`] or [`is_cap_touched_from_start`].
448///
449/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
450/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
451#[property(EVENT)]
452pub fn is_cap_pointer_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
453    let pressed = var_state();
454    let child = is_cap_mouse_pressed(child, pressed.clone());
455
456    let touched = var_state();
457    let child = is_cap_touched_from_start(child, touched.clone());
458
459    bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
460}
461
462/// If [`is_cap_mouse_pressed`], [`is_cap_touched_from_start`] or [`is_shortcut_pressed`].
463///
464/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
465/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
466/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
467#[property(EVENT)]
468pub fn is_cap_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
469    let pressed = var_state();
470    let child = is_cap_mouse_pressed(child, pressed.clone());
471
472    let touched = var_state();
473    let child = is_cap_touched_from_start(child, touched.clone());
474
475    let shortcut_pressed = var_state();
476    let child = is_shortcut_pressed(child, pressed.clone());
477
478    bind_state(
479        child,
480        merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
481        state,
482    )
483}
484
485/// If the mouse pointer moved over or interacted with the widget within a time duration defined by contextual [`mouse_active_config`].
486///
487/// This property is useful for implementing things like a media player widget, where the mouse cursor and controls vanish
488/// after the mouse stops moving for a time.
489///
490/// See also [`is_pointer_active`] for an aggregate gesture that covers mouse and touch.
491///
492/// [`mouse_active_config`]: fn@mouse_active_config
493/// [`is_pointer_active`]: fn@is_pointer_active
494#[property(EVENT)]
495pub fn is_mouse_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
496    let state = state.into_var();
497    enum State {
498        False,
499        Maybe(DipPoint),
500        True(DipPoint, TimerVar),
501    }
502    let mut raw_state = State::False;
503    match_node(child, move |_, op| match op {
504        UiNodeOp::Init => {
505            validate_getter_var(&state);
506            WIDGET.sub_event(&MOUSE_MOVE_EVENT).sub_var(&MOUSE_ACTIVE_CONFIG_VAR);
507            state.set(true);
508        }
509        UiNodeOp::Deinit => {
510            state.set(false);
511            raw_state = State::False;
512        }
513        UiNodeOp::Event { update } => {
514            let mut start = None;
515            if let Some(args) = MOUSE_MOVE_EVENT.on(update) {
516                match &mut raw_state {
517                    State::False => {
518                        let cfg = MOUSE_ACTIVE_CONFIG_VAR.get();
519                        if cfg.area.width <= Dip::new(1) || cfg.area.height <= Dip::new(1) {
520                            start = Some((cfg.duration, args.position));
521                        } else {
522                            raw_state = State::Maybe(args.position);
523                        }
524                    }
525                    State::Maybe(s) => {
526                        let cfg = MOUSE_ACTIVE_CONFIG_VAR.get();
527                        if (args.position.x - s.x).abs() >= cfg.area.width || (args.position.y - s.y).abs() >= cfg.area.height {
528                            start = Some((cfg.duration, args.position));
529                        }
530                    }
531                    State::True(p, timer) => {
532                        if (args.position.x - p.x).abs() >= Dip::new(1) || (args.position.y - p.y).abs() >= Dip::new(1) {
533                            // reset
534                            timer.get().play(true);
535                            *p = args.position;
536                        }
537                    }
538                }
539            } else {
540                let pos = if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
541                    Some(args.position)
542                } else {
543                    MOUSE_WHEEL_EVENT.on(update).map(|args| args.position)
544                };
545                if let Some(pos) = pos {
546                    match &raw_state {
547                        State::True(_, timer) => {
548                            // reset
549                            timer.get().play(true);
550                        }
551                        _ => {
552                            start = Some((MOUSE_ACTIVE_CONFIG_VAR.get().duration, pos));
553                        }
554                    }
555                }
556            }
557            if let Some((t, pos)) = start {
558                let timer = TIMERS.interval(t, false);
559                timer.subscribe(UpdateOp::Update, WIDGET.id()).perm();
560                state.set(true);
561                raw_state = State::True(pos, timer);
562            }
563        }
564        UiNodeOp::Update { .. } => {
565            if let State::True(_, timer) = &raw_state {
566                if let Some(timer) = timer.get_new() {
567                    timer.stop();
568                    state.set(false);
569                    raw_state = State::False;
570                } else if let Some(cfg) = MOUSE_ACTIVE_CONFIG_VAR.get_new() {
571                    timer.get().set_interval(cfg.duration);
572                }
573            }
574        }
575        _ => {}
576    })
577}
578
579/// Contextual configuration for [`is_mouse_active`].
580///
581/// Note that the [`MouseActiveConfig`] converts from duration, so you can set this to a time *literal*, like `5.secs()`, directly.
582///
583/// This property sets the [`MOUSE_ACTIVE_CONFIG_VAR`].
584///
585/// [`is_mouse_active`]: fn@is_mouse_active
586#[property(CONTEXT, default(MOUSE_ACTIVE_CONFIG_VAR))]
587pub fn mouse_active_config(child: impl IntoUiNode, config: impl IntoVar<MouseActiveConfig>) -> UiNode {
588    with_context_var(child, MOUSE_ACTIVE_CONFIG_VAR, config)
589}
590
591/// If an unhandled touch tap has happened on the widget within a time duration defined by contextual [`touch_active_config`].
592///
593/// This property is the touch equivalent to [`is_mouse_active`].
594///
595/// [`touch_active_config`]: fn@touch_active_config
596/// [`is_mouse_active`]: fn@is_mouse_active
597#[property(EVENT)]
598pub fn is_touch_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
599    let state = state.into_var();
600    enum State {
601        False,
602        True(TimerVar),
603    }
604    let mut raw_state = State::False;
605    match_node(child, move |c, op| match op {
606        UiNodeOp::Init => {
607            validate_getter_var(&state);
608            WIDGET.sub_event(&TOUCH_TAP_EVENT).sub_var(&TOUCH_ACTIVE_CONFIG_VAR);
609            state.set(false);
610        }
611        UiNodeOp::Deinit => {
612            state.set(false);
613            raw_state = State::False;
614        }
615        UiNodeOp::Event { update } => {
616            c.event(update);
617            if TOUCH_TAP_EVENT.on_unhandled(update).is_some() {
618                match &raw_state {
619                    State::False => {
620                        let t = TOUCH_ACTIVE_CONFIG_VAR.get().duration;
621                        let timer = TIMERS.interval(t, false);
622                        timer.subscribe(UpdateOp::Update, WIDGET.id()).perm();
623                        state.set(true);
624                        raw_state = State::True(timer);
625                    }
626                    State::True(timer) => {
627                        let cfg = TOUCH_ACTIVE_CONFIG_VAR.get();
628                        if cfg.toggle {
629                            state.set(false);
630                            timer.get().stop();
631                        } else {
632                            timer.get().play(true);
633                        }
634                    }
635                }
636            }
637        }
638        UiNodeOp::Update { .. } => {
639            if let State::True(timer) = &raw_state {
640                if let Some(timer) = timer.get_new() {
641                    timer.stop();
642                    state.set(false);
643                    raw_state = State::False;
644                } else if let Some(cfg) = TOUCH_ACTIVE_CONFIG_VAR.get_new() {
645                    timer.get().set_interval(cfg.duration);
646                }
647            }
648        }
649        _ => {}
650    })
651}
652
653/// Contextual configuration for [`is_touch_active`].
654///
655/// Note that the [`TouchActiveConfig`] converts from duration, so you can set this to a time *literal*, like `5.secs()`, directly.
656///
657/// This property sets the [`MOUSE_ACTIVE_CONFIG_VAR`].
658///
659/// [`is_touch_active`]: fn@is_touch_active
660#[property(CONTEXT, default(TOUCH_ACTIVE_CONFIG_VAR))]
661pub fn touch_active_config(child: impl IntoUiNode, config: impl IntoVar<TouchActiveConfig>) -> UiNode {
662    with_context_var(child, TOUCH_ACTIVE_CONFIG_VAR, config)
663}
664
665/// If [`is_mouse_active`] or [`is_touch_active`].
666///
667/// [`is_mouse_active`]: fn@is_mouse_active
668/// [`is_touch_active`]: fn@is_touch_active
669#[property(EVENT)]
670pub fn is_pointer_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
671    let mouse_active = var_state();
672    let child = is_mouse_active(child, mouse_active.clone());
673
674    let touch_active = var_state();
675    let child = is_touch_active(child, touch_active.clone());
676
677    bind_state(
678        child,
679        expr_var! {
680            *#{mouse_active} || *#{touch_active}
681        },
682        state,
683    )
684}
685
686context_var! {
687    /// Configuration for [`is_mouse_active`].
688    ///
689    /// [`is_mouse_active`]: fn@is_mouse_active
690    pub static MOUSE_ACTIVE_CONFIG_VAR: MouseActiveConfig = MouseActiveConfig::default();
691    /// Configuration for [`is_touch_active`].
692    ///
693    /// [`is_touch_active`]: fn@is_touch_active
694    pub static TOUCH_ACTIVE_CONFIG_VAR: TouchActiveConfig = TouchActiveConfig::default();
695}
696
697/// Configuration for mouse active property.
698///
699/// See [`mouse_active_config`] for more details.
700///
701/// [`mouse_active_config`]: fn@mouse_active_config
702#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
703#[non_exhaustive]
704pub struct MouseActiveConfig {
705    /// Maximum time the state remains active after mouse leave or stops moving.
706    pub duration: Duration,
707    /// Minimum distance the pointer must move before state changes to active.
708    pub area: DipSize,
709}
710impl Default for MouseActiveConfig {
711    /// `(3s, 1)`
712    fn default() -> Self {
713        Self {
714            duration: 3.secs(),
715            area: DipSize::splat(Dip::new(1)),
716        }
717    }
718}
719impl_from_and_into_var! {
720    fn from(duration: Duration) -> MouseActiveConfig {
721        MouseActiveConfig {
722            duration,
723            ..Default::default()
724        }
725    }
726
727    fn from(area: DipSize) -> MouseActiveConfig {
728        MouseActiveConfig {
729            area,
730            ..Default::default()
731        }
732    }
733
734    fn from((duration, area): (Duration, DipSize)) -> MouseActiveConfig {
735        MouseActiveConfig { duration, area }
736    }
737}
738
739/// Configuration for touch active property.
740///
741/// See [`touch_active_config`] for more details.
742#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
743#[non_exhaustive]
744pub struct TouchActiveConfig {
745    /// Maximum time the state remains active after no touch interaction.
746    pub duration: Duration,
747    /// If a second unhandled interaction deactivates.
748    pub toggle: bool,
749}
750impl Default for TouchActiveConfig {
751    /// `(3s, false)`
752    fn default() -> Self {
753        Self {
754            duration: 3.secs(),
755            toggle: false,
756        }
757    }
758}
759impl_from_and_into_var! {
760    fn from(duration: Duration) -> TouchActiveConfig {
761        TouchActiveConfig {
762            duration,
763            ..Default::default()
764        }
765    }
766
767    fn from((duration, toggle): (Duration, bool)) -> TouchActiveConfig {
768        TouchActiveConfig { duration, toggle }
769    }
770}