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