Skip to main content

frus_widgets/
router.rs

1//! **A router**: locations, pages, and moving between them.
2//!
3//! An application with more than one screen names them. A [`GoRoute`] says which pattern of
4//! path it answers to and what page it builds; a [`GoRouter`] holds the routes and the stack
5//! of pages the person is looking at, and moves that stack when told a *location* — the
6//! string a link, a deep link or a button would carry: `/users/42?tab=posts`.
7//!
8//! ```
9//! use frus_widgets::{text, GoRoute, GoRouter};
10//!
11//! let router = GoRouter::new(vec![
12//!     GoRoute::new("/", |_, _| text("home")).routes(vec![
13//!         // relative to its parent: `/users/:id`
14//!         GoRoute::new("users/:id", |_, state| {
15//!             let id = state.param("id").unwrap_or("?").to_string();
16//!             text(format!("user {id}"))
17//!         })
18//!         .name("user"),
19//!     ]),
20//!     GoRoute::new("/settings", |_, _| text("settings")),
21//! ]);
22//!
23//! router.go("/users/42?tab=posts");
24//! assert_eq!(router.location(), "/users/42?tab=posts");
25//! assert!(router.can_pop(), "home is underneath");
26//!
27//! router.pop();
28//! assert_eq!(router.location(), "/");
29//! ```
30//!
31//! ## Two ways to move
32//!
33//! [`GoRouter::go`] says *where*: it makes the stack whatever the location's routes are — a
34//! location that lives under `/` has `/` beneath it, so there is somewhere to go back to.
35//! [`GoRouter::push`] says *on top of what is here*: it adds one page and leaves the rest,
36//! which is how the same page can be open twice.
37//!
38//! ## Redirects
39//!
40//! A redirect looks at where a navigation is heading and may send it elsewhere — to a sign-in
41//! page while nobody is signed in, away from it once someone is. It runs on every navigation,
42//! and again when a [`refresh listenable`](GoRouter::refresh_listenable) changes; a redirect
43//! that answers with another location is asked again about that one, up to a limit.
44//!
45//! ## Pages under the top
46//!
47//! A page covered by another is kept: what its components hold is still there when the top
48//! page is popped. It is built, but not laid out or drawn, until it is on show again.
49
50use std::any::Any;
51use std::cell::{Cell, RefCell};
52use std::collections::HashMap;
53use std::fmt;
54use std::hash::{Hash, Hasher};
55use std::rc::{Rc, Weak};
56
57use frus_core::{AnimationController, SpringDescription};
58
59use crate::component::{request_rebuild, BuildContext};
60use crate::navigator::Navigator;
61use crate::notifier::{Listenable, Subscription};
62use crate::widget::Widget;
63
64// ---------------------------------------------------------------------------------------
65// Motion
66// ---------------------------------------------------------------------------------------
67
68/// Horizon (s) over which a finger's velocity is projected to decide between going back and
69/// staying.
70const BACK_PROJECT: f32 = 0.12;
71/// Projected position (a fraction) beyond which going back is committed.
72const BACK_COMMIT_POS: f32 = 0.5;
73/// Stiffness of the page transition's spring.
74const SPRING_K: f32 = 220.0;
75/// Its damping: close to critical, so a page arrives without overshooting.
76const SPRING_C: f32 = 30.0;
77
78fn spring() -> SpringDescription {
79    SpringDescription::new(1.0, SPRING_K, SPRING_C)
80}
81
82// ---------------------------------------------------------------------------------------
83// Locations
84// ---------------------------------------------------------------------------------------
85
86fn decode(text: &str, plus_is_space: bool) -> String {
87    let bytes = text.as_bytes();
88    let mut out = Vec::with_capacity(bytes.len());
89    let mut i = 0;
90    while i < bytes.len() {
91        match bytes[i] {
92            b'%' if i + 2 < bytes.len() => {
93                let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
94                match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
95                    Some(byte) => {
96                        out.push(byte);
97                        i += 3;
98                    }
99                    None => {
100                        out.push(b'%');
101                        i += 1;
102                    }
103                }
104            }
105            b'+' if plus_is_space => {
106                out.push(b' ');
107                i += 1;
108            }
109            byte => {
110                out.push(byte);
111                i += 1;
112            }
113        }
114    }
115    String::from_utf8_lossy(&out).into_owned()
116}
117
118fn encode(text: &str) -> String {
119    let mut out = String::with_capacity(text.len());
120    for byte in text.bytes() {
121        match byte {
122            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
123                out.push(byte as char)
124            }
125            other => out.push_str(&format!("%{other:02X}")),
126        }
127    }
128    out
129}
130
131/// A location taken apart: its path's segments, and its query.
132struct Parts {
133    segments: Vec<String>,
134    query: Vec<(String, String)>,
135}
136
137fn split_location(location: &str) -> Parts {
138    let without_fragment = location.split('#').next().unwrap_or("");
139    let (path, query) = without_fragment
140        .split_once('?')
141        .unwrap_or((without_fragment, ""));
142    let segments = path
143        .split('/')
144        .filter(|segment| !segment.is_empty())
145        .map(str::to_string)
146        .collect();
147    let query = query
148        .split('&')
149        .filter(|pair| !pair.is_empty())
150        .map(|pair| {
151            let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
152            (decode(key, true), decode(value, true))
153        })
154        .collect();
155    Parts { segments, query }
156}
157
158// ---------------------------------------------------------------------------------------
159// State
160// ---------------------------------------------------------------------------------------
161
162/// Where a page is: what location it was reached at, which route it is, and what the
163/// location said — the values of the path's `:parameters`, and the query.
164#[derive(Clone)]
165pub struct GoRouterState {
166    location: String,
167    path: String,
168    name: Option<String>,
169    params: HashMap<String, String>,
170    query: HashMap<String, String>,
171    extra: Option<Rc<dyn Any>>,
172    entering: f32,
173}
174
175impl GoRouterState {
176    /// How far into view this page is: `1.0` once it has settled, and while a transition
177    /// brings it in or takes it out, the number that transition is driven by — `0.0` not yet
178    /// arrived. A page that wants to move its own contents as it arrives reads it, and one
179    /// that does not ignores it.
180    pub fn entering(&self) -> f32 {
181        self.entering
182    }
183
184    /// The whole location, query included: `/users/42?tab=posts`.
185    pub fn location(&self) -> &str {
186        &self.location
187    }
188
189    /// The pattern of the route that answered: `/users/:id`. Empty when no route did.
190    pub fn path(&self) -> &str {
191        &self.path
192    }
193
194    /// The route's name, if it has one.
195    pub fn name(&self) -> Option<&str> {
196        self.name.as_deref()
197    }
198
199    /// The value of the path parameter `name` — `42` for `:id` in `/users/42`.
200    pub fn param(&self, name: &str) -> Option<&str> {
201        self.params.get(name).map(String::as_str)
202    }
203
204    /// Every path parameter of the location.
205    pub fn params(&self) -> &HashMap<String, String> {
206        &self.params
207    }
208
209    /// The value of the query parameter `name` — `posts` for `tab` in `?tab=posts`. When it
210    /// is given more than once, the last one.
211    pub fn query(&self, name: &str) -> Option<&str> {
212        self.query.get(name).map(String::as_str)
213    }
214
215    /// Every query parameter of the location.
216    pub fn query_params(&self) -> &HashMap<String, String> {
217        &self.query
218    }
219
220    /// The object that came along with the navigation ([`GoRouter::go_with_extra`]), if it
221    /// is a `T`.
222    pub fn extra<T: 'static>(&self) -> Option<Rc<T>> {
223        self.extra.clone().and_then(|extra| extra.downcast().ok())
224    }
225}
226
227impl fmt::Debug for GoRouterState {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_struct("GoRouterState")
230            .field("location", &self.location)
231            .field("path", &self.path)
232            .field("name", &self.name)
233            .field("params", &self.params)
234            .field("query", &self.query)
235            .finish()
236    }
237}
238
239// ---------------------------------------------------------------------------------------
240// Routes
241// ---------------------------------------------------------------------------------------
242
243type RouteBuilder = Rc<dyn Fn(&BuildContext, &GoRouterState) -> Box<dyn Widget>>;
244type Redirect = Rc<dyn Fn(&GoRouterState) -> Option<String>>;
245
246/// One place the router can go: a pattern of path, and the page it builds.
247///
248/// A path is made of segments: a literal (`users`), a parameter (`:id`), or a trailing `*`
249/// that takes whatever is left. A top-level route's path starts with `/`; a sub-route's is
250/// relative to its parent's and does not.
251pub struct GoRoute {
252    path: String,
253    name: Option<String>,
254    builder: RouteBuilder,
255    routes: Vec<GoRoute>,
256    redirect: Option<Redirect>,
257}
258
259impl GoRoute {
260    /// A route answering to `path`, building its page with `builder`.
261    ///
262    /// `builder` is told the [`GoRouterState`] — where the location put the person — and may
263    /// return any widget: a [`Component`](crate::Component), a `Box<dyn Widget>`, a
264    /// scaffold.
265    pub fn new<W: Widget + 'static>(
266        path: impl Into<String>,
267        builder: impl Fn(&BuildContext, &GoRouterState) -> W + 'static,
268    ) -> Self {
269        Self {
270            path: path.into(),
271            name: None,
272            builder: Rc::new(move |cx, state| Box::new(builder(cx, state))),
273            routes: Vec::new(),
274            redirect: None,
275        }
276    }
277
278    /// Names the route, so it can be reached without writing its path:
279    /// [`GoRouter::go_named`].
280    pub fn name(mut self, name: impl Into<String>) -> Self {
281        self.name = Some(name.into());
282        self
283    }
284
285    /// The routes under this one. A location that one of them answers to has this route's page
286    /// beneath its own.
287    pub fn routes(mut self, routes: Vec<GoRoute>) -> Self {
288        self.routes = routes;
289        self
290    }
291
292    /// Where to send a navigation heading to this route or below it, if not here — `None` to
293    /// let it through.
294    pub fn redirect(
295        mut self,
296        redirect: impl Fn(&GoRouterState) -> Option<String> + 'static,
297    ) -> Self {
298        self.redirect = Some(Rc::new(redirect));
299        self
300    }
301}
302
303enum Segment {
304    Literal(String),
305    Param(String),
306    Splat,
307}
308
309struct Node {
310    full_path: String,
311    segments: Vec<Segment>,
312    name: Option<String>,
313    builder: RouteBuilder,
314    redirect: Option<Redirect>,
315    children: Vec<Rc<Node>>,
316}
317
318fn build_node(
319    route: GoRoute,
320    parent: Option<&str>,
321    names: &mut HashMap<String, Rc<Node>>,
322) -> Rc<Node> {
323    let full_path = match parent {
324        None => {
325            assert!(
326                route.path.starts_with('/'),
327                "a top-level route's path starts with `/`: `{}`",
328                route.path
329            );
330            route.path.clone()
331        }
332        Some(parent) => {
333            assert!(
334                !route.path.starts_with('/'),
335                "a sub-route's path is relative to its parent's, and does not start with `/`: `{}`",
336                route.path
337            );
338            assert!(!route.path.is_empty(), "a sub-route needs a path");
339            format!("{}/{}", parent.trim_end_matches('/'), route.path)
340        }
341    };
342    let segments = route
343        .path
344        .split('/')
345        .filter(|segment| !segment.is_empty())
346        .map(|segment| match segment {
347            "*" => Segment::Splat,
348            other => match other.strip_prefix(':') {
349                Some(name) => Segment::Param(name.to_string()),
350                None => Segment::Literal(other.to_string()),
351            },
352        })
353        .collect();
354    let children = route
355        .routes
356        .into_iter()
357        .map(|child| build_node(child, Some(&full_path), names))
358        .collect();
359    let node = Rc::new(Node {
360        full_path,
361        segments,
362        name: route.name.clone(),
363        builder: route.builder,
364        redirect: route.redirect,
365        children,
366    });
367    if let Some(name) = route.name {
368        let clash = names.insert(name.clone(), node.clone());
369        assert!(clash.is_none(), "two routes are named `{name}`");
370    }
371    node
372}
373
374/// How many segments of `input` the pattern takes, filling `params` — or `None` if it does
375/// not match the start of it.
376fn consume(
377    segments: &[Segment],
378    input: &[String],
379    params: &mut HashMap<String, String>,
380) -> Option<usize> {
381    let mut at = 0;
382    for segment in segments {
383        match segment {
384            Segment::Literal(literal) => {
385                if input.get(at)? != literal {
386                    return None;
387                }
388                at += 1;
389            }
390            Segment::Param(name) => {
391                params.insert(name.clone(), decode(input.get(at)?, false));
392                at += 1;
393            }
394            Segment::Splat => {
395                params.insert("*".to_string(), input[at.min(input.len())..].join("/"));
396                at = input.len();
397            }
398        }
399    }
400    Some(at)
401}
402
403/// The chain of routes that answers to `input`, the parent first — the deepest route whose
404/// pattern takes all of it.
405fn match_nodes(
406    nodes: &[Rc<Node>],
407    input: &[String],
408    params: &mut HashMap<String, String>,
409) -> Option<Vec<Rc<Node>>> {
410    for node in nodes {
411        let mut found = params.clone();
412        let Some(used) = consume(&node.segments, input, &mut found) else {
413            continue;
414        };
415        if used == input.len() {
416            *params = found;
417            return Some(vec![node.clone()]);
418        }
419        if let Some(mut below) = match_nodes(&node.children, &input[used..], &mut found) {
420            *params = found;
421            below.insert(0, node.clone());
422            return Some(below);
423        }
424    }
425    None
426}
427
428// ---------------------------------------------------------------------------------------
429// The router
430// ---------------------------------------------------------------------------------------
431
432/// One page of the stack.
433#[derive(Clone)]
434struct Page {
435    /// What identifies it: a page reached again by `go` keeps it, and so keeps its state; one
436    /// that was pushed has its own.
437    key: u64,
438    node: Option<Rc<Node>>,
439    state: Rc<GoRouterState>,
440}
441
442struct Transition {
443    from: Page,
444    controller: AnimationController,
445    forward: bool,
446}
447
448struct Back {
449    progress: f32,
450    settle: Option<AnimationController>,
451    commit: bool,
452}
453
454#[derive(Default)]
455struct Stack {
456    pages: Vec<Page>,
457    transition: Option<Transition>,
458    back: Option<Back>,
459}
460
461struct Config {
462    nodes: Vec<Rc<Node>>,
463    names: HashMap<String, Rc<Node>>,
464    initial: String,
465    redirect: Option<Redirect>,
466    error: Option<RouteBuilder>,
467    limit: usize,
468    animate: bool,
469}
470
471struct Inner {
472    config: RefCell<Config>,
473    stack: RefCell<Stack>,
474    started: Cell<bool>,
475    /// Whether a tree has been built from this router yet: until then nobody has seen a page,
476    /// so there is nothing to move away from.
477    built: Cell<bool>,
478    /// Whether the last move swapped the page on top rather than adding or removing one.
479    replaced: Cell<bool>,
480    unique: Cell<u64>,
481    refresh: RefCell<Option<Subscription>>,
482}
483
484/// The routes, and the stack of pages showing which of them the person has been through.
485///
486/// A cheap handle: clone it into a handler and navigate from there.
487#[derive(Clone)]
488pub struct GoRouter {
489    inner: Rc<Inner>,
490}
491
492#[derive(Clone, Copy, PartialEq)]
493enum Mode {
494    Go,
495    Push,
496    Replace,
497}
498
499fn hash_of(value: impl Hash) -> u64 {
500    let mut hasher = std::collections::hash_map::DefaultHasher::new();
501    value.hash(&mut hasher);
502    hasher.finish()
503}
504
505impl GoRouter {
506    /// A router over `routes`, starting at `/`.
507    pub fn new(routes: Vec<GoRoute>) -> Self {
508        let mut names = HashMap::new();
509        let nodes = routes
510            .into_iter()
511            .map(|route| build_node(route, None, &mut names))
512            .collect();
513        Self {
514            inner: Rc::new(Inner {
515                config: RefCell::new(Config {
516                    nodes,
517                    names,
518                    initial: "/".to_string(),
519                    redirect: None,
520                    error: None,
521                    limit: 5,
522                    animate: true,
523                }),
524                stack: RefCell::new(Stack::default()),
525                started: Cell::new(false),
526                built: Cell::new(false),
527                replaced: Cell::new(false),
528                unique: Cell::new(0),
529                refresh: RefCell::new(None),
530            }),
531        }
532    }
533
534    /// Where the router starts. `/` by default.
535    pub fn initial_location(self, location: impl Into<String>) -> Self {
536        self.inner.config.borrow_mut().initial = location.into();
537        self
538    }
539
540    /// A redirect for every navigation: it is told where it is heading and may answer with
541    /// another location, or `None` to let it through.
542    ///
543    /// It closes over whatever it needs to decide — a handle to the signed-in state, say.
544    pub fn redirect(self, redirect: impl Fn(&GoRouterState) -> Option<String> + 'static) -> Self {
545        self.inner.config.borrow_mut().redirect = Some(Rc::new(redirect));
546        self
547    }
548
549    /// The page shown when no route answers to a location. A plain "not found" page by
550    /// default.
551    pub fn error_builder<W: Widget + 'static>(
552        self,
553        builder: impl Fn(&BuildContext, &GoRouterState) -> W + 'static,
554    ) -> Self {
555        self.inner.config.borrow_mut().error =
556            Some(Rc::new(move |cx, state| Box::new(builder(cx, state))));
557        self
558    }
559
560    /// How many redirects in a row are followed before the router gives up and shows the
561    /// error page. Five by default.
562    pub fn redirect_limit(self, limit: usize) -> Self {
563        self.inner.config.borrow_mut().limit = limit;
564        self
565    }
566
567    /// Whether a page slides in and out. On by default.
568    pub fn animate(self, animate: bool) -> Self {
569        self.inner.config.borrow_mut().animate = animate;
570        self
571    }
572
573    /// Runs the redirects again whenever `listenable` changes — the way a sign-in or sign-out
574    /// moves the person without anyone having navigated.
575    pub fn refresh_listenable(self, listenable: &impl Listenable) -> Self {
576        let weak: Weak<Inner> = Rc::downgrade(&self.inner);
577        let subscription = listenable.add_listener(move || {
578            if let Some(inner) = weak.upgrade() {
579                GoRouter { inner }.refresh();
580            }
581        });
582        *self.inner.refresh.borrow_mut() = Some(subscription);
583        self
584    }
585
586    // ---- reading -----------------------------------------------------------------------
587
588    /// The location of the page on top.
589    pub fn location(&self) -> String {
590        self.start();
591        self.inner
592            .stack
593            .borrow()
594            .pages
595            .last()
596            .map(|page| page.state.location.clone())
597            .unwrap_or_default()
598    }
599
600    /// The state of the page on top: what its location put in it.
601    pub fn state(&self) -> Option<GoRouterState> {
602        self.start();
603        self.inner
604            .stack
605            .borrow()
606            .pages
607            .last()
608            .map(|page| (*page.state).clone())
609    }
610
611    /// The location of every page in the stack, the one at the bottom first and the one on
612    /// top last.
613    pub fn locations(&self) -> Vec<String> {
614        self.start();
615        self.inner
616            .stack
617            .borrow()
618            .pages
619            .iter()
620            .map(|page| page.state.location.clone())
621            .collect()
622    }
623
624    /// Whether the last move swapped the page on top for another — [`replace`](Self::replace)
625    /// — rather than adding a page, removing one or rebuilding the stack. What an address bar
626    /// wants to know, to swap its entry too instead of adding one.
627    pub fn replaced(&self) -> bool {
628        self.inner.replaced.get()
629    }
630
631    /// How many pages the stack holds.
632    pub fn depth(&self) -> usize {
633        self.start();
634        self.inner.stack.borrow().pages.len()
635    }
636
637    /// Whether there is a page beneath the top to go back to.
638    pub fn can_pop(&self) -> bool {
639        self.depth() > 1
640    }
641
642    /// The location of the route called `name`, with its `:parameters` filled in and `query`
643    /// appended. `None` if there is no such route, or a parameter is missing.
644    pub fn location_for(
645        &self,
646        name: &str,
647        params: &[(&str, &str)],
648        query: &[(&str, &str)],
649    ) -> Option<String> {
650        let node = self.inner.config.borrow().names.get(name).cloned()?;
651        let mut path = String::new();
652        for segment in node.full_path.split('/').filter(|s| !s.is_empty()) {
653            path.push('/');
654            match segment.strip_prefix(':') {
655                Some(param) => {
656                    let (_, value) = params.iter().find(|(key, _)| *key == param)?;
657                    path.push_str(&encode(value));
658                }
659                None => path.push_str(segment),
660            }
661        }
662        if path.is_empty() {
663            path.push('/');
664        }
665        if !query.is_empty() {
666            let pairs: Vec<String> = query
667                .iter()
668                .map(|(key, value)| format!("{}={}", encode(key), encode(value)))
669                .collect();
670            path.push('?');
671            path.push_str(&pairs.join("&"));
672        }
673        Some(path)
674    }
675
676    // ---- moving ------------------------------------------------------------------------
677
678    /// Goes to `location`: the stack becomes the routes that location is made of.
679    pub fn go(&self, location: impl AsRef<str>) {
680        self.navigate(location.as_ref(), None, Mode::Go);
681    }
682
683    /// [`go`](Self::go), carrying an object the page can read with
684    /// [`GoRouterState::extra`].
685    pub fn go_with_extra(&self, location: impl AsRef<str>, extra: impl Any) {
686        self.navigate(location.as_ref(), Some(Rc::new(extra)), Mode::Go);
687    }
688
689    /// Goes to the route called `name`.
690    pub fn go_named(&self, name: &str, params: &[(&str, &str)], query: &[(&str, &str)]) {
691        match self.location_for(name, params, query) {
692            Some(location) => self.go(location),
693            None => debug_assert!(false, "no route called `{name}`, or a parameter is missing"),
694        }
695    }
696
697    /// Pushes the page for `location` on top of what is here.
698    pub fn push(&self, location: impl AsRef<str>) {
699        self.navigate(location.as_ref(), None, Mode::Push);
700    }
701
702    /// [`push`](Self::push), carrying an object the page can read with
703    /// [`GoRouterState::extra`].
704    pub fn push_with_extra(&self, location: impl AsRef<str>, extra: impl Any) {
705        self.navigate(location.as_ref(), Some(Rc::new(extra)), Mode::Push);
706    }
707
708    /// Pushes the route called `name`.
709    pub fn push_named(&self, name: &str, params: &[(&str, &str)], query: &[(&str, &str)]) {
710        match self.location_for(name, params, query) {
711            Some(location) => self.push(location),
712            None => debug_assert!(false, "no route called `{name}`, or a parameter is missing"),
713        }
714    }
715
716    /// Goes to a location that came from **outside** the application: an address typed, a
717    /// link followed, the browser's back button. Like [`go`](Self::go), except that a
718    /// location the router is already at is left alone, and that before anything has been
719    /// built there is no page to slide away from, so the router simply starts there.
720    ///
721    /// It is what the shell's `FrusApp` hands an address to.
722    pub fn open(&self, location: impl AsRef<str>) {
723        let location = location.as_ref();
724        if self.location() == location {
725            return;
726        }
727        self.go(location);
728        if !self.inner.built.get() {
729            self.inner.stack.borrow_mut().transition = None;
730        }
731    }
732
733    /// Replaces the page on top with the one for `location`, so going back skips the one it
734    /// replaced.
735    pub fn replace(&self, location: impl AsRef<str>) {
736        self.navigate(location.as_ref(), None, Mode::Replace);
737    }
738
739    /// Removes the page on top. Whether there was one to remove: the last page stays.
740    pub fn pop(&self) -> bool {
741        self.start();
742        let pages = {
743            let stack = self.inner.stack.borrow();
744            if stack.pages.len() <= 1 {
745                return false;
746            }
747            stack.pages[..stack.pages.len() - 1].to_vec()
748        };
749        self.commit(pages);
750        self.inner.replaced.set(false);
751        true
752    }
753
754    /// Asks the redirects again about where the top page is. What a
755    /// [`refresh listenable`](Self::refresh_listenable) does when it changes.
756    pub fn refresh(&self) {
757        self.start();
758        let target = self
759            .inner
760            .stack
761            .borrow()
762            .pages
763            .last()
764            .map(|page| (page.state.location.clone(), page.state.extra.clone()));
765        if let Some((location, extra)) = target {
766            self.navigate(&location, extra, Mode::Go);
767        }
768    }
769
770    // ---- the machinery -----------------------------------------------------------------
771
772    fn start(&self) {
773        if self.inner.started.replace(true) {
774            return;
775        }
776        let initial = self.inner.config.borrow().initial.clone();
777        let pages = self.pages_for(&initial, None, Mode::Go, &[]);
778        self.inner.stack.borrow_mut().pages = pages;
779    }
780
781    fn navigate(&self, location: &str, extra: Option<Rc<dyn Any>>, mode: Mode) {
782        self.start();
783        let current = self.inner.stack.borrow().pages.clone();
784        let pages = self.pages_for(location, extra, mode, &current);
785        self.commit(pages);
786        self.inner.replaced.set(mode == Mode::Replace);
787    }
788
789    /// The stack that results from `mode` to `location`, redirects followed.
790    fn pages_for(
791        &self,
792        location: &str,
793        extra: Option<Rc<dyn Any>>,
794        mode: Mode,
795        current: &[Page],
796    ) -> Vec<Page> {
797        let (nodes, redirect, limit) = {
798            let config = self.inner.config.borrow();
799            (config.nodes.clone(), config.redirect.clone(), config.limit)
800        };
801        let mut location = location.to_string();
802        let mut seen = 0usize;
803        loop {
804            let parts = split_location(&location);
805            let mut params = HashMap::new();
806            let chain = match_nodes(&nodes, &parts.segments, &mut params);
807            let query: HashMap<String, String> = parts.query.iter().cloned().collect();
808            let leaf = chain.as_ref().and_then(|chain| chain.last());
809            let state = GoRouterState {
810                location: location.clone(),
811                path: leaf.map(|n| n.full_path.clone()).unwrap_or_default(),
812                name: leaf.and_then(|n| n.name.clone()),
813                params: params.clone(),
814                query: query.clone(),
815                extra: extra.clone(),
816                entering: 1.0,
817            };
818
819            // Redirects: the router's own, then each route's, parent first.
820            let mut sent = redirect.as_ref().and_then(|r| r(&state));
821            if sent.is_none() {
822                if let Some(chain) = &chain {
823                    sent = chain
824                        .iter()
825                        .filter_map(|node| node.redirect.as_ref())
826                        .find_map(|r| r(&state));
827                }
828            }
829            if let Some(next) = sent.filter(|next| *next != location) {
830                seen += 1;
831                if seen > limit {
832                    let state = GoRouterState {
833                        location: location.clone(),
834                        path: String::new(),
835                        name: None,
836                        params: HashMap::new(),
837                        query: HashMap::new(),
838                        extra: None,
839                        entering: 1.0,
840                    };
841                    return self.error_pages(state, current, mode);
842                }
843                location = next;
844                continue;
845            }
846
847            let Some(chain) = chain else {
848                return self.error_pages(state, current, mode);
849            };
850            return match mode {
851                Mode::Go => {
852                    // One page per route of the chain, each keyed by the part of the location
853                    // it answered to, so a page that is still on the way keeps its state.
854                    let mut prefix = String::new();
855                    let mut used = 0;
856                    chain
857                        .iter()
858                        .enumerate()
859                        .map(|(at, node)| {
860                            let mut scratch = HashMap::new();
861                            let took =
862                                consume(&node.segments, &parts.segments[used..], &mut scratch)
863                                    .unwrap_or(0);
864                            for segment in &parts.segments[used..used + took] {
865                                prefix.push('/');
866                                prefix.push_str(segment);
867                            }
868                            used += took;
869                            let page_state = if at + 1 == chain.len() {
870                                state.clone()
871                            } else {
872                                // A page underneath stands for the part of the location it
873                                // answered — `/` under `/users/42` — so going back to it lands
874                                // on *its* location, not on where the person had been heading.
875                                GoRouterState {
876                                    location: if prefix.is_empty() {
877                                        "/".to_string()
878                                    } else {
879                                        prefix.clone()
880                                    },
881                                    path: node.full_path.clone(),
882                                    name: node.name.clone(),
883                                    ..state.clone()
884                                }
885                            };
886                            Page {
887                                key: hash_of(("go", prefix.clone(), node.full_path.clone())),
888                                node: Some(node.clone()),
889                                state: Rc::new(page_state),
890                            }
891                        })
892                        .collect()
893                }
894                Mode::Push | Mode::Replace => {
895                    let mut pages = current.to_vec();
896                    if mode == Mode::Replace {
897                        pages.pop();
898                    }
899                    let leaf = chain.last().expect("a match has a route").clone();
900                    let unique = self.inner.unique.get() + 1;
901                    self.inner.unique.set(unique);
902                    pages.push(Page {
903                        key: hash_of(("push", unique)),
904                        node: Some(leaf),
905                        state: Rc::new(state),
906                    });
907                    pages
908                }
909            };
910        }
911    }
912
913    fn error_pages(&self, state: GoRouterState, current: &[Page], mode: Mode) -> Vec<Page> {
914        let unique = self.inner.unique.get() + 1;
915        self.inner.unique.set(unique);
916        let page = Page {
917            key: hash_of(("error", unique)),
918            node: None,
919            state: Rc::new(state),
920        };
921        match mode {
922            Mode::Go => vec![page],
923            Mode::Push => {
924                let mut pages = current.to_vec();
925                pages.push(page);
926                pages
927            }
928            Mode::Replace => {
929                let mut pages = current.to_vec();
930                pages.pop();
931                pages.push(page);
932                pages
933            }
934        }
935    }
936
937    /// Makes `pages` the stack, starting the transition from the page that was on top.
938    fn commit(&self, pages: Vec<Page>) {
939        let animate = self.inner.config.borrow().animate;
940        let mut stack = self.inner.stack.borrow_mut();
941        let old_top = stack.pages.last().cloned();
942        let new_top_key = pages.last().map(|page| page.key);
943        if let (true, Some(old), Some(new_key)) = (animate, old_top, new_top_key) {
944            if old.key != new_key {
945                let forward = !stack.pages.iter().any(|page| page.key == new_key);
946                let mut controller = AnimationController::unit();
947                controller.set_value(0.0);
948                controller.spring_to(1.0, spring(), 0.0);
949                stack.transition = Some(Transition {
950                    from: old,
951                    controller,
952                    forward,
953                });
954            }
955        }
956        stack.pages = pages;
957        stack.back = None;
958        drop(stack);
959        request_rebuild();
960    }
961
962    // ---- driven by the shell -----------------------------------------------------------
963
964    /// Advances the page transition and a back gesture's settling by `dt` seconds. Whether
965    /// anything is still moving.
966    pub fn tick(&self, dt: f32) -> bool {
967        let mut animating = false;
968        let mut pop = false;
969        {
970            let mut stack = self.inner.stack.borrow_mut();
971            let arrived = match stack.transition.as_mut() {
972                Some(transition) => {
973                    if transition.controller.tick(dt) {
974                        animating = true;
975                        false
976                    } else {
977                        true
978                    }
979                }
980                None => false,
981            };
982            if arrived {
983                stack.transition = None;
984            }
985            let mut settled = false;
986            if let Some(back) = stack.back.as_mut() {
987                if let Some(settle) = back.settle.as_mut() {
988                    if settle.tick(dt) {
989                        back.progress = settle.value();
990                        animating = true;
991                    } else {
992                        pop = back.commit;
993                        settled = true;
994                    }
995                }
996            }
997            if settled {
998                stack.back = None;
999            }
1000        }
1001        if pop {
1002            // The finger has already carried the page away: there is no transition left to
1003            // play, only the stack to catch up with.
1004            let pages = {
1005                let stack = self.inner.stack.borrow();
1006                stack.pages[..stack.pages.len().saturating_sub(1)].to_vec()
1007            };
1008            if !pages.is_empty() {
1009                self.inner.stack.borrow_mut().pages = pages;
1010            }
1011            request_rebuild();
1012        }
1013        animating
1014    }
1015
1016    /// Whether a back gesture can start: there is a page to go back to.
1017    pub fn can_go_back(&self) -> bool {
1018        self.can_pop()
1019    }
1020
1021    /// A back gesture is under way, `progress` of the way from `0` to `1`.
1022    pub fn back_gesture(&self, progress: f32) {
1023        let mut stack = self.inner.stack.borrow_mut();
1024        match stack.back.as_mut() {
1025            Some(back) => back.progress = progress,
1026            None => {
1027                stack.back = Some(Back {
1028                    progress,
1029                    settle: None,
1030                    commit: false,
1031                })
1032            }
1033        }
1034        drop(stack);
1035        request_rebuild();
1036    }
1037
1038    /// The finger lifted, moving at `velocity` (fractions per second): the page goes back or
1039    /// stays, whichever the momentum and the position say.
1040    pub fn back_gesture_end(&self, velocity: f32) {
1041        let mut stack = self.inner.stack.borrow_mut();
1042        let deep_enough = stack.pages.len() > 1;
1043        if let Some(back) = stack.back.as_mut() {
1044            let projected = back.progress + velocity * BACK_PROJECT;
1045            let commit = projected > BACK_COMMIT_POS && deep_enough;
1046            back.commit = commit;
1047            let mut settle = AnimationController::unit();
1048            settle.set_value(back.progress);
1049            settle.spring_to(if commit { 1.0 } else { 0.0 }, spring(), velocity);
1050            back.settle = Some(settle);
1051        }
1052        drop(stack);
1053        request_rebuild();
1054    }
1055
1056    // ---- building ----------------------------------------------------------------------
1057
1058    /// The widget for the router's pages: what an application whose root is a router
1059    /// builds. It also makes the router reachable from below, with [`BuildContext::router`].
1060    pub fn build(&self, cx: &BuildContext) -> Box<dyn Widget> {
1061        self.start();
1062        self.inner.built.set(true);
1063        cx.runtime().states.provide(Rc::new(self.clone()));
1064        let error_builder = self.inner.config.borrow().error.clone();
1065        // What is on the stack is copied out, so that a page's builder can ask the router
1066        // things without finding it borrowed.
1067        let (pages, transition, back) = {
1068            let stack = self.inner.stack.borrow();
1069            (
1070                stack.pages.clone(),
1071                stack
1072                    .transition
1073                    .as_ref()
1074                    .map(|t| (t.from.clone(), t.controller.value(), t.forward)),
1075                stack.back.as_ref().map(|b| b.progress),
1076            )
1077        };
1078        let depth = pages.len();
1079        // A page is built told how far in it is: the transition's own number, seen from
1080        // this page — the one arriving has it as it is, the one leaving has it the other way.
1081        let make = |page: &Page, entering: f32| -> Box<dyn Widget> {
1082            let state = GoRouterState {
1083                entering,
1084                ..(*page.state).clone()
1085            };
1086            match &page.node {
1087                Some(node) => (node.builder)(cx, &state),
1088                None => match &error_builder {
1089                    Some(builder) => builder(cx, &state),
1090                    None => Box::new(crate::Text::new(format!(
1091                        "Page not found: {}",
1092                        state.location
1093                    ))),
1094                },
1095            }
1096        };
1097        let key_of = |page: &Page, depth: usize| (depth, page.key);
1098        let top = pages.last().expect("a router has at least one page");
1099
1100        // A back gesture previews the pop, driven by the finger.
1101        if let Some(progress) = back {
1102            if depth >= 2 {
1103                let below = &pages[depth - 2];
1104                let mut navigator = Navigator::new(key_of(below, depth - 2), make(below, progress))
1105                    .from(
1106                        key_of(top, depth - 1),
1107                        make(top, 1.0 - progress),
1108                        progress,
1109                        false,
1110                    );
1111                for (at, page) in pages[..depth - 2].iter().enumerate() {
1112                    navigator = navigator.retain(key_of(page, at), make(page, 1.0));
1113                }
1114                return Box::new(navigator);
1115            }
1116        }
1117
1118        match transition {
1119            Some((from, progress, forward)) => {
1120                // The page leaving is either still in the stack (a push left it one entry
1121                // down) or gone from it (a pop took it off the top).
1122                let from_depth = pages
1123                    .iter()
1124                    .position(|page| page.key == from.key)
1125                    .unwrap_or(depth);
1126                let mut navigator = Navigator::new(key_of(top, depth - 1), make(top, progress))
1127                    .from(
1128                        key_of(&from, from_depth),
1129                        make(&from, 1.0 - progress),
1130                        progress,
1131                        forward,
1132                    );
1133                for (at, page) in pages[..depth - 1].iter().enumerate() {
1134                    if page.key != from.key {
1135                        navigator = navigator.retain(key_of(page, at), make(page, 1.0));
1136                    }
1137                }
1138                Box::new(navigator)
1139            }
1140            None => {
1141                let mut navigator = Navigator::new(key_of(top, depth - 1), make(top, 1.0));
1142                for (at, page) in pages[..depth - 1].iter().enumerate() {
1143                    navigator = navigator.retain(key_of(page, at), make(page, 1.0));
1144                }
1145                Box::new(navigator)
1146            }
1147        }
1148    }
1149}
1150
1151impl BuildContext<'_> {
1152    /// The application's router. Handlers capture it and navigate from there:
1153    ///
1154    /// ```ignore
1155    /// let router = cx.router();
1156    /// button("Open", move || router.go("/settings"))
1157    /// ```
1158    ///
1159    /// Panics if this application has no router — one made with `FrusApp::router`.
1160    pub fn router(&self) -> GoRouter {
1161        match self.service::<GoRouter>() {
1162            Some(router) => (*router).clone(),
1163            None => {
1164                panic!("there is no router: this application was not made with `FrusApp::router`")
1165            }
1166        }
1167    }
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173    use crate::Container;
1174
1175    fn page(_: &BuildContext, _: &GoRouterState) -> Container {
1176        Container::new()
1177    }
1178
1179    fn routes() -> Vec<GoRoute> {
1180        vec![
1181            GoRoute::new("/", page).routes(vec![
1182                GoRoute::new("users/:id", page)
1183                    .name("user")
1184                    .routes(vec![GoRoute::new("posts/:post", page).name("post")]),
1185                GoRoute::new("files/*", page).name("files"),
1186            ]),
1187            GoRoute::new("/settings", page).name("settings"),
1188        ]
1189    }
1190
1191    fn router() -> GoRouter {
1192        GoRouter::new(routes()).animate(false)
1193    }
1194
1195    fn keys(router: &GoRouter) -> Vec<String> {
1196        router
1197            .inner
1198            .stack
1199            .borrow()
1200            .pages
1201            .iter()
1202            .map(|p| p.state.path.clone())
1203            .collect()
1204    }
1205
1206    #[test]
1207    fn it_starts_at_the_initial_location() {
1208        assert_eq!(router().location(), "/");
1209        assert_eq!(
1210            GoRouter::new(routes())
1211                .initial_location("/settings")
1212                .location(),
1213            "/settings"
1214        );
1215    }
1216
1217    #[test]
1218    fn go_makes_the_stack_the_chain_of_routes_the_location_is_made_of() {
1219        let r = router();
1220        r.go("/users/42/posts/7");
1221        assert_eq!(
1222            keys(&r),
1223            ["/", "/users/:id", "/users/:id/posts/:post"],
1224            "home, the user, then the post: something to go back to at each step"
1225        );
1226        let state = r.state().unwrap();
1227        assert_eq!(state.param("id"), Some("42"));
1228        assert_eq!(state.param("post"), Some("7"));
1229        assert_eq!(state.path(), "/users/:id/posts/:post");
1230        assert_eq!(state.name(), Some("post"));
1231    }
1232
1233    #[test]
1234    fn a_page_underneath_stands_for_its_own_location() {
1235        let r = router();
1236        r.go("/users/42/posts/7?x=1");
1237        let locations: Vec<String> = r
1238            .inner
1239            .stack
1240            .borrow()
1241            .pages
1242            .iter()
1243            .map(|p| p.state.location.clone())
1244            .collect();
1245        assert_eq!(
1246            locations,
1247            ["/", "/users/42", "/users/42/posts/7?x=1"],
1248            "each page is the part of the location it answered; the top one, all of it"
1249        );
1250        r.pop();
1251        assert_eq!(
1252            r.location(),
1253            "/users/42",
1254            "so going back lands where that page is"
1255        );
1256    }
1257
1258    #[test]
1259    fn a_page_underneath_knows_the_parameters_too() {
1260        let r = router();
1261        r.go("/users/42/posts/7");
1262        let below = r.inner.stack.borrow().pages[1].state.clone();
1263        assert_eq!(below.param("id"), Some("42"));
1264        assert_eq!(below.path(), "/users/:id");
1265    }
1266
1267    #[test]
1268    fn the_query_is_read_and_decoded() {
1269        let r = router();
1270        r.go("/users/1?tab=posts&q=a%20b+c&flag");
1271        let state = r.state().unwrap();
1272        assert_eq!(state.query("tab"), Some("posts"));
1273        assert_eq!(state.query("q"), Some("a b c"));
1274        assert_eq!(state.query("flag"), Some(""));
1275        assert_eq!(state.location(), "/users/1?tab=posts&q=a%20b+c&flag");
1276    }
1277
1278    #[test]
1279    fn a_parameter_is_percent_decoded() {
1280        let r = router();
1281        r.go("/users/a%2Fb");
1282        assert_eq!(r.state().unwrap().param("id"), Some("a/b"));
1283    }
1284
1285    #[test]
1286    fn a_wildcard_takes_what_is_left() {
1287        let r = router();
1288        r.go("/files/a/b/c.txt");
1289        assert_eq!(r.state().unwrap().param("*"), Some("a/b/c.txt"));
1290    }
1291
1292    #[test]
1293    fn a_location_no_route_answers_to_is_an_error_page() {
1294        let r = router();
1295        r.go("/nowhere");
1296        assert_eq!(r.depth(), 1);
1297        assert!(r.inner.stack.borrow().pages[0].node.is_none());
1298        assert_eq!(r.location(), "/nowhere");
1299    }
1300
1301    #[test]
1302    fn push_adds_a_page_and_pop_removes_it() {
1303        let r = router();
1304        r.push("/settings");
1305        assert_eq!((r.depth(), r.location()), (2, "/settings".to_string()));
1306        r.push("/settings");
1307        assert_eq!(r.depth(), 3, "the same page may be open twice");
1308        assert!(r.pop());
1309        assert!(r.pop());
1310        assert!(!r.pop(), "the last page stays");
1311        assert_eq!(r.location(), "/");
1312    }
1313
1314    #[test]
1315    fn replace_swaps_the_top_page() {
1316        let r = router();
1317        r.push("/settings");
1318        r.replace("/users/3");
1319        assert_eq!(keys(&r), ["/", "/users/:id"]);
1320    }
1321
1322    #[test]
1323    fn going_back_to_a_page_already_underneath_keeps_it() {
1324        let r = router();
1325        r.go("/users/1");
1326        let home = r.inner.stack.borrow().pages[0].key;
1327        r.go("/settings");
1328        r.go("/users/1");
1329        assert_eq!(
1330            r.inner.stack.borrow().pages[0].key,
1331            home,
1332            "the same place, the same page — and so the same state"
1333        );
1334    }
1335
1336    #[test]
1337    fn a_named_route_is_reached_by_its_name() {
1338        let r = router();
1339        assert_eq!(
1340            r.location_for("post", &[("id", "4 2"), ("post", "9")], &[("x", "a&b")]),
1341            Some("/users/4%202/posts/9?x=a%26b".to_string())
1342        );
1343        assert_eq!(
1344            r.location_for("settings", &[], &[]),
1345            Some("/settings".to_string())
1346        );
1347        assert_eq!(
1348            r.location_for("user", &[], &[]),
1349            None,
1350            "a parameter is missing"
1351        );
1352        assert_eq!(r.location_for("nope", &[], &[]), None);
1353        r.go_named("user", &[("id", "5")], &[]);
1354        assert_eq!(r.location(), "/users/5");
1355        r.push_named("settings", &[], &[]);
1356        assert_eq!(r.location(), "/settings");
1357    }
1358
1359    #[test]
1360    fn a_redirect_sends_the_navigation_elsewhere() {
1361        let signed_in = Rc::new(Cell::new(false));
1362        let gate = signed_in.clone();
1363        let r = GoRouter::new(vec![
1364            GoRoute::new("/", page),
1365            GoRoute::new("/login", page),
1366            GoRoute::new("/account", page),
1367        ])
1368        .animate(false)
1369        .redirect(move |state| {
1370            (state.location() == "/account" && !gate.get()).then(|| "/login".to_string())
1371        });
1372        r.go("/account");
1373        assert_eq!(r.location(), "/login");
1374        signed_in.set(true);
1375        r.go("/account");
1376        assert_eq!(r.location(), "/account");
1377    }
1378
1379    #[test]
1380    fn a_route_can_redirect_for_itself() {
1381        let r = GoRouter::new(vec![
1382            GoRoute::new("/", page),
1383            GoRoute::new("/old", page).redirect(|_| Some("/new".to_string())),
1384            GoRoute::new("/new", page),
1385        ])
1386        .animate(false);
1387        r.go("/old");
1388        assert_eq!(r.location(), "/new");
1389    }
1390
1391    #[test]
1392    fn a_redirect_loop_ends_in_the_error_page() {
1393        let r = GoRouter::new(vec![
1394            GoRoute::new("/", page),
1395            GoRoute::new("/a", page),
1396            GoRoute::new("/b", page),
1397        ])
1398        .animate(false)
1399        .redirect(|state| match state.location() {
1400            "/a" => Some("/b".to_string()),
1401            "/b" => Some("/a".to_string()),
1402            _ => None,
1403        });
1404        r.go("/a");
1405        assert!(r.inner.stack.borrow().pages[0].node.is_none());
1406    }
1407
1408    #[test]
1409    fn a_refresh_listenable_runs_the_redirects_again() {
1410        use crate::ValueNotifier;
1411        let auth = ValueNotifier::new(true);
1412        let watched = auth.clone();
1413        let r = GoRouter::new(vec![
1414            GoRoute::new("/", page),
1415            GoRoute::new("/login", page),
1416            GoRoute::new("/home", page),
1417        ])
1418        .animate(false)
1419        .redirect(move |state| {
1420            let signed_in = watched.get();
1421            match (state.location(), signed_in) {
1422                ("/home", false) => Some("/login".to_string()),
1423                ("/login", true) => Some("/home".to_string()),
1424                _ => None,
1425            }
1426        })
1427        .refresh_listenable(&auth);
1428        r.go("/home");
1429        assert_eq!(r.location(), "/home");
1430        auth.set(false);
1431        assert_eq!(
1432            r.location(),
1433            "/login",
1434            "signing out moves the page with nobody navigating"
1435        );
1436    }
1437
1438    #[test]
1439    fn extra_travels_with_the_navigation() {
1440        let r = router();
1441        r.go_with_extra("/settings", 41u32);
1442        assert_eq!(r.state().unwrap().extra::<u32>().as_deref(), Some(&41));
1443        assert!(r.state().unwrap().extra::<String>().is_none());
1444    }
1445
1446    #[test]
1447    fn a_push_slides_in_and_a_pop_slides_out() {
1448        let r = GoRouter::new(routes());
1449        r.go("/settings");
1450        assert!(
1451            r.inner.stack.borrow().transition.is_some(),
1452            "a page arrives"
1453        );
1454        assert!(r.inner.stack.borrow().transition.as_ref().unwrap().forward);
1455        let mut frames = 0;
1456        while r.tick(1.0 / 60.0) {
1457            frames += 1;
1458            assert!(frames < 600, "the spring settles");
1459        }
1460        assert!(r.inner.stack.borrow().transition.is_none());
1461
1462        r.push("/users/1");
1463        r.pop();
1464        assert!(
1465            !r.inner.stack.borrow().transition.as_ref().unwrap().forward,
1466            "going back"
1467        );
1468    }
1469
1470    #[test]
1471    fn a_back_gesture_past_halfway_pops_and_a_short_one_does_not() {
1472        let r = router();
1473        r.push("/settings");
1474        assert!(r.can_go_back());
1475        r.back_gesture(0.2);
1476        r.back_gesture_end(0.0);
1477        while r.tick(1.0 / 60.0) {}
1478        assert_eq!(r.depth(), 2, "let go early: the page comes back");
1479
1480        r.back_gesture(0.8);
1481        r.back_gesture_end(0.0);
1482        while r.tick(1.0 / 60.0) {}
1483        assert_eq!(r.depth(), 1, "carried far enough: it goes");
1484    }
1485
1486    #[test]
1487    fn a_route_needs_a_slash_at_the_top_and_none_below() {
1488        let bad_top = std::panic::catch_unwind(|| GoRouter::new(vec![GoRoute::new("nope", page)]));
1489        assert!(bad_top.is_err());
1490        let bad_child = std::panic::catch_unwind(|| {
1491            GoRouter::new(vec![
1492                GoRoute::new("/", page).routes(vec![GoRoute::new("/x", page)])
1493            ])
1494        });
1495        assert!(bad_child.is_err());
1496    }
1497}
1498
1499#[cfg(test)]
1500mod animated_tests {
1501    use super::*;
1502    use crate::Container;
1503
1504    fn page(_: &BuildContext, _: &GoRouterState) -> Container {
1505        Container::new()
1506    }
1507
1508    /// The same moves with the slide on — the default — must land where they land with it off.
1509    #[test]
1510    fn the_slide_does_not_change_where_the_stack_ends_up() {
1511        let router = GoRouter::new(vec![
1512            GoRoute::new("/", page).routes(vec![GoRoute::new("users/:id", page)]),
1513            GoRoute::new("/settings", page),
1514        ]);
1515        router.go("/users/42?tab=posts");
1516        assert_eq!(router.location(), "/users/42?tab=posts");
1517        assert!(router.can_pop());
1518        assert!(router.pop());
1519        assert_eq!(router.location(), "/");
1520    }
1521}
1522
1523#[cfg(test)]
1524mod open_tests {
1525    use super::*;
1526    use crate::Container;
1527
1528    fn page(_: &BuildContext, _: &GoRouterState) -> Container {
1529        Container::new()
1530    }
1531
1532    fn router() -> GoRouter {
1533        GoRouter::new(vec![
1534            GoRoute::new("/", page).routes(vec![GoRoute::new("users/:id", page)]),
1535            GoRoute::new("/settings", page),
1536        ])
1537    }
1538
1539    fn sliding(router: &GoRouter) -> bool {
1540        router.inner.stack.borrow().transition.is_some()
1541    }
1542
1543    #[test]
1544    fn an_address_the_router_is_already_at_changes_nothing() {
1545        let r = router();
1546        r.open("/");
1547        assert_eq!((r.location(), r.depth()), ("/".to_string(), 1));
1548        assert!(!sliding(&r));
1549    }
1550
1551    #[test]
1552    fn before_anything_is_built_the_router_starts_at_the_address_without_a_slide() {
1553        let r = router();
1554        r.open("/users/42");
1555        assert_eq!(r.location(), "/users/42");
1556        assert_eq!(r.depth(), 2, "home underneath, as with any `go`");
1557        assert!(
1558            !sliding(&r),
1559            "nobody has seen a page to slide away from: an address is where it opens"
1560        );
1561    }
1562
1563    #[test]
1564    fn once_a_page_has_been_seen_an_address_slides_to_it_like_any_other_move() {
1565        let r = router();
1566        r.inner.built.set(true);
1567        r.open("/settings");
1568        assert_eq!(r.location(), "/settings");
1569        assert!(sliding(&r));
1570    }
1571
1572    #[test]
1573    fn the_locations_of_the_stack_run_from_the_bottom_to_the_top() {
1574        let r = router();
1575        r.go("/users/42?tab=posts");
1576        assert_eq!(r.locations(), ["/", "/users/42?tab=posts"]);
1577    }
1578
1579    #[test]
1580    fn only_a_replace_is_a_swap() {
1581        let r = router();
1582        assert!(!r.replaced());
1583        r.push("/settings");
1584        assert!(!r.replaced());
1585        r.replace("/users/1");
1586        assert!(r.replaced());
1587        r.go("/settings");
1588        assert!(!r.replaced(), "a go rebuilds the stack");
1589        r.push("/users/3");
1590        r.replace("/users/2");
1591        assert!(r.replaced());
1592        assert!(r.pop());
1593        assert!(!r.replaced(), "and a pop removes a page");
1594    }
1595
1596    #[test]
1597    fn an_address_that_is_a_page_underneath_goes_back_to_it() {
1598        let r = router();
1599        r.go("/users/42");
1600        r.inner.built.set(true);
1601        r.open("/");
1602        assert_eq!((r.location(), r.depth()), ("/".to_string(), 1));
1603        let stack = r.inner.stack.borrow();
1604        assert!(
1605            !stack.transition.as_ref().unwrap().forward,
1606            "the browser's back button slides the page out, not in"
1607        );
1608    }
1609}