repose_navigation/
lib.rs

1#![allow(non_snake_case)]
2use std::{any::Any, cell::RefCell, fmt::Debug, rc::Rc};
3
4use repose_core::*;
5use repose_ui::{Box as VBox, Stack, ViewExt, anim::animate_f32};
6use serde::{Deserialize, Serialize};
7
8pub trait NavKey: Clone + Debug + 'static + Serialize + for<'de> Deserialize<'de> {}
9impl<T> NavKey for T where T: Clone + Debug + 'static + Serialize + for<'de> Deserialize<'de> {}
10
11#[derive(Clone, Copy, PartialEq, Eq, Debug)]
12pub enum TransitionDir {
13    None,
14    Push,
15    Pop,
16}
17
18#[derive(Default)]
19pub struct SavedState {
20    map: RefCell<std::collections::HashMap<&'static str, Box<dyn Any>>>,
21    results: RefCell<std::collections::HashMap<&'static str, Box<dyn Any>>>,
22}
23impl SavedState {
24    pub fn remember<T: 'static + Clone>(
25        &self,
26        key: &'static str,
27        init: impl FnOnce() -> T,
28    ) -> Rc<RefCell<T>> {
29        if let Some(b) = self.map.borrow().get(key) {
30            if let Some(rc) = b.downcast_ref::<Rc<RefCell<T>>>() {
31                return rc.clone();
32            }
33        }
34        let rc = Rc::new(RefCell::new(init()));
35        self.map.borrow_mut().insert(key, Box::new(rc.clone()));
36        rc
37    }
38    pub fn set_result<T: 'static>(&self, key: &'static str, val: T) {
39        self.results.borrow_mut().insert(key, Box::new(val));
40    }
41    pub fn take_result<T: 'static>(&self, key: &'static str) -> Option<T> {
42        self.results
43            .borrow_mut()
44            .remove(key)?
45            .downcast::<T>()
46            .ok()
47            .map(|b| *b)
48    }
49}
50
51struct Entry<K: NavKey> {
52    id: u64,
53    key: K,
54    saved: Rc<SavedState>,
55}
56
57struct BackState<K: NavKey> {
58    entries: Vec<Entry<K>>,
59    next_id: u64,
60    last_dir: TransitionDir,
61}
62
63#[derive(Clone)]
64pub struct NavBackStack<K: NavKey> {
65    inner: Rc<RefCell<BackState<K>>>,
66    version: Rc<Signal<u64>>,
67}
68impl<K: NavKey> NavBackStack<K> {
69    pub fn top(&self) -> Option<(u64, K, Rc<SavedState>)> {
70        let s = self.inner.borrow();
71        s.entries
72            .last()
73            .map(|e| (e.id, e.key.clone(), e.saved.clone()))
74    }
75    pub fn size(&self) -> usize {
76        self.inner.borrow().entries.len()
77    }
78    pub fn last_dir(&self) -> TransitionDir {
79        self.inner.borrow().last_dir
80    }
81    fn bump(&self) {
82        let v = self.version.get();
83        self.version.set(v.wrapping_add(1));
84    }
85
86    fn push_inner(&self, key: K) {
87        let mut s = self.inner.borrow_mut();
88        let id = s.next_id;
89        s.next_id += 1;
90        s.entries.push(Entry {
91            id,
92            key,
93            saved: Rc::new(SavedState::default()),
94        });
95        s.last_dir = TransitionDir::Push;
96    }
97    fn pop_inner(&self) -> Option<Entry<K>> {
98        let mut s = self.inner.borrow_mut();
99        s.last_dir = TransitionDir::Pop;
100        s.entries.pop()
101    }
102    fn replace_inner(&self, key: K) {
103        let mut s = self.inner.borrow_mut();
104        if let Some(last) = s.entries.last_mut() {
105            last.key = key;
106        } else {
107            let id = s.next_id;
108            s.next_id += 1;
109            s.entries.push(Entry {
110                id,
111                key,
112                saved: Rc::new(SavedState::default()),
113            });
114        }
115        s.last_dir = TransitionDir::Push;
116    }
117
118    pub fn to_json(&self) -> String
119    where
120        K: Serialize,
121    {
122        let s = self.inner.borrow();
123        let keys: Vec<&K> = s.entries.iter().map(|e| &e.key).collect();
124        serde_json::to_string(&keys).unwrap_or("[]".into())
125    }
126    pub fn from_json(&self, json: &str)
127    where
128        K: for<'de> Deserialize<'de>,
129    {
130        if let Ok(keys) = serde_json::from_str::<Vec<K>>(json) {
131            let mut s = self.inner.borrow_mut();
132            s.entries.clear();
133            for k in keys {
134                let id = s.next_id;
135                s.next_id += 1;
136                s.entries.push(Entry {
137                    id,
138                    key: k,
139                    saved: Rc::new(SavedState::default()),
140                });
141            }
142            s.last_dir = TransitionDir::None;
143            drop(s);
144            self.bump();
145        }
146    }
147}
148
149#[derive(Clone)]
150pub struct Navigator<K: NavKey> {
151    pub stack: NavBackStack<K>,
152}
153impl<K: NavKey> Navigator<K> {
154    pub fn push(&self, k: K) {
155        self.stack.push_inner(k);
156        self.stack.bump();
157    }
158    pub fn replace(&self, k: K) {
159        self.stack.replace_inner(k);
160        self.stack.bump();
161    }
162    pub fn pop(&self) -> bool {
163        // Don't pop if only one entry is present
164        if self.stack.size() <= 1 {
165            return false;
166        }
167        let ok = self.stack.pop_inner().is_some();
168        if ok {
169            self.stack.bump();
170        }
171        ok
172    }
173    pub fn clear_and_push(&self, k: K) {
174        while self.stack.pop_inner().is_some() {}
175        self.stack.push_inner(k);
176        self.stack.bump();
177    }
178    pub fn pop_to<F: Fn(&K) -> bool>(&self, pred: F, inclusive: bool) {
179        let count = {
180            let s = self.stack.inner.borrow();
181            if let Some(idx) = s.entries.iter().rposition(|e| pred(&e.key)) {
182                s.entries.len() - idx - (if inclusive { 0 } else { 1 })
183            } else {
184                0
185            }
186        };
187        for _ in 0..count {
188            self.stack.pop_inner();
189        }
190        if count > 0 {
191            self.stack.bump();
192        }
193    }
194}
195
196pub fn remember_back_stack<K: NavKey>(start: K) -> std::rc::Rc<NavBackStack<K>> {
197    remember_with_key("nav3:stack", || NavBackStack {
198        inner: std::rc::Rc::new(std::cell::RefCell::new(BackState {
199            entries: vec![Entry {
200                id: 1,
201                key: start,
202                saved: std::rc::Rc::new(SavedState::default()),
203            }],
204            next_id: 2,
205            last_dir: TransitionDir::None,
206        })),
207        version: std::rc::Rc::new(signal(0)),
208    })
209}
210
211pub struct EntryScope<K: NavKey> {
212    id: u64,
213    key: K,
214    saved: Rc<SavedState>,
215    nav: Navigator<K>,
216}
217impl<K: NavKey> EntryScope<K> {
218    pub fn id(&self) -> u64 {
219        self.id
220    }
221    pub fn key(&self) -> &K {
222        &self.key
223    }
224    pub fn navigator(&self) -> Navigator<K> {
225        self.nav.clone()
226    }
227    pub fn remember_saveable<T: 'static + Clone>(
228        &self,
229        slot: &'static str,
230        init: impl FnOnce() -> T,
231    ) -> Rc<RefCell<T>> {
232        self.saved.remember(slot, init)
233    }
234    pub fn set_result<T: 'static>(&self, slot: &'static str, v: T) {
235        self.saved.set_result(slot, v)
236    }
237    pub fn take_result<T: 'static>(&self, slot: &'static str) -> Option<T> {
238        self.saved.take_result(slot)
239    }
240}
241
242pub type EntryRenderer<K> = Rc<dyn Fn(&EntryScope<K>) -> View>;
243pub fn renderer<K: NavKey>(f: impl Fn(&EntryScope<K>) -> View + 'static) -> EntryRenderer<K> {
244    Rc::new(f)
245}
246
247#[derive(Clone, Copy)]
248pub struct NavTransition {
249    pub slide_px: f32,
250    pub fade: bool,
251    pub spec: AnimationSpec,
252}
253impl Default for NavTransition {
254    fn default() -> Self {
255        Self {
256            slide_px: 60.0,
257            fade: true,
258            spec: AnimationSpec::fast(),
259        }
260    }
261}
262
263pub fn NavDisplay<K: NavKey>(
264    stack: Rc<NavBackStack<K>>,
265    make_view: EntryRenderer<K>,
266    on_back: Option<Rc<dyn Fn()>>,
267    transition: NavTransition,
268) -> View {
269    let _v = stack.version.get(); // join reactive graph
270    let (id, key, saved) = match stack.top() {
271        Some(t) => t,
272        None => return VBox(Modifier::new()),
273    };
274    let scope = EntryScope {
275        id,
276        key,
277        saved,
278        nav: Navigator {
279            stack: (*stack).clone(),
280        },
281    };
282
283    let dir = stack.last_dir();
284    if dir == TransitionDir::None {
285        let v = (make_view)(&scope);
286        return maybe_intercept_back(v, on_back);
287    }
288
289    let t = animate_f32(
290        format!("nav3:{id}"),
291        if dir == TransitionDir::Push { 1.0 } else { 0.0 },
292        transition.spec,
293    );
294    let slide = if dir == TransitionDir::Push {
295        1.0 - t
296    } else {
297        t
298    };
299    let dx = slide
300        * transition.slide_px
301        * if dir == TransitionDir::Push {
302            1.0
303        } else {
304            -1.0
305        };
306    let alpha = if transition.fade {
307        0.75 + 0.25 * (1.0 - slide)
308    } else {
309        1.0
310    };
311
312    let v = (make_view)(&scope);
313    let framed = Stack(Modifier::new().fill_max_size())
314        .child(VBox(Modifier::new().translate(dx, 0.0).alpha(alpha)).child(v));
315    maybe_intercept_back(framed, on_back)
316}
317
318fn maybe_intercept_back(v: View, _on_back: Option<Rc<dyn Fn()>>) -> View {
319    // placeholder: platform loop will call the back handler; we expose setter below.
320    v
321}
322
323/// Back-dispatcher
324///
325/// platform calls handle_back(); app sets handler during composition.
326pub mod back {
327    use std::{cell::RefCell, rc::Rc};
328
329    type Handler = Rc<dyn Fn() -> bool>;
330
331    thread_local! {
332        static H: RefCell<Option<Handler>> = RefCell::new(None);
333    }
334
335    pub fn set(handler: Option<Handler>) {
336        H.with(|h| *h.borrow_mut() = handler);
337    }
338
339    pub fn handle() -> bool {
340        H.with(|h| {
341            if let Some(handler) = h.borrow().as_ref() {
342                handler()
343            } else {
344                false
345            }
346        })
347    }
348}
349
350/// Install/uninstall the global back handler for the displayed stack.
351pub fn InstallBackHandler<K: NavKey>(stack: NavBackStack<K>) -> Dispose {
352    let nav = Navigator {
353        stack: stack.clone(),
354    };
355    back::set(Some(Rc::new(move || nav.pop())));
356    on_unmount(|| back::set(None))
357}