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, Curve, 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 CUPERTINO_FLING: f32 = 1.0;
76const CUPERTINO_SETTLE: f32 = 0.35;
79const SPRING_K: f32 = 220.0;
81const SPRING_C: f32 = 30.0;
83
84fn spring() -> SpringDescription {
85 SpringDescription::new(1.0, SPRING_K, SPRING_C)
86}
87
88fn decode(text: &str, plus_is_space: bool) -> String {
93 let bytes = text.as_bytes();
94 let mut out = Vec::with_capacity(bytes.len());
95 let mut i = 0;
96 while i < bytes.len() {
97 match bytes[i] {
98 b'%' if i + 2 < bytes.len() => {
99 let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
100 match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
101 Some(byte) => {
102 out.push(byte);
103 i += 3;
104 }
105 None => {
106 out.push(b'%');
107 i += 1;
108 }
109 }
110 }
111 b'+' if plus_is_space => {
112 out.push(b' ');
113 i += 1;
114 }
115 byte => {
116 out.push(byte);
117 i += 1;
118 }
119 }
120 }
121 String::from_utf8_lossy(&out).into_owned()
122}
123
124fn encode(text: &str) -> String {
125 let mut out = String::with_capacity(text.len());
126 for byte in text.bytes() {
127 match byte {
128 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
129 out.push(byte as char)
130 }
131 other => out.push_str(&format!("%{other:02X}")),
132 }
133 }
134 out
135}
136
137struct Parts {
139 segments: Vec<String>,
140 query: Vec<(String, String)>,
141}
142
143fn split_location(location: &str) -> Parts {
144 let without_fragment = location.split('#').next().unwrap_or("");
145 let (path, query) = without_fragment
146 .split_once('?')
147 .unwrap_or((without_fragment, ""));
148 let segments = path
149 .split('/')
150 .filter(|segment| !segment.is_empty())
151 .map(str::to_string)
152 .collect();
153 let query = query
154 .split('&')
155 .filter(|pair| !pair.is_empty())
156 .map(|pair| {
157 let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
158 (decode(key, true), decode(value, true))
159 })
160 .collect();
161 Parts { segments, query }
162}
163
164#[derive(Clone)]
171pub struct GoRouterState {
172 location: String,
173 path: String,
174 name: Option<String>,
175 params: HashMap<String, String>,
176 query: HashMap<String, String>,
177 extra: Option<Rc<dyn Any>>,
178 entering: f32,
179}
180
181impl GoRouterState {
182 pub fn entering(&self) -> f32 {
187 self.entering
188 }
189
190 pub fn location(&self) -> &str {
192 &self.location
193 }
194
195 pub fn path(&self) -> &str {
197 &self.path
198 }
199
200 pub fn name(&self) -> Option<&str> {
202 self.name.as_deref()
203 }
204
205 pub fn param(&self, name: &str) -> Option<&str> {
207 self.params.get(name).map(String::as_str)
208 }
209
210 pub fn params(&self) -> &HashMap<String, String> {
212 &self.params
213 }
214
215 pub fn query(&self, name: &str) -> Option<&str> {
218 self.query.get(name).map(String::as_str)
219 }
220
221 pub fn query_params(&self) -> &HashMap<String, String> {
223 &self.query
224 }
225
226 pub fn extra<T: 'static>(&self) -> Option<Rc<T>> {
229 self.extra.clone().and_then(|extra| extra.downcast().ok())
230 }
231}
232
233impl fmt::Debug for GoRouterState {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 f.debug_struct("GoRouterState")
236 .field("location", &self.location)
237 .field("path", &self.path)
238 .field("name", &self.name)
239 .field("params", &self.params)
240 .field("query", &self.query)
241 .finish()
242 }
243}
244
245type RouteBuilder = Rc<dyn Fn(&BuildContext, &GoRouterState) -> Box<dyn Widget>>;
250type Redirect = Rc<dyn Fn(&GoRouterState) -> Option<String>>;
251
252pub struct GoRoute {
258 path: String,
259 name: Option<String>,
260 builder: RouteBuilder,
261 routes: Vec<GoRoute>,
262 redirect: Option<Redirect>,
263}
264
265impl GoRoute {
266 pub fn new<W: Widget + 'static>(
272 path: impl Into<String>,
273 builder: impl Fn(&BuildContext, &GoRouterState) -> W + 'static,
274 ) -> Self {
275 Self {
276 path: path.into(),
277 name: None,
278 builder: Rc::new(move |cx, state| Box::new(builder(cx, state))),
279 routes: Vec::new(),
280 redirect: None,
281 }
282 }
283
284 pub fn name(mut self, name: impl Into<String>) -> Self {
287 self.name = Some(name.into());
288 self
289 }
290
291 pub fn routes(mut self, routes: Vec<GoRoute>) -> Self {
294 self.routes = routes;
295 self
296 }
297
298 pub fn redirect(
301 mut self,
302 redirect: impl Fn(&GoRouterState) -> Option<String> + 'static,
303 ) -> Self {
304 self.redirect = Some(Rc::new(redirect));
305 self
306 }
307}
308
309enum Segment {
310 Literal(String),
311 Param(String),
312 Splat,
313}
314
315struct Node {
316 full_path: String,
317 segments: Vec<Segment>,
318 name: Option<String>,
319 builder: RouteBuilder,
320 redirect: Option<Redirect>,
321 children: Vec<Rc<Node>>,
322}
323
324fn build_node(
325 route: GoRoute,
326 parent: Option<&str>,
327 names: &mut HashMap<String, Rc<Node>>,
328) -> Rc<Node> {
329 let full_path = match parent {
330 None => {
331 assert!(
332 route.path.starts_with('/'),
333 "a top-level route's path starts with `/`: `{}`",
334 route.path
335 );
336 route.path.clone()
337 }
338 Some(parent) => {
339 assert!(
340 !route.path.starts_with('/'),
341 "a sub-route's path is relative to its parent's, and does not start with `/`: `{}`",
342 route.path
343 );
344 assert!(!route.path.is_empty(), "a sub-route needs a path");
345 format!("{}/{}", parent.trim_end_matches('/'), route.path)
346 }
347 };
348 let segments = route
349 .path
350 .split('/')
351 .filter(|segment| !segment.is_empty())
352 .map(|segment| match segment {
353 "*" => Segment::Splat,
354 other => match other.strip_prefix(':') {
355 Some(name) => Segment::Param(name.to_string()),
356 None => Segment::Literal(other.to_string()),
357 },
358 })
359 .collect();
360 let children = route
361 .routes
362 .into_iter()
363 .map(|child| build_node(child, Some(&full_path), names))
364 .collect();
365 let node = Rc::new(Node {
366 full_path,
367 segments,
368 name: route.name.clone(),
369 builder: route.builder,
370 redirect: route.redirect,
371 children,
372 });
373 if let Some(name) = route.name {
374 let clash = names.insert(name.clone(), node.clone());
375 assert!(clash.is_none(), "two routes are named `{name}`");
376 }
377 node
378}
379
380fn consume(
383 segments: &[Segment],
384 input: &[String],
385 params: &mut HashMap<String, String>,
386) -> Option<usize> {
387 let mut at = 0;
388 for segment in segments {
389 match segment {
390 Segment::Literal(literal) => {
391 if input.get(at)? != literal {
392 return None;
393 }
394 at += 1;
395 }
396 Segment::Param(name) => {
397 params.insert(name.clone(), decode(input.get(at)?, false));
398 at += 1;
399 }
400 Segment::Splat => {
401 params.insert("*".to_string(), input[at.min(input.len())..].join("/"));
402 at = input.len();
403 }
404 }
405 }
406 Some(at)
407}
408
409fn match_nodes(
412 nodes: &[Rc<Node>],
413 input: &[String],
414 params: &mut HashMap<String, String>,
415) -> Option<Vec<Rc<Node>>> {
416 for node in nodes {
417 let mut found = params.clone();
418 let Some(used) = consume(&node.segments, input, &mut found) else {
419 continue;
420 };
421 if used == input.len() {
422 *params = found;
423 return Some(vec![node.clone()]);
424 }
425 if let Some(mut below) = match_nodes(&node.children, &input[used..], &mut found) {
426 *params = found;
427 below.insert(0, node.clone());
428 return Some(below);
429 }
430 }
431 None
432}
433
434#[derive(Clone)]
440struct Page {
441 key: u64,
444 node: Option<Rc<Node>>,
445 state: Rc<GoRouterState>,
446}
447
448struct Transition {
449 from: Page,
450 controller: AnimationController,
451 forward: bool,
452}
453
454struct Back {
455 progress: f32,
456 settle: Option<AnimationController>,
457 commit: bool,
458}
459
460#[derive(Default)]
461struct Stack {
462 pages: Vec<Page>,
463 transition: Option<Transition>,
464 back: Option<Back>,
465}
466
467struct Config {
468 nodes: Vec<Rc<Node>>,
469 names: HashMap<String, Rc<Node>>,
470 initial: String,
471 redirect: Option<Redirect>,
472 error: Option<RouteBuilder>,
473 limit: usize,
474 animate: bool,
475}
476
477struct Inner {
478 config: RefCell<Config>,
479 stack: RefCell<Stack>,
480 started: Cell<bool>,
481 built: Cell<bool>,
484 replaced: Cell<bool>,
486 transitions: Cell<crate::PageTransitionsBuilder>,
490 unique: Cell<u64>,
491 refresh: RefCell<Option<Subscription>>,
492}
493
494#[derive(Clone)]
498pub struct GoRouter {
499 inner: Rc<Inner>,
500}
501
502#[derive(Clone, Copy, PartialEq)]
503enum Mode {
504 Go,
505 Push,
506 Replace,
507}
508
509fn hash_of(value: impl Hash) -> u64 {
510 let mut hasher = std::collections::hash_map::DefaultHasher::new();
511 value.hash(&mut hasher);
512 hasher.finish()
513}
514
515impl GoRouter {
516 pub fn new(routes: Vec<GoRoute>) -> Self {
518 let mut names = HashMap::new();
519 let nodes = routes
520 .into_iter()
521 .map(|route| build_node(route, None, &mut names))
522 .collect();
523 Self {
524 inner: Rc::new(Inner {
525 config: RefCell::new(Config {
526 nodes,
527 names,
528 initial: "/".to_string(),
529 redirect: None,
530 error: None,
531 limit: 5,
532 animate: true,
533 }),
534 stack: RefCell::new(Stack::default()),
535 started: Cell::new(false),
536 built: Cell::new(false),
537 transitions: Cell::new(crate::PageTransitionsBuilder::Zoom),
538 replaced: Cell::new(false),
539 unique: Cell::new(0),
540 refresh: RefCell::new(None),
541 }),
542 }
543 }
544
545 pub fn initial_location(self, location: impl Into<String>) -> Self {
547 self.inner.config.borrow_mut().initial = location.into();
548 self
549 }
550
551 pub fn redirect(self, redirect: impl Fn(&GoRouterState) -> Option<String> + 'static) -> Self {
556 self.inner.config.borrow_mut().redirect = Some(Rc::new(redirect));
557 self
558 }
559
560 pub fn error_builder<W: Widget + 'static>(
563 self,
564 builder: impl Fn(&BuildContext, &GoRouterState) -> W + 'static,
565 ) -> Self {
566 self.inner.config.borrow_mut().error =
567 Some(Rc::new(move |cx, state| Box::new(builder(cx, state))));
568 self
569 }
570
571 pub fn redirect_limit(self, limit: usize) -> Self {
574 self.inner.config.borrow_mut().limit = limit;
575 self
576 }
577
578 pub fn animate(self, animate: bool) -> Self {
580 self.inner.config.borrow_mut().animate = animate;
581 self
582 }
583
584 pub fn refresh_listenable(self, listenable: &impl Listenable) -> Self {
587 let weak: Weak<Inner> = Rc::downgrade(&self.inner);
588 let subscription = listenable.add_listener(move || {
589 if let Some(inner) = weak.upgrade() {
590 GoRouter { inner }.refresh();
591 }
592 });
593 *self.inner.refresh.borrow_mut() = Some(subscription);
594 self
595 }
596
597 pub fn location(&self) -> String {
601 self.start();
602 self.inner
603 .stack
604 .borrow()
605 .pages
606 .last()
607 .map(|page| page.state.location.clone())
608 .unwrap_or_default()
609 }
610
611 pub fn state(&self) -> Option<GoRouterState> {
613 self.start();
614 self.inner
615 .stack
616 .borrow()
617 .pages
618 .last()
619 .map(|page| (*page.state).clone())
620 }
621
622 pub fn locations(&self) -> Vec<String> {
625 self.start();
626 self.inner
627 .stack
628 .borrow()
629 .pages
630 .iter()
631 .map(|page| page.state.location.clone())
632 .collect()
633 }
634
635 pub fn replaced(&self) -> bool {
639 self.inner.replaced.get()
640 }
641
642 pub fn depth(&self) -> usize {
644 self.start();
645 self.inner.stack.borrow().pages.len()
646 }
647
648 pub fn can_pop(&self) -> bool {
650 self.depth() > 1
651 }
652
653 pub fn location_for(
656 &self,
657 name: &str,
658 params: &[(&str, &str)],
659 query: &[(&str, &str)],
660 ) -> Option<String> {
661 let node = self.inner.config.borrow().names.get(name).cloned()?;
662 let mut path = String::new();
663 for segment in node.full_path.split('/').filter(|s| !s.is_empty()) {
664 path.push('/');
665 match segment.strip_prefix(':') {
666 Some(param) => {
667 let (_, value) = params.iter().find(|(key, _)| *key == param)?;
668 path.push_str(&encode(value));
669 }
670 None => path.push_str(segment),
671 }
672 }
673 if path.is_empty() {
674 path.push('/');
675 }
676 if !query.is_empty() {
677 let pairs: Vec<String> = query
678 .iter()
679 .map(|(key, value)| format!("{}={}", encode(key), encode(value)))
680 .collect();
681 path.push('?');
682 path.push_str(&pairs.join("&"));
683 }
684 Some(path)
685 }
686
687 pub fn go(&self, location: impl AsRef<str>) {
691 self.navigate(location.as_ref(), None, Mode::Go);
692 }
693
694 pub fn go_with_extra(&self, location: impl AsRef<str>, extra: impl Any) {
697 self.navigate(location.as_ref(), Some(Rc::new(extra)), Mode::Go);
698 }
699
700 pub fn go_named(&self, name: &str, params: &[(&str, &str)], query: &[(&str, &str)]) {
702 match self.location_for(name, params, query) {
703 Some(location) => self.go(location),
704 None => debug_assert!(false, "no route called `{name}`, or a parameter is missing"),
705 }
706 }
707
708 pub fn push(&self, location: impl AsRef<str>) {
710 self.navigate(location.as_ref(), None, Mode::Push);
711 }
712
713 pub fn push_with_extra(&self, location: impl AsRef<str>, extra: impl Any) {
716 self.navigate(location.as_ref(), Some(Rc::new(extra)), Mode::Push);
717 }
718
719 pub fn push_named(&self, name: &str, params: &[(&str, &str)], query: &[(&str, &str)]) {
721 match self.location_for(name, params, query) {
722 Some(location) => self.push(location),
723 None => debug_assert!(false, "no route called `{name}`, or a parameter is missing"),
724 }
725 }
726
727 pub fn open(&self, location: impl AsRef<str>) {
734 let location = location.as_ref();
735 if self.location() == location {
736 return;
737 }
738 self.go(location);
739 if !self.inner.built.get() {
740 self.inner.stack.borrow_mut().transition = None;
741 }
742 }
743
744 pub fn replace(&self, location: impl AsRef<str>) {
747 self.navigate(location.as_ref(), None, Mode::Replace);
748 }
749
750 pub fn pop(&self) -> bool {
752 self.start();
753 let pages = {
754 let stack = self.inner.stack.borrow();
755 if stack.pages.len() <= 1 {
756 return false;
757 }
758 stack.pages[..stack.pages.len() - 1].to_vec()
759 };
760 self.commit(pages);
761 self.inner.replaced.set(false);
762 true
763 }
764
765 pub fn refresh(&self) {
768 self.start();
769 let target = self
770 .inner
771 .stack
772 .borrow()
773 .pages
774 .last()
775 .map(|page| (page.state.location.clone(), page.state.extra.clone()));
776 if let Some((location, extra)) = target {
777 self.navigate(&location, extra, Mode::Go);
778 }
779 }
780
781 fn start(&self) {
784 if self.inner.started.replace(true) {
785 return;
786 }
787 let initial = self.inner.config.borrow().initial.clone();
788 let pages = self.pages_for(&initial, None, Mode::Go, &[]);
789 self.inner.stack.borrow_mut().pages = pages;
790 }
791
792 fn navigate(&self, location: &str, extra: Option<Rc<dyn Any>>, mode: Mode) {
793 self.start();
794 let current = self.inner.stack.borrow().pages.clone();
795 let pages = self.pages_for(location, extra, mode, ¤t);
796 self.commit(pages);
797 self.inner.replaced.set(mode == Mode::Replace);
798 }
799
800 fn pages_for(
802 &self,
803 location: &str,
804 extra: Option<Rc<dyn Any>>,
805 mode: Mode,
806 current: &[Page],
807 ) -> Vec<Page> {
808 let (nodes, redirect, limit) = {
809 let config = self.inner.config.borrow();
810 (config.nodes.clone(), config.redirect.clone(), config.limit)
811 };
812 let mut location = location.to_string();
813 let mut seen = 0usize;
814 loop {
815 let parts = split_location(&location);
816 let mut params = HashMap::new();
817 let chain = match_nodes(&nodes, &parts.segments, &mut params);
818 let query: HashMap<String, String> = parts.query.iter().cloned().collect();
819 let leaf = chain.as_ref().and_then(|chain| chain.last());
820 let state = GoRouterState {
821 location: location.clone(),
822 path: leaf.map(|n| n.full_path.clone()).unwrap_or_default(),
823 name: leaf.and_then(|n| n.name.clone()),
824 params: params.clone(),
825 query: query.clone(),
826 extra: extra.clone(),
827 entering: 1.0,
828 };
829
830 let mut sent = redirect.as_ref().and_then(|r| r(&state));
832 if sent.is_none() {
833 if let Some(chain) = &chain {
834 sent = chain
835 .iter()
836 .filter_map(|node| node.redirect.as_ref())
837 .find_map(|r| r(&state));
838 }
839 }
840 if let Some(next) = sent.filter(|next| *next != location) {
841 seen += 1;
842 if seen > limit {
843 let state = GoRouterState {
844 location: location.clone(),
845 path: String::new(),
846 name: None,
847 params: HashMap::new(),
848 query: HashMap::new(),
849 extra: None,
850 entering: 1.0,
851 };
852 return self.error_pages(state, current, mode);
853 }
854 location = next;
855 continue;
856 }
857
858 let Some(chain) = chain else {
859 return self.error_pages(state, current, mode);
860 };
861 return match mode {
862 Mode::Go => {
863 let mut prefix = String::new();
866 let mut used = 0;
867 chain
868 .iter()
869 .enumerate()
870 .map(|(at, node)| {
871 let mut scratch = HashMap::new();
872 let took =
873 consume(&node.segments, &parts.segments[used..], &mut scratch)
874 .unwrap_or(0);
875 for segment in &parts.segments[used..used + took] {
876 prefix.push('/');
877 prefix.push_str(segment);
878 }
879 used += took;
880 let page_state = if at + 1 == chain.len() {
881 state.clone()
882 } else {
883 GoRouterState {
887 location: if prefix.is_empty() {
888 "/".to_string()
889 } else {
890 prefix.clone()
891 },
892 path: node.full_path.clone(),
893 name: node.name.clone(),
894 ..state.clone()
895 }
896 };
897 Page {
898 key: hash_of(("go", prefix.clone(), node.full_path.clone())),
899 node: Some(node.clone()),
900 state: Rc::new(page_state),
901 }
902 })
903 .collect()
904 }
905 Mode::Push | Mode::Replace => {
906 let mut pages = current.to_vec();
907 if mode == Mode::Replace {
908 pages.pop();
909 }
910 let leaf = chain.last().expect("a match has a route").clone();
911 let unique = self.inner.unique.get() + 1;
912 self.inner.unique.set(unique);
913 pages.push(Page {
914 key: hash_of(("push", unique)),
915 node: Some(leaf),
916 state: Rc::new(state),
917 });
918 pages
919 }
920 };
921 }
922 }
923
924 fn error_pages(&self, state: GoRouterState, current: &[Page], mode: Mode) -> Vec<Page> {
925 let unique = self.inner.unique.get() + 1;
926 self.inner.unique.set(unique);
927 let page = Page {
928 key: hash_of(("error", unique)),
929 node: None,
930 state: Rc::new(state),
931 };
932 match mode {
933 Mode::Go => vec![page],
934 Mode::Push => {
935 let mut pages = current.to_vec();
936 pages.push(page);
937 pages
938 }
939 Mode::Replace => {
940 let mut pages = current.to_vec();
941 pages.pop();
942 pages.push(page);
943 pages
944 }
945 }
946 }
947
948 fn commit(&self, pages: Vec<Page>) {
950 let animate = self.inner.config.borrow().animate;
951 let mut stack = self.inner.stack.borrow_mut();
952 let old_top = stack.pages.last().cloned();
953 let new_top_key = pages.last().map(|page| page.key);
954 if let (true, Some(old), Some(new_key)) = (animate, old_top, new_top_key) {
955 if old.key != new_key {
956 let forward = !stack.pages.iter().any(|page| page.key == new_key);
957 let mut controller = AnimationController::unit();
961 controller.set_value(0.0);
962 controller.forward(
963 self.inner.transitions.get().transition_duration(),
964 Curve::Linear,
965 );
966 stack.transition = Some(Transition {
967 from: old,
968 controller,
969 forward,
970 });
971 }
972 }
973 stack.pages = pages;
974 stack.back = None;
975 drop(stack);
976 request_rebuild();
977 }
978
979 pub fn tick(&self, dt: f32) -> bool {
984 let mut animating = false;
985 let mut pop = false;
986 {
987 let mut stack = self.inner.stack.borrow_mut();
988 let arrived = match stack.transition.as_mut() {
989 Some(transition) => {
990 if transition.controller.tick(dt) {
991 animating = true;
992 false
993 } else {
994 true
995 }
996 }
997 None => false,
998 };
999 if arrived {
1000 stack.transition = None;
1001 }
1002 let mut settled = false;
1003 if let Some(back) = stack.back.as_mut() {
1004 if let Some(settle) = back.settle.as_mut() {
1005 if settle.tick(dt) {
1006 back.progress = settle.value();
1007 animating = true;
1008 } else {
1009 pop = back.commit;
1010 settled = true;
1011 }
1012 }
1013 }
1014 if settled {
1015 stack.back = None;
1016 }
1017 }
1018 if pop {
1019 let pages = {
1022 let stack = self.inner.stack.borrow();
1023 stack.pages[..stack.pages.len().saturating_sub(1)].to_vec()
1024 };
1025 if !pages.is_empty() {
1026 self.inner.stack.borrow_mut().pages = pages;
1027 }
1028 request_rebuild();
1029 }
1030 animating
1031 }
1032
1033 pub fn can_go_back(&self) -> bool {
1035 self.can_pop()
1036 }
1037
1038 pub fn back_gesture(&self, progress: f32) {
1040 let mut stack = self.inner.stack.borrow_mut();
1041 match stack.back.as_mut() {
1042 Some(back) => back.progress = progress,
1043 None => {
1044 stack.back = Some(Back {
1045 progress,
1046 settle: None,
1047 commit: false,
1048 })
1049 }
1050 }
1051 drop(stack);
1052 request_rebuild();
1053 }
1054
1055 pub fn back_gesture_end(&self, velocity: f32) {
1058 let mut stack = self.inner.stack.borrow_mut();
1059 let deep_enough = stack.pages.len() > 1;
1060 let cupertino = self.inner.transitions.get() == crate::PageTransitionsBuilder::Cupertino;
1061 if let Some(back) = stack.back.as_mut() {
1062 let mut settle = AnimationController::unit();
1063 settle.set_value(back.progress);
1064 let commit = if cupertino {
1065 let goes = if velocity.abs() >= CUPERTINO_FLING {
1069 velocity > 0.0
1070 } else {
1071 back.progress >= 0.5
1072 };
1073 let commit = goes && deep_enough;
1074 settle.animate_to(
1075 if commit { 1.0 } else { 0.0 },
1076 CUPERTINO_SETTLE,
1077 Curve::fast_ease_in_to_slow_ease_out(),
1078 );
1079 commit
1080 } else {
1081 let projected = back.progress + velocity * BACK_PROJECT;
1084 let commit = projected > BACK_COMMIT_POS && deep_enough;
1085 settle.spring_to(if commit { 1.0 } else { 0.0 }, spring(), velocity);
1086 commit
1087 };
1088 back.commit = commit;
1089 back.settle = Some(settle);
1090 }
1091 drop(stack);
1092 request_rebuild();
1093 }
1094
1095 pub fn build(&self, cx: &BuildContext) -> Box<dyn Widget> {
1100 self.start();
1101 self.inner.built.set(true);
1102 let theme = cx.theme();
1103 self.inner
1104 .transitions
1105 .set(theme.page_transitions.builder_for(theme.platform));
1106 cx.runtime().states.provide(Rc::new(self.clone()));
1107 let error_builder = self.inner.config.borrow().error.clone();
1108 let (pages, transition, back) = {
1111 let stack = self.inner.stack.borrow();
1112 (
1113 stack.pages.clone(),
1114 stack
1115 .transition
1116 .as_ref()
1117 .map(|t| (t.from.clone(), t.controller.value(), t.forward)),
1118 stack.back.as_ref().map(|b| b.progress),
1119 )
1120 };
1121 let depth = pages.len();
1122 let make = |page: &Page, entering: f32| -> Box<dyn Widget> {
1125 let state = GoRouterState {
1126 entering,
1127 ..(*page.state).clone()
1128 };
1129 match &page.node {
1130 Some(node) => (node.builder)(cx, &state),
1131 None => match &error_builder {
1132 Some(builder) => builder(cx, &state),
1133 None => Box::new(crate::Text::new(format!(
1134 "Page not found: {}",
1135 state.location
1136 ))),
1137 },
1138 }
1139 };
1140 let key_of = |page: &Page, depth: usize| (depth, page.key);
1141 let top = pages.last().expect("a router has at least one page");
1142
1143 if let Some(progress) = back {
1145 if depth >= 2 {
1146 let below = &pages[depth - 2];
1147 let mut navigator = Navigator::new(key_of(below, depth - 2), make(below, progress))
1148 .from(
1149 key_of(top, depth - 1),
1150 make(top, 1.0 - progress),
1151 progress,
1152 false,
1153 )
1154 .gesture(true);
1156 for (at, page) in pages[..depth - 2].iter().enumerate() {
1157 navigator = navigator.retain(key_of(page, at), make(page, 1.0));
1158 }
1159 return Box::new(navigator);
1160 }
1161 }
1162
1163 match transition {
1164 Some((from, progress, forward)) => {
1165 let from_depth = pages
1168 .iter()
1169 .position(|page| page.key == from.key)
1170 .unwrap_or(depth);
1171 let mut navigator = Navigator::new(key_of(top, depth - 1), make(top, progress))
1172 .from(
1173 key_of(&from, from_depth),
1174 make(&from, 1.0 - progress),
1175 progress,
1176 forward,
1177 );
1178 for (at, page) in pages[..depth - 1].iter().enumerate() {
1179 if page.key != from.key {
1180 navigator = navigator.retain(key_of(page, at), make(page, 1.0));
1181 }
1182 }
1183 Box::new(navigator)
1184 }
1185 None => {
1186 let mut navigator = Navigator::new(key_of(top, depth - 1), make(top, 1.0));
1187 for (at, page) in pages[..depth - 1].iter().enumerate() {
1188 navigator = navigator.retain(key_of(page, at), make(page, 1.0));
1189 }
1190 Box::new(navigator)
1191 }
1192 }
1193 }
1194}
1195
1196impl BuildContext<'_> {
1197 pub fn router(&self) -> GoRouter {
1206 match self.service::<GoRouter>() {
1207 Some(router) => (*router).clone(),
1208 None => {
1209 panic!("there is no router: this application was not made with `FrusApp::router`")
1210 }
1211 }
1212 }
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218 use crate::Container;
1219
1220 fn page(_: &BuildContext, _: &GoRouterState) -> Container {
1221 Container::new()
1222 }
1223
1224 fn routes() -> Vec<GoRoute> {
1225 vec![
1226 GoRoute::new("/", page).routes(vec![
1227 GoRoute::new("users/:id", page)
1228 .name("user")
1229 .routes(vec![GoRoute::new("posts/:post", page).name("post")]),
1230 GoRoute::new("files/*", page).name("files"),
1231 ]),
1232 GoRoute::new("/settings", page).name("settings"),
1233 ]
1234 }
1235
1236 fn router() -> GoRouter {
1237 GoRouter::new(routes()).animate(false)
1238 }
1239
1240 fn keys(router: &GoRouter) -> Vec<String> {
1241 router
1242 .inner
1243 .stack
1244 .borrow()
1245 .pages
1246 .iter()
1247 .map(|p| p.state.path.clone())
1248 .collect()
1249 }
1250
1251 #[test]
1252 fn it_starts_at_the_initial_location() {
1253 assert_eq!(router().location(), "/");
1254 assert_eq!(
1255 GoRouter::new(routes())
1256 .initial_location("/settings")
1257 .location(),
1258 "/settings"
1259 );
1260 }
1261
1262 #[test]
1263 fn go_makes_the_stack_the_chain_of_routes_the_location_is_made_of() {
1264 let r = router();
1265 r.go("/users/42/posts/7");
1266 assert_eq!(
1267 keys(&r),
1268 ["/", "/users/:id", "/users/:id/posts/:post"],
1269 "home, the user, then the post: something to go back to at each step"
1270 );
1271 let state = r.state().unwrap();
1272 assert_eq!(state.param("id"), Some("42"));
1273 assert_eq!(state.param("post"), Some("7"));
1274 assert_eq!(state.path(), "/users/:id/posts/:post");
1275 assert_eq!(state.name(), Some("post"));
1276 }
1277
1278 #[test]
1279 fn a_page_underneath_stands_for_its_own_location() {
1280 let r = router();
1281 r.go("/users/42/posts/7?x=1");
1282 let locations: Vec<String> = r
1283 .inner
1284 .stack
1285 .borrow()
1286 .pages
1287 .iter()
1288 .map(|p| p.state.location.clone())
1289 .collect();
1290 assert_eq!(
1291 locations,
1292 ["/", "/users/42", "/users/42/posts/7?x=1"],
1293 "each page is the part of the location it answered; the top one, all of it"
1294 );
1295 r.pop();
1296 assert_eq!(
1297 r.location(),
1298 "/users/42",
1299 "so going back lands where that page is"
1300 );
1301 }
1302
1303 #[test]
1304 fn a_page_underneath_knows_the_parameters_too() {
1305 let r = router();
1306 r.go("/users/42/posts/7");
1307 let below = r.inner.stack.borrow().pages[1].state.clone();
1308 assert_eq!(below.param("id"), Some("42"));
1309 assert_eq!(below.path(), "/users/:id");
1310 }
1311
1312 #[test]
1313 fn the_query_is_read_and_decoded() {
1314 let r = router();
1315 r.go("/users/1?tab=posts&q=a%20b+c&flag");
1316 let state = r.state().unwrap();
1317 assert_eq!(state.query("tab"), Some("posts"));
1318 assert_eq!(state.query("q"), Some("a b c"));
1319 assert_eq!(state.query("flag"), Some(""));
1320 assert_eq!(state.location(), "/users/1?tab=posts&q=a%20b+c&flag");
1321 }
1322
1323 #[test]
1324 fn a_parameter_is_percent_decoded() {
1325 let r = router();
1326 r.go("/users/a%2Fb");
1327 assert_eq!(r.state().unwrap().param("id"), Some("a/b"));
1328 }
1329
1330 #[test]
1331 fn a_wildcard_takes_what_is_left() {
1332 let r = router();
1333 r.go("/files/a/b/c.txt");
1334 assert_eq!(r.state().unwrap().param("*"), Some("a/b/c.txt"));
1335 }
1336
1337 #[test]
1338 fn a_location_no_route_answers_to_is_an_error_page() {
1339 let r = router();
1340 r.go("/nowhere");
1341 assert_eq!(r.depth(), 1);
1342 assert!(r.inner.stack.borrow().pages[0].node.is_none());
1343 assert_eq!(r.location(), "/nowhere");
1344 }
1345
1346 #[test]
1347 fn push_adds_a_page_and_pop_removes_it() {
1348 let r = router();
1349 r.push("/settings");
1350 assert_eq!((r.depth(), r.location()), (2, "/settings".to_string()));
1351 r.push("/settings");
1352 assert_eq!(r.depth(), 3, "the same page may be open twice");
1353 assert!(r.pop());
1354 assert!(r.pop());
1355 assert!(!r.pop(), "the last page stays");
1356 assert_eq!(r.location(), "/");
1357 }
1358
1359 #[test]
1360 fn replace_swaps_the_top_page() {
1361 let r = router();
1362 r.push("/settings");
1363 r.replace("/users/3");
1364 assert_eq!(keys(&r), ["/", "/users/:id"]);
1365 }
1366
1367 #[test]
1368 fn going_back_to_a_page_already_underneath_keeps_it() {
1369 let r = router();
1370 r.go("/users/1");
1371 let home = r.inner.stack.borrow().pages[0].key;
1372 r.go("/settings");
1373 r.go("/users/1");
1374 assert_eq!(
1375 r.inner.stack.borrow().pages[0].key,
1376 home,
1377 "the same place, the same page — and so the same state"
1378 );
1379 }
1380
1381 #[test]
1382 fn a_named_route_is_reached_by_its_name() {
1383 let r = router();
1384 assert_eq!(
1385 r.location_for("post", &[("id", "4 2"), ("post", "9")], &[("x", "a&b")]),
1386 Some("/users/4%202/posts/9?x=a%26b".to_string())
1387 );
1388 assert_eq!(
1389 r.location_for("settings", &[], &[]),
1390 Some("/settings".to_string())
1391 );
1392 assert_eq!(
1393 r.location_for("user", &[], &[]),
1394 None,
1395 "a parameter is missing"
1396 );
1397 assert_eq!(r.location_for("nope", &[], &[]), None);
1398 r.go_named("user", &[("id", "5")], &[]);
1399 assert_eq!(r.location(), "/users/5");
1400 r.push_named("settings", &[], &[]);
1401 assert_eq!(r.location(), "/settings");
1402 }
1403
1404 #[test]
1405 fn a_redirect_sends_the_navigation_elsewhere() {
1406 let signed_in = Rc::new(Cell::new(false));
1407 let gate = signed_in.clone();
1408 let r = GoRouter::new(vec![
1409 GoRoute::new("/", page),
1410 GoRoute::new("/login", page),
1411 GoRoute::new("/account", page),
1412 ])
1413 .animate(false)
1414 .redirect(move |state| {
1415 (state.location() == "/account" && !gate.get()).then(|| "/login".to_string())
1416 });
1417 r.go("/account");
1418 assert_eq!(r.location(), "/login");
1419 signed_in.set(true);
1420 r.go("/account");
1421 assert_eq!(r.location(), "/account");
1422 }
1423
1424 #[test]
1425 fn a_route_can_redirect_for_itself() {
1426 let r = GoRouter::new(vec![
1427 GoRoute::new("/", page),
1428 GoRoute::new("/old", page).redirect(|_| Some("/new".to_string())),
1429 GoRoute::new("/new", page),
1430 ])
1431 .animate(false);
1432 r.go("/old");
1433 assert_eq!(r.location(), "/new");
1434 }
1435
1436 #[test]
1437 fn a_redirect_loop_ends_in_the_error_page() {
1438 let r = GoRouter::new(vec![
1439 GoRoute::new("/", page),
1440 GoRoute::new("/a", page),
1441 GoRoute::new("/b", page),
1442 ])
1443 .animate(false)
1444 .redirect(|state| match state.location() {
1445 "/a" => Some("/b".to_string()),
1446 "/b" => Some("/a".to_string()),
1447 _ => None,
1448 });
1449 r.go("/a");
1450 assert!(r.inner.stack.borrow().pages[0].node.is_none());
1451 }
1452
1453 #[test]
1454 fn a_refresh_listenable_runs_the_redirects_again() {
1455 use crate::ValueNotifier;
1456 let auth = ValueNotifier::new(true);
1457 let watched = auth.clone();
1458 let r = GoRouter::new(vec![
1459 GoRoute::new("/", page),
1460 GoRoute::new("/login", page),
1461 GoRoute::new("/home", page),
1462 ])
1463 .animate(false)
1464 .redirect(move |state| {
1465 let signed_in = watched.get();
1466 match (state.location(), signed_in) {
1467 ("/home", false) => Some("/login".to_string()),
1468 ("/login", true) => Some("/home".to_string()),
1469 _ => None,
1470 }
1471 })
1472 .refresh_listenable(&auth);
1473 r.go("/home");
1474 assert_eq!(r.location(), "/home");
1475 auth.set(false);
1476 assert_eq!(
1477 r.location(),
1478 "/login",
1479 "signing out moves the page with nobody navigating"
1480 );
1481 }
1482
1483 #[test]
1484 fn extra_travels_with_the_navigation() {
1485 let r = router();
1486 r.go_with_extra("/settings", 41u32);
1487 assert_eq!(r.state().unwrap().extra::<u32>().as_deref(), Some(&41));
1488 assert!(r.state().unwrap().extra::<String>().is_none());
1489 }
1490
1491 #[test]
1492 fn a_push_slides_in_and_a_pop_slides_out() {
1493 let r = GoRouter::new(routes());
1494 r.go("/settings");
1495 assert!(
1496 r.inner.stack.borrow().transition.is_some(),
1497 "a page arrives"
1498 );
1499 assert!(r.inner.stack.borrow().transition.as_ref().unwrap().forward);
1500 let mut frames = 0;
1501 while r.tick(1.0 / 60.0) {
1502 frames += 1;
1503 assert!(frames < 600, "the spring settles");
1504 }
1505 assert!(r.inner.stack.borrow().transition.is_none());
1506
1507 r.push("/users/1");
1508 r.pop();
1509 assert!(
1510 !r.inner.stack.borrow().transition.as_ref().unwrap().forward,
1511 "going back"
1512 );
1513 }
1514
1515 #[test]
1519 fn a_push_lasts_as_long_as_the_platform_s_transition() {
1520 let frames = |platform: frus_core::TargetPlatform| {
1521 let r = GoRouter::new(routes());
1523 let runtime = crate::Runtime::default();
1524 let theme = crate::Theme::default().with_platform(platform);
1525 let _ = r.build(&BuildContext::for_test(&runtime, &theme));
1526 r.push("/settings");
1527 let mut frames = 0;
1528 while r.tick(0.01) {
1529 frames += 1;
1530 assert!(frames < 1000);
1531 }
1532 frames
1533 };
1534 let slide = frames(frus_core::TargetPlatform::Ios);
1535 let zoom = frames(frus_core::TargetPlatform::Linux);
1536 assert!(
1537 (49..=51).contains(&slide),
1538 "half a second: {slide} frames of 10 ms"
1539 );
1540 assert!(
1541 (29..=31).contains(&zoom),
1542 "three tenths: {zoom} frames of 10 ms"
1543 );
1544 }
1545
1546 #[test]
1549 fn a_back_gesture_is_drawn_as_the_finger_moves() {
1550 let r = router();
1551 let runtime = crate::Runtime::default();
1552 let theme = crate::Theme::default();
1553 r.push("/settings");
1554 while r.tick(0.05) {}
1555 r.back_gesture(0.3);
1556 let tree = r.build(&BuildContext::for_test(&runtime, &theme));
1557 assert!(tree.navigator_gesture(), "under the finger");
1558 r.back_gesture_end(0.0);
1559 while r.tick(0.05) {}
1560 let tree = r.build(&BuildContext::for_test(&runtime, &theme));
1561 assert!(!tree.navigator_gesture(), "and not once it has let go");
1562 }
1563
1564 #[test]
1569 fn an_ios_page_let_go_follows_the_ios_rules() {
1570 let released = |progress: f32, velocity: f32| {
1571 let r = router();
1572 let runtime = crate::Runtime::default();
1573 let theme = crate::Theme::default().with_platform(frus_core::TargetPlatform::Ios);
1574 let _ = r.build(&BuildContext::for_test(&runtime, &theme));
1575 r.push("/settings");
1576 r.back_gesture(progress);
1577 r.back_gesture_end(velocity);
1578 let mut frames = 0;
1579 while r.tick(0.01) {
1580 frames += 1;
1581 assert!(frames < 1000);
1582 }
1583 (r.depth(), frames)
1584 };
1585 assert_eq!(
1586 released(0.2, 1.2).0,
1587 1,
1588 "a flick back pops it from near the start"
1589 );
1590 assert_eq!(released(0.8, -1.2).0, 2, "a flick the other way keeps it");
1591 assert_eq!(released(0.6, 0.5).0, 1, "slow, past halfway: it goes");
1592 assert_eq!(released(0.4, 0.9).0, 2, "slow, not halfway: it stays");
1593 let (_, frames) = released(0.6, 0.0);
1594 assert!(
1595 (34..=36).contains(&frames),
1596 "350 ms: {frames} frames of 10 ms"
1597 );
1598 let (_, frames) = released(0.1, 0.0);
1599 assert!(
1600 (34..=36).contains(&frames),
1601 "from wherever it was: {frames}"
1602 );
1603 }
1604
1605 #[test]
1606 fn a_back_gesture_past_halfway_pops_and_a_short_one_does_not() {
1607 let r = router();
1608 r.push("/settings");
1609 assert!(r.can_go_back());
1610 r.back_gesture(0.2);
1611 r.back_gesture_end(0.0);
1612 while r.tick(1.0 / 60.0) {}
1613 assert_eq!(r.depth(), 2, "let go early: the page comes back");
1614
1615 r.back_gesture(0.8);
1616 r.back_gesture_end(0.0);
1617 while r.tick(1.0 / 60.0) {}
1618 assert_eq!(r.depth(), 1, "carried far enough: it goes");
1619 }
1620
1621 #[test]
1622 fn a_route_needs_a_slash_at_the_top_and_none_below() {
1623 let bad_top = std::panic::catch_unwind(|| GoRouter::new(vec![GoRoute::new("nope", page)]));
1624 assert!(bad_top.is_err());
1625 let bad_child = std::panic::catch_unwind(|| {
1626 GoRouter::new(vec![
1627 GoRoute::new("/", page).routes(vec![GoRoute::new("/x", page)])
1628 ])
1629 });
1630 assert!(bad_child.is_err());
1631 }
1632}
1633
1634#[cfg(test)]
1635mod animated_tests {
1636 use super::*;
1637 use crate::Container;
1638
1639 fn page(_: &BuildContext, _: &GoRouterState) -> Container {
1640 Container::new()
1641 }
1642
1643 #[test]
1645 fn the_slide_does_not_change_where_the_stack_ends_up() {
1646 let router = GoRouter::new(vec![
1647 GoRoute::new("/", page).routes(vec![GoRoute::new("users/:id", page)]),
1648 GoRoute::new("/settings", page),
1649 ]);
1650 router.go("/users/42?tab=posts");
1651 assert_eq!(router.location(), "/users/42?tab=posts");
1652 assert!(router.can_pop());
1653 assert!(router.pop());
1654 assert_eq!(router.location(), "/");
1655 }
1656}
1657
1658#[cfg(test)]
1659mod open_tests {
1660 use super::*;
1661 use crate::Container;
1662
1663 fn page(_: &BuildContext, _: &GoRouterState) -> Container {
1664 Container::new()
1665 }
1666
1667 fn router() -> GoRouter {
1668 GoRouter::new(vec![
1669 GoRoute::new("/", page).routes(vec![GoRoute::new("users/:id", page)]),
1670 GoRoute::new("/settings", page),
1671 ])
1672 }
1673
1674 fn sliding(router: &GoRouter) -> bool {
1675 router.inner.stack.borrow().transition.is_some()
1676 }
1677
1678 #[test]
1679 fn an_address_the_router_is_already_at_changes_nothing() {
1680 let r = router();
1681 r.open("/");
1682 assert_eq!((r.location(), r.depth()), ("/".to_string(), 1));
1683 assert!(!sliding(&r));
1684 }
1685
1686 #[test]
1687 fn before_anything_is_built_the_router_starts_at_the_address_without_a_slide() {
1688 let r = router();
1689 r.open("/users/42");
1690 assert_eq!(r.location(), "/users/42");
1691 assert_eq!(r.depth(), 2, "home underneath, as with any `go`");
1692 assert!(
1693 !sliding(&r),
1694 "nobody has seen a page to slide away from: an address is where it opens"
1695 );
1696 }
1697
1698 #[test]
1699 fn once_a_page_has_been_seen_an_address_slides_to_it_like_any_other_move() {
1700 let r = router();
1701 r.inner.built.set(true);
1702 r.open("/settings");
1703 assert_eq!(r.location(), "/settings");
1704 assert!(sliding(&r));
1705 }
1706
1707 #[test]
1708 fn the_locations_of_the_stack_run_from_the_bottom_to_the_top() {
1709 let r = router();
1710 r.go("/users/42?tab=posts");
1711 assert_eq!(r.locations(), ["/", "/users/42?tab=posts"]);
1712 }
1713
1714 #[test]
1715 fn only_a_replace_is_a_swap() {
1716 let r = router();
1717 assert!(!r.replaced());
1718 r.push("/settings");
1719 assert!(!r.replaced());
1720 r.replace("/users/1");
1721 assert!(r.replaced());
1722 r.go("/settings");
1723 assert!(!r.replaced(), "a go rebuilds the stack");
1724 r.push("/users/3");
1725 r.replace("/users/2");
1726 assert!(r.replaced());
1727 assert!(r.pop());
1728 assert!(!r.replaced(), "and a pop removes a page");
1729 }
1730
1731 #[test]
1732 fn an_address_that_is_a_page_underneath_goes_back_to_it() {
1733 let r = router();
1734 r.go("/users/42");
1735 r.inner.built.set(true);
1736 r.open("/");
1737 assert_eq!((r.location(), r.depth()), ("/".to_string(), 1));
1738 let stack = r.inner.stack.borrow();
1739 assert!(
1740 !stack.transition.as_ref().unwrap().forward,
1741 "the browser's back button slides the page out, not in"
1742 );
1743 }
1744}