Skip to main content

freya_components/
portal.rs

1use std::{
2    collections::HashMap,
3    fmt::Debug,
4    hash::{
5        DefaultHasher,
6        Hash,
7        Hasher,
8    },
9    time::Duration,
10};
11
12use freya_animation::prelude::*;
13use freya_core::{
14    prelude::*,
15    scope_id::ScopeId,
16};
17use torin::{
18    prelude::{
19        Area,
20        Point2D,
21        Position,
22        Size2D,
23    },
24    size::Size,
25};
26
27#[derive(PartialEq)]
28pub struct Portal<T> {
29    key: DiffKey,
30    children: Vec<Element>,
31    id: T,
32    function: Function,
33    duration: Duration,
34    ease: Ease,
35    layout: LayoutData,
36    show: bool,
37    dependency: Option<u64>,
38}
39
40impl<T> ChildrenExt for Portal<T> {
41    fn get_children(&mut self) -> &mut Vec<Element> {
42        &mut self.children
43    }
44}
45
46impl<T> Portal<T> {
47    pub fn new(id: T) -> Self {
48        Self {
49            key: DiffKey::None,
50            children: vec![],
51            id,
52            function: Function::default(),
53            duration: Duration::from_millis(750),
54            ease: Ease::default(),
55            layout: LayoutData::default(),
56            show: true,
57            dependency: None,
58        }
59    }
60
61    pub fn function(mut self, function: Function) -> Self {
62        self.function = function;
63        self
64    }
65
66    pub fn duration(mut self, duration: Duration) -> Self {
67        self.duration = duration;
68        self
69    }
70
71    pub fn ease(mut self, ease: Ease) -> Self {
72        self.ease = ease;
73        self
74    }
75
76    pub fn show(mut self, show: bool) -> Self {
77        self.show = show;
78        self
79    }
80
81    /// Only animate when this value changes, other layout changes move the portal instantly.
82    pub fn animation_dependency(mut self, dependency: impl Hash) -> Self {
83        let mut hasher = DefaultHasher::default();
84        dependency.hash(&mut hasher);
85        self.dependency = Some(hasher.finish());
86        self
87    }
88}
89
90impl<T> LayoutExt for Portal<T> {
91    fn get_layout(&mut self) -> &mut LayoutData {
92        &mut self.layout
93    }
94}
95
96impl<T> ContainerSizeExt for Portal<T> {}
97
98impl<T> KeyExt for Portal<T> {
99    fn write_key(&mut self) -> &mut DiffKey {
100        &mut self.key
101    }
102}
103
104impl<T: Clone + Eq + Hash + Debug + 'static> Component for Portal<T> {
105    fn render(&self) -> impl IntoElement {
106        let mut positions = use_hook(|| match try_consume_context::<PortalsMap<T>>() {
107            Some(ctx) => ctx,
108            None => {
109                let ctx = PortalsMap {
110                    ids: State::create_in_scope(HashMap::default(), ScopeId::ROOT),
111                };
112                provide_context_for_scope_id(ctx.clone(), ScopeId::ROOT);
113                ctx
114            }
115        });
116        let id = self.id.clone();
117        let dependency = self.dependency;
118        // Area left by the previous mount of this same portal
119        let init_size = use_hook(move || {
120            positions
121                .ids
122                .write()
123                .remove(&id)
124                .filter(|(_, last)| dependency.is_none() || *last != dependency)
125                .map(|(area, _)| area)
126        });
127        let mut previous_size = use_state::<Option<Area>>(|| None);
128        let mut current_size = use_state::<Option<Area>>(|| None);
129        let mut last_dependency = use_state::<Option<u64>>(|| None);
130        let mut should_animate = use_state(|| false);
131
132        let mut animation = use_animation_with_dependencies(
133            &(
134                self.function,
135                self.duration,
136                self.ease,
137                previous_size(),
138                current_size(),
139            ),
140            move |conf, (function, duration, ease, previous_size, current_size)| {
141                conf.on_change(OnChange::Nothing);
142                let from_size = previous_size.or(init_size).unwrap_or_default();
143                let to_size = current_size.unwrap_or_default();
144                (
145                    AnimNum::new(from_size.origin.x, to_size.origin.x)
146                        .duration(*duration)
147                        .ease(*ease)
148                        .function(*function),
149                    AnimNum::new(from_size.origin.y, to_size.origin.y)
150                        .duration(*duration)
151                        .ease(*ease)
152                        .function(*function),
153                    AnimNum::new(from_size.size.width, to_size.size.width)
154                        .duration(*duration)
155                        .ease(*ease)
156                        .function(*function),
157                    AnimNum::new(from_size.size.height, to_size.size.height)
158                        .duration(*duration)
159                        .ease(*ease)
160                        .function(*function),
161                )
162            },
163        );
164
165        // Rest the portal once the animation stops
166        use_side_effect(move || {
167            if !*animation.is_running().read() {
168                should_animate.set_if_modified(false);
169            }
170        });
171
172        // Area the children are placed at
173        let at_rest = !should_animate() && current_size.read().is_some();
174        let area = if at_rest {
175            current_size.read().unwrap_or_default()
176        } else {
177            let (x, y, width, height) = animation.get().value();
178            Area::new(Point2D::new(x, y), Size2D::new(width, height))
179        };
180
181        // Resting portals are placed by the layout rather than positioned globally
182        let is_new = init_size.is_none() && current_size.read().is_none();
183        let is_stacked = self.dependency.is_some()
184            && (is_new || (at_rest && self.dependency == *last_dependency.read()));
185        let global_area = (!is_stacked).then_some(area);
186
187        let id = self.id.clone();
188        let show = self.show;
189
190        rect()
191            .a11y_focusable(false)
192            .on_sized(move |e: Event<SizedEventData>| {
193                if !show || *current_size.peek() == Some(e.area) {
194                    return;
195                }
196
197                positions
198                    .ids
199                    .write()
200                    .insert(id.clone(), (e.area, dependency));
201
202                // Portals with a dependency only animate when it changes
203                let dependency_changed =
204                    dependency.is_none() || *last_dependency.peek() != dependency;
205                last_dependency.set_if_modified(dependency);
206                let animate =
207                    dependency_changed && (init_size.is_some() || current_size.peek().is_some());
208
209                previous_size.set(current_size());
210                current_size.set(Some(e.area));
211                should_animate.set_if_modified(animate);
212
213                spawn(async move {
214                    if animate {
215                        animation.start();
216                    } else {
217                        animation.finish();
218                    }
219                });
220            })
221            .width(self.layout.width.clone())
222            .height(self.layout.height.clone())
223            .child(
224                rect()
225                    .map(global_area, |el, area| {
226                        el.offset_x(area.min_x())
227                            .offset_y(area.min_y())
228                            .position(Position::new_global())
229                    })
230                    .child(
231                        rect()
232                            .width(global_area.map_or(Size::fill(), |area| Size::px(area.width())))
233                            .height(
234                                global_area.map_or(Size::fill(), |area| Size::px(area.height())),
235                            )
236                            // Only show the element after it has been sized
237                            .opacity(if is_stacked || !is_new { 1. } else { 0. })
238                            .children(if self.show {
239                                self.children.clone()
240                            } else {
241                                vec![]
242                            }),
243                    ),
244            )
245    }
246
247    fn render_key(&self) -> DiffKey {
248        self.key.clone().or(self.default_key())
249    }
250}
251
252#[derive(Clone)]
253pub struct PortalsMap<T: Clone + PartialEq + 'static> {
254    pub ids: State<HashMap<T, (Area, Option<u64>)>>,
255}