1use freya_core::prelude::*;
2use thiserror::Error;
3use torin::{
4 content::Content,
5 prelude::{
6 Area,
7 Direction,
8 },
9 size::Size,
10};
11
12use crate::{
13 get_theme,
14 theming::component_themes::{
15 ResizableHandleTheme,
16 ResizableHandleThemePartial,
17 },
18};
19
20#[derive(Error, Debug)]
21pub enum ResizableError {
22 #[error("Panel does not exist")]
23 PanelNotFound,
24}
25
26#[derive(Clone, Copy, Debug)]
27pub struct Panel {
28 pub size: f32,
29 pub initial_size: f32,
30 pub min_size: f32,
31 pub id: usize,
32}
33
34#[derive(Default)]
35pub struct ResizableContext {
36 pub panels: Vec<Panel>,
37 pub direction: Direction,
38}
39
40impl ResizableContext {
41 pub fn direction(&self) -> Direction {
42 self.direction
43 }
44
45 pub fn panels(&mut self) -> &mut Vec<Panel> {
46 &mut self.panels
47 }
48
49 pub fn push_panel(&mut self, panel: Panel, order: Option<usize>) {
50 let mut buffer = panel.size;
51
52 for panel in &mut self.panels.iter_mut() {
53 let resized_sized = (panel.initial_size - panel.size).min(buffer);
54
55 if resized_sized >= 0. {
56 panel.size = (panel.size - resized_sized).max(panel.min_size);
57 let new_resized_sized = panel.initial_size - panel.size;
58 buffer -= new_resized_sized;
59 }
60 }
61
62 if let Some(order) = order {
63 if self.panels.len() <= order {
64 self.panels.push(panel);
65 } else {
66 self.panels.insert(order, panel);
67 }
68 } else {
69 self.panels.push(panel);
70 }
71 }
72
73 pub fn remove_panel(&mut self, id: usize) -> Result<(), ResizableError> {
74 let removed_panel = self
75 .panels
76 .iter()
77 .find(|p| p.id == id)
78 .cloned()
79 .ok_or(ResizableError::PanelNotFound)?;
80 self.panels.retain(|e| e.id != id);
81
82 let mut buffer = removed_panel.size;
83
84 for panel in &mut self.panels.iter_mut() {
85 let resized_sized = (panel.initial_size - panel.size).min(buffer);
86
87 panel.size = (panel.size + resized_sized).max(panel.min_size);
88 let new_resized_sized = panel.initial_size - panel.size;
89 buffer -= new_resized_sized;
90 }
91
92 Ok(())
93 }
94
95 pub fn apply_resize(&mut self, panel_index: usize, distance: f32) -> bool {
96 let mut changed_panels = false;
97
98 let (corrected_distance, behind_range, forward_range) = if distance >= 0. {
99 (distance, 0..panel_index, panel_index..self.panels.len())
100 } else {
101 (-distance, panel_index..self.panels.len(), 0..panel_index)
102 };
103
104 let mut acc_per = 0.0;
105
106 for panel in &mut self.panels[forward_range].iter_mut() {
108 let old_size = panel.size;
109 let new_size = (panel.size - corrected_distance).clamp(panel.min_size, 100.);
110
111 if panel.size != new_size {
112 changed_panels = true
113 }
114
115 panel.size = new_size;
116 acc_per -= new_size - old_size;
117
118 if old_size > panel.min_size {
119 break;
120 }
121 }
122
123 if let Some(panel) = &mut self.panels[behind_range].iter_mut().next_back() {
125 let new_size = (panel.size + acc_per).clamp(panel.min_size, 100.);
126
127 if panel.size != new_size {
128 changed_panels = true
129 }
130
131 panel.size = new_size;
132 }
133
134 changed_panels
135 }
136
137 pub fn reset(&mut self) {
138 for panel in &mut self.panels {
139 panel.size = panel.initial_size;
140 }
141 }
142}
143
144#[cfg_attr(feature = "docs",
168 doc = embed_doc_image::embed_image!("resizable_container", "images/gallery_resizable_container.png"),
169)]
170#[derive(PartialEq, Clone)]
171pub struct ResizableContainer {
172 direction: Direction,
173 panels: Vec<ResizablePanel>,
174 controller: Option<Writable<ResizableContext>>,
175}
176
177impl Default for ResizableContainer {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183impl ResizableContainer {
184 pub fn new() -> Self {
185 Self {
186 direction: Direction::Vertical,
187 panels: vec![],
188 controller: None,
189 }
190 }
191
192 pub fn direction(mut self, direction: Direction) -> Self {
193 self.direction = direction;
194 self
195 }
196
197 pub fn panel(mut self, panel: impl Into<Option<ResizablePanel>>) -> Self {
198 if let Some(panel) = panel.into() {
199 self.panels.push(panel);
200 }
201 self
202 }
203
204 pub fn panels_iter(mut self, panels: impl Iterator<Item = ResizablePanel>) -> Self {
205 self.panels.extend(panels);
206 self
207 }
208
209 pub fn controller(mut self, controller: impl Into<Writable<ResizableContext>>) -> Self {
210 self.controller = Some(controller.into());
211 self
212 }
213}
214
215impl Component for ResizableContainer {
216 fn render(&self) -> impl IntoElement {
217 let mut size = use_state(Area::default);
218 use_provide_context(|| size);
219
220 use_provide_context(|| {
221 self.controller.clone().unwrap_or_else(|| {
222 State::create(ResizableContext {
223 direction: self.direction,
224 ..Default::default()
225 })
226 .into_writable()
227 })
228 });
229
230 rect()
231 .direction(self.direction)
232 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
233 .expanded()
234 .content(Content::flex())
235 .children(self.panels.iter().enumerate().flat_map(|(i, e)| {
236 if i > 0 {
237 vec![ResizableHandle::new(i).into(), e.clone().into()]
238 } else {
239 vec![e.clone().into()]
240 }
241 }))
242 }
243}
244
245#[derive(PartialEq, Clone)]
246pub struct ResizablePanel {
247 key: DiffKey,
248 initial_size: f32,
249 min_size: Option<f32>,
250 children: Vec<Element>,
251 order: Option<usize>,
252}
253
254impl KeyExt for ResizablePanel {
255 fn write_key(&mut self) -> &mut DiffKey {
256 &mut self.key
257 }
258}
259
260impl ChildrenExt for ResizablePanel {
261 fn get_children(&mut self) -> &mut Vec<Element> {
262 &mut self.children
263 }
264}
265
266impl ResizablePanel {
267 pub fn new(initial_size: f32) -> Self {
268 Self {
269 key: DiffKey::None,
270 initial_size,
271 min_size: None,
272 children: vec![],
273 order: None,
274 }
275 }
276
277 pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
278 self.key = key.into();
279 self
280 }
281
282 pub fn initial_size(mut self, initial_size: impl Into<f32>) -> Self {
283 self.initial_size = initial_size.into();
284 self
285 }
286
287 pub fn min_size(mut self, min_size: impl Into<f32>) -> Self {
288 self.min_size = Some(min_size.into());
289 self
290 }
291
292 pub fn order(mut self, order: impl Into<usize>) -> Self {
293 self.order = Some(order.into());
294 self
295 }
296}
297
298impl Component for ResizablePanel {
299 fn render(&self) -> impl IntoElement {
300 let registry = use_consume::<Writable<ResizableContext>>();
301
302 let id = use_hook({
303 let mut registry = registry.clone();
304 move || {
305 let id = UseId::<ResizableContext>::get_in_hook();
306 let panel = Panel {
307 initial_size: self.initial_size,
308 size: self.initial_size,
309 min_size: self.min_size.unwrap_or(self.initial_size * 0.25),
310 id,
311 };
312 registry.write().push_panel(panel, self.order);
313 id
314 }
315 });
316
317 use_drop({
318 let mut registry = registry.clone();
319 move || {
320 let _ = registry.write().remove_panel(id);
321 }
322 });
323
324 let registry = registry.read();
325 let index = registry
326 .panels
327 .iter()
328 .position(|e| e.id == id)
329 .unwrap_or_default();
330
331 let Panel { size, .. } = registry.panels[index];
332
333 let (width, height) = match registry.direction {
334 Direction::Horizontal => (Size::flex(size), Size::fill()),
335 Direction::Vertical => (Size::fill(), Size::flex(size)),
336 };
337
338 rect()
339 .a11y_role(AccessibilityRole::Pane)
340 .width(width)
341 .height(height)
342 .overflow(Overflow::Clip)
343 .children(self.children.clone())
344 }
345
346 fn render_key(&self) -> DiffKey {
347 self.key.clone().or(DiffKey::None)
348 }
349}
350
351#[derive(Debug, Default, PartialEq, Clone, Copy)]
353pub enum HandleStatus {
354 #[default]
356 Idle,
357 Hovering,
359}
360
361#[derive(PartialEq)]
362pub struct ResizableHandle {
363 panel_index: usize,
364 pub(crate) theme: Option<ResizableHandleThemePartial>,
366}
367
368impl ResizableHandle {
369 pub fn new(panel_index: usize) -> Self {
370 Self {
371 panel_index,
372 theme: None,
373 }
374 }
375}
376
377impl Component for ResizableHandle {
378 fn render(&self) -> impl IntoElement {
379 let ResizableHandleTheme {
380 background,
381 hover_background,
382 corner_radius,
383 } = get_theme!(&self.theme, resizable_handle);
384 let mut size = use_state(Area::default);
385 let mut clicking = use_state(|| false);
386 let mut status = use_state(HandleStatus::default);
387 let registry = use_consume::<Writable<ResizableContext>>();
388 let container_size = use_consume::<State<Area>>();
389 let mut allow_resizing = use_state(|| false);
390
391 let panel_index = self.panel_index;
392
393 use_drop(move || {
394 if *status.peek() == HandleStatus::Hovering {
395 Cursor::set(CursorIcon::default());
396 }
397 });
398
399 let cursor = match registry.read().direction {
400 Direction::Horizontal => CursorIcon::ColResize,
401 _ => CursorIcon::RowResize,
402 };
403
404 let on_pointer_leave = move |_| {
405 *status.write() = HandleStatus::Idle;
406 if !clicking() {
407 Cursor::set(CursorIcon::default());
408 }
409 };
410
411 let on_pointer_enter = move |_| {
412 *status.write() = HandleStatus::Hovering;
413 Cursor::set(cursor);
414 };
415
416 let on_capture_global_mouse_move = {
417 let mut registry = registry.clone();
418 move |e: Event<MouseEventData>| {
419 if *clicking.read() {
420 e.prevent_default();
421
422 if !*allow_resizing.read() {
423 return;
424 }
425
426 let coordinates = e.global_location;
427 let mut registry = registry.write();
428
429 let total_size = registry.panels.iter().fold(0., |acc, p| acc + p.size);
430
431 let distance = match registry.direction {
432 Direction::Horizontal => {
433 let container_width = container_size.read().width();
434 let displacement = coordinates.x as f32 - size.read().min_x();
435 total_size / container_width * displacement
436 }
437 Direction::Vertical => {
438 let container_height = container_size.read().height();
439 let displacement = coordinates.y as f32 - size.read().min_y();
440 total_size / container_height * displacement
441 }
442 };
443
444 let changed_panels = registry.apply_resize(panel_index, distance);
445
446 if changed_panels {
447 allow_resizing.set(false);
448 }
449 }
450 }
451 };
452
453 let on_pointer_down = move |e: Event<PointerEventData>| {
454 e.stop_propagation();
455 e.prevent_default();
456 clicking.set(true);
457 };
458
459 let on_global_mouse_up = move |_| {
460 if *clicking.read() {
461 if *status.peek() != HandleStatus::Hovering {
462 Cursor::set(CursorIcon::default());
463 }
464 clicking.set(false);
465 }
466 };
467
468 let (width, height) = match registry.read().direction {
469 Direction::Horizontal => (Size::px(4.), Size::fill()),
470 Direction::Vertical => (Size::fill(), Size::px(4.)),
471 };
472
473 let background = match *status.read() {
474 _ if *clicking.read() => hover_background,
475 HandleStatus::Hovering => hover_background,
476 HandleStatus::Idle => background,
477 };
478
479 rect()
480 .width(width)
481 .height(height)
482 .background(background)
483 .corner_radius(corner_radius)
484 .on_sized(move |e: Event<SizedEventData>| {
485 size.set(e.area);
486 allow_resizing.set(true);
487 })
488 .on_pointer_down(on_pointer_down)
489 .on_global_mouse_up(on_global_mouse_up)
490 .on_pointer_enter(on_pointer_enter)
491 .on_capture_global_mouse_move(on_capture_global_mouse_move)
492 .on_pointer_leave(on_pointer_leave)
493 }
494}