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