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 => {
318 if jump.next.is_some() {
319 context
320 .signals
321 .write(NavSignal::Select(jump.next.to_owned()));
322 }
323 }
324 _ => {}
325 }
326 }
327 }
328 });
329}
330
331pub fn use_nav_jump(context: &mut WidgetContext) {
332 context.life_cycle.change(|context| {
333 let mode = match context.props.read::<NavJumpActive>() {
334 Ok(data) => data.0,
335 Err(_) => return,
336 };
337 let looped = context.props.has::<NavJumpLooped>();
338 let jump = context.props.read_cloned_or_default::<NavJumpMapProps>();
339 for msg in context.messenger.messages {
340 if let Some(msg) = msg.as_any().downcast_ref() {
341 match (mode, msg) {
342 (NavJumpMode::Direction, NavSignal::Up) => {
343 if looped {
344 context
345 .signals
346 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Up)));
347 } else {
348 context.signals.write(NavSignal::Jump(NavJump::Escape(
349 NavDirection::Up,
350 jump.up.to_owned(),
351 )));
352 }
353 }
354 (NavJumpMode::Direction, NavSignal::Down) => {
355 if looped {
356 context
357 .signals
358 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Down)));
359 } else {
360 context.signals.write(NavSignal::Jump(NavJump::Escape(
361 NavDirection::Down,
362 jump.down.to_owned(),
363 )));
364 }
365 }
366 (NavJumpMode::Direction, NavSignal::Left) => {
367 if looped {
368 context
369 .signals
370 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Left)));
371 } else {
372 context.signals.write(NavSignal::Jump(NavJump::Escape(
373 NavDirection::Left,
374 jump.left.to_owned(),
375 )));
376 }
377 }
378 (NavJumpMode::Direction, NavSignal::Right) => {
379 if looped {
380 context
381 .signals
382 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Right)));
383 } else {
384 context.signals.write(NavSignal::Jump(NavJump::Escape(
385 NavDirection::Right,
386 jump.right.to_owned(),
387 )));
388 }
389 }
390 (NavJumpMode::StepHorizontal, NavSignal::Left) => {
391 if looped {
392 context
393 .signals
394 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev)));
395 } else {
396 context.signals.write(NavSignal::Jump(NavJump::Escape(
397 NavDirection::Prev,
398 jump.left.to_owned(),
399 )));
400 }
401 }
402 (NavJumpMode::StepHorizontal, NavSignal::Right) => {
403 if looped {
404 context
405 .signals
406 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next)));
407 } else {
408 context.signals.write(NavSignal::Jump(NavJump::Escape(
409 NavDirection::Next,
410 jump.right.to_owned(),
411 )));
412 }
413 }
414 (NavJumpMode::StepVertical, NavSignal::Up) => {
415 if looped {
416 context
417 .signals
418 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev)));
419 } else {
420 context.signals.write(NavSignal::Jump(NavJump::Escape(
421 NavDirection::Prev,
422 jump.up.to_owned(),
423 )));
424 }
425 }
426 (NavJumpMode::StepVertical, NavSignal::Down) => {
427 if looped {
428 context
429 .signals
430 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next)));
431 } else {
432 context.signals.write(NavSignal::Jump(NavJump::Escape(
433 NavDirection::Next,
434 jump.down.to_owned(),
435 )));
436 }
437 }
438 (NavJumpMode::StepPages, NavSignal::Prev) => {
439 if looped {
440 context
441 .signals
442 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev)));
443 } else {
444 context.signals.write(NavSignal::Jump(NavJump::Escape(
445 NavDirection::Prev,
446 jump.prev.to_owned(),
447 )));
448 }
449 }
450 (NavJumpMode::StepPages, NavSignal::Next) => {
451 if looped {
452 context
453 .signals
454 .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next)));
455 } else {
456 context.signals.write(NavSignal::Jump(NavJump::Escape(
457 NavDirection::Next,
458 jump.next.to_owned(),
459 )));
460 }
461 }
462 _ => {}
463 }
464 }
465 }
466 });
467}
468
469#[post_hooks(use_nav_jump)]
470pub fn use_nav_jump_direction_active(context: &mut WidgetContext) {
471 context.props.write(NavJumpActive(NavJumpMode::Direction));
472}
473
474#[post_hooks(use_nav_jump)]
475pub fn use_nav_jump_horizontal_step_active(context: &mut WidgetContext) {
476 context
477 .props
478 .write(NavJumpActive(NavJumpMode::StepHorizontal));
479}
480
481#[post_hooks(use_nav_jump)]
482pub fn use_nav_jump_vertical_step_active(context: &mut WidgetContext) {
483 context
484 .props
485 .write(NavJumpActive(NavJumpMode::StepVertical));
486}
487
488#[post_hooks(use_nav_jump)]
489pub fn use_nav_jump_step_pages_active(context: &mut WidgetContext) {
490 context.props.write(NavJumpActive(NavJumpMode::StepPages));
491}
492
493pub fn use_nav_item(context: &mut WidgetContext) {
494 context.life_cycle.mount(|context| {
495 if context.props.has::<NavItemActive>() {
496 context.signals.write(NavSignal::Register(NavType::Item));
497 }
498 if context.props.has::<NavAutoSelect>() {
499 context
500 .signals
501 .write(NavSignal::Select(context.id.to_owned().into()));
502 }
503 });
504
505 context.life_cycle.unmount(|context| {
506 context.signals.write(NavSignal::Unregister(NavType::Item));
507 });
508}
509
510#[post_hooks(use_nav_item)]
511pub fn use_nav_item_active(context: &mut WidgetContext) {
512 context.props.write(NavItemActive);
513}
514
515pub fn use_nav_button(context: &mut WidgetContext) {
516 context.life_cycle.mount(|context| {
517 context.signals.write(NavSignal::Register(NavType::Button));
518 });
519
520 context.life_cycle.unmount(|context| {
521 context
522 .signals
523 .write(NavSignal::Unregister(NavType::Button));
524 });
525}
526
527pub fn use_nav_tracking(context: &mut WidgetContext) {
528 context.life_cycle.mount(|context| {
529 if let Ok(tracking) = context.props.read::<NavTrackingActive>() {
530 context
531 .signals
532 .write(NavSignal::Register(NavType::Tracking(tracking.0.clone())));
533 let _ = context.state.write_with(NavTrackingProps::default());
534 }
535 });
536
537 context.life_cycle.unmount(|context| {
538 context
539 .signals
540 .write(NavSignal::Unregister(NavType::Tracking(Default::default())));
541 });
542
543 context.life_cycle.change(|context| {
544 if let Ok(tracking) = context.props.read::<NavTrackingActive>() {
545 if !context.state.has::<NavTrackingProps>() {
546 context
547 .signals
548 .write(NavSignal::Register(NavType::Tracking(tracking.0.clone())));
549 let _ = context.state.write_with(NavTrackingProps::default());
550 }
551 let mut dirty = false;
552 let mut data = context.state.read_cloned_or_default::<NavTrackingProps>();
553 let prev = data;
554 for msg in context.messenger.messages {
555 if let Some(NavSignal::Axis(axis, value)) = msg.as_any().downcast_ref::<NavSignal>()
556 {
557 match axis.as_str() {
558 "pointer-x" => {
559 data.factor.x = *value;
560 dirty = true;
561 }
562 "pointer-y" => {
563 data.factor.y = *value;
564 dirty = true;
565 }
566 "pointer-x-unscaled" => {
567 data.unscaled.x = *value;
568 dirty = true;
569 }
570 "pointer-y-unscaled" => {
571 data.unscaled.y = *value;
572 dirty = true;
573 }
574 "pointer-x-ui" => {
575 data.ui_space.x = *value;
576 dirty = true;
577 }
578 "pointer-y-ui" => {
579 data.ui_space.y = *value;
580 dirty = true;
581 }
582 _ => {}
583 }
584 }
585 }
586 if dirty {
587 if let Ok(NavTrackingNotifyProps(notify)) = context.props.read()
588 && let Some(to) = notify.read()
589 {
590 context.messenger.write(
591 to,
592 NavTrackingNotifyMessage {
593 sender: context.id.to_owned(),
594 state: data.to_owned(),
595 prev,
596 },
597 );
598 }
599 let _ = context.state.write_with(data);
600 }
601 } else if context.state.has::<NavTrackingProps>() {
602 context
603 .signals
604 .write(NavSignal::Unregister(NavType::Tracking(Default::default())));
605 let _ = context.state.write_without::<NavTrackingProps>();
606 }
607 });
608}
609
610#[pre_hooks(use_nav_tracking)]
611pub fn use_nav_tracking_self(context: &mut WidgetContext) {
612 context
613 .props
614 .write(NavTrackingActive(context.id.to_owned().into()));
615}
616
617#[pre_hooks(use_nav_tracking)]
618pub fn use_nav_tracking_active_portals_container(context: &mut WidgetContext) {
619 if let Ok(data) = context.shared_props.read::<PortalsContainer>() {
620 context
621 .props
622 .write(NavTrackingActive(data.0.to_owned().into()));
623 }
624}
625
626pub fn use_nav_tracking_notified_state(context: &mut WidgetContext) {
627 context.life_cycle.change(|context| {
628 for msg in context.messenger.messages {
629 if let Some(msg) = msg.as_any().downcast_ref::<NavTrackingNotifyMessage>() {
630 let _ = context.state.write_with(msg.state);
631 }
632 }
633 });
634}
635
636pub fn use_nav_locking(context: &mut WidgetContext) {
637 context.life_cycle.mount(|context| {
638 if context.props.has::<NavLockingActive>() {
639 context.signals.write(NavSignal::Lock);
640 let _ = context.state.write_with(NavLockingActive);
641 }
642 });
643
644 context.life_cycle.unmount(|context| {
645 context.signals.write(NavSignal::Unlock);
646 });
647
648 context.life_cycle.change(|context| {
649 if context.props.has::<NavLockingActive>() {
650 if !context.state.has::<NavLockingActive>() {
651 context.signals.write(NavSignal::Lock);
652 let _ = context.state.write_with(NavLockingActive);
653 }
654 } else if context.state.has::<NavLockingActive>()
655 && !context.props.has::<NavLockingActive>()
656 {
657 context.signals.write(NavSignal::Unlock);
658 let _ = context.state.write_without::<NavLockingActive>();
659 }
660 });
661}
662
663pub fn use_nav_text_input(context: &mut WidgetContext) {
664 context.life_cycle.mount(|context| {
665 context
666 .signals
667 .write(NavSignal::Register(NavType::TextInput));
668 });
669
670 context.life_cycle.unmount(|context| {
671 context
672 .signals
673 .write(NavSignal::Unregister(NavType::TextInput));
674 });
675}
676
677pub fn use_nav_scroll_view(context: &mut WidgetContext) {
678 context.life_cycle.mount(|context| {
679 context
680 .signals
681 .write(NavSignal::Register(NavType::ScrollView));
682 });
683
684 context.life_cycle.unmount(|context| {
685 context
686 .signals
687 .write(NavSignal::Unregister(NavType::ScrollView));
688 });
689}
690
691pub fn use_nav_scroll_view_content(context: &mut WidgetContext) {
692 context.life_cycle.mount(|context| {
693 context
694 .signals
695 .write(NavSignal::Register(NavType::ScrollViewContent));
696 });
697
698 context.life_cycle.unmount(|context| {
699 context
700 .signals
701 .write(NavSignal::Unregister(NavType::ScrollViewContent));
702 });
703}
704
705#[pre_hooks(use_nav_button)]
706pub fn navigation_barrier(mut context: WidgetContext) -> WidgetNode {
707 let WidgetContext {
708 id, named_slots, ..
709 } = context;
710 unpack_named_slots!(named_slots => content);
711
712 AreaBoxNode {
713 id: id.to_owned(),
714 slot: Box::new(content),
715 }
716 .into()
717}
718
719#[pre_hooks(use_nav_tracking)]
720pub fn tracking(mut context: WidgetContext) -> WidgetNode {
721 let WidgetContext {
722 id, named_slots, ..
723 } = context;
724 unpack_named_slots!(named_slots => content);
725
726 AreaBoxNode {
727 id: id.to_owned(),
728 slot: Box::new(content),
729 }
730 .into()
731}
732
733#[pre_hooks(use_nav_tracking_self)]
734pub fn self_tracking(mut context: WidgetContext) -> WidgetNode {
735 tracking(context)
736}