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)]
167pub fn is_cap_mouse_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
168 event_state2(
169 child,
170 state,
171 false,
172 MOUSE_INPUT_EVENT,
173 false,
174 |input_args| {
175 if input_args.is_primary() {
176 match input_args.state {
177 ButtonState::Pressed => {
178 if input_args.capture_allows() {
179 return Some(input_args.target.contains_enabled(WIDGET.id()));
180 }
181 }
182 ButtonState::Released => return Some(false),
183 }
184 }
185 None
186 },
187 POINTER_CAPTURE_EVENT,
188 false,
189 |cap_args| {
190 if cap_args.is_got(WIDGET.id()) {
191 Some(true)
192 } else if cap_args.is_lost(WIDGET.id()) {
193 Some(false)
194 } else {
195 None
196 }
197 },
198 |is_down, is_captured| Some(is_down || is_captured),
199 )
200}
201
202#[property(EVENT)]
206pub fn is_shortcut_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
207 let state = state.into_var();
208 let mut shortcut_press = None;
209
210 match_node(child, move |child, op| match op {
211 UiNodeOp::Init => {
212 state.set(false);
213 WIDGET.sub_event(&CLICK_EVENT);
214 }
215 UiNodeOp::Deinit => {
216 state.set(false);
217 }
218 UiNodeOp::Event { update } => {
219 if let Some(args) = CLICK_EVENT.on(update)
220 && (args.is_from_keyboard() || args.is_from_access())
221 && args.target.contains_enabled(WIDGET.id())
222 {
223 if shortcut_press.take().is_none() {
227 let duration = GESTURES.shortcut_pressed_duration().get();
228 if duration != Duration::default() {
229 let dl = TIMERS.deadline(duration);
230 dl.subscribe(UpdateOp::Update, WIDGET.id()).perm();
231 shortcut_press = Some(dl);
232 state.set(true);
233 }
234 } else {
235 state.set(false);
236 }
237 }
238 }
239 UiNodeOp::Update { updates } => {
240 child.update(updates);
241
242 if let Some(timer) = &shortcut_press
243 && timer.is_new()
244 {
245 shortcut_press = None;
246 state.set(false);
247 }
248 }
249 _ => {}
250 })
251}
252
253#[property(EVENT)]
267pub fn is_touched(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
268 event_state(child, state, false, TOUCHED_EVENT, |args| {
269 if args.is_touch_enter_enabled() {
270 Some(true)
271 } else if args.is_touch_leave_enabled() {
272 Some(false)
273 } else {
274 None
275 }
276 })
277}
278
279#[property(EVENT)]
289pub fn is_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
290 #[expect(clippy::mutable_key_type)] let mut touches_started = HashSet::new();
292 event_state(child, state, false, TOUCHED_EVENT, move |args| {
293 if args.is_touch_enter_enabled() {
294 match args.phase {
295 TouchPhase::Start => {
296 touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); touches_started.insert(args.touch_propagation.clone());
298 Some(true)
299 }
300 TouchPhase::Move => Some(touches_started.contains(&args.touch_propagation)),
301 TouchPhase::End | TouchPhase::Cancel => Some(false), }
303 } else if args.is_touch_leave_enabled() {
304 if let TouchPhase::End | TouchPhase::Cancel = args.phase {
305 touches_started.remove(&args.touch_propagation);
306 }
307 Some(false)
308 } else {
309 None
310 }
311 })
312}
313
314#[property(EVENT)]
320pub fn is_cap_touched(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
321 event_state2(
322 child,
323 state,
324 false,
325 TOUCHED_EVENT,
326 false,
327 |hovered_args| {
328 if hovered_args.is_touch_enter_enabled() {
329 Some(true)
330 } else if hovered_args.is_touch_leave_enabled() {
331 Some(false)
332 } else {
333 None
334 }
335 },
336 POINTER_CAPTURE_EVENT,
337 false,
338 |cap_args| {
339 if cap_args.is_got(WIDGET.id()) {
340 Some(true)
341 } else if cap_args.is_lost(WIDGET.id()) {
342 Some(false)
343 } else {
344 None
345 }
346 },
347 |hovered, captured| Some(hovered || captured),
348 )
349}
350
351#[property(EVENT)]
357pub fn is_cap_touched_from_start(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
358 #[expect(clippy::mutable_key_type)] let mut touches_started = HashSet::new();
360 event_state2(
361 child,
362 state,
363 false,
364 TOUCHED_EVENT,
365 false,
366 move |hovered_args| {
367 if hovered_args.is_touch_enter_enabled() {
368 match hovered_args.phase {
369 TouchPhase::Start => {
370 touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); touches_started.insert(hovered_args.touch_propagation.clone());
372 Some(true)
373 }
374 TouchPhase::Move => Some(touches_started.contains(&hovered_args.touch_propagation)),
375 TouchPhase::End | TouchPhase::Cancel => Some(false), }
377 } else if hovered_args.is_touch_leave_enabled() {
378 if let TouchPhase::End | TouchPhase::Cancel = hovered_args.phase {
379 touches_started.remove(&hovered_args.touch_propagation);
380 }
381 Some(false)
382 } else {
383 None
384 }
385 },
386 POINTER_CAPTURE_EVENT,
387 false,
388 |cap_args| {
389 if cap_args.is_got(WIDGET.id()) {
390 Some(true)
391 } else if cap_args.is_lost(WIDGET.id()) {
392 Some(false)
393 } else {
394 None
395 }
396 },
397 |hovered, captured| Some(hovered || captured),
398 )
399}
400
401#[property(EVENT)]
410pub fn is_pointer_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
411 let pressed = var_state();
412 let child = is_mouse_pressed(child, pressed.clone());
413
414 let touched = var_state();
415 let child = is_touched_from_start(child, touched.clone());
416
417 bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
418}
419
420#[property(EVENT)]
430pub fn is_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
431 let pressed = var_state();
432 let child = is_mouse_pressed(child, pressed.clone());
433
434 let touched = var_state();
435 let child = is_touched_from_start(child, touched.clone());
436
437 let shortcut_pressed = var_state();
438 let child = is_shortcut_pressed(child, shortcut_pressed.clone());
439
440 bind_state(
441 child,
442 merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
443 state,
444 )
445}
446
447#[property(EVENT)]
452pub fn is_cap_pointer_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
453 let pressed = var_state();
454 let child = is_cap_mouse_pressed(child, pressed.clone());
455
456 let touched = var_state();
457 let child = is_cap_touched_from_start(child, touched.clone());
458
459 bind_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
460}
461
462#[property(EVENT)]
468pub fn is_cap_pressed(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
469 let pressed = var_state();
470 let child = is_cap_mouse_pressed(child, pressed.clone());
471
472 let touched = var_state();
473 let child = is_cap_touched_from_start(child, touched.clone());
474
475 let shortcut_pressed = var_state();
476 let child = is_shortcut_pressed(child, pressed.clone());
477
478 bind_state(
479 child,
480 merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
481 state,
482 )
483}
484
485#[property(EVENT)]
495pub fn is_mouse_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
496 let state = state.into_var();
497 enum State {
498 False,
499 Maybe(DipPoint),
500 True(DipPoint, TimerVar),
501 }
502 let mut raw_state = State::False;
503 match_node(child, move |_, op| match op {
504 UiNodeOp::Init => {
505 validate_getter_var(&state);
506 WIDGET.sub_event(&MOUSE_MOVE_EVENT).sub_var(&MOUSE_ACTIVE_CONFIG_VAR);
507 state.set(true);
508 }
509 UiNodeOp::Deinit => {
510 state.set(false);
511 raw_state = State::False;
512 }
513 UiNodeOp::Event { update } => {
514 let mut start = None;
515 if let Some(args) = MOUSE_MOVE_EVENT.on(update) {
516 match &mut raw_state {
517 State::False => {
518 let cfg = MOUSE_ACTIVE_CONFIG_VAR.get();
519 if cfg.area.width <= Dip::new(1) || cfg.area.height <= Dip::new(1) {
520 start = Some((cfg.duration, args.position));
521 } else {
522 raw_state = State::Maybe(args.position);
523 }
524 }
525 State::Maybe(s) => {
526 let cfg = MOUSE_ACTIVE_CONFIG_VAR.get();
527 if (args.position.x - s.x).abs() >= cfg.area.width || (args.position.y - s.y).abs() >= cfg.area.height {
528 start = Some((cfg.duration, args.position));
529 }
530 }
531 State::True(p, timer) => {
532 if (args.position.x - p.x).abs() >= Dip::new(1) || (args.position.y - p.y).abs() >= Dip::new(1) {
533 timer.get().play(true);
535 *p = args.position;
536 }
537 }
538 }
539 } else {
540 let pos = if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
541 Some(args.position)
542 } else {
543 MOUSE_WHEEL_EVENT.on(update).map(|args| args.position)
544 };
545 if let Some(pos) = pos {
546 match &raw_state {
547 State::True(_, timer) => {
548 timer.get().play(true);
550 }
551 _ => {
552 start = Some((MOUSE_ACTIVE_CONFIG_VAR.get().duration, pos));
553 }
554 }
555 }
556 }
557 if let Some((t, pos)) = start {
558 let timer = TIMERS.interval(t, false);
559 timer.subscribe(UpdateOp::Update, WIDGET.id()).perm();
560 state.set(true);
561 raw_state = State::True(pos, timer);
562 }
563 }
564 UiNodeOp::Update { .. } => {
565 if let State::True(_, timer) = &raw_state {
566 if let Some(timer) = timer.get_new() {
567 timer.stop();
568 state.set(false);
569 raw_state = State::False;
570 } else if let Some(cfg) = MOUSE_ACTIVE_CONFIG_VAR.get_new() {
571 timer.get().set_interval(cfg.duration);
572 }
573 }
574 }
575 _ => {}
576 })
577}
578
579#[property(CONTEXT, default(MOUSE_ACTIVE_CONFIG_VAR))]
587pub fn mouse_active_config(child: impl IntoUiNode, config: impl IntoVar<MouseActiveConfig>) -> UiNode {
588 with_context_var(child, MOUSE_ACTIVE_CONFIG_VAR, config)
589}
590
591#[property(EVENT)]
598pub fn is_touch_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
599 let state = state.into_var();
600 enum State {
601 False,
602 True(TimerVar),
603 }
604 let mut raw_state = State::False;
605 match_node(child, move |c, op| match op {
606 UiNodeOp::Init => {
607 validate_getter_var(&state);
608 WIDGET.sub_event(&TOUCH_TAP_EVENT).sub_var(&TOUCH_ACTIVE_CONFIG_VAR);
609 state.set(false);
610 }
611 UiNodeOp::Deinit => {
612 state.set(false);
613 raw_state = State::False;
614 }
615 UiNodeOp::Event { update } => {
616 c.event(update);
617 if TOUCH_TAP_EVENT.on_unhandled(update).is_some() {
618 match &raw_state {
619 State::False => {
620 let t = TOUCH_ACTIVE_CONFIG_VAR.get().duration;
621 let timer = TIMERS.interval(t, false);
622 timer.subscribe(UpdateOp::Update, WIDGET.id()).perm();
623 state.set(true);
624 raw_state = State::True(timer);
625 }
626 State::True(timer) => {
627 let cfg = TOUCH_ACTIVE_CONFIG_VAR.get();
628 if cfg.toggle {
629 state.set(false);
630 timer.get().stop();
631 } else {
632 timer.get().play(true);
633 }
634 }
635 }
636 }
637 }
638 UiNodeOp::Update { .. } => {
639 if let State::True(timer) = &raw_state {
640 if let Some(timer) = timer.get_new() {
641 timer.stop();
642 state.set(false);
643 raw_state = State::False;
644 } else if let Some(cfg) = TOUCH_ACTIVE_CONFIG_VAR.get_new() {
645 timer.get().set_interval(cfg.duration);
646 }
647 }
648 }
649 _ => {}
650 })
651}
652
653#[property(CONTEXT, default(TOUCH_ACTIVE_CONFIG_VAR))]
661pub fn touch_active_config(child: impl IntoUiNode, config: impl IntoVar<TouchActiveConfig>) -> UiNode {
662 with_context_var(child, TOUCH_ACTIVE_CONFIG_VAR, config)
663}
664
665#[property(EVENT)]
670pub fn is_pointer_active(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
671 let mouse_active = var_state();
672 let child = is_mouse_active(child, mouse_active.clone());
673
674 let touch_active = var_state();
675 let child = is_touch_active(child, touch_active.clone());
676
677 bind_state(
678 child,
679 expr_var! {
680 *#{mouse_active} || *#{touch_active}
681 },
682 state,
683 )
684}
685
686context_var! {
687 pub static MOUSE_ACTIVE_CONFIG_VAR: MouseActiveConfig = MouseActiveConfig::default();
691 pub static TOUCH_ACTIVE_CONFIG_VAR: TouchActiveConfig = TouchActiveConfig::default();
695}
696
697#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
703#[non_exhaustive]
704pub struct MouseActiveConfig {
705 pub duration: Duration,
707 pub area: DipSize,
709}
710impl Default for MouseActiveConfig {
711 fn default() -> Self {
713 Self {
714 duration: 3.secs(),
715 area: DipSize::splat(Dip::new(1)),
716 }
717 }
718}
719impl_from_and_into_var! {
720 fn from(duration: Duration) -> MouseActiveConfig {
721 MouseActiveConfig {
722 duration,
723 ..Default::default()
724 }
725 }
726
727 fn from(area: DipSize) -> MouseActiveConfig {
728 MouseActiveConfig {
729 area,
730 ..Default::default()
731 }
732 }
733
734 fn from((duration, area): (Duration, DipSize)) -> MouseActiveConfig {
735 MouseActiveConfig { duration, area }
736 }
737}
738
739#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
743#[non_exhaustive]
744pub struct TouchActiveConfig {
745 pub duration: Duration,
747 pub toggle: bool,
749}
750impl Default for TouchActiveConfig {
751 fn default() -> Self {
753 Self {
754 duration: 3.secs(),
755 toggle: false,
756 }
757 }
758}
759impl_from_and_into_var! {
760 fn from(duration: Duration) -> TouchActiveConfig {
761 TouchActiveConfig {
762 duration,
763 ..Default::default()
764 }
765 }
766
767 fn from((duration, toggle): (Duration, bool)) -> TouchActiveConfig {
768 TouchActiveConfig { duration, toggle }
769 }
770}