1use dioxus::prelude::*;
2use freya_core::{
3 custom_attributes::NodeReferenceLayout,
4 platform::CursorIcon,
5};
6use freya_elements::{
7 self as dioxus_elements,
8 events::MouseEvent,
9};
10use freya_hooks::{
11 use_applied_theme,
12 use_node_signal,
13 use_platform,
14 ResizableHandleTheme,
15 ResizableHandleThemeWith,
16};
17
18struct Panel {
19 pub size: f32,
20 pub min_size: f32,
21}
22
23enum ResizableItem {
24 Panel(Panel),
25 Handle,
26}
27
28impl ResizableItem {
29 fn panel(&self) -> &Panel {
31 match self {
32 Self::Panel(panel) => panel,
33 Self::Handle => panic!("Not a Panel"),
34 }
35 }
36
37 fn try_panel_mut(&mut self) -> Option<&mut Panel> {
39 match self {
40 Self::Panel(panel) => Some(panel),
41 Self::Handle => None,
42 }
43 }
44}
45
46#[derive(Default)]
47struct ResizableContext {
48 pub registry: Vec<ResizableItem>,
49 pub direction: String,
50}
51
52#[component]
81pub fn ResizableContainer(
82 #[props(default = "vertical".to_string())]
85 direction: String,
86 children: Element,
88) -> Element {
89 let (node_reference, size) = use_node_signal();
90 use_context_provider(|| size);
91
92 use_context_provider(|| {
93 Signal::new(ResizableContext {
94 direction: direction.clone(),
95 ..Default::default()
96 })
97 });
98
99 rsx!(
100 rect {
101 reference: node_reference,
102 direction: "{direction}",
103 width: "fill",
104 height: "fill",
105 content: "flex",
106 {children}
107 }
108 )
109}
110
111#[component]
113pub fn ResizablePanel(
114 #[props(default = 10.)]
116 initial_size: f32, #[props(default = 4.)]
119 min_size: f32,
120 children: Element,
122) -> Element {
123 let mut registry = use_context::<Signal<ResizableContext>>();
124
125 let index = use_hook(move || {
126 registry.write().registry.push(ResizableItem::Panel(Panel {
127 size: initial_size,
128 min_size,
129 }));
130 registry.peek().registry.len() - 1
131 });
132
133 let registry = registry.read();
134
135 let Panel { size, .. } = registry.registry[index].panel();
136
137 let (width, height) = match registry.direction.as_str() {
138 "horizontal" => (format!("flex({size})"), "fill".to_owned()),
139 _ => ("fill".to_owned(), format!("flex({size}")),
140 };
141
142 rsx!(
143 rect {
144 width: "{width}",
145 height: "{height}",
146 overflow: "clip",
147 {children}
148 }
149 )
150}
151
152#[derive(Debug, Default, PartialEq, Clone, Copy)]
154pub enum HandleStatus {
155 #[default]
157 Idle,
158 Hovering,
160}
161
162#[component]
164pub fn ResizableHandle(
165 theme: Option<ResizableHandleThemeWith>,
167) -> Element {
168 let ResizableHandleTheme {
169 background,
170 hover_background,
171 } = use_applied_theme!(&theme, resizable_handle);
172 let (node_reference, size) = use_node_signal();
173 let mut clicking = use_signal(|| false);
174 let mut status = use_signal(HandleStatus::default);
175 let mut registry = use_context::<Signal<ResizableContext>>();
176 let container_size = use_context::<ReadOnlySignal<NodeReferenceLayout>>();
177 let platform = use_platform();
178 let mut allow_resizing = use_signal(|| false);
179
180 use_memo(move || {
181 size.read();
182 allow_resizing.set(true);
183
184 });
186
187 use_drop(move || {
188 if *status.peek() == HandleStatus::Hovering {
189 platform.set_cursor(CursorIcon::default());
190 }
191 });
192
193 let index = use_hook(move || {
194 registry.write().registry.push(ResizableItem::Handle);
195 registry.peek().registry.len() - 1
196 });
197
198 let cursor = match registry.read().direction.as_str() {
199 "horizontal" => CursorIcon::ColResize,
200 _ => CursorIcon::RowResize,
201 };
202
203 let onmouseleave = move |_: MouseEvent| {
204 *status.write() = HandleStatus::Idle;
205 if !clicking() {
206 platform.set_cursor(CursorIcon::default());
207 }
208 };
209
210 let onmouseenter = move |e: MouseEvent| {
211 e.stop_propagation();
212 *status.write() = HandleStatus::Hovering;
213 platform.set_cursor(cursor);
214 };
215
216 let onmousemove = move |e: MouseEvent| {
217 if clicking() {
218 if !allow_resizing() {
219 return;
220 }
221
222 let coordinates = e.get_screen_coordinates();
223 let mut registry = registry.write();
224
225 let displacement_per: f32 = match registry.direction.as_str() {
226 "horizontal" => {
227 let container_width = container_size.read().area.width();
228 let displacement = coordinates.x as f32 - size.read().area.min_x();
229 100. / container_width * displacement
230 }
231 _ => {
232 let container_height = container_size.read().area.height();
233 let displacement = coordinates.y as f32 - size.read().area.min_y();
234 100. / container_height * displacement
235 }
236 };
237
238 let mut changed_panels = false;
239
240 if displacement_per >= 0. {
241 let mut acc_per = 0.0;
244
245 for next_item in &mut registry.registry[index..].iter_mut() {
247 if let Some(panel) = next_item.try_panel_mut() {
248 let old_size = panel.size;
249 let new_size = (panel.size - displacement_per).clamp(panel.min_size, 100.);
250
251 if panel.size != new_size {
252 changed_panels = true
253 }
254
255 panel.size = new_size;
256 acc_per -= new_size - old_size;
257
258 if old_size > panel.min_size {
259 break;
260 }
261 }
262 }
263
264 for prev_item in &mut registry.registry[0..index].iter_mut().rev() {
266 if let Some(panel) = prev_item.try_panel_mut() {
267 let new_size = (panel.size + acc_per).clamp(panel.min_size, 100.);
268
269 if panel.size != new_size {
270 changed_panels = true
271 }
272
273 panel.size = new_size;
274 break;
275 }
276 }
277 } else {
278 let mut acc_per = 0.0;
281
282 for prev_item in &mut registry.registry[0..index].iter_mut().rev() {
284 if let Some(panel) = prev_item.try_panel_mut() {
285 let old_size = panel.size;
286 let new_size = (panel.size + displacement_per).clamp(panel.min_size, 100.);
287
288 if panel.size != new_size {
289 changed_panels = true
290 }
291
292 panel.size = new_size;
293 acc_per += new_size - old_size;
294
295 if old_size > panel.min_size {
296 break;
297 }
298 }
299 }
300
301 for next_item in &mut registry.registry[index..].iter_mut() {
303 if let Some(panel) = next_item.try_panel_mut() {
304 let new_size = (panel.size - acc_per).clamp(panel.min_size, 100.);
305
306 if panel.size != new_size {
307 changed_panels = true
308 }
309
310 panel.size = new_size;
311 break;
312 }
313 }
314 }
315
316 if changed_panels {
317 allow_resizing.set(false);
318 }
319 }
320 };
321
322 let onmousedown = move |e: MouseEvent| {
323 e.stop_propagation();
324 clicking.set(true);
325 };
326
327 let onclick = move |_: MouseEvent| {
328 if clicking() {
329 if *status.peek() != HandleStatus::Hovering {
330 platform.set_cursor(CursorIcon::default());
331 }
332 clicking.set(false);
333 }
334 };
335
336 let (width, height) = match registry.read().direction.as_str() {
337 "horizontal" => ("4", "fill"),
338 _ => ("fill", "4"),
339 };
340
341 let background = match status() {
342 _ if clicking() => hover_background,
343 HandleStatus::Hovering => hover_background,
344 HandleStatus::Idle => background,
345 };
346
347 rsx!(rect {
348 reference: node_reference,
349 width: "{width}",
350 height: "{height}",
351 background: "{background}",
352 onmousedown,
353 onglobalclick: onclick,
354 onmouseenter,
355 onglobalmousemove: onmousemove,
356 onmouseleave,
357 })
358}
359
360#[cfg(test)]
361mod test {
362 use freya::prelude::*;
363 use freya_testing::prelude::*;
364
365 #[tokio::test]
366 pub async fn resizable_container() {
367 fn resizable_container_app() -> Element {
368 rsx!(
369 ResizableContainer {
370 ResizablePanel {
371 initial_size: 50.,
372 label {
373 "Panel 0"
374 }
375 }
376 ResizableHandle { }
377 ResizablePanel { initial_size: 50.,
379 ResizableContainer {
380 direction: "horizontal",
381 ResizablePanel {
382 initial_size: 33.33,
383 label {
384 "Panel 2"
385 }
386 }
387 ResizableHandle { }
388 ResizablePanel {
389 initial_size: 33.33,
390 label {
391 "Panel 3"
392 }
393 }
394 ResizableHandle { }
395 ResizablePanel {
396 initial_size: 33.33,
397 label {
398 "Panel 4"
399 }
400 }
401 }
402 }
403 }
404 )
405 }
406
407 let mut utils = launch_test(resizable_container_app);
408 utils.wait_for_update().await;
409 let root = utils.root();
410
411 let container = root.get(0);
412 let panel_0 = container.get(0);
413 let panel_1 = container.get(2);
414 let panel_2 = panel_1.get(0).get(0);
415 let panel_3 = panel_1.get(0).get(2);
416 let panel_4 = panel_1.get(0).get(4);
417
418 assert_eq!(panel_0.layout().unwrap().area.height().round(), 248.0);
419 assert_eq!(panel_1.layout().unwrap().area.height().round(), 248.0);
420 assert_eq!(panel_2.layout().unwrap().area.width().round(), 164.0);
421 assert_eq!(panel_3.layout().unwrap().area.width().round(), 164.0);
422 assert_eq!(panel_4.layout().unwrap().area.width().round(), 164.0);
423
424 utils.push_event(TestEvent::Mouse {
426 name: EventName::MouseDown,
427 cursor: (100.0, 250.0).into(),
428 button: Some(MouseButton::Left),
429 });
430 utils.push_event(TestEvent::Mouse {
431 name: EventName::MouseMove,
432 cursor: (100.0, 200.0).into(),
433 button: Some(MouseButton::Left),
434 });
435 utils.push_event(TestEvent::Mouse {
436 name: EventName::MouseUp,
437 cursor: (0.0, 0.0).into(),
438 button: Some(MouseButton::Left),
439 });
440 utils.wait_for_update().await;
441
442 assert_eq!(panel_0.layout().unwrap().area.height().round(), 200.0); assert_eq!(panel_1.layout().unwrap().area.height().round(), 296.0); utils.push_event(TestEvent::Mouse {
447 name: EventName::MouseDown,
448 cursor: (167.0, 300.0).into(),
449 button: Some(MouseButton::Left),
450 });
451 utils.push_event(TestEvent::Mouse {
452 name: EventName::MouseMove,
453 cursor: (187.0, 300.0).into(),
454 button: Some(MouseButton::Left),
455 });
456 utils.push_event(TestEvent::Mouse {
457 name: EventName::MouseUp,
458 cursor: (0.0, 0.0).into(),
459 button: Some(MouseButton::Left),
460 });
461 utils.wait_for_update().await;
462 utils.wait_for_update().await;
463 utils.wait_for_update().await;
464
465 assert_eq!(panel_2.layout().unwrap().area.width().round(), 187.0); assert_eq!(panel_3.layout().unwrap().area.width().round(), 141.0);
467 }
468}