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 => {
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}