freya_components/
resizable_container.rs

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        // Resize panels to the right
107        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        // Resize panels to the left
124        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/// A container with resizable panels.
145///
146/// # Example
147///
148/// ```rust
149/// # use freya::prelude::*;
150/// fn app() -> impl IntoElement {
151///     ResizableContainer::new()
152///         .panel(ResizablePanel::new(50.).child("Panel 1"))
153///         .panel(ResizablePanel::new(50.).child("Panel 2"))
154/// }
155/// # use freya_testing::prelude::*;
156/// # launch_doc(|| {
157/// #   rect().center().expanded().child(
158/// #       ResizableContainer::new()
159/// #           .panel(ResizablePanel::new(50.).child("Panel 1"))
160/// #           .panel(ResizablePanel::new(50.).child("Panel 2"))
161/// #   )
162/// # }, "./images/gallery_resizable_container.png").render();
163/// ```
164///
165/// # Preview
166/// ![ResizableContainer Preview][resizable_container]
167#[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        let direction = use_reactive(&self.direction);
221        use_provide_context(|| {
222            self.controller.clone().unwrap_or_else(|| {
223                let mut state = State::create(ResizableContext {
224                    direction: self.direction,
225                    ..Default::default()
226                });
227
228                Effect::create_sync_with_gen(move |current_gen| {
229                    let direction = direction();
230                    if current_gen > 0 {
231                        state.write().direction = direction;
232                    }
233                });
234
235                state.into_writable()
236            })
237        });
238
239        rect()
240            .direction(self.direction)
241            .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
242            .expanded()
243            .content(Content::flex())
244            .children(self.panels.iter().enumerate().flat_map(|(i, e)| {
245                if i > 0 {
246                    vec![ResizableHandle::new(i).into(), e.clone().into()]
247                } else {
248                    vec![e.clone().into()]
249                }
250            }))
251    }
252}
253
254#[derive(PartialEq, Clone)]
255pub struct ResizablePanel {
256    key: DiffKey,
257    initial_size: f32,
258    min_size: Option<f32>,
259    children: Vec<Element>,
260    order: Option<usize>,
261}
262
263impl KeyExt for ResizablePanel {
264    fn write_key(&mut self) -> &mut DiffKey {
265        &mut self.key
266    }
267}
268
269impl ChildrenExt for ResizablePanel {
270    fn get_children(&mut self) -> &mut Vec<Element> {
271        &mut self.children
272    }
273}
274
275impl ResizablePanel {
276    pub fn new(initial_size: f32) -> Self {
277        Self {
278            key: DiffKey::None,
279            initial_size,
280            min_size: None,
281            children: vec![],
282            order: None,
283        }
284    }
285
286    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
287        self.key = key.into();
288        self
289    }
290
291    pub fn initial_size(mut self, initial_size: impl Into<f32>) -> Self {
292        self.initial_size = initial_size.into();
293        self
294    }
295
296    pub fn min_size(mut self, min_size: impl Into<f32>) -> Self {
297        self.min_size = Some(min_size.into());
298        self
299    }
300
301    pub fn order(mut self, order: impl Into<usize>) -> Self {
302        self.order = Some(order.into());
303        self
304    }
305}
306
307impl Component for ResizablePanel {
308    fn render(&self) -> impl IntoElement {
309        let registry = use_consume::<Writable<ResizableContext>>();
310
311        let id = use_hook({
312            let mut registry = registry.clone();
313            move || {
314                let id = UseId::<ResizableContext>::get_in_hook();
315                let panel = Panel {
316                    initial_size: self.initial_size,
317                    size: self.initial_size,
318                    min_size: self.min_size.unwrap_or(self.initial_size * 0.25),
319                    id,
320                };
321                registry.write().push_panel(panel, self.order);
322                id
323            }
324        });
325
326        use_drop({
327            let mut registry = registry.clone();
328            move || {
329                let _ = registry.write().remove_panel(id);
330            }
331        });
332
333        let registry = registry.read();
334        let index = registry
335            .panels
336            .iter()
337            .position(|e| e.id == id)
338            .unwrap_or_default();
339
340        let Panel { size, .. } = registry.panels[index];
341
342        let (width, height) = match registry.direction {
343            Direction::Horizontal => (Size::flex(size), Size::fill()),
344            Direction::Vertical => (Size::fill(), Size::flex(size)),
345        };
346
347        rect()
348            .a11y_role(AccessibilityRole::Pane)
349            .width(width)
350            .height(height)
351            .overflow(Overflow::Clip)
352            .children(self.children.clone())
353    }
354
355    fn render_key(&self) -> DiffKey {
356        self.key.clone().or(DiffKey::None)
357    }
358}
359
360/// Describes the current status of the Handle.
361#[derive(Debug, Default, PartialEq, Clone, Copy)]
362pub enum HandleStatus {
363    /// Default state.
364    #[default]
365    Idle,
366    /// Mouse is hovering the handle.
367    Hovering,
368}
369
370#[derive(PartialEq)]
371pub struct ResizableHandle {
372    panel_index: usize,
373    /// Theme override.
374    pub(crate) theme: Option<ResizableHandleThemePartial>,
375}
376
377impl ResizableHandle {
378    pub fn new(panel_index: usize) -> Self {
379        Self {
380            panel_index,
381            theme: None,
382        }
383    }
384}
385
386impl Component for ResizableHandle {
387    fn render(&self) -> impl IntoElement {
388        let ResizableHandleTheme {
389            background,
390            hover_background,
391            corner_radius,
392        } = get_theme!(&self.theme, resizable_handle);
393        let mut size = use_state(Area::default);
394        let mut clicking = use_state(|| false);
395        let mut status = use_state(HandleStatus::default);
396        let registry = use_consume::<Writable<ResizableContext>>();
397        let container_size = use_consume::<State<Area>>();
398        let mut allow_resizing = use_state(|| false);
399
400        let panel_index = self.panel_index;
401
402        use_drop(move || {
403            if *status.peek() == HandleStatus::Hovering {
404                Cursor::set(CursorIcon::default());
405            }
406        });
407
408        let cursor = match registry.read().direction {
409            Direction::Horizontal => CursorIcon::ColResize,
410            _ => CursorIcon::RowResize,
411        };
412
413        let on_pointer_leave = move |_| {
414            *status.write() = HandleStatus::Idle;
415            if !clicking() {
416                Cursor::set(CursorIcon::default());
417            }
418        };
419
420        let on_pointer_enter = move |_| {
421            *status.write() = HandleStatus::Hovering;
422            Cursor::set(cursor);
423        };
424
425        let on_capture_global_pointer_move = {
426            let mut registry = registry.clone();
427            move |e: Event<PointerEventData>| {
428                if *clicking.read() {
429                    e.prevent_default();
430
431                    if !*allow_resizing.read() {
432                        return;
433                    }
434
435                    let coordinates = e.global_location();
436                    let mut registry = registry.write();
437
438                    let total_size = registry.panels.iter().fold(0., |acc, p| acc + p.size);
439
440                    let distance = match registry.direction {
441                        Direction::Horizontal => {
442                            let container_width = container_size.read().width();
443                            let displacement = coordinates.x as f32 - size.read().min_x();
444                            total_size / container_width * displacement
445                        }
446                        Direction::Vertical => {
447                            let container_height = container_size.read().height();
448                            let displacement = coordinates.y as f32 - size.read().min_y();
449                            total_size / container_height * displacement
450                        }
451                    };
452
453                    let changed_panels = registry.apply_resize(panel_index, distance);
454
455                    if changed_panels {
456                        allow_resizing.set(false);
457                    }
458                }
459            }
460        };
461
462        let on_pointer_down = move |e: Event<PointerEventData>| {
463            e.stop_propagation();
464            e.prevent_default();
465            clicking.set(true);
466        };
467
468        let on_global_pointer_press = move |_: Event<PointerEventData>| {
469            if *clicking.read() {
470                if *status.peek() != HandleStatus::Hovering {
471                    Cursor::set(CursorIcon::default());
472                }
473                clicking.set(false);
474            }
475        };
476
477        let (width, height) = match registry.read().direction {
478            Direction::Horizontal => (Size::px(4.), Size::fill()),
479            Direction::Vertical => (Size::fill(), Size::px(4.)),
480        };
481
482        let background = match *status.read() {
483            _ if *clicking.read() => hover_background,
484            HandleStatus::Hovering => hover_background,
485            HandleStatus::Idle => background,
486        };
487
488        rect()
489            .width(width)
490            .height(height)
491            .background(background)
492            .corner_radius(corner_radius)
493            .on_sized(move |e: Event<SizedEventData>| {
494                size.set(e.area);
495                allow_resizing.set(true);
496            })
497            .on_pointer_down(on_pointer_down)
498            .on_global_pointer_press(on_global_pointer_press)
499            .on_pointer_enter(on_pointer_enter)
500            .on_capture_global_pointer_move(on_capture_global_pointer_move)
501            .on_pointer_leave(on_pointer_leave)
502    }
503}