1mod routes;
2
3use freya::{
4 animation::*,
5 icons::lucide,
6 material_design::FloatingTabRippleExt,
7 prelude::*,
8 router::*,
9};
10use routes::*;
11
12pub fn app() -> impl IntoElement {
13 Router::<Route>::new(RouterConfig::default)
14}
15
16#[derive(Routable, Clone, PartialEq)]
17#[rustfmt::skip]
18pub enum Route {
19 #[layout(AppTopBar)]
20 #[route("/")]
21 ScrollViewDemo,
22 #[route("/widgets")]
23 WidgetsDemo,
24 #[route("/portal")]
25 PortalDemo,
26 #[route("/editor")]
27 EditorDemo,
28 #[route("/markdown")]
29 MarkdownDemo,
30}
31
32const ROUTES: [Route; 5] = [
33 Route::ScrollViewDemo,
34 Route::WidgetsDemo,
35 Route::PortalDemo,
36 Route::EditorDemo,
37 Route::MarkdownDemo,
38];
39
40fn route_index(route: &Route) -> usize {
41 ROUTES.iter().position(|r| r == route).unwrap_or(0)
42}
43
44fn route_element(route: &Route) -> Element {
45 match route {
46 Route::ScrollViewDemo => ScrollViewDemo.into_element(),
47 Route::WidgetsDemo => WidgetsDemo.into_element(),
48 Route::PortalDemo => PortalDemo.into_element(),
49 Route::EditorDemo => EditorDemo.into_element(),
50 Route::MarkdownDemo => MarkdownDemo.into_element(),
51 }
52}
53
54#[derive(PartialEq)]
55struct AppTopBar;
56
57impl Component for AppTopBar {
58 fn render(&self) -> impl IntoElement {
59 use_init_theme(|| LIGHT_THEME);
60
61 NativeRouter::new().child(AnimatedRouter::<Route>::new(
62 rect()
63 .content(Content::flex())
64 .theme_background()
65 .theme_color()
66 .child(
67 rect()
68 .width(Size::fill())
69 .height(Size::flex(1.))
70 .padding((40., 0., 8., 0.))
71 .child(AnimatedOutlet),
72 )
73 .child(
74 rect()
75 .horizontal()
76 .width(Size::fill())
77 .main_align(Alignment::center())
78 .padding((4., 4., 20., 4.))
79 .spacing(4.)
80 .child(tab(Route::ScrollViewDemo, "Scroll", lucide::scroll_text))
81 .child(tab(
82 Route::WidgetsDemo,
83 "Widgets",
84 lucide::sliders_horizontal,
85 ))
86 .child(tab(Route::PortalDemo, "Portal", lucide::layers))
87 .child(tab(Route::EditorDemo, "Editor", lucide::code))
88 .child(tab(Route::MarkdownDemo, "Markdown", lucide::notebook_text)),
89 ),
90 ))
91 }
92}
93
94fn tab(route: Route, label: &'static str, icon: fn() -> Bytes) -> ActivableRoute<Route> {
95 let theme = get_theme_or_default();
96 ActivableRoute::new(
97 route.clone(),
98 Link::new(route).child(
99 FloatingTab::new().ripple().child(
100 rect()
101 .center()
102 .spacing(2.)
103 .child(
104 svg(icon())
105 .stroke(theme.read().colors.text_primary)
106 .width(Size::px(18.))
107 .height(Size::px(18.)),
108 )
109 .child(label),
110 ),
111 ),
112 )
113 .exact(true)
114}
115
116fn animated_page(scale: f32, corner_radius: f32, content: impl Into<Element>) -> Rect {
117 rect()
118 .width(Size::percent(100.))
119 .height(Size::percent(100.))
120 .center()
121 .theme_background()
122 .scale(scale)
123 .corner_radius(corner_radius)
124 .child(content)
125}
126
127#[derive(Clone, PartialEq)]
128struct FromRouteToCurrent {
129 from: Element,
130 left_to_right: bool,
131 area: State<Area>,
132}
133
134impl Component for FromRouteToCurrent {
135 fn render(&self) -> impl IntoElement {
136 let mut animated_router = use_animated_router::<Route>();
137 let animations = use_animation_with_dependencies(
138 &(self.left_to_right, self.from.clone()),
139 move |conf, (left_to_right, _)| {
140 conf.on_change(OnChange::Rerun);
141 conf.on_creation(OnCreation::Run);
142
143 let (start, end) = if *left_to_right { (1., 0.) } else { (0., 1.) };
144 (
145 AnimNum::new(start, end)
146 .time(500)
147 .ease(Ease::Out)
148 .function(Function::Expo),
149 AnimNum::new(1., 0.4)
150 .time(500)
151 .ease(Ease::Out)
152 .function(Function::Expo),
153 AnimNum::new(0.4, 1.)
154 .time(500)
155 .ease(Ease::Out)
156 .function(Function::Expo),
157 AnimNum::new(50., 0.)
158 .time(500)
159 .ease(Ease::Out)
160 .function(Function::Expo),
161 )
162 },
163 );
164
165 use_side_effect(move || {
166 if !*animations.is_running().read() && *animations.has_run_yet().read() {
167 animated_router.write().settle();
168 }
169 });
170
171 let (offset, scale_a, scale_b, corner_radius) = animations.get().value();
172 let (scale_out, scale_in) = if self.left_to_right {
173 (scale_a, scale_b)
174 } else {
175 (scale_b, scale_a)
176 };
177
178 let width = self.area.read().width();
179 let offset = width - (offset * width);
180
181 let to = Outlet::<Route>::new().into_element();
182 let (left, right) = if self.left_to_right {
183 (self.from.clone(), to)
184 } else {
185 (to, self.from.clone())
186 };
187
188 rect()
189 .expanded()
190 .offset_x(-offset)
191 .horizontal()
192 .child(animated_page(scale_out, corner_radius, left))
193 .child(animated_page(scale_in, corner_radius, right))
194 }
195}
196
197#[derive(Clone, PartialEq)]
198struct AnimatedOutlet;
199
200impl Component for AnimatedOutlet {
201 fn render(&self) -> impl IntoElement {
202 let mut area = use_state(Area::default);
203 let mut animated_router = use_animated_router();
204 let involves_scroll = matches!(
205 &*animated_router.read(),
206 AnimatedRouterContext::FromTo(from, to)
207 if *from == Route::ScrollViewDemo || *to == Route::ScrollViewDemo
208 );
209
210 let from_route = if involves_scroll {
211 animated_router.write().settle();
212 None
213 } else {
214 match &*animated_router.read() {
215 AnimatedRouterContext::FromTo(from, to) => {
216 let left_to_right = route_index(to) > route_index(from);
217 Some((route_element(from), left_to_right))
218 }
219 _ => None,
220 }
221 };
222
223 rect()
224 .on_sized(move |e: Event<SizedEventData>| area.set(e.area))
225 .child(match from_route {
226 Some((from, left_to_right)) => FromRouteToCurrent {
227 left_to_right,
228 from,
229 area,
230 }
231 .into_element(),
232 None => animated_page(1., 0., Outlet::<Route>::new()).into_element(),
233 })
234 }
235}