Skip to main content

raui_core/widget/component/interactive/
navigation.rs

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    /// (tracked widget)
116    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 location, relative)
220    Factor(Vec2, bool),
221    /// (scroll view id or ref, factor location, relative)
222    DirectFactor(WidgetIdOrRef, Vec2, bool),
223    /// (local space units location, relative)
224    Units(Vec2, bool),
225    /// (scroll view id or ref, local space units location, relative)
226    DirectUnits(WidgetIdOrRef, Vec2, bool),
227    /// (id or ref, widget local space anchor point)
228    Widget(WidgetIdOrRef, Vec2),
229    /// (factor, content to container ratio, relative)
230    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}