Skip to main content

repose_ui/
navigation.rs

1use crate::anim_ext::AnimatedContent;
2use crate::{Box, ViewExt};
3use repose_core::*;
4use std::any::Any;
5use std::cell::RefCell;
6use std::collections::{HashMap, VecDeque};
7use std::rc::Rc;
8
9pub struct NavController {
10    stack: RefCell<VecDeque<NavEntry>>,
11    pub current: Signal<String>,
12    pub transitions: Signal<Option<Transition>>,
13}
14
15pub struct NavEntry {
16    pub route: String,
17    pub args: HashMap<String, String>,
18    pub state: Box<dyn Any>,
19}
20
21#[derive(Clone)]
22pub enum Transition {
23    Push { from: String, to: String },
24    Pop { from: String, to: String },
25    Replace { from: String, to: String },
26}
27
28impl NavController {
29    pub fn new(initial: impl Into<String>) -> Rc<Self> {
30        let route = initial.into();
31        Rc::new(Self {
32            stack: RefCell::new({
33                let mut dq = VecDeque::new();
34                dq.push_back(NavEntry {
35                    route: route.clone(),
36                    args: HashMap::new(),
37                    state: Box::new(()),
38                });
39                dq
40            }),
41            current: signal(route),
42            transitions: signal(None),
43        })
44    }
45
46    pub fn navigate(&self, route: impl Into<String>) {
47        let route = route.into();
48        let mut stack = self.stack.borrow_mut();
49        let from = self.current.get();
50
51        stack.push_back(NavEntry {
52            route: route.clone(),
53            args: HashMap::new(),
54            state: Box::new(()),
55        });
56
57        self.transitions.set(Some(Transition::Push {
58            from,
59            to: route.clone(),
60        }));
61        self.current.set(route);
62    }
63
64    pub fn replace(&self, route: impl Into<String>) {
65        let route = route.into();
66        let mut stack = self.stack.borrow_mut();
67        let from = self.current.get();
68
69        if let Some(_last) = stack.pop_back() {
70            // drop last
71        }
72        stack.push_back(NavEntry {
73            route: route.clone(),
74            args: HashMap::new(),
75            state: Box::new(()),
76        });
77        self.transitions.set(Some(Transition::Replace {
78            from,
79            to: route.clone(),
80        }));
81        self.current.set(route);
82    }
83
84    pub fn pop(&self) -> bool {
85        let mut stack = self.stack.borrow_mut();
86        if stack.len() > 1 {
87            let from = self.current.get();
88            stack.pop_back();
89            if let Some(entry) = stack.back() {
90                self.transitions.set(Some(Transition::Pop {
91                    from,
92                    to: entry.route.clone(),
93                }));
94                self.current.set(entry.route.clone());
95                return true;
96            }
97        }
98        false
99    }
100
101    pub fn take_transition(&self) -> Option<Transition> {
102        let t = self.transitions.get();
103        self.transitions.set(None);
104        t
105    }
106}
107
108pub fn NavHost(
109    controller: Rc<NavController>,
110    routes: HashMap<String, Box<dyn Fn() -> View>>,
111) -> View {
112    let current = controller.current.get();
113    let trans = controller.take_transition();
114
115    if let Some(builder) = routes.get(&current) {
116        let page = Box(Modifier::new().fill_max_size()).child((builder)());
117        AnimatedContent(current.clone(), trans, page)
118    } else {
119        // Empty fallback still fills so layouts/scrolls get a definite size
120        Box(Modifier::new().fill_max_size())
121    }
122}