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
138#[derive(PartialEq)]
139pub struct ResizableContainer {
140    /// Direction of the container.
141    /// Default to [Direction::Vertical].
142    direction: Direction,
143    /// Inner children for the [ResizableContainer()].
144    panels: Vec<ResizablePanel>,
145}
146
147impl Default for ResizableContainer {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl ResizableContainer {
154    pub fn new() -> Self {
155        Self {
156            direction: Direction::Vertical,
157            panels: vec![],
158        }
159    }
160
161    pub fn direction(mut self, direction: Direction) -> Self {
162        self.direction = direction;
163        self
164    }
165
166    pub fn panel(mut self, panel: impl Into<Option<ResizablePanel>>) -> Self {
167        if let Some(panel) = panel.into() {
168            self.panels.push(panel);
169        }
170
171        self
172    }
173
174    pub fn panels_iter(mut self, panels: impl Iterator<Item = ResizablePanel>) -> Self {
175        self.panels.extend(panels);
176
177        self
178    }
179}
180
181impl Render for ResizableContainer {
182    fn render(&self) -> impl IntoElement {
183        let mut size = use_state(Area::default);
184        use_provide_context(|| size);
185
186        use_provide_context(|| {
187            State::create(ResizableContext {
188                direction: self.direction,
189                ..Default::default()
190            })
191        });
192
193        rect()
194            .direction(self.direction)
195            .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
196            .expanded()
197            .content(Content::flex())
198            .children_iter(self.panels.iter().enumerate().flat_map(|(i, e)| {
199                if i > 0 {
200                    vec![ResizableHandle::new(i).into(), e.clone().into()]
201                } else {
202                    vec![e.clone().into()]
203                }
204            }))
205    }
206}
207
208#[derive(PartialEq, Clone)]
209pub struct ResizablePanel {
210    key: DiffKey,
211    initial_size: f32,
212    min_size: Option<f32>,
213    children: Vec<Element>,
214    order: Option<usize>,
215}
216
217impl KeyExt for ResizablePanel {
218    fn write_key(&mut self) -> &mut DiffKey {
219        &mut self.key
220    }
221}
222
223impl ChildrenExt for ResizablePanel {
224    fn get_children(&mut self) -> &mut Vec<Element> {
225        &mut self.children
226    }
227}
228
229impl ResizablePanel {
230    pub fn new(initial_size: f32) -> Self {
231        Self {
232            key: DiffKey::None,
233            initial_size,
234            min_size: None,
235            children: vec![],
236            order: None,
237        }
238    }
239
240    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
241        self.key = key.into();
242        self
243    }
244
245    pub fn initial_size(mut self, initial_size: impl Into<f32>) -> Self {
246        self.initial_size = initial_size.into();
247        self
248    }
249
250    pub fn min_size(mut self, min_size: impl Into<f32>) -> Self {
251        self.min_size = Some(min_size.into());
252        self
253    }
254
255    pub fn order(mut self, order: impl Into<usize>) -> Self {
256        self.order = Some(order.into());
257        self
258    }
259}
260
261impl Render for ResizablePanel {
262    fn render(&self) -> impl IntoElement {
263        let mut registry = use_consume::<State<ResizableContext>>();
264
265        let id = use_hook(|| {
266            let id = UseId::<ResizableContext>::get_in_hook();
267
268            let panel = Panel {
269                initial_size: self.initial_size,
270                size: self.initial_size,
271                min_size: self.min_size.unwrap_or(self.initial_size * 0.25),
272                id,
273            };
274
275            registry.write().push_panel(panel, self.order);
276
277            id
278        });
279
280        use_drop(move || {
281            // Safe to ignore any error as we are dropping
282            let _ = registry.write().remove_panel(id);
283        });
284
285        let registry = registry.read();
286        let index = registry
287            .panels
288            .iter()
289            .position(|e| e.id == id)
290            .unwrap_or_default();
291
292        let Panel { size, .. } = registry.panels[index];
293
294        let (width, height) = match registry.direction {
295            Direction::Horizontal => (Size::flex(size), Size::fill()),
296            Direction::Vertical => (Size::fill(), Size::flex(size)),
297        };
298
299        rect()
300            .a11y_role(AccessibilityRole::Pane)
301            .width(width)
302            .height(height)
303            .overflow(Overflow::Clip)
304            .children(self.children.clone())
305    }
306
307    fn render_key(&self) -> DiffKey {
308        self.key.clone().or(DiffKey::None)
309    }
310}
311
312/// Describes the current status of the Handle.
313#[derive(Debug, Default, PartialEq, Clone, Copy)]
314pub enum HandleStatus {
315    /// Default state.
316    #[default]
317    Idle,
318    /// Mouse is hovering the handle.
319    Hovering,
320}
321
322#[derive(PartialEq)]
323pub struct ResizableHandle {
324    panel_index: usize,
325    /// Theme override.
326    pub(crate) theme: Option<ResizableHandleThemePartial>,
327}
328
329impl ResizableHandle {
330    pub fn new(panel_index: usize) -> Self {
331        Self {
332            panel_index,
333            theme: None,
334        }
335    }
336}
337
338impl Render for ResizableHandle {
339    fn render(&self) -> impl IntoElement {
340        let ResizableHandleTheme {
341            background,
342            hover_background,
343            corner_radius,
344        } = get_theme!(&self.theme, resizable_handle);
345        let mut size = use_state(Area::default);
346        let mut clicking = use_state(|| false);
347        let mut status = use_state(HandleStatus::default);
348        let mut registry = use_consume::<State<ResizableContext>>();
349        let container_size = use_consume::<State<Area>>();
350        let mut allow_resizing = use_state(|| false);
351
352        let panel_index = self.panel_index;
353
354        use_drop(move || {
355            if *status.peek() == HandleStatus::Hovering {
356                Cursor::set(CursorIcon::default());
357            }
358        });
359
360        let cursor = match registry.read().direction {
361            Direction::Horizontal => CursorIcon::ColResize,
362            _ => CursorIcon::RowResize,
363        };
364
365        let on_pointer_leave = move |_| {
366            *status.write() = HandleStatus::Idle;
367            if !clicking() {
368                Cursor::set(CursorIcon::default());
369            }
370        };
371
372        let on_pointer_enter = move |_| {
373            *status.write() = HandleStatus::Hovering;
374            Cursor::set(cursor);
375        };
376
377        let on_capture_global_mouse_move = move |e: Event<MouseEventData>| {
378            if *clicking.read() {
379                e.prevent_default();
380
381                if !*allow_resizing.read() {
382                    return;
383                }
384
385                let coordinates = e.global_location;
386                let mut registry = registry.write();
387
388                let total_size = registry.panels.iter().fold(0., |acc, p| acc + p.size);
389
390                let distance = match registry.direction {
391                    Direction::Horizontal => {
392                        let container_width = container_size.read().width();
393                        let displacement = coordinates.x as f32 - size.read().min_x();
394                        total_size / container_width * displacement
395                    }
396                    Direction::Vertical => {
397                        let container_height = container_size.read().height();
398                        let displacement = coordinates.y as f32 - size.read().min_y();
399                        total_size / container_height * displacement
400                    }
401                };
402
403                let changed_panels = registry.apply_resize(panel_index, distance);
404
405                if changed_panels {
406                    allow_resizing.set(false);
407                }
408            }
409        };
410
411        let on_pointer_down = move |e: Event<PointerEventData>| {
412            e.stop_propagation();
413            e.prevent_default();
414            clicking.set(true);
415        };
416
417        let on_global_mouse_up = move |_| {
418            if *clicking.read() {
419                if *status.peek() != HandleStatus::Hovering {
420                    Cursor::set(CursorIcon::default());
421                }
422                clicking.set(false);
423            }
424        };
425
426        let (width, height) = match registry.read().direction {
427            Direction::Horizontal => (Size::px(4.), Size::fill()),
428            Direction::Vertical => (Size::fill(), Size::px(4.)),
429        };
430
431        let background = match *status.read() {
432            _ if *clicking.read() => hover_background,
433            HandleStatus::Hovering => hover_background,
434            HandleStatus::Idle => background,
435        };
436
437        rect()
438            .width(width)
439            .height(height)
440            .background(background)
441            .corner_radius(corner_radius)
442            .on_sized(move |e: Event<SizedEventData>| {
443                size.set(e.area);
444                allow_resizing.set(true);
445            })
446            .on_pointer_down(on_pointer_down)
447            .on_global_mouse_up(on_global_mouse_up)
448            .on_pointer_enter(on_pointer_enter)
449            .on_capture_global_mouse_move(on_capture_global_mouse_move)
450            .on_pointer_leave(on_pointer_leave)
451    }
452}