Skip to main content

freya_components/
resizable_container.rs

1use freya_core::prelude::*;
2use torin::{
3    content::Content,
4    node::Node,
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/// Sizing mode for a resizable panel.
29#[derive(PartialEq, Clone, Copy, Debug)]
30pub enum PanelSize {
31    /// Fixed pixel size.
32    Pixels(Length),
33    /// Proportional flex weight distributed among other percentage panels.
34    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    /// The upper bound for this sizing mode.
53    fn max_size(&self) -> f32 {
54        match self {
55            Self::Pixels(_) => f32::MAX,
56            Self::Percentage(_) => 100.,
57        }
58    }
59
60    /// Scale factor to convert between pixels and this panel's unit system.
61    fn flex_scale(&self, flex_factor: f32) -> f32 {
62        match self {
63            Self::Pixels(_) => 1.0,
64            Self::Percentage(_) => flex_factor,
65        }
66    }
67}
68
69/// A panel's size and constraints.
70#[derive(Clone, Copy, Debug)]
71pub struct Panel {
72    pub size: f32,
73    pub initial_size: f32,
74    pub min_size: f32,
75    pub sizing: PanelSize,
76}
77
78impl Panel {
79    /// Rebalances percentage panel sizes when this panel is added or removed,
80    /// leaving fixed-size panels unchanged and respecting minimum sizes.
81    fn redistribute<'a>(&self, panels: impl Iterator<Item = &'a mut Panel>, adding: bool) {
82        if matches!(self.sizing, PanelSize::Pixels(_)) {
83            return;
84        }
85
86        let mut buffer = self.size;
87        for panel in panels.filter(|panel| matches!(panel.sizing, PanelSize::Percentage(_))) {
88            let adjustment = (panel.initial_size - panel.size).min(buffer);
89            if !adding || adjustment >= 0. {
90                let adjustment = if adding { -adjustment } else { adjustment };
91                panel.size = (panel.size + adjustment).max(panel.min_size);
92                buffer -= panel.initial_size - panel.size;
93            }
94        }
95    }
96}
97
98/// Resize state for a container. Panel membership and order come from its declarations.
99#[derive(Default)]
100pub struct ResizableContext {
101    panels: Vec<(DiffKey, Panel)>,
102    direction: Direction,
103    dragging: Option<usize>,
104}
105
106impl ResizableContext {
107    pub const HANDLE_SIZE: f32 = 4.0;
108
109    pub fn direction(&self) -> Direction {
110        self.direction
111    }
112
113    /// Returns the panels in declaration order.
114    pub fn panels(&self) -> Vec<Panel> {
115        self.panels.iter().map(|(_, panel)| *panel).collect()
116    }
117
118    fn reconcile(&mut self, mut panels: Vec<(DiffKey, Panel)>, direction: Direction) -> bool {
119        let unchanged = self.panels.len() == panels.len()
120            && self
121                .panels
122                .iter()
123                .zip(&panels)
124                .all(|((old_key, old), (key, new))| {
125                    old_key == key && old.sizing == new.sizing && old.min_size == new.min_size
126                });
127        if unchanged && self.direction == direction {
128            return false;
129        }
130
131        let mut previous = std::mem::take(&mut self.panels);
132        let mut index = 0;
133        while index < previous.len() {
134            if panels.iter().any(|(key, _)| *key == previous[index].0) {
135                index += 1;
136            } else {
137                let (_, removed) = previous.remove(index);
138                removed.redistribute(previous.iter_mut().map(|(_, panel)| panel), false);
139            }
140        }
141
142        for (key, panel) in &panels {
143            if !previous.iter().any(|(old_key, _)| old_key == key) {
144                panel.redistribute(previous.iter_mut().map(|(_, panel)| panel), true);
145                previous.push((key.clone(), *panel));
146            }
147        }
148
149        for (key, panel) in &mut panels {
150            if let Some((_, old)) = previous.iter().find(|(old_key, old)| {
151                old_key == key
152                    && std::mem::discriminant(&old.sizing) == std::mem::discriminant(&panel.sizing)
153            }) {
154                panel.size = old.size;
155            }
156            panel.size = panel.size.clamp(panel.min_size, panel.sizing.max_size());
157        }
158        self.panels = panels;
159        self.direction = direction;
160        self.dragging = None;
161        true
162    }
163
164    /// Resizes panels around a divider at the given declaration index (1..panel count).
165    pub fn apply_resize(
166        &mut self,
167        divider_index: usize,
168        drag_delta: f32,
169        container_size: f32,
170    ) -> bool {
171        if divider_index == 0 || divider_index >= self.panels.len() {
172            return false;
173        }
174
175        let panels = &mut self.panels;
176        let mut sizes_changed = false;
177
178        let handle_space = panels.len().saturating_sub(1) as f32 * Self::HANDLE_SIZE;
179        let (total_fixed_size, total_flex_weight) = panels.iter().fold(
180            (0.0, 0.0),
181            |(fixed_size, flex_weight): (f32, f32), (_, panel)| match panel.sizing {
182                PanelSize::Pixels(_) => (fixed_size + panel.size, flex_weight),
183                PanelSize::Percentage(_) => (fixed_size, flex_weight + panel.size),
184            },
185        );
186        let flex_per_pixel =
187            total_flex_weight / (container_size - total_fixed_size - handle_space).max(1.0);
188
189        let shrink_amount = drag_delta.abs();
190        let (growing_index, mut shrinking_range) = if drag_delta >= 0. {
191            (divider_index - 1, divider_index..panels.len())
192        } else {
193            (divider_index, 0..divider_index)
194        };
195
196        let mut freed_space = 0.0;
197
198        while let Some(index) = if drag_delta >= 0. {
199            shrinking_range.next()
200        } else {
201            shrinking_range.next_back()
202        } {
203            let (_, panel) = &mut panels[index];
204            let previous_size = panel.size;
205            let units_per_pixel = panel.sizing.flex_scale(flex_per_pixel);
206            let new_size = (panel.size - shrink_amount * units_per_pixel)
207                .clamp(panel.min_size, panel.sizing.max_size());
208            sizes_changed |= panel.size != new_size;
209            panel.size = new_size;
210            freed_space -= (new_size - previous_size) / units_per_pixel.max(f32::MIN_POSITIVE);
211
212            if previous_size > panel.min_size {
213                break;
214            }
215        }
216
217        let (_, panel) = &mut panels[growing_index];
218        let units_per_pixel = panel.sizing.flex_scale(flex_per_pixel);
219        let new_size = (panel.size + freed_space * units_per_pixel)
220            .clamp(panel.min_size, panel.sizing.max_size());
221        sizes_changed |= panel.size != new_size;
222        panel.size = new_size;
223
224        sizes_changed
225    }
226
227    pub fn reset(&mut self) {
228        for (_, panel) in &mut self.panels {
229            panel.size = panel
230                .initial_size
231                .clamp(panel.min_size, panel.sizing.max_size());
232        }
233    }
234}
235
236/// A container with resizable panels.
237///
238/// # Example
239///
240/// ```rust
241/// # use freya::prelude::*;
242/// fn app() -> impl IntoElement {
243///     ResizableContainer::new()
244///         .panel(ResizablePanel::new(PanelSize::percent(50.)).child("Panel 1"))
245///         .panel(ResizablePanel::new(PanelSize::percent(50.)).child("Panel 2"))
246/// }
247/// ```
248///
249/// See the [interactive components demo](https://freyaui.dev/demo).
250#[derive(PartialEq, Clone)]
251pub struct ResizableContainer {
252    key: DiffKey,
253    layout: LayoutData,
254    panels: Vec<ResizablePanel>,
255    controller: Option<Writable<ResizableContext>>,
256}
257
258impl Default for ResizableContainer {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264impl LayoutExt for ResizableContainer {
265    fn layout_mut(&mut self) -> &mut LayoutData {
266        &mut self.layout
267    }
268}
269
270impl ContainerExt for ResizableContainer {}
271
272impl KeyExt for ResizableContainer {
273    fn key_mut(&mut self) -> &mut DiffKey {
274        &mut self.key
275    }
276}
277
278impl ResizableContainer {
279    pub fn new() -> Self {
280        Self {
281            key: DiffKey::None,
282            layout: Node {
283                width: Size::fill(),
284                height: Size::fill(),
285                content: Content::flex(),
286                ..Default::default()
287            }
288            .into(),
289            panels: vec![],
290            controller: None,
291        }
292    }
293
294    pub fn direction(mut self, direction: Direction) -> Self {
295        self.layout.direction = direction;
296        self
297    }
298
299    pub fn panel(mut self, panel: impl Into<Option<ResizablePanel>>) -> Self {
300        if let Some(panel) = panel.into() {
301            self.panels.push(panel);
302        }
303        self
304    }
305
306    pub fn panels_iter(mut self, panels: impl Iterator<Item = ResizablePanel>) -> Self {
307        self.panels.extend(panels);
308        self
309    }
310
311    pub fn controller(mut self, controller: impl Into<Writable<ResizableContext>>) -> Self {
312        self.controller = Some(controller.into());
313        self
314    }
315}
316
317impl Component for ResizableContainer {
318    fn render(&self) -> impl IntoElement {
319        let mut size = use_state(Area::default);
320        use_provide_context(|| size);
321
322        let mut controller = use_provide_context(|| {
323            self.controller
324                .clone()
325                .unwrap_or_else(|| State::create(ResizableContext::default()).into_writable())
326        });
327        let declarations: Vec<_> = self
328            .panels
329            .iter()
330            .enumerate()
331            .map(|(index, panel)| {
332                let initial_size = panel.initial_size.value();
333                (
334                    panel.key.clone().or(DiffKey::DefaultU64(index as u64)),
335                    Panel {
336                        size: initial_size,
337                        initial_size,
338                        min_size: panel.min_size.unwrap_or(initial_size * 0.25),
339                        sizing: panel.initial_size,
340                    },
341                )
342            })
343            .collect();
344        controller.write_if(|mut context| context.reconcile(declarations, self.layout.direction));
345        let context = controller.read();
346
347        rect()
348            .layout(self.layout.clone())
349            .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
350            .on_global_pointer_up({
351                let mut controller = controller.clone();
352                move |_: Event<PointerEventData>| {
353                    controller.write_if(|mut context| context.dragging.take().is_some());
354                }
355            })
356            .children(
357                context
358                    .panels
359                    .iter()
360                    .zip(&self.panels)
361                    .enumerate()
362                    .flat_map(|(index, ((key, state), panel))| {
363                        let main_size = match state.sizing {
364                            PanelSize::Pixels(_) => Size::px(state.size),
365                            PanelSize::Percentage(_) => Size::flex(state.size),
366                        };
367                        let (width, height) = match context.direction {
368                            Direction::Horizontal => (main_size, Size::fill()),
369                            Direction::Vertical => (Size::fill(), main_size),
370                        };
371
372                        [
373                            (index > 0)
374                                .then(|| ResizableHandle::new(index).key(index).into_element()),
375                            Some(
376                                rect()
377                                    .key(key)
378                                    .a11y_role(AccessibilityRole::Pane)
379                                    .width(width)
380                                    .height(height)
381                                    .overflow(panel.overflow)
382                                    .children(panel.children.clone())
383                                    .into_element(),
384                            ),
385                        ]
386                        .into_iter()
387                        .flatten()
388                    }),
389            )
390    }
391
392    fn render_key(&self) -> DiffKey {
393        self.key.clone().or(self.default_key())
394    }
395}
396
397/// A panel declaration. Its position in the container determines its order.
398/// Use a key to preserve its size and child state when moving it.
399#[derive(PartialEq, Clone)]
400pub struct ResizablePanel {
401    key: DiffKey,
402    initial_size: PanelSize,
403    min_size: Option<f32>,
404    children: Vec<Element>,
405    overflow: Overflow,
406}
407
408impl KeyExt for ResizablePanel {
409    fn key_mut(&mut self) -> &mut DiffKey {
410        &mut self.key
411    }
412}
413
414impl ChildrenExt for ResizablePanel {
415    fn children_mut(&mut self) -> &mut Vec<Element> {
416        &mut self.children
417    }
418}
419
420impl ResizablePanel {
421    pub fn new(initial_size: PanelSize) -> Self {
422        Self {
423            key: DiffKey::None,
424            initial_size,
425            min_size: None,
426            children: vec![],
427            overflow: Overflow::Clip,
428        }
429    }
430
431    /// Sets the initial size, also used by the controller's reset operation.
432    /// Existing resized sizes are retained unless the sizing mode changes.
433    pub fn initial_size(mut self, initial_size: PanelSize) -> Self {
434        self.initial_size = initial_size;
435        self
436    }
437
438    /// Set the minimum size for this panel (in the same units as the panel's sizing mode).
439    pub fn min_size(mut self, min_size: f32) -> Self {
440        self.min_size = Some(min_size);
441        self
442    }
443
444    /// Sets how content is clipped inside the panel bounds.
445    pub fn overflow(mut self, overflow: impl Into<Overflow>) -> Self {
446        self.overflow = overflow.into();
447        self
448    }
449}
450
451/// Describes the current status of the Handle.
452#[derive(Debug, Default, PartialEq, Clone, Copy)]
453pub enum HandleStatus {
454    /// Default state.
455    #[default]
456    Idle,
457    /// Mouse is hovering the handle.
458    Hovering,
459}
460
461#[derive(PartialEq)]
462pub struct ResizableHandle {
463    key: DiffKey,
464    panel_index: usize,
465    /// Theme override.
466    pub(crate) theme: Option<ResizableHandleThemePartial>,
467}
468
469impl ResizableHandle {
470    pub fn new(panel_index: usize) -> Self {
471        Self {
472            key: DiffKey::None,
473            panel_index,
474            theme: None,
475        }
476    }
477}
478
479impl KeyExt for ResizableHandle {
480    fn key_mut(&mut self) -> &mut DiffKey {
481        &mut self.key
482    }
483}
484
485impl Component for ResizableHandle {
486    fn render(&self) -> impl IntoElement {
487        let ResizableHandleTheme {
488            background,
489            hover_background,
490            corner_radius,
491        } = get_theme!(
492            &self.theme,
493            ResizableHandleThemePreference,
494            "resizable_handle"
495        );
496        let mut size = use_state(Area::default);
497        let mut status = use_state(HandleStatus::default);
498        let mut controller = use_consume::<Writable<ResizableContext>>();
499        let container_size = use_consume::<State<Area>>();
500        let mut allow_resizing = use_state(|| false);
501
502        let panel_index = self.panel_index;
503        let (direction, dragging) = {
504            let context = controller.read();
505            (context.direction, context.dragging == Some(panel_index))
506        };
507
508        let cursor = match direction {
509            Direction::Horizontal => CursorIcon::ColResize,
510            _ => CursorIcon::RowResize,
511        };
512
513        let on_capture_global_pointer_move = {
514            let mut controller = controller.clone();
515            move |e: Event<PointerEventData>| {
516                if controller.peek().dragging == Some(panel_index) {
517                    e.prevent_default();
518
519                    if !*allow_resizing.read() {
520                        return;
521                    }
522
523                    let coords = e.global_location();
524                    let handle = size.read();
525                    let container = container_size.read();
526                    let mut context = controller.write();
527
528                    let (drag_delta, container_axis_size) = match context.direction {
529                        Direction::Horizontal => {
530                            (coords.x as f32 - handle.min_x(), container.width())
531                        }
532                        Direction::Vertical => {
533                            (coords.y as f32 - handle.min_y(), container.height())
534                        }
535                    };
536
537                    let sizes_changed =
538                        context.apply_resize(panel_index, drag_delta, container_axis_size);
539
540                    if sizes_changed {
541                        allow_resizing.set(false);
542                    }
543                }
544            }
545        };
546
547        let handle_size = Size::px(ResizableContext::HANDLE_SIZE);
548        let (width, height) = match direction {
549            Direction::Horizontal => (handle_size, Size::fill()),
550            Direction::Vertical => (Size::fill(), handle_size),
551        };
552
553        let background = match *status.read() {
554            HandleStatus::Idle if !dragging => background,
555            _ => hover_background,
556        };
557
558        rect()
559            .width(width)
560            .height(height)
561            .background(background)
562            .corner_radius(corner_radius)
563            .cursor(cursor)
564            .on_sized(move |e: Event<SizedEventData>| {
565                size.set(e.area);
566                allow_resizing.set(true);
567            })
568            .on_pointer_down(move |e: Event<PointerEventData>| {
569                if !e.data().is_primary() {
570                    return;
571                }
572                e.stop_propagation();
573                e.prevent_default();
574                controller.write().dragging = Some(panel_index);
575            })
576            .on_pointer_enter(move |_| status.set(HandleStatus::Hovering))
577            .on_capture_global_pointer_move(on_capture_global_pointer_move)
578            .on_pointer_leave(move |_| status.set(HandleStatus::Idle))
579    }
580
581    fn render_key(&self) -> DiffKey {
582        self.key.clone().or(self.default_key())
583    }
584}