1use 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
64const BACK_PROJECT: f32 = 0.12;
71const BACK_COMMIT_POS: f32 = 0.5;
73const SPRING_K: f32 = 220.0;
75const SPRING_C: f32 = 30.0;
77
78fn spring() -> SpringDescription {
79 SpringDescription::new(1.0, SPRING_K, SPRING_C)
80}
81
82fn 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
131struct 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#[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 pub fn entering(&self) -> f32 {
181 self.entering
182 }
183
184 pub fn location(&self) -> &str {
186 &self.location
187 }
188
189 pub fn path(&self) -> &str {
191 &self.path
192 }
193
194 pub fn name(&self) -> Option<&str> {
196 self.name.as_deref()
197 }
198
199 pub fn param(&self, name: &str) -> Option<&str> {
201 self.params.get(name).map(String::as_str)
202 }
203
204 pub fn params(&self) -> &HashMap<String, String> {
206 &self.params
207 }
208
209 pub fn query(&self, name: &str) -> Option<&str> {
212 self.query.get(name).map(String::as_str)
213 }
214
215 pub fn query_params(&self) -> &HashMap<String, String> {
217 &self.query
218 }
219
220 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
239type RouteBuilder = Rc<dyn Fn(&BuildContext, &GoRouterState) -> Box<dyn Widget>>;
244type Redirect = Rc<dyn Fn(&GoRouterState) -> Option<String>>;
245
246pub 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 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 pub fn name(mut self, name: impl Into<String>) -> Self {
281 self.name = Some(name.into());
282 self
283 }
284
285 pub fn routes(mut self, routes: Vec<GoRoute>) -> Self {
288 self.routes = routes;
289 self
290 }
291
292 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
374fn 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
403fn 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#[derive(Clone)]
434struct Page {
435 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 built: Cell<bool>,
478 replaced: Cell<bool>,
480 unique: Cell<u64>,
481 refresh: RefCell<Option<Subscription>>,
482}
483
484#[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 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 pub fn initial_location(self, location: impl Into<String>) -> Self {
536 self.inner.config.borrow_mut().initial = location.into();
537 self
538 }
539
540 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 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 pub fn redirect_limit(self, limit: usize) -> Self {
563 self.inner.config.borrow_mut().limit = limit;
564 self
565 }
566
567 pub fn animate(self, animate: bool) -> Self {
569 self.inner.config.borrow_mut().animate = animate;
570 self
571 }
572
573 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 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 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 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 pub fn replaced(&self) -> bool {
628 self.inner.replaced.get()
629 }
630
631 pub fn depth(&self) -> usize {
633 self.start();
634 self.inner.stack.borrow().pages.len()
635 }
636
637 pub fn can_pop(&self) -> bool {
639 self.depth() > 1
640 }
641
642 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 pub fn go(&self, location: impl AsRef<str>) {
680 self.navigate(location.as_ref(), None, Mode::Go);
681 }
682
683 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 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 pub fn push(&self, location: impl AsRef<str>) {
699 self.navigate(location.as_ref(), None, Mode::Push);
700 }
701
702 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 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 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 pub fn replace(&self, location: impl AsRef<str>) {
736 self.navigate(location.as_ref(), None, Mode::Replace);
737 }
738
739 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 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 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, ¤t);
785 self.commit(pages);
786 self.inner.replaced.set(mode == Mode::Replace);
787 }
788
789 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 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 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 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 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 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 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 pub fn can_go_back(&self) -> bool {
1018 self.can_pop()
1019 }
1020
1021 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 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 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 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 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 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 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 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 #[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}