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