1use crate::{
2 MessageData, PropsData, Scalar, post_hooks, pre_hooks, unpack_named_slots,
3 widget::{
4 WidgetId, WidgetIdOrRef, component::containers::portal_box::PortalsContainer,
5 context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode, utils::Vec2,
6 },
7};
8use serde::{Deserialize, Serialize};
9
10#[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[props_data(crate::props::PropsData)]
12#[prefab(crate::Prefab)]
13pub struct NavAutoSelect;
14
15#[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[props_data(crate::props::PropsData)]
17#[prefab(crate::Prefab)]
18pub struct NavItemActive;
19
20#[derive(PropsData, Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
21#[props_data(crate::props::PropsData)]
22#[prefab(crate::Prefab)]
23pub struct NavTrackingActive(#[serde(default)] pub WidgetIdOrRef);
24
25#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
26#[props_data(crate::props::PropsData)]
27#[prefab(crate::Prefab)]
28pub struct NavTrackingNotifyProps(
29 #[serde(default)]
30 #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
31 pub WidgetIdOrRef,
32);
33
34#[derive(PropsData, Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
35#[props_data(crate::props::PropsData)]
36#[prefab(crate::Prefab)]
37pub struct NavTrackingProps {
38 #[serde(default)]
39 pub factor: Vec2,
40 #[serde(default)]
41 pub unscaled: Vec2,
42 #[serde(default)]
43 pub ui_space: Vec2,
44}
45
46#[derive(MessageData, Debug, Default, Clone)]
47#[message_data(crate::messenger::MessageData)]
48pub struct NavTrackingNotifyMessage {
49 pub sender: WidgetId,
50 pub state: NavTrackingProps,
51 pub prev: NavTrackingProps,
52}
53
54impl NavTrackingNotifyMessage {
55 pub fn pointer_delta_factor(&self) -> Vec2 {
56 Vec2 {
57 x: self.state.factor.x - self.prev.factor.x,
58 y: self.state.factor.y - self.prev.factor.y,
59 }
60 }
61
62 pub fn pointer_delta_unscaled(&self) -> Vec2 {
63 Vec2 {
64 x: self.state.unscaled.x - self.prev.unscaled.x,
65 y: self.state.unscaled.y - self.prev.unscaled.y,
66 }
67 }
68
69 pub fn pointer_delta_ui_space(&self) -> Vec2 {
70 Vec2 {
71 x: self.state.ui_space.x - self.prev.ui_space.x,
72 y: self.state.ui_space.y - self.prev.ui_space.y,
73 }
74 }
75
76 pub fn pointer_moved(&self) -> bool {
77 (self.state.factor.x - self.prev.factor.x) + (self.state.factor.y - self.prev.factor.y)
78 > 1.0e-6
79 }
80}
81
82#[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[props_data(crate::props::PropsData)]
84#[prefab(crate::Prefab)]
85pub struct NavLockingActive;
86
87#[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[props_data(crate::props::PropsData)]
89#[prefab(crate::Prefab)]
90pub struct NavContainerActive;
91
92#[derive(PropsData, Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[props_data(crate::props::PropsData)]
94#[prefab(crate::Prefab)]
95pub struct NavContainerDesiredSelection(#[serde(default)] pub WidgetIdOrRef);
96
97#[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[props_data(crate::props::PropsData)]
99#[prefab(crate::Prefab)]
100pub struct NavJumpActive(#[serde(default)] pub NavJumpMode);
101
102#[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[props_data(crate::props::PropsData)]
104#[prefab(crate::Prefab)]
105pub struct NavJumpLooped;
106
107#[derive(Debug, Clone, PartialEq)]
108pub enum NavType {
109 Container,
110 Item,
111 Button,
112 TextInput,
113 ScrollView,
114 ScrollViewContent,
115 Tracking(WidgetIdOrRef),
117}
118
119#[derive(MessageData, Debug, Default, Clone)]
120#[message_data(crate::messenger::MessageData)]
121pub enum NavSignal {
122 #[default]
123 None,
124 Register(NavType),
125 Unregister(NavType),
126 Select(WidgetIdOrRef),
127 Unselect,
128 Lock,
129 Unlock,
130 Accept(bool),
131 Context(bool),
132 Cancel(bool),
133 Up,
134 Down,
135 Left,
136 Right,
137 Prev,
138 Next,
139 Jump(NavJump),
140 FocusTextInput(WidgetIdOrRef),
141 TextChange(NavTextChange),
142 Axis(String, Scalar),
143 Custom(WidgetIdOrRef, String),
144}
145
146#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub enum NavJumpMode {
148 #[default]
149 Direction,
150 StepHorizontal,
151 StepVertical,
152 StepPages,
153}
154
155#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
156#[props_data(crate::props::PropsData)]
157#[prefab(crate::Prefab)]
158pub struct NavJumpMapProps {
159 #[serde(default)]
160 #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
161 pub up: WidgetIdOrRef,
162 #[serde(default)]
163 #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
164 pub down: WidgetIdOrRef,
165 #[serde(default)]
166 #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
167 pub left: WidgetIdOrRef,
168 #[serde(default)]
169 #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
170 pub right: WidgetIdOrRef,
171 #[serde(default)]
172 #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
173 pub prev: WidgetIdOrRef,
174 #[serde(default)]
175 #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
176 pub next: WidgetIdOrRef,
177}
178
179#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub enum NavDirection {
181 #[default]
182 None,
183 Up,
184 Down,
185 Left,
186 Right,
187 Prev,
188 Next,
189}
190
191#[derive(Debug, Clone)]
192pub enum NavJump {
193 First,
194 Last,
195 TopLeft,
196 TopRight,
197 BottomLeft,
198 BottomRight,
199 MiddleCenter,
200 Loop(NavDirection),
201 Escape(NavDirection, WidgetIdOrRef),
202 Scroll(NavScroll),
203}
204
205#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub enum NavTextChange {
207 InsertCharacter(char),
208 MoveCursorLeft,
209 MoveCursorRight,
210 MoveCursorStart,
211 MoveCursorEnd,
212 DeleteLeft,
213 DeleteRight,
214 NewLine,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub enum NavScroll {
219 Factor(Vec2, bool),
221 DirectFactor(WidgetIdOrRef, Vec2, bool),
223 Units(Vec2, bool),
225 DirectUnits(WidgetIdOrRef, Vec2, bool),
227 Widget(WidgetIdOrRef, Vec2),
229 Change(Vec2, Vec2, bool),
231}
232
233pub fn use_nav_container(context: &mut WidgetContext) {
234 context.life_cycle.mount(|context| {
235 if context.props.has::<NavContainerActive>() {
236 context
237 .signals
238 .write(NavSignal::Register(NavType::Container));
239 }
240 if context.props.has::<NavAutoSelect>() {
241 context
242 .signals
243 .write(NavSignal::Select(context.id.to_owned().into()));
244 }
245 });
246
247 context.life_cycle.unmount(|context| {
248 context
249 .signals
250 .write(NavSignal::Unregister(NavType::Container));
251 });
252
253 context.life_cycle.change(move |context| {
254 if let Ok(props) = context.props.read::<NavContainerDesiredSelection>() {
255 for msg in context.messenger.messages {
256 if let Some(NavSignal::Select(idref)) = msg.as_any().downcast_ref::<NavSignal>()
257 && idref.read().map(|id| &id == context.id).unwrap_or_default()
258 {
259 context.signals.write(NavSignal::Select(props.0.to_owned()));
260 }
261 }
262 }
263 });
264}
265
266#[post_hooks(use_nav_container)]
267pub fn use_nav_container_active(context: &mut WidgetContext) {
268 context.props.write(NavContainerActive);
269}
270
271pub fn use_nav_jump_map(context: &mut WidgetContext) {
272 if !context.props.has::<NavJumpActive>() {
273 return;
274 }
275
276 context.life_cycle.change(|context| {
277 let jump = match context.props.read::<NavJumpMapProps>() {
278 Ok(jump) => jump,
279 _ => return,
280 };
281 for msg in context.messenger.messages {
282 if let Some(msg) = msg.as_any().downcast_ref() {
283 match msg {
284 NavSignal::Up => {
285 if jump.up.is_some() {
286 context.signals.write(NavSignal::Select(jump.up.to_owned()));
287 }
288 }
289 NavSignal::Down => {
290 if jump.down.is_some() {
291 context
292 .signals
293 .write(NavSignal::Select(jump.down.to_owned()));
294 }
295 }
296 NavSignal::Left => {
297 if jump.left.is_some() {
298 context
299 .signals
300 .write(NavSignal::Select(jump.left.to_owned()));
301 }
302 }
303 NavSignal::Right => {
304 if jump.right.is_some() {
305 context
306 .signals
307 .write(NavSignal::Select(jump.right.to_owned()));
308 }
309 }
310 NavSignal::Prev => {
311 if jump.prev.is_some() {
312 context
313 .signals
314 .write(NavSignal::Select(jump.prev.to_owned()));
315 }
316 }
317 NavSignal::Next if jump.next.is_some() => {
318 context
319 .signals
320 .write(NavSignal::Select(jump.next.to_owned()));
321 }
322 _ => {}
323 }
324 }
325 }
326 });
327}
328
329pub fn use_nav_jump(context: &mut WidgetContext) {
330 context.life_cycle.change(|context| {
331 let mode = match context.props.read::<NavJumpActive>() {
332 Ok(data) => data.0,
333 Err(_) => return,
334 };
335 let looped = context.props.has::<NavJumpLooped>();
336 let jump = context.props.read_cloned_or_default::<NavJumpMapProps>();
337 for msg in context.messenger.messages {
338 if let Some(msg) = msg.as_any().downcast_ref() {
339 match (mode, msg) {
340 (NavJumpMode::Direction, NavSignal::Up) => {
341 if looped {
342 context
343 .signals
344 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Up)));
345 } else {
346 context.signals.write(NavSignal::Jump(NavJump::Escape(
347 NavDirection::Up,
348 jump.up.to_owned(),
349 )));
350 }
351 }
352 (NavJumpMode::Direction, NavSignal::Down) => {
353 if looped {
354 context
355 .signals
356 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Down)));
357 } else {
358 context.signals.write(NavSignal::Jump(NavJump::Escape(
359 NavDirection::Down,
360 jump.down.to_owned(),
361 )));
362 }
363 }
364 (NavJumpMode::Direction, NavSignal::Left) => {
365 if looped {
366 context
367 .signals
368 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Left)));
369 } else {
370 context.signals.write(NavSignal::Jump(NavJump::Escape(
371 NavDirection::Left,
372 jump.left.to_owned(),
373 )));
374 }
375 }
376 (NavJumpMode::Direction, NavSignal::Right) => {
377 if looped {
378 context
379 .signals
380 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Right)));
381 } else {
382 context.signals.write(NavSignal::Jump(NavJump::Escape(
383 NavDirection::Right,
384 jump.right.to_owned(),
385 )));
386 }
387 }
388 (NavJumpMode::StepHorizontal, NavSignal::Left) => {
389 if looped {
390 context
391 .signals
392 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev)));
393 } else {
394 context.signals.write(NavSignal::Jump(NavJump::Escape(
395 NavDirection::Prev,
396 jump.left.to_owned(),
397 )));
398 }
399 }
400 (NavJumpMode::StepHorizontal, NavSignal::Right) => {
401 if looped {
402 context
403 .signals
404 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next)));
405 } else {
406 context.signals.write(NavSignal::Jump(NavJump::Escape(
407 NavDirection::Next,
408 jump.right.to_owned(),
409 )));
410 }
411 }
412 (NavJumpMode::StepVertical, NavSignal::Up) => {
413 if looped {
414 context
415 .signals
416 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev)));
417 } else {
418 context.signals.write(NavSignal::Jump(NavJump::Escape(
419 NavDirection::Prev,
420 jump.up.to_owned(),
421 )));
422 }
423 }
424 (NavJumpMode::StepVertical, NavSignal::Down) => {
425 if looped {
426 context
427 .signals
428 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next)));
429 } else {
430 context.signals.write(NavSignal::Jump(NavJump::Escape(
431 NavDirection::Next,
432 jump.down.to_owned(),
433 )));
434 }
435 }
436 (NavJumpMode::StepPages, NavSignal::Prev) => {
437 if looped {
438 context
439 .signals
440 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev)));
441 } else {
442 context.signals.write(NavSignal::Jump(NavJump::Escape(
443 NavDirection::Prev,
444 jump.prev.to_owned(),
445 )));
446 }
447 }
448 (NavJumpMode::StepPages, NavSignal::Next) => {
449 if looped {
450 context
451 .signals
452 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next)));
453 } else {
454 context.signals.write(NavSignal::Jump(NavJump::Escape(
455 NavDirection::Next,
456 jump.next.to_owned(),
457 )));
458 }
459 }
460 _ => {}
461 }
462 }
463 }
464 });
465}
466
467#[post_hooks(use_nav_jump)]
468pub fn use_nav_jump_direction_active(context: &mut WidgetContext) {
469 context.props.write(NavJumpActive(NavJumpMode::Direction));
470}
471
472#[post_hooks(use_nav_jump)]
473pub fn use_nav_jump_horizontal_step_active(context: &mut WidgetContext) {
474 context
475 .props
476 .write(NavJumpActive(NavJumpMode::StepHorizontal));
477}
478
479#[post_hooks(use_nav_jump)]
480pub fn use_nav_jump_vertical_step_active(context: &mut WidgetContext) {
481 context
482 .props
483 .write(NavJumpActive(NavJumpMode::StepVertical));
484}
485
486#[post_hooks(use_nav_jump)]
487pub fn use_nav_jump_step_pages_active(context: &mut WidgetContext) {
488 context.props.write(NavJumpActive(NavJumpMode::StepPages));
489}
490
491pub fn use_nav_item(context: &mut WidgetContext) {
492 context.life_cycle.mount(|context| {
493 if context.props.has::<NavItemActive>() {
494 context.signals.write(NavSignal::Register(NavType::Item));
495 }
496 if context.props.has::<NavAutoSelect>() {
497 context
498 .signals
499 .write(NavSignal::Select(context.id.to_owned().into()));
500 }
501 });
502
503 context.life_cycle.unmount(|context| {
504 context.signals.write(NavSignal::Unregister(NavType::Item));
505 });
506}
507
508#[post_hooks(use_nav_item)]
509pub fn use_nav_item_active(context: &mut WidgetContext) {
510 context.props.write(NavItemActive);
511}
512
513pub fn use_nav_button(context: &mut WidgetContext) {
514 context.life_cycle.mount(|context| {
515 context.signals.write(NavSignal::Register(NavType::Button));
516 });
517
518 context.life_cycle.unmount(|context| {
519 context
520 .signals
521 .write(NavSignal::Unregister(NavType::Button));
522 });
523}
524
525pub fn use_nav_tracking(context: &mut WidgetContext) {
526 context.life_cycle.mount(|context| {
527 if let Ok(tracking) = context.props.read::<NavTrackingActive>() {
528 context
529 .signals
530 .write(NavSignal::Register(NavType::Tracking(tracking.0.clone())));
531 let _ = context.state.write_with(NavTrackingProps::default());
532 }
533 });
534
535 context.life_cycle.unmount(|context| {
536 context
537 .signals
538 .write(NavSignal::Unregister(NavType::Tracking(Default::default())));
539 });
540
541 context.life_cycle.change(|context| {
542 if let Ok(tracking) = context.props.read::<NavTrackingActive>() {
543 if !context.state.has::<NavTrackingProps>() {
544 context
545 .signals
546 .write(NavSignal::Register(NavType::Tracking(tracking.0.clone())));
547 let _ = context.state.write_with(NavTrackingProps::default());
548 }
549 let mut dirty = false;
550 let mut data = context.state.read_cloned_or_default::<NavTrackingProps>();
551 let prev = data;
552 for msg in context.messenger.messages {
553 if let Some(NavSignal::Axis(axis, value)) = msg.as_any().downcast_ref::<NavSignal>()
554 {
555 match axis.as_str() {
556 "pointer-x" => {
557 data.factor.x = *value;
558 dirty = true;
559 }
560 "pointer-y" => {
561 data.factor.y = *value;
562 dirty = true;
563 }
564 "pointer-x-unscaled" => {
565 data.unscaled.x = *value;
566 dirty = true;
567 }
568 "pointer-y-unscaled" => {
569 data.unscaled.y = *value;
570 dirty = true;
571 }
572 "pointer-x-ui" => {
573 data.ui_space.x = *value;
574 dirty = true;
575 }
576 "pointer-y-ui" => {
577 data.ui_space.y = *value;
578 dirty = true;
579 }
580 _ => {}
581 }
582 }
583 }
584 if dirty {
585 if let Ok(NavTrackingNotifyProps(notify)) = context.props.read()
586 && let Some(to) = notify.read()
587 {
588 context.messenger.write(
589 to,
590 NavTrackingNotifyMessage {
591 sender: context.id.to_owned(),
592 state: data.to_owned(),
593 prev,
594 },
595 );
596 }
597 let _ = context.state.write_with(data);
598 }
599 } else if context.state.has::<NavTrackingProps>() {
600 context
601 .signals
602 .write(NavSignal::Unregister(NavType::Tracking(Default::default())));
603 let _ = context.state.write_without::<NavTrackingProps>();
604 }
605 });
606}
607
608#[pre_hooks(use_nav_tracking)]
609pub fn use_nav_tracking_self(context: &mut WidgetContext) {
610 context
611 .props
612 .write(NavTrackingActive(context.id.to_owned().into()));
613}
614
615#[pre_hooks(use_nav_tracking)]
616pub fn use_nav_tracking_active_portals_container(context: &mut WidgetContext) {
617 if let Ok(data) = context.shared_props.read::<PortalsContainer>() {
618 context
619 .props
620 .write(NavTrackingActive(data.0.to_owned().into()));
621 }
622}
623
624pub fn use_nav_tracking_notified_state(context: &mut WidgetContext) {
625 context.life_cycle.change(|context| {
626 for msg in context.messenger.messages {
627 if let Some(msg) = msg.as_any().downcast_ref::<NavTrackingNotifyMessage>() {
628 let _ = context.state.write_with(msg.state);
629 }
630 }
631 });
632}
633
634pub fn use_nav_locking(context: &mut WidgetContext) {
635 context.life_cycle.mount(|context| {
636 if context.props.has::<NavLockingActive>() {
637 context.signals.write(NavSignal::Lock);
638 let _ = context.state.write_with(NavLockingActive);
639 }
640 });
641
642 context.life_cycle.unmount(|context| {
643 context.signals.write(NavSignal::Unlock);
644 });
645
646 context.life_cycle.change(|context| {
647 if context.props.has::<NavLockingActive>() {
648 if !context.state.has::<NavLockingActive>() {
649 context.signals.write(NavSignal::Lock);
650 let _ = context.state.write_with(NavLockingActive);
651 }
652 } else if context.state.has::<NavLockingActive>()
653 && !context.props.has::<NavLockingActive>()
654 {
655 context.signals.write(NavSignal::Unlock);
656 let _ = context.state.write_without::<NavLockingActive>();
657 }
658 });
659}
660
661pub fn use_nav_text_input(context: &mut WidgetContext) {
662 context.life_cycle.mount(|context| {
663 context
664 .signals
665 .write(NavSignal::Register(NavType::TextInput));
666 });
667
668 context.life_cycle.unmount(|context| {
669 context
670 .signals
671 .write(NavSignal::Unregister(NavType::TextInput));
672 });
673}
674
675pub fn use_nav_scroll_view(context: &mut WidgetContext) {
676 context.life_cycle.mount(|context| {
677 context
678 .signals
679 .write(NavSignal::Register(NavType::ScrollView));
680 });
681
682 context.life_cycle.unmount(|context| {
683 context
684 .signals
685 .write(NavSignal::Unregister(NavType::ScrollView));
686 });
687}
688
689pub fn use_nav_scroll_view_content(context: &mut WidgetContext) {
690 context.life_cycle.mount(|context| {
691 context
692 .signals
693 .write(NavSignal::Register(NavType::ScrollViewContent));
694 });
695
696 context.life_cycle.unmount(|context| {
697 context
698 .signals
699 .write(NavSignal::Unregister(NavType::ScrollViewContent));
700 });
701}
702
703#[pre_hooks(use_nav_button)]
704pub fn navigation_barrier(mut context: WidgetContext) -> WidgetNode {
705 let WidgetContext {
706 id, named_slots, ..
707 } = context;
708 unpack_named_slots!(named_slots => content);
709
710 AreaBoxNode {
711 id: id.to_owned(),
712 slot: Box::new(content),
713 }
714 .into()
715}
716
717#[pre_hooks(use_nav_tracking)]
718pub fn tracking(mut context: WidgetContext) -> WidgetNode {
719 let WidgetContext {
720 id, named_slots, ..
721 } = context;
722 unpack_named_slots!(named_slots => content);
723
724 AreaBoxNode {
725 id: id.to_owned(),
726 slot: Box::new(content),
727 }
728 .into()
729}
730
731#[pre_hooks(use_nav_tracking_self)]
732pub fn self_tracking(mut context: WidgetContext) -> WidgetNode {
733 tracking(context)
734}