raui_core/widget/component/interactive/
navigation.rs

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    /// (tracked widget)
95    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 location, relative)
199    Factor(Vec2, bool),
200    /// (scroll view id or ref, factor location, relative)
201    DirectFactor(WidgetIdOrRef, Vec2, bool),
202    /// (local space units location, relative)
203    Units(Vec2, bool),
204    /// (scroll view id or ref, local space units location, relative)
205    DirectUnits(WidgetIdOrRef, Vec2, bool),
206    /// (id or ref, widget local space anchor point)
207    Widget(WidgetIdOrRef, Vec2),
208    /// (factor, content to container ratio, relative)
209    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}