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