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