1use freya_core::prelude::*;
2use thiserror::Error;
3use torin::{
4 content::Content,
5 prelude::{
6 Area,
7 Direction,
8 Length,
9 },
10 size::Size,
11};
12
13use crate::{
14 get_theme,
15 theming::component_themes::{
16 ResizableHandleTheme,
17 ResizableHandleThemePartial,
18 },
19};
20
21#[derive(PartialEq, Clone, Copy, Debug)]
23pub enum PanelSize {
24 Pixels(Length),
26 Percentage(Length),
28}
29
30impl PanelSize {
31 pub fn px(v: f32) -> Self {
32 Self::Pixels(Length::new(v))
33 }
34
35 pub fn percent(v: f32) -> Self {
36 Self::Percentage(Length::new(v))
37 }
38
39 pub fn value(&self) -> f32 {
40 match self {
41 Self::Pixels(v) | Self::Percentage(v) => v.get(),
42 }
43 }
44
45 fn to_layout_size(self, value: f32) -> Size {
47 match self {
48 Self::Pixels(_) => Size::px(value),
49 Self::Percentage(_) => Size::flex(value),
50 }
51 }
52
53 fn max_size(&self) -> f32 {
55 match self {
56 Self::Pixels(_) => f32::MAX,
57 Self::Percentage(_) => 100.,
58 }
59 }
60
61 fn flex_scale(&self, flex_factor: f32) -> f32 {
63 match self {
64 Self::Pixels(_) => 1.0,
65 Self::Percentage(_) => flex_factor,
66 }
67 }
68}
69
70#[derive(Error, Debug)]
71pub enum ResizableError {
72 #[error("Panel does not exist")]
73 PanelNotFound,
74}
75
76#[derive(Clone, Copy, Debug)]
77pub struct Panel {
78 pub size: f32,
79 pub initial_size: f32,
80 pub min_size: f32,
81 pub sizing: PanelSize,
82 pub id: usize,
83}
84
85#[derive(Default)]
86pub struct ResizableContext {
87 pub panels: Vec<Panel>,
88 pub direction: Direction,
89}
90
91impl ResizableContext {
92 pub const HANDLE_SIZE: f32 = 4.0;
93
94 pub fn direction(&self) -> Direction {
95 self.direction
96 }
97
98 pub fn panels(&mut self) -> &mut Vec<Panel> {
99 &mut self.panels
100 }
101
102 pub fn push_panel(&mut self, panel: Panel, order: Option<usize>) {
103 if matches!(panel.sizing, PanelSize::Percentage(_)) {
105 let mut buffer = panel.size;
106
107 for panel in &mut self
108 .panels
109 .iter_mut()
110 .filter(|p| matches!(p.sizing, PanelSize::Percentage(_)))
111 {
112 let resized_sized = (panel.initial_size - panel.size).min(buffer);
113
114 if resized_sized >= 0. {
115 panel.size = (panel.size - resized_sized).max(panel.min_size);
116 let new_resized_sized = panel.initial_size - panel.size;
117 buffer -= new_resized_sized;
118 }
119 }
120 }
121
122 match order {
123 Some(order) if order < self.panels.len() => self.panels.insert(order, panel),
124 _ => self.panels.push(panel),
125 }
126 }
127
128 pub fn remove_panel(&mut self, id: usize) -> Result<(), ResizableError> {
129 let removed_panel = self
130 .panels
131 .iter()
132 .copied()
133 .find(|p| p.id == id)
134 .ok_or(ResizableError::PanelNotFound)?;
135 self.panels.retain(|e| e.id != id);
136
137 if matches!(removed_panel.sizing, PanelSize::Percentage(_)) {
139 let mut buffer = removed_panel.size;
140
141 for panel in &mut self
142 .panels
143 .iter_mut()
144 .filter(|p| matches!(p.sizing, PanelSize::Percentage(_)))
145 {
146 let resized_sized = (panel.initial_size - panel.size).min(buffer);
147
148 panel.size = (panel.size + resized_sized).max(panel.min_size);
149 let new_resized_sized = panel.initial_size - panel.size;
150 buffer -= new_resized_sized;
151 }
152 }
153
154 Ok(())
155 }
156
157 pub fn apply_resize(
158 &mut self,
159 panel_index: usize,
160 pixel_distance: f32,
161 container_size: f32,
162 ) -> bool {
163 let mut changed_panels = false;
164
165 let handle_space = self.panels.len().saturating_sub(1) as f32 * Self::HANDLE_SIZE;
167 let (px_total, flex_total) =
168 self.panels
169 .iter()
170 .fold((0.0, 0.0), |(px, flex): (f32, f32), p| match p.sizing {
171 PanelSize::Pixels(_) => (px + p.size, flex),
172 PanelSize::Percentage(_) => (px, flex + p.size),
173 });
174 let flex_factor = flex_total / (container_size - px_total - handle_space).max(1.0);
175
176 let abs_distance = pixel_distance.abs();
177 let (behind_range, forward_range) = if pixel_distance >= 0. {
178 (0..panel_index, panel_index..self.panels.len())
179 } else {
180 (panel_index..self.panels.len(), 0..panel_index)
181 };
182
183 let mut acc_pixels = 0.0;
184
185 for panel in &mut self.panels[forward_range].iter_mut() {
187 let old_size = panel.size;
188 let scale = panel.sizing.flex_scale(flex_factor);
189 let new_size =
190 (panel.size - abs_distance * scale).clamp(panel.min_size, panel.sizing.max_size());
191 changed_panels |= panel.size != new_size;
192 panel.size = new_size;
193 acc_pixels -= (new_size - old_size) / scale.max(f32::MIN_POSITIVE);
194
195 if old_size > panel.min_size {
196 break;
197 }
198 }
199
200 if let Some(panel) = &mut self.panels[behind_range].iter_mut().next_back() {
202 let scale = panel.sizing.flex_scale(flex_factor);
203 let new_size =
204 (panel.size + acc_pixels * scale).clamp(panel.min_size, panel.sizing.max_size());
205 changed_panels |= panel.size != new_size;
206 panel.size = new_size;
207 }
208
209 changed_panels
210 }
211
212 pub fn reset(&mut self) {
213 for panel in &mut self.panels {
214 panel.size = panel.initial_size;
215 }
216 }
217}
218
219#[cfg_attr(feature = "docs",
243 doc = embed_doc_image::embed_image!("resizable_container", "images/gallery_resizable_container.png"),
244)]
245#[derive(PartialEq, Clone)]
246pub struct ResizableContainer {
247 direction: Direction,
248 panels: Vec<ResizablePanel>,
249 controller: Option<Writable<ResizableContext>>,
250}
251
252impl Default for ResizableContainer {
253 fn default() -> Self {
254 Self::new()
255 }
256}
257
258impl ResizableContainer {
259 pub fn new() -> Self {
260 Self {
261 direction: Direction::Vertical,
262 panels: vec![],
263 controller: None,
264 }
265 }
266
267 pub fn direction(mut self, direction: Direction) -> Self {
268 self.direction = direction;
269 self
270 }
271
272 pub fn panel(mut self, panel: impl Into<Option<ResizablePanel>>) -> Self {
273 if let Some(panel) = panel.into() {
274 self.panels.push(panel);
275 }
276 self
277 }
278
279 pub fn panels_iter(mut self, panels: impl Iterator<Item = ResizablePanel>) -> Self {
280 self.panels.extend(panels);
281 self
282 }
283
284 pub fn controller(mut self, controller: impl Into<Writable<ResizableContext>>) -> Self {
285 self.controller = Some(controller.into());
286 self
287 }
288}
289
290impl Component for ResizableContainer {
291 fn render(&self) -> impl IntoElement {
292 let mut size = use_state(Area::default);
293 use_provide_context(|| size);
294
295 let direction = use_reactive(&self.direction);
296 use_provide_context(|| {
297 self.controller.clone().unwrap_or_else(|| {
298 let mut state = State::create(ResizableContext {
299 direction: self.direction,
300 ..Default::default()
301 });
302
303 Effect::create_sync_with_gen(move |current_gen| {
304 let direction = direction();
305 if current_gen > 0 {
306 state.write().direction = direction;
307 }
308 });
309
310 state.into_writable()
311 })
312 });
313
314 rect()
315 .direction(self.direction)
316 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
317 .expanded()
318 .content(Content::flex())
319 .children(self.panels.iter().enumerate().flat_map(|(i, e)| {
320 if i > 0 {
321 vec![ResizableHandle::new(i).into(), e.clone().into()]
322 } else {
323 vec![e.clone().into()]
324 }
325 }))
326 }
327}
328
329#[derive(PartialEq, Clone)]
330pub struct ResizablePanel {
331 key: DiffKey,
332 initial_size: PanelSize,
333 min_size: Option<f32>,
334 children: Vec<Element>,
335 order: Option<usize>,
336}
337
338impl KeyExt for ResizablePanel {
339 fn write_key(&mut self) -> &mut DiffKey {
340 &mut self.key
341 }
342}
343
344impl ChildrenExt for ResizablePanel {
345 fn get_children(&mut self) -> &mut Vec<Element> {
346 &mut self.children
347 }
348}
349
350impl ResizablePanel {
351 pub fn new(initial_size: PanelSize) -> Self {
352 Self {
353 key: DiffKey::None,
354 initial_size,
355 min_size: None,
356 children: vec![],
357 order: None,
358 }
359 }
360
361 pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
362 self.key = key.into();
363 self
364 }
365
366 pub fn initial_size(mut self, initial_size: PanelSize) -> Self {
367 self.initial_size = initial_size;
368 self
369 }
370
371 pub fn min_size(mut self, min_size: impl Into<f32>) -> Self {
373 self.min_size = Some(min_size.into());
374 self
375 }
376
377 pub fn order(mut self, order: impl Into<usize>) -> Self {
378 self.order = Some(order.into());
379 self
380 }
381}
382
383impl Component for ResizablePanel {
384 fn render(&self) -> impl IntoElement {
385 let registry = use_consume::<Writable<ResizableContext>>();
386
387 let initial_value = self.initial_size.value();
388 let id = use_hook({
389 let mut registry = registry.clone();
390 move || {
391 let id = UseId::<ResizableContext>::get_in_hook();
392 let panel = Panel {
393 initial_size: initial_value,
394 size: initial_value,
395 min_size: self.min_size.unwrap_or(initial_value * 0.25),
396 sizing: self.initial_size,
397 id,
398 };
399 registry.write().push_panel(panel, self.order);
400 id
401 }
402 });
403
404 use_drop({
405 let mut registry = registry.clone();
406 move || {
407 let _ = registry.write().remove_panel(id);
408 }
409 });
410
411 let registry = registry.read();
412 let index = registry
413 .panels
414 .iter()
415 .position(|e| e.id == id)
416 .unwrap_or_default();
417
418 let Panel { size, sizing, .. } = registry.panels[index];
419 let main_size = sizing.to_layout_size(size);
420
421 let (width, height) = match registry.direction {
422 Direction::Horizontal => (main_size, Size::fill()),
423 Direction::Vertical => (Size::fill(), main_size),
424 };
425
426 rect()
427 .a11y_role(AccessibilityRole::Pane)
428 .width(width)
429 .height(height)
430 .overflow(Overflow::Clip)
431 .children(self.children.clone())
432 }
433
434 fn render_key(&self) -> DiffKey {
435 self.key.clone().or(DiffKey::None)
436 }
437}
438
439#[derive(Debug, Default, PartialEq, Clone, Copy)]
441pub enum HandleStatus {
442 #[default]
444 Idle,
445 Hovering,
447}
448
449#[derive(PartialEq)]
450pub struct ResizableHandle {
451 panel_index: usize,
452 pub(crate) theme: Option<ResizableHandleThemePartial>,
454}
455
456impl ResizableHandle {
457 pub fn new(panel_index: usize) -> Self {
458 Self {
459 panel_index,
460 theme: None,
461 }
462 }
463}
464
465impl Component for ResizableHandle {
466 fn render(&self) -> impl IntoElement {
467 let ResizableHandleTheme {
468 background,
469 hover_background,
470 corner_radius,
471 } = get_theme!(&self.theme, resizable_handle);
472 let mut size = use_state(Area::default);
473 let mut clicking = use_state(|| false);
474 let mut status = use_state(HandleStatus::default);
475 let registry = use_consume::<Writable<ResizableContext>>();
476 let container_size = use_consume::<State<Area>>();
477 let mut allow_resizing = use_state(|| false);
478
479 let panel_index = self.panel_index;
480 let direction = registry.read().direction;
481
482 use_drop(move || {
483 if *status.peek() == HandleStatus::Hovering {
484 Cursor::set(CursorIcon::default());
485 }
486 });
487
488 let cursor = match direction {
489 Direction::Horizontal => CursorIcon::ColResize,
490 _ => CursorIcon::RowResize,
491 };
492
493 let on_pointer_leave = move |_| {
494 *status.write() = HandleStatus::Idle;
495 if !clicking() {
496 Cursor::set(CursorIcon::default());
497 }
498 };
499
500 let on_pointer_enter = move |_| {
501 *status.write() = HandleStatus::Hovering;
502 Cursor::set(cursor);
503 };
504
505 let on_capture_global_pointer_move = {
506 let mut registry = registry.clone();
507 move |e: Event<PointerEventData>| {
508 if *clicking.read() {
509 e.prevent_default();
510
511 if !*allow_resizing.read() {
512 return;
513 }
514
515 let coords = e.global_location();
516 let handle = size.read();
517 let container = container_size.read();
518 let mut registry = registry.write();
519
520 let (pixel_displacement, container_axis_size) = match registry.direction {
521 Direction::Horizontal => {
522 (coords.x as f32 - handle.min_x(), container.width())
523 }
524 Direction::Vertical => {
525 (coords.y as f32 - handle.min_y(), container.height())
526 }
527 };
528
529 let changed_panels =
530 registry.apply_resize(panel_index, pixel_displacement, container_axis_size);
531
532 if changed_panels {
533 allow_resizing.set(false);
534 }
535 }
536 }
537 };
538
539 let on_pointer_down = move |e: Event<PointerEventData>| {
540 e.stop_propagation();
541 e.prevent_default();
542 clicking.set(true);
543 };
544
545 let on_global_pointer_press = move |_: Event<PointerEventData>| {
546 if *clicking.read() {
547 if *status.peek() != HandleStatus::Hovering {
548 Cursor::set(CursorIcon::default());
549 }
550 clicking.set(false);
551 }
552 };
553
554 let handle_size = Size::px(ResizableContext::HANDLE_SIZE);
555 let (width, height) = match direction {
556 Direction::Horizontal => (handle_size, Size::fill()),
557 Direction::Vertical => (Size::fill(), handle_size),
558 };
559
560 let background = match *status.read() {
561 HandleStatus::Idle if !*clicking.read() => background,
562 _ => hover_background,
563 };
564
565 rect()
566 .width(width)
567 .height(height)
568 .background(background)
569 .corner_radius(corner_radius)
570 .on_sized(move |e: Event<SizedEventData>| {
571 size.set(e.area);
572 allow_resizing.set(true);
573 })
574 .on_pointer_down(on_pointer_down)
575 .on_global_pointer_press(on_global_pointer_press)
576 .on_pointer_enter(on_pointer_enter)
577 .on_capture_global_pointer_move(on_capture_global_pointer_move)
578 .on_pointer_leave(on_pointer_leave)
579 }
580}