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