Skip to main content

desktop_example/app/
mod.rs

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}