Skip to main content

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