freya_components/
portal.rs1use 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 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 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 use_side_effect(move || {
167 if !*animation.is_running().read() {
168 should_animate.set_if_modified(false);
169 }
170 });
171
172 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 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 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 .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}