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#[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#[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#[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#[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 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#[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#[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 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#[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#[property(EVENT)]
291pub fn is_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
292 #[expect(clippy::mutable_key_type)] 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()); 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), }
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#[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#[property(EVENT)]
359pub fn is_cap_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
360 #[expect(clippy::mutable_key_type)] 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()); 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), }
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#[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#[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#[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#[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#[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 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 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#[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#[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#[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#[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 pub static MOUSE_ACTIVE_CONFIG_VAR: MouseActiveConfig = MouseActiveConfig::default();
693 pub static TOUCH_ACTIVE_CONFIG_VAR: TouchActiveConfig = TouchActiveConfig::default();
697}
698
699#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
705#[non_exhaustive]
706pub struct MouseActiveConfig {
707 pub duration: Duration,
709 pub area: DipSize,
711}
712impl Default for MouseActiveConfig {
713 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#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
745#[non_exhaustive]
746pub struct TouchActiveConfig {
747 pub duration: Duration,
749 pub toggle: bool,
751}
752impl Default for TouchActiveConfig {
753 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}