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.deadline().has_elapsed() {
485                                    // activate again
486                                    state.set(true);
487                                }
488                                // update cfg if needed
489                                t.set_interval(cfg.duration);
490                                // restart or reset running timer
491                                t.play(true);
492                            });
493                        } else {
494                            state.set(true);
495                            // start timer that will disable the state on elapsed and pause,
496                            // the timer is reused on subsequent activations
497                            let t = TIMERS.interval(cfg.duration, true);
498                            t.hook(clmv!(state, |t| {
499                                let t = t.value();
500                                if t.deadline().has_elapsed() {
501                                    t.pause();
502                                    state.set(false);
503                                }
504                                true
505                            }))
506                            .perm();
507                            t.with(|t| t.play(false));
508                            timer = Some(t);
509                        }
510                    }
511
512                    true
513                }))
514                .perm();
515
516            let id = WIDGET.id();
517
518            // activate on mouse move >= cfg.area
519            let mut first_pos = None::<DipPoint>;
520            let handle = MOUSE_MOVE_EVENT.hook(clmv!(activate, cfg, |args| {
521                if args.target.contains(id) {
522                    let dist = if let Some(prev_pos) = first_pos {
523                        (prev_pos - args.position).abs()
524                    } else {
525                        first_pos = Some(args.position);
526                        DipVector::zero()
527                    };
528                    let cfg = cfg.get();
529                    if dist.x >= cfg.area.width || dist.y >= cfg.area.height {
530                        activate.modify(|c| **c += 1);
531                    }
532                } else {
533                    first_pos = None;
534                }
535                true
536            }));
537            // activate on mouse wheel
538            let handle = MOUSE_WHEEL_EVENT.hook(clmv!(activate, |args| {
539                let _hold = &handle;
540                if args.target.contains(id) {
541                    activate.modify(|c| **c += 1);
542                }
543                true
544            }));
545            // activate on mouse input
546            let handle = MOUSE_INPUT_EVENT.hook(clmv!(activate, |args| {
547                let _hold = &handle;
548                if args.target.contains(id) {
549                    activate.modify(|c| **c += 1);
550                }
551                true
552            }));
553            // update timer interval
554            let handle = cfg.hook(move |_| {
555                let _hold = &handle;
556                activate.update();
557                true
558            });
559            WIDGET.push_var_handle(handle);
560        }
561        UiNodeOp::Deinit => {
562            if state.get() {
563                state.set(false);
564            }
565        }
566        _ => {}
567    })
568}
569
570/// Contextual configuration for [`is_mouse_active`].
571///
572/// Note that the [`MouseActiveConfig`] converts from duration, so you can set this to a time *literal*, like `5.secs()`, directly.
573///
574/// This property sets the [`MOUSE_ACTIVE_CONFIG_VAR`].
575///
576/// [`is_mouse_active`]: fn@is_mouse_active
577#[property(CONTEXT, default(MOUSE_ACTIVE_CONFIG_VAR))]
578pub fn mouse_active_config(child: impl IntoUiNode, config: impl IntoVar<MouseActiveConfig>) -> UiNode {
579    with_context_var(child, MOUSE_ACTIVE_CONFIG_VAR, config)
580}
581
582/// If an unhandled touch tap has happened on the widget within a time duration defined by contextual [`touch_active_config`].
583///
584/// This property is the touch equivalent to [`is_mouse_active`].
585///
586/// [`touch_active_config`]: fn@touch_active_config
587/// [`is_mouse_active`]: fn@is_mouse_active
588#[property(EVENT)]
589pub fn is_touch_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
590    let state = state.into_var();
591    enum State {
592        False,
593        True(TimerVar),
594    }
595    let mut raw_state = State::False;
596    match_node(child, move |c, op| match op {
597        UiNodeOp::Init => {
598            validate_getter_var(&state);
599            WIDGET.sub_event(&TOUCH_TAP_EVENT).sub_var(&TOUCH_ACTIVE_CONFIG_VAR);
600            state.set(false);
601        }
602        UiNodeOp::Deinit => {
603            state.set(false);
604            raw_state = State::False;
605        }
606        UiNodeOp::Update { updates } => {
607            c.update(updates);
608
609            if TOUCH_TAP_EVENT.has_update(false) {
610                match &raw_state {
611                    State::False => {
612                        let t = TOUCH_ACTIVE_CONFIG_VAR.get().duration;
613                        let timer = TIMERS.interval(t, false);
614                        timer.subscribe(UpdateOp::Update, WIDGET.id()).perm();
615                        state.set(true);
616                        raw_state = State::True(timer);
617                    }
618                    State::True(timer) => {
619                        let cfg = TOUCH_ACTIVE_CONFIG_VAR.get();
620                        if cfg.toggle {
621                            state.set(false);
622                            timer.get().stop();
623                        } else {
624                            timer.get().play(true);
625                        }
626                    }
627                }
628            }
629
630            if let State::True(timer) = &raw_state {
631                if let Some(timer) = timer.get_new() {
632                    timer.stop();
633                    state.set(false);
634                    raw_state = State::False;
635                } else if let Some(cfg) = TOUCH_ACTIVE_CONFIG_VAR.get_new() {
636                    timer.get().set_interval(cfg.duration);
637                }
638            }
639        }
640        _ => {}
641    })
642}
643
644/// Contextual configuration for [`is_touch_active`].
645///
646/// Note that the [`TouchActiveConfig`] converts from duration, so you can set this to a time *literal*, like `5.secs()`, directly.
647///
648/// This property sets the [`MOUSE_ACTIVE_CONFIG_VAR`].
649///
650/// [`is_touch_active`]: fn@is_touch_active
651#[property(CONTEXT, default(TOUCH_ACTIVE_CONFIG_VAR))]
652pub fn touch_active_config(child: impl IntoUiNode, config: impl IntoVar<TouchActiveConfig>) -> UiNode {
653    with_context_var(child, TOUCH_ACTIVE_CONFIG_VAR, config)
654}
655
656/// If [`is_mouse_active`] or [`is_touch_active`].
657///
658/// [`is_mouse_active`]: fn@is_mouse_active
659/// [`is_touch_active`]: fn@is_touch_active
660#[property(EVENT)]
661pub fn is_pointer_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
662    let mouse_active = var_state();
663    let child = is_mouse_active(child, mouse_active.clone());
664
665    let touch_active = var_state();
666    let child = is_touch_active(child, touch_active.clone());
667
668    bind_state(
669        child,
670        expr_var! {
671            *#{mouse_active} || *#{touch_active}
672        },
673        state,
674    )
675}
676
677context_var! {
678    /// Configuration for [`is_mouse_active`].
679    ///
680    /// [`is_mouse_active`]: fn@is_mouse_active
681    pub static MOUSE_ACTIVE_CONFIG_VAR: MouseActiveConfig = MouseActiveConfig::default();
682    /// Configuration for [`is_touch_active`].
683    ///
684    /// [`is_touch_active`]: fn@is_touch_active
685    pub static TOUCH_ACTIVE_CONFIG_VAR: TouchActiveConfig = TouchActiveConfig::default();
686}
687
688/// Configuration for mouse active property.
689///
690/// See [`mouse_active_config`] for more details.
691///
692/// [`mouse_active_config`]: fn@mouse_active_config
693#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
694#[non_exhaustive]
695pub struct MouseActiveConfig {
696    /// Maximum time the state remains active after mouse leave or stops moving.
697    pub duration: Duration,
698    /// Minimum distance the pointer must move before state changes to active.
699    pub area: DipSize,
700}
701impl Default for MouseActiveConfig {
702    /// `(3s, 1)`
703    fn default() -> Self {
704        Self {
705            duration: 3.secs(),
706            area: DipSize::splat(Dip::new(1)),
707        }
708    }
709}
710impl_from_and_into_var! {
711    fn from(duration: Duration) -> MouseActiveConfig {
712        MouseActiveConfig {
713            duration,
714            ..Default::default()
715        }
716    }
717
718    fn from(area: DipSize) -> MouseActiveConfig {
719        MouseActiveConfig {
720            area,
721            ..Default::default()
722        }
723    }
724
725    fn from((duration, area): (Duration, DipSize)) -> MouseActiveConfig {
726        MouseActiveConfig { duration, area }
727    }
728}
729
730/// Configuration for touch active property.
731///
732/// See [`touch_active_config`] for more details.
733#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
734#[non_exhaustive]
735pub struct TouchActiveConfig {
736    /// Maximum time the state remains active after no touch interaction.
737    pub duration: Duration,
738    /// If a second unhandled interaction deactivates.
739    pub toggle: bool,
740}
741impl Default for TouchActiveConfig {
742    /// `(3s, false)`
743    fn default() -> Self {
744        Self {
745            duration: 3.secs(),
746            toggle: false,
747        }
748    }
749}
750impl_from_and_into_var! {
751    fn from(duration: Duration) -> TouchActiveConfig {
752        TouchActiveConfig {
753            duration,
754            ..Default::default()
755        }
756    }
757
758    fn from((duration, toggle): (Duration, bool)) -> TouchActiveConfig {
759        TouchActiveConfig { duration, toggle }
760    }
761}