zng_wgt_input/
state.rs

1use std::{collections::HashSet, num::Wrapping, 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_ext_window::WINDOWS;
11use zng_view_api::{mouse::ButtonState, touch::TouchPhase};
12use zng_wgt::{
13    node::{bind_state_init, validate_getter_var},
14    prelude::*,
15};
16
17/// If the mouse pointer is over the widget or a descendant and the widget is disabled.
18#[property(EVENT)]
19pub fn is_hovered_disabled(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
20    bind_state_init(child, state, |s| {
21        let wgt = (WINDOW.id(), WIDGET.id());
22        MOUSE_HOVERED_EVENT.var_bind(s, move |args| {
23            if args.is_mouse_enter_disabled(wgt) {
24                Some(true)
25            } else if args.is_mouse_leave_disabled(wgt) {
26                Some(false)
27            } else {
28                None
29            }
30        })
31    })
32}
33
34/// If the mouse pointer is over the widget or a descendant and the widget is enabled.
35///
36/// This state property does not consider pointer capture, if the pointer is captured by the widget
37/// but is not actually over the widget this is `false`, use [`is_cap_hovered`] to include the captured state.
38///
39/// The value is always `false` when the widget is not [`ENABLED`], use [`is_hovered_disabled`] to implement *disabled hovered* visuals.
40///
41/// [`is_cap_hovered`]: fn@is_cap_hovered
42/// [`ENABLED`]: Interactivity::ENABLED
43/// [`is_hovered_disabled`]: fn@is_hovered_disabled
44#[property(EVENT)]
45pub fn is_hovered(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
46    bind_state_init(child, state, |s| {
47        let wgt = (WINDOW.id(), WIDGET.id());
48        MOUSE_HOVERED_EVENT.var_bind(s, move |args| {
49            if args.is_mouse_enter_enabled(wgt) {
50                Some(true)
51            } else if args.is_mouse_leave_enabled(wgt) {
52                Some(false)
53            } else {
54                None
55            }
56        })
57    })
58}
59
60fn hovered_var(wgt: (WindowId, WidgetId)) -> Var<bool> {
61    MOUSE_HOVERED_EVENT.var_map(
62        clmv!(wgt, |args| {
63            if args.is_mouse_enter_enabled(wgt) {
64                Some(true)
65            } else if args.is_mouse_leave_enabled(wgt) {
66                Some(false)
67            } else {
68                None
69            }
70        }),
71        || false,
72    )
73}
74
75fn captured_var(id: WidgetId) -> Var<bool> {
76    POINTER_CAPTURE_EVENT.var_map(
77        move |args| {
78            if args.is_got(id) {
79                Some(true)
80            } else if args.is_lost(id) {
81                Some(false)
82            } else {
83                None
84            }
85        },
86        || false,
87    )
88}
89
90/// If the mouse pointer is over the widget, or a descendant, or is captured by it.
91///
92/// The value is always `false` when the widget is not [`ENABLED`].
93///
94/// [`ENABLED`]: Interactivity::ENABLED
95#[property(EVENT)]
96pub fn is_cap_hovered(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
97    bind_state_init(child, state, |s| {
98        let wgt = (WINDOW.id(), WIDGET.id());
99
100        let actual_state = expr_var!(*#{hovered_var(wgt)} || *#{captured_var(wgt.1)});
101        actual_state.set_bind(s).perm();
102        s.hold(actual_state)
103    })
104}
105
106fn pressed_var(wgt: (WindowId, WidgetId)) -> Var<bool> {
107    MOUSE_INPUT_EVENT.var_map(
108        move |args| {
109            if args.is_primary() {
110                match args.state {
111                    ButtonState::Pressed => {
112                        if args.capture_allows(wgt) {
113                            return Some(args.target.contains_enabled(wgt.1));
114                        }
115                    }
116                    ButtonState::Released => return Some(false),
117                }
118            }
119            None
120        },
121        || false,
122    )
123}
124
125/// If the mouse pointer is pressed in the widget and it is enabled.
126///
127/// This is `true` when the mouse primary button started pressing in the widget
128/// and the pointer is over the widget and the primary button is still pressed and
129/// the widget is fully [`ENABLED`].
130///
131/// This state property only considers pointer capture for repeat [click modes](ClickMode), if the pointer is captured by a widget
132/// with [`ClickMode::repeat`] `false` and the pointer is not actually over the widget the state is `false`,
133/// use [`is_cap_mouse_pressed`] to always include the captured state.
134///
135/// [`ENABLED`]: Interactivity::ENABLED
136/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
137/// [`ClickMode::repeat`]: zng_ext_input::mouse::ClickMode::repeat
138#[property(EVENT)]
139pub fn is_mouse_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
140    bind_state_init(child, state, |s| {
141        let wgt = (WINDOW.id(), WIDGET.id());
142        let mut info_gen = 0;
143        let mut mode = ClickMode::default();
144        let actual_state = expr_var! {
145            let hovered = *#{hovered_var(wgt)};
146            let captured = *#{captured_var(wgt.1)};
147            let pressed = *#{pressed_var(wgt)};
148
149            if let Some(tree) = WINDOWS.widget_tree(wgt.0) {
150                let t_gen = tree.stats().generation;
151                if info_gen != t_gen {
152                    // cache mode to avoid some queries
153                    info_gen = t_gen;
154                    if let Some(w) = tree.get(wgt.1) {
155                        mode = w.click_mode();
156                    }
157                }
158
159                if mode.repeat { pressed || captured } else { hovered && pressed }
160            } else {
161                false
162            }
163        };
164        actual_state.set_bind(s).perm();
165        s.hold(actual_state)
166    })
167}
168
169/// If the mouse pointer is pressed or captured by the widget and it is enabled.
170#[property(EVENT)]
171pub fn is_cap_mouse_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
172    bind_state_init(child, state, |s| {
173        let wgt = (WINDOW.id(), WIDGET.id());
174        let actual_state = expr_var! {
175            *#{pressed_var(wgt)} || *#{captured_var(wgt.1)}
176        };
177        actual_state.set_bind(s).perm();
178        s.hold(actual_state)
179    })
180}
181
182/// If the widget was clicked by shortcut or accessibility event and the [`shortcut_pressed_duration`] has not elapsed.
183///
184/// [`shortcut_pressed_duration`]: GESTURES::shortcut_pressed_duration
185#[property(EVENT)]
186pub fn is_shortcut_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
187    bind_state_init(child, state, |s| {
188        let id = WIDGET.id();
189        let mut shortcut_press = None::<DeadlineVar>;
190        CLICK_EVENT.hook(clmv!(s, |args| {
191            if (args.is_from_keyboard() || args.is_from_access()) && args.target.contains_enabled(id) {
192                // if a shortcut click happened, we show pressed for the duration of `shortcut_pressed_duration`
193                // unless we where already doing that, then we just stop showing pressed, this causes
194                // a flickering effect when rapid clicks are happening.
195                let d = shortcut_press.take();
196                if d.is_none() || matches!(d, Some(p) if p.get().has_elapsed()) {
197                    let duration = GESTURES.shortcut_pressed_duration().get();
198                    if duration > Duration::ZERO {
199                        let dl = TIMERS.deadline(duration);
200                        dl.hook(clmv!(s, |t| {
201                            let elapsed = t.value().has_elapsed();
202                            if elapsed {
203                                s.set(false);
204                            }
205                            !elapsed
206                        }))
207                        .perm();
208                        shortcut_press = Some(dl);
209                        s.set(true);
210                    }
211                } else {
212                    s.set(false);
213                }
214            }
215            true
216        }))
217    })
218}
219
220/// If a touch contact point is over the widget or a descendant and the it is enabled.
221///
222/// This state property does not consider pointer capture, if the pointer is captured by the widget
223/// but is not actually over the widget this is `false`, use [`is_cap_touched`] to include the captured state.
224///
225/// This state property also does not consider where the touch started, if it started in a different widget
226/// and is not over this widget the widget is touched, use [`is_touched_from_start`] to ignore touched that move in.
227///
228/// The value is always `false` when the widget is not [`ENABLED`].
229///
230/// [`is_cap_touched`]: fn@is_cap_touched
231/// [`is_touched_from_start`]: fn@is_touched_from_start
232/// [`ENABLED`]: Interactivity::ENABLED
233#[property(EVENT)]
234pub fn is_touched(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
235    bind_state_init(child, state, |s| {
236        let wgt = (WINDOW.id(), WIDGET.id());
237        TOUCHED_EVENT.var_bind(s, move |args| {
238            if args.is_touch_enter_enabled(wgt) {
239                Some(true)
240            } else if args.is_touch_leave_enabled(wgt) {
241                Some(false)
242            } else {
243                None
244            }
245        })
246    })
247}
248fn touched_var(wgt: (WindowId, WidgetId)) -> Var<bool> {
249    TOUCHED_EVENT.var_map(
250        move |args| {
251            if args.is_touch_enter_enabled(wgt) {
252                Some(true)
253            } else if args.is_touch_leave_enabled(wgt) {
254                Some(false)
255            } else {
256                None
257            }
258        },
259        || false,
260    )
261}
262
263/// If a touch contact that started over the widget is over it and it is enabled.
264///
265/// This state property does not consider pointer capture, if the pointer is captured by the widget
266/// but is not actually over the widget this is `false`, use [`is_cap_touched_from_start`] to include the captured state.
267///
268/// The value is always `false` when the widget is not [`ENABLED`].
269///
270/// [`ENABLED`]: Interactivity::ENABLED
271/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
272#[property(EVENT)]
273pub fn is_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
274    bind_state_init(child, state, |s| {
275        #[expect(clippy::mutable_key_type)] // EventPropagationHandle compares pointers, not value
276        let mut touches_started = HashSet::new();
277        let wgt = (WINDOW.id(), WIDGET.id());
278        TOUCHED_EVENT.var_bind(s, move |args| {
279            if args.is_touch_enter_enabled(wgt) {
280                match args.phase {
281                    TouchPhase::Start => {
282                        touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
283                        touches_started.insert(args.touch_propagation.clone());
284                        Some(true)
285                    }
286                    TouchPhase::Move => Some(touches_started.contains(&args.touch_propagation)),
287                    TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
288                }
289            } else if args.is_touch_leave_enabled(wgt) {
290                if let TouchPhase::End | TouchPhase::Cancel = args.phase {
291                    touches_started.remove(&args.touch_propagation);
292                }
293                Some(false)
294            } else {
295                None
296            }
297        })
298    })
299}
300fn touched_from_start_var(wgt: (WindowId, WidgetId)) -> Var<bool> {
301    #[expect(clippy::mutable_key_type)] // EventPropagationHandle compares pointers, not value
302    let mut touches_started = HashSet::new();
303    TOUCHED_EVENT.var_map(
304        move |args| {
305            if args.is_touch_enter_enabled(wgt) {
306                match args.phase {
307                    TouchPhase::Start => {
308                        touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
309                        touches_started.insert(args.touch_propagation.clone());
310                        Some(true)
311                    }
312                    TouchPhase::Move => Some(touches_started.contains(&args.touch_propagation)),
313                    TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
314                }
315            } else if args.is_touch_leave_enabled(wgt) {
316                if let TouchPhase::End | TouchPhase::Cancel = args.phase {
317                    touches_started.remove(&args.touch_propagation);
318                }
319                Some(false)
320            } else {
321                None
322            }
323        },
324        || false,
325    )
326}
327
328/// If a touch contact point is over the widget, or is over a descendant, or is captured by it.
329///
330/// The value is always `false` when the widget is not [`ENABLED`].
331///
332/// [`ENABLED`]: Interactivity::ENABLED
333#[property(EVENT)]
334pub fn is_cap_touched(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
335    bind_state_init(child, state, |s| {
336        let wgt = (WINDOW.id(), WIDGET.id());
337        let actual_state = expr_var! {
338            *#{touched_var(wgt)} || *#{captured_var(wgt.1)}
339        };
340        actual_state.set_bind(s).perm();
341        s.hold(actual_state)
342    })
343}
344
345/// If a touch contact point is over the widget, or is over a descendant, or is captured by it.
346///
347/// The value is always `false` when the widget is not [`ENABLED`].
348///
349/// [`ENABLED`]: Interactivity::ENABLED
350#[property(EVENT)]
351pub fn is_cap_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
352    bind_state_init(child, state, |s| {
353        let wgt = (WINDOW.id(), WIDGET.id());
354        let actual_state = expr_var! {
355            *#{touched_from_start_var(wgt)} || *#{captured_var(wgt.1)}
356        };
357        actual_state.set_bind(s).perm();
358        s.hold(actual_state)
359    })
360}
361
362/// If [`is_mouse_pressed`] or [`is_touched_from_start`].
363///
364/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pointer_pressed`] to
365/// include the captured state.
366///
367/// [`is_mouse_pressed`]: fn@is_mouse_pressed
368/// [`is_touched_from_start`]: fn@is_touched_from_start
369/// [`is_cap_pointer_pressed`]: fn@is_cap_pointer_pressed
370#[property(EVENT)]
371pub fn is_pointer_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
372    let pressed = var_state();
373    let child = is_mouse_pressed(child, pressed.clone());
374
375    let touched = var_state();
376    let child = is_touched_from_start(child, touched.clone());
377
378    bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
379}
380
381/// If [`is_mouse_pressed`], [`is_touched_from_start`] or [`is_shortcut_pressed`].
382///
383/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pressed`] to
384/// include the captured state.
385///
386/// [`is_mouse_pressed`]: fn@is_mouse_pressed
387/// [`is_touched_from_start`]: fn@is_touched_from_start
388/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
389/// [`is_cap_pressed`]: fn@is_cap_pressed
390#[property(EVENT)]
391pub fn is_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
392    let pressed = var_state();
393    let child = is_mouse_pressed(child, pressed.clone());
394
395    let touched = var_state();
396    let child = is_touched_from_start(child, touched.clone());
397
398    let shortcut_pressed = var_state();
399    let child = is_shortcut_pressed(child, shortcut_pressed.clone());
400
401    bind_state(
402        child,
403        merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
404        state,
405    )
406}
407
408/// If [`is_cap_mouse_pressed`] or [`is_cap_touched_from_start`].
409///
410/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
411/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
412#[property(EVENT)]
413pub fn is_cap_pointer_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
414    let pressed = var_state();
415    let child = is_cap_mouse_pressed(child, pressed.clone());
416
417    let touched = var_state();
418    let child = is_cap_touched_from_start(child, touched.clone());
419
420    bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
421}
422
423/// If [`is_cap_mouse_pressed`], [`is_cap_touched_from_start`] or [`is_shortcut_pressed`].
424///
425/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
426/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
427/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
428#[property(EVENT)]
429pub fn is_cap_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
430    let pressed = var_state();
431    let child = is_cap_mouse_pressed(child, pressed.clone());
432
433    let touched = var_state();
434    let child = is_cap_touched_from_start(child, touched.clone());
435
436    let shortcut_pressed = var_state();
437    let child = is_shortcut_pressed(child, pressed.clone());
438
439    bind_state(
440        child,
441        merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
442        state,
443    )
444}
445
446/// If the mouse pointer moved over or interacted with the widget within a time duration defined by contextual [`mouse_active_config`].
447///
448/// This property is useful for implementing things like a media player widget, where the mouse cursor and controls vanish
449/// after the mouse stops moving for a time.
450///
451/// See also [`is_pointer_active`] for an aggregate gesture that covers mouse and touch.
452///
453/// [`mouse_active_config`]: fn@mouse_active_config
454/// [`is_pointer_active`]: fn@is_pointer_active
455#[property(EVENT)]
456pub fn is_mouse_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
457    let state = state.into_var();
458    match_node(child, move |_, op| match op {
459        UiNodeOp::Init => {
460            let mut timer = None::<TimerVar>;
461            let cfg = MOUSE_ACTIVE_CONFIG_VAR.current_context();
462
463            // variable set by mouse events when mouse activity is observed
464            let activate = var(Wrapping(0u8));
465            let mut last = u8::MAX;
466            activate
467                .hook(clmv!(cfg, state, |args| {
468                    let cfg = cfg.get();
469                    if cfg.duration == Duration::ZERO {
470                        if timer.take().is_some() {
471                            // just disabled
472                            state.set(false);
473                        }
474                        return true;
475                    }
476
477                    let n = args.value().0;
478                    let is_activate = last != n;
479                    last = n;
480
481                    if is_activate {
482                        if let Some(t) = &timer {
483                            t.with(|t| {
484                                if t.count() > 0 {
485                                    // activate again
486                                    state.set(true);
487                                    t.set_count(0);
488                                }
489                                // update cfg if needed
490                                t.set_interval(cfg.duration);
491                                // restart or reset running timer
492                                t.play(true);
493                            });
494                        } else {
495                            state.set(true);
496                            // start timer that will disable the state on elapsed and pause,
497                            // the timer is reused on subsequent activations
498                            let t = TIMERS.interval(cfg.duration, true);
499                            t.hook(clmv!(state, |t| {
500                                let t = t.value();
501                                if t.count() > 0 {
502                                    t.pause();
503                                    state.set(false);
504                                }
505                                true
506                            }))
507                            .perm();
508                            t.with(|t| t.play(false));
509                            timer = Some(t);
510                        }
511                    }
512
513                    true
514                }))
515                .perm();
516
517            let id = WIDGET.id();
518
519            // activate on mouse move >= cfg.area
520            let mut first_pos = None::<DipPoint>;
521            let handle = MOUSE_MOVE_EVENT.hook(clmv!(activate, cfg, |args| {
522                if args.target.contains(id) {
523                    let dist = if let Some(prev_pos) = first_pos {
524                        (prev_pos - args.position).abs()
525                    } else {
526                        first_pos = Some(args.position);
527                        DipVector::zero()
528                    };
529                    let cfg = cfg.get();
530                    if dist.x >= cfg.area.width || dist.y >= cfg.area.height {
531                        activate.modify(|c| **c += 1);
532                    }
533                } else {
534                    first_pos = None;
535                }
536                true
537            }));
538            // activate on mouse wheel
539            let handle = MOUSE_WHEEL_EVENT.hook(clmv!(activate, |args| {
540                let _hold = &handle;
541                if args.target.contains(id) {
542                    activate.modify(|c| **c += 1);
543                }
544                true
545            }));
546            // activate on mouse input
547            let handle = MOUSE_INPUT_EVENT.hook(clmv!(activate, |args| {
548                let _hold = &handle;
549                if args.target.contains(id) {
550                    activate.modify(|c| **c += 1);
551                }
552                true
553            }));
554            // update timer interval
555            let handle = cfg.hook(move |_| {
556                let _hold = &handle;
557                activate.update();
558                true
559            });
560            WIDGET.push_var_handle(handle);
561        }
562        UiNodeOp::Deinit => {
563            if state.get() {
564                state.set(false);
565            }
566        }
567        _ => {}
568    })
569}
570
571/// Contextual configuration for [`is_mouse_active`].
572///
573/// Note that the [`MouseActiveConfig`] converts from duration, so you can set this to a time *literal*, like `5.secs()`, directly.
574///
575/// This property sets the [`MOUSE_ACTIVE_CONFIG_VAR`].
576///
577/// [`is_mouse_active`]: fn@is_mouse_active
578#[property(CONTEXT, default(MOUSE_ACTIVE_CONFIG_VAR))]
579pub fn mouse_active_config(child: impl IntoUiNode, config: impl IntoVar<MouseActiveConfig>) -> UiNode {
580    with_context_var(child, MOUSE_ACTIVE_CONFIG_VAR, config)
581}
582
583/// If an unhandled touch tap has happened on the widget within a time duration defined by contextual [`touch_active_config`].
584///
585/// This property is the touch equivalent to [`is_mouse_active`].
586///
587/// [`touch_active_config`]: fn@touch_active_config
588/// [`is_mouse_active`]: fn@is_mouse_active
589#[property(EVENT - 1)]
590pub fn is_touch_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
591    let state = state.into_var();
592    enum State {
593        False,
594        True(TimerVar),
595    }
596    let mut raw_state = State::False;
597    match_node(child, move |c, op| match op {
598        UiNodeOp::Init => {
599            validate_getter_var(&state);
600            WIDGET.sub_event(&TOUCH_TAP_EVENT).sub_var(&TOUCH_ACTIVE_CONFIG_VAR);
601            state.set(false);
602        }
603        UiNodeOp::Deinit => {
604            state.set(false);
605            raw_state = State::False;
606        }
607        UiNodeOp::Update { updates } => {
608            c.update(updates);
609
610            if TOUCH_TAP_EVENT.has_update(false) {
611                match &raw_state {
612                    State::False => {
613                        let t = TOUCH_ACTIVE_CONFIG_VAR.get().duration;
614                        let timer = TIMERS.interval(t, false);
615                        timer.subscribe(UpdateOp::Update, WIDGET.id()).perm();
616                        state.set(true);
617                        raw_state = State::True(timer);
618                    }
619                    State::True(timer) => {
620                        let cfg = TOUCH_ACTIVE_CONFIG_VAR.get();
621                        if cfg.toggle {
622                            state.set(false);
623                            timer.get().stop();
624                        } else {
625                            timer.get().play(true);
626                        }
627                    }
628                }
629            }
630
631            if let State::True(timer) = &raw_state {
632                if let Some(timer) = timer.get_new() {
633                    timer.stop();
634                    state.set(false);
635                    raw_state = State::False;
636                } else if let Some(cfg) = TOUCH_ACTIVE_CONFIG_VAR.get_new() {
637                    timer.get().set_interval(cfg.duration);
638                }
639            }
640        }
641        _ => {}
642    })
643}
644
645/// Contextual configuration for [`is_touch_active`].
646///
647/// Note that the [`TouchActiveConfig`] converts from duration, so you can set this to a time *literal*, like `5.secs()`, directly.
648///
649/// This property sets the [`MOUSE_ACTIVE_CONFIG_VAR`].
650///
651/// [`is_touch_active`]: fn@is_touch_active
652#[property(CONTEXT, default(TOUCH_ACTIVE_CONFIG_VAR))]
653pub fn touch_active_config(child: impl IntoUiNode, config: impl IntoVar<TouchActiveConfig>) -> UiNode {
654    with_context_var(child, TOUCH_ACTIVE_CONFIG_VAR, config)
655}
656
657/// If [`is_mouse_active`] or [`is_touch_active`].
658///
659/// [`is_mouse_active`]: fn@is_mouse_active
660/// [`is_touch_active`]: fn@is_touch_active
661#[property(EVENT)]
662pub fn is_pointer_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
663    let mouse_active = var_state();
664    let child = is_mouse_active(child, mouse_active.clone());
665
666    let touch_active = var_state();
667    let child = is_touch_active(child, touch_active.clone());
668
669    bind_state(
670        child,
671        expr_var! {
672            *#{mouse_active} || *#{touch_active}
673        },
674        state,
675    )
676}
677
678context_var! {
679    /// Configuration for [`is_mouse_active`].
680    ///
681    /// [`is_mouse_active`]: fn@is_mouse_active
682    pub static MOUSE_ACTIVE_CONFIG_VAR: MouseActiveConfig = MouseActiveConfig::default();
683    /// Configuration for [`is_touch_active`].
684    ///
685    /// [`is_touch_active`]: fn@is_touch_active
686    pub static TOUCH_ACTIVE_CONFIG_VAR: TouchActiveConfig = TouchActiveConfig::default();
687}
688
689/// Configuration for mouse active property.
690///
691/// See [`mouse_active_config`] for more details.
692///
693/// [`mouse_active_config`]: fn@mouse_active_config
694#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
695#[non_exhaustive]
696pub struct MouseActiveConfig {
697    /// Maximum time the state remains active after mouse leave or stops moving.
698    pub duration: Duration,
699    /// Minimum distance the pointer must move before state changes to active.
700    pub area: DipSize,
701}
702impl Default for MouseActiveConfig {
703    /// `(3s, 1)`
704    fn default() -> Self {
705        Self {
706            duration: 3.secs(),
707            area: DipSize::splat(Dip::new(1)),
708        }
709    }
710}
711impl_from_and_into_var! {
712    fn from(duration: Duration) -> MouseActiveConfig {
713        MouseActiveConfig {
714            duration,
715            ..Default::default()
716        }
717    }
718
719    fn from(area: DipSize) -> MouseActiveConfig {
720        MouseActiveConfig {
721            area,
722            ..Default::default()
723        }
724    }
725
726    fn from((duration, area): (Duration, DipSize)) -> MouseActiveConfig {
727        MouseActiveConfig { duration, area }
728    }
729}
730
731/// Configuration for touch active property.
732///
733/// See [`touch_active_config`] for more details.
734#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
735#[non_exhaustive]
736pub struct TouchActiveConfig {
737    /// Maximum time the state remains active after no touch interaction.
738    pub duration: Duration,
739    /// If a second unhandled interaction deactivates.
740    pub toggle: bool,
741}
742impl Default for TouchActiveConfig {
743    /// `(3s, false)`
744    fn default() -> Self {
745        Self {
746            duration: 3.secs(),
747            toggle: false,
748        }
749    }
750}
751impl_from_and_into_var! {
752    fn from(duration: Duration) -> TouchActiveConfig {
753        TouchActiveConfig {
754            duration,
755            ..Default::default()
756        }
757    }
758
759    fn from((duration, toggle): (Duration, bool)) -> TouchActiveConfig {
760        TouchActiveConfig { duration, toggle }
761    }
762}