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#[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#[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#[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#[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 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#[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#[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 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#[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#[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)] 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()); 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), }
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)] 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()); 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), }
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#[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#[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#[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#[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#[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#[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#[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 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 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 state.set(true);
487 }
488 t.set_interval(cfg.duration);
490 t.play(true);
492 });
493 } else {
494 state.set(true);
495 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 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 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 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 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#[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#[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#[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#[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 pub static MOUSE_ACTIVE_CONFIG_VAR: MouseActiveConfig = MouseActiveConfig::default();
682 pub static TOUCH_ACTIVE_CONFIG_VAR: TouchActiveConfig = TouchActiveConfig::default();
686}
687
688#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
694#[non_exhaustive]
695pub struct MouseActiveConfig {
696 pub duration: Duration,
698 pub area: DipSize,
700}
701impl Default for MouseActiveConfig {
702 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#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
734#[non_exhaustive]
735pub struct TouchActiveConfig {
736 pub duration: Duration,
738 pub toggle: bool,
740}
741impl Default for TouchActiveConfig {
742 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}