Skip to main content

repose_navigation/
lib.rs

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