Skip to main content

ferroui_controls/
stack_panel.rs

1// This source file is adapted from the Windows Presentation Foundation
2// project (https://github.com/dotnet/wpf/), MIT License, courtesy of The .NET
3// Foundation.
4
5use crate::primitives::{IScrollSnapPointsInfo, SnapPointsAlignment, SnapPointsChangedHandler};
6use crate::{Control, ControlImpl, Panel, PanelImpl};
7use ferroui_base::input::{INavigableContainer, InputElement, InputElementImpl, NavigationDirection};
8use ferroui_base::interactivity::{
9    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
10};
11use ferroui_base::layout::{Layoutable, LayoutableImpl, Orientation};
12use ferroui_base::utilities::math_utilities::max;
13use ferroui_base::{
14    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, FerroObjectImpl, FerroProperty, Rect, Ref, Size, StyledElementImpl, StyledProperty, VisualImpl,
15};
16use ferroui_base::reactive::IDisposable;
17use std::rc::Rc;
18
19/// A panel which lays out its children horizontally or vertically.
20#[repr(C)]
21pub struct StackPanel {
22    base: Panel,
23}
24
25ferro_class! {
26    StackPanel: Panel, virtuals StackPanelImpl: PanelImpl {
27        /// Arranges a single child in the slot computed by the panel.
28        fn arrange_child(this, child: &Ref<Control>, rect: Rect, panel_size: Size, orientation: Orientation);
29        /// Gets the next control in the specified direction.
30        ///
31        /// `from` is the control from which movement begins.
32        fn get_control_in_direction(this, direction: NavigationDirection, from: Option<&Ref<Control>>) -> Option<Ref<InputElement>>;
33    }
34}
35ferroui_base::ferro_class_info!(StackPanel { new: StackPanel::new });
36
37ferro_impl_classes!(
38    StackPanel: StyledElementImpl,
39    VisualImpl,
40    InteractiveImpl,
41    InputElementImpl,
42    ControlImpl,
43    PanelImpl
44);
45
46ferroui_base::ferro_impl_classes!(StackPanel: FerroObjectImpl);
47
48impl LayoutableImpl for StackPanel {
49    /// General stack panel layout behaviour is to grow unbounded in the
50    /// "stacking" direction (size to content). Children in this dimension are
51    /// encouraged to be as large as they like. In the other dimension, the
52    /// stack panel will assume the maximum size of its children.
53    fn measure_override(this: &Self, available_size: Size) -> Size {
54        let mut stack_desired_size = Size::default();
55        let children = this.children().snapshot();
56        let mut layout_slot_size = available_size;
57        let horizontal = this.orientation() == Orientation::Horizontal;
58        let spacing = this.spacing();
59        let mut has_visible_child = false;
60
61        // Initialize child sizing and iterator data.
62        // Allow children as much size as they want along the stack.
63        if horizontal {
64            layout_slot_size = layout_slot_size.with_width(f64::INFINITY);
65        } else {
66            layout_slot_size = layout_slot_size.with_height(f64::INFINITY);
67        }
68
69        // Iterate through children.
70        for child in children.iter() {
71            // Measure the child.
72            child.measure(layout_slot_size);
73
74            let is_visible = child.is_visible();
75
76            if is_visible && !has_visible_child {
77                has_visible_child = true;
78            }
79
80            let child_desired_size = child.desired_size();
81
82            // Accumulate child size.
83            if horizontal {
84                stack_desired_size = stack_desired_size.with_width(
85                    stack_desired_size.width + (if is_visible { spacing } else { 0.0 }) + child_desired_size.width,
86                );
87                stack_desired_size =
88                    stack_desired_size.with_height(max(stack_desired_size.height, child_desired_size.height));
89            } else {
90                stack_desired_size =
91                    stack_desired_size.with_width(max(stack_desired_size.width, child_desired_size.width));
92                stack_desired_size = stack_desired_size.with_height(
93                    stack_desired_size.height + (if is_visible { spacing } else { 0.0 }) + child_desired_size.height,
94                );
95            }
96        }
97
98        if horizontal {
99            stack_desired_size = stack_desired_size
100                .with_width(stack_desired_size.width - (if has_visible_child { spacing } else { 0.0 }));
101        } else {
102            stack_desired_size = stack_desired_size
103                .with_height(stack_desired_size.height - (if has_visible_child { spacing } else { 0.0 }));
104        }
105
106        stack_desired_size
107    }
108
109    /// Content arrangement.
110    fn arrange_override(this: &Self, final_size: Size) -> Size {
111        let children = this.children().snapshot();
112        let horizontal = this.orientation() == Orientation::Horizontal;
113        let mut rc_child = Rect::from_size(final_size);
114        let mut previous_child_size = 0.0;
115        let spacing = this.spacing();
116
117        // Arrange and position children.
118        for child in children.iter() {
119            if !child.is_visible() {
120                continue;
121            }
122
123            if horizontal {
124                rc_child = rc_child.with_x(rc_child.x + previous_child_size);
125                previous_child_size = child.desired_size().width;
126                rc_child = rc_child.with_width(previous_child_size);
127                rc_child = rc_child.with_height(max(final_size.height, child.desired_size().height));
128                previous_child_size += spacing;
129            } else {
130                rc_child = rc_child.with_y(rc_child.y + previous_child_size);
131                previous_child_size = child.desired_size().height;
132                rc_child = rc_child.with_height(previous_child_size);
133                rc_child = rc_child.with_width(max(final_size.width, child.desired_size().width));
134                previous_child_size += spacing;
135            }
136
137            this.arrange_child(child, rc_child, final_size, this.orientation());
138        }
139
140        this.raise_snap_points_changed();
141
142        final_size
143    }
144}
145
146impl StackPanelImpl for StackPanel {
147    fn arrange_child(_this: &Self, child: &Ref<Control>, rect: Rect, _panel_size: Size, _orientation: Orientation) {
148        child.arrange(rect);
149    }
150
151    fn get_control_in_direction(
152        this: &Self,
153        direction: NavigationDirection,
154        from: Option<&Ref<Control>>,
155    ) -> Option<Ref<InputElement>> {
156        let horiz = this.orientation() == Orientation::Horizontal;
157        let children = this.children();
158        let mut index: i64 = from.and_then(|from| children.index_of(from)).map_or(-1, |index| index as i64);
159
160        match direction {
161            NavigationDirection::First => index = 0,
162            NavigationDirection::Last => index = children.count() as i64 - 1,
163            NavigationDirection::Next => index += 1,
164            NavigationDirection::Previous => {
165                if index != -1 {
166                    index -= 1;
167                }
168            }
169            NavigationDirection::Left => {
170                if index != -1 {
171                    index = if horiz { index - 1 } else { -1 };
172                }
173            }
174            NavigationDirection::Right => {
175                if index != -1 {
176                    index = if horiz { index + 1 } else { -1 };
177                }
178            }
179            NavigationDirection::Up => {
180                if index != -1 {
181                    index = if horiz { -1 } else { index - 1 };
182                }
183            }
184            NavigationDirection::Down => {
185                if index != -1 {
186                    index = if horiz { -1 } else { index + 1 };
187                }
188            }
189            _ => index = -1,
190        }
191
192        if index >= 0 && (index as usize) < children.count() {
193            Some(children.get(index as usize).upcast())
194        } else {
195            None
196        }
197    }
198}
199
200impl INavigableContainer for StackPanel {
201    fn get_control(
202        &self,
203        direction: NavigationDirection,
204        from: Option<&Ref<InputElement>>,
205        wrap: bool,
206    ) -> Option<Ref<InputElement>> {
207        let from = from.and_then(|from| from.cast::<Control>());
208        let mut result = self.get_control_in_direction(direction, from.as_ref());
209
210        if result.is_none() && wrap {
211            use NavigationDirection::*;
212            if self.orientation() == Orientation::Vertical {
213                match direction {
214                    Up | Previous | PageUp => result = self.get_control_in_direction(Last, None),
215                    Down | Next | PageDown => result = self.get_control_in_direction(First, None),
216                    _ => {}
217                }
218            } else {
219                match direction {
220                    Left | Previous | PageUp => result = self.get_control_in_direction(Last, None),
221                    Right | Next | PageDown => result = self.get_control_in_direction(First, None),
222                    _ => {}
223                }
224            }
225        }
226
227        result
228    }
229}
230
231ferroui_base::ferro_properties! { impl StackPanel {
232    ferro_property!(
233        /// Defines the `Spacing` property.
234        pub fn spacing_property() -> StyledProperty<f64> {
235            FerroProperty::register::<StackPanel, _>("Spacing", 0.0)
236        }
237    );
238
239    ferro_property!(
240        /// Defines the `Orientation` property.
241        pub fn orientation_property() -> StyledProperty<Orientation> {
242            FerroProperty::register::<StackPanel, _>("Orientation", Orientation::Vertical)
243        }
244    );
245
246    ferro_property!(
247        /// Defines the `AreHorizontalSnapPointsRegular` property.
248        pub fn are_horizontal_snap_points_regular_property() -> StyledProperty<bool> {
249            FerroProperty::register::<StackPanel, _>("AreHorizontalSnapPointsRegular", false)
250        }
251    );
252
253    ferro_property!(
254        /// Defines the `AreVerticalSnapPointsRegular` property.
255        pub fn are_vertical_snap_points_regular_property() -> StyledProperty<bool> {
256            FerroProperty::register::<StackPanel, _>("AreVerticalSnapPointsRegular", false)
257        }
258    );
259} }
260
261impl StackPanel {
262    ferro_routed_event!(
263        /// Defines the `HorizontalSnapPointsChanged` event.
264        pub fn horizontal_snap_points_changed_event() -> RoutedEvent<RoutedEventArgs> {
265            RoutedEvent::register::<StackPanel, _>("HorizontalSnapPointsChanged", RoutingStrategies::BUBBLE)
266        }
267    );
268
269    ferro_routed_event!(
270        /// Defines the `VerticalSnapPointsChanged` event.
271        pub fn vertical_snap_points_changed_event() -> RoutedEvent<RoutedEventArgs> {
272            RoutedEvent::register::<StackPanel, _>("VerticalSnapPointsChanged", RoutingStrategies::BUBBLE)
273        }
274    );
275
276    fn static_constructor() {
277        Layoutable::affects_measure::<StackPanel>(&[Self::spacing_property().as_property()]);
278        Layoutable::affects_measure::<StackPanel>(&[Self::orientation_property().as_property()]);
279    }
280
281    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
282    pub fn construct() -> Self {
283        Self { base: Panel::construct() }
284    }
285
286    pub fn new() -> Ref<Self> {
287        instantiate(Self::construct())
288    }
289
290    /// The size of the spacing to place between child controls.
291    pub fn spacing(&self) -> f64 {
292        self.get_value(Self::spacing_property())
293    }
294
295    pub fn set_spacing(&self, value: f64) {
296        self.set_value(Self::spacing_property(), value)
297    }
298
299    /// The orientation in which child controls will be layed out.
300    pub fn orientation(&self) -> Orientation {
301        self.get_value(Self::orientation_property())
302    }
303
304    pub fn set_orientation(&self, value: Orientation) {
305        self.set_value(Self::orientation_property(), value)
306    }
307
308    /// Occurs when the measurements for horizontal snap points change.
309    pub fn horizontal_snap_points_changed(
310        &self,
311        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
312    ) -> RoutedEventHandlerToken {
313        self.add_handler(Self::horizontal_snap_points_changed_event(), handler)
314    }
315
316    /// Occurs when the measurements for vertical snap points change.
317    pub fn vertical_snap_points_changed(
318        &self,
319        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
320    ) -> RoutedEventHandlerToken {
321        self.add_handler(Self::vertical_snap_points_changed_event(), handler)
322    }
323
324    /// Whether the horizontal snap points for the panel are equidistant from
325    /// each other.
326    pub fn are_horizontal_snap_points_regular(&self) -> bool {
327        self.get_value(Self::are_horizontal_snap_points_regular_property())
328    }
329
330    pub fn set_are_horizontal_snap_points_regular(&self, value: bool) {
331        self.set_value(Self::are_horizontal_snap_points_regular_property(), value)
332    }
333
334    /// Whether the vertical snap points for the panel are equidistant from
335    /// each other.
336    pub fn are_vertical_snap_points_regular(&self) -> bool {
337        self.get_value(Self::are_vertical_snap_points_regular_property())
338    }
339
340    pub fn set_are_vertical_snap_points_regular(&self, value: bool) {
341        self.set_value(Self::are_vertical_snap_points_regular_property(), value)
342    }
343
344    /// Raises the snap points changed event of the panel's orientation.
345    fn raise_snap_points_changed(&self) {
346        let event = if self.orientation() == Orientation::Horizontal {
347            Self::horizontal_snap_points_changed_event()
348        } else {
349            Self::vertical_snap_points_changed_event()
350        };
351        self.raise_event(&RoutedEventArgs::with_event(event));
352    }
353
354    /// Returns the set of distances between irregular snap points for a
355    /// specified orientation and alignment.
356    ///
357    /// Panics if the snap points of the orientation are regular.
358    pub fn get_irregular_snap_points(
359        &self,
360        orientation: Orientation,
361        snap_points_alignment: SnapPointsAlignment,
362    ) -> Vec<f64> {
363        let mut snap_points = Vec::new();
364
365        match orientation {
366            Orientation::Horizontal => {
367                if self.are_horizontal_snap_points_regular() {
368                    panic!("The horizontal snap points are regular.");
369                }
370                if self.orientation() == Orientation::Horizontal {
371                    for child in self.visual_children().snapshot().iter() {
372                        let bounds = child.bounds();
373                        snap_points.push(match snap_points_alignment {
374                            SnapPointsAlignment::Near => bounds.left(),
375                            SnapPointsAlignment::Center => bounds.center().x,
376                            SnapPointsAlignment::Far => bounds.right(),
377                        });
378                    }
379                }
380            }
381            Orientation::Vertical => {
382                if self.are_vertical_snap_points_regular() {
383                    panic!("The vertical snap points are regular.");
384                }
385                if self.orientation() == Orientation::Vertical {
386                    for child in self.visual_children().snapshot().iter() {
387                        let bounds = child.bounds();
388                        snap_points.push(match snap_points_alignment {
389                            SnapPointsAlignment::Near => bounds.top(),
390                            SnapPointsAlignment::Center => bounds.center().y,
391                            SnapPointsAlignment::Far => bounds.bottom(),
392                        });
393                    }
394                }
395            }
396        }
397
398        snap_points
399    }
400
401    /// Gets the distance between regular snap points for a specified
402    /// orientation and alignment, and the offset of the first snap point.
403    ///
404    /// Panics if the snap points of the panel's orientation are not regular.
405    pub fn get_regular_snap_points(
406        &self,
407        _orientation: Orientation,
408        snap_points_alignment: SnapPointsAlignment,
409    ) -> (f64, f64) {
410        let mut offset = 0.0;
411        let Some(first_child) = self.visual_children().try_get(0) else {
412            return (0.0, offset);
413        };
414        let bounds = first_child.bounds();
415
416        let snap_point = match self.orientation() {
417            Orientation::Horizontal => {
418                if !self.are_horizontal_snap_points_regular() {
419                    panic!("The horizontal snap points are not regular.");
420                }
421
422                offset = match snap_points_alignment {
423                    SnapPointsAlignment::Near => bounds.left(),
424                    SnapPointsAlignment::Center => bounds.center().x,
425                    SnapPointsAlignment::Far => bounds.right(),
426                };
427                bounds.width
428            }
429            Orientation::Vertical => {
430                if !self.are_vertical_snap_points_regular() {
431                    panic!("The vertical snap points are not regular.");
432                }
433
434                offset = match snap_points_alignment {
435                    SnapPointsAlignment::Near => bounds.top(),
436                    SnapPointsAlignment::Center => bounds.center().y,
437                    SnapPointsAlignment::Far => bounds.bottom(),
438                };
439                bounds.height
440            }
441        };
442
443        (snap_point + self.spacing(), offset)
444    }
445}
446
447impl IScrollSnapPointsInfo for StackPanel {
448    fn are_horizontal_snap_points_regular(&self) -> bool {
449        StackPanel::are_horizontal_snap_points_regular(self)
450    }
451
452    fn set_are_horizontal_snap_points_regular(&self, value: bool) {
453        StackPanel::set_are_horizontal_snap_points_regular(self, value)
454    }
455
456    fn are_vertical_snap_points_regular(&self) -> bool {
457        StackPanel::are_vertical_snap_points_regular(self)
458    }
459
460    fn set_are_vertical_snap_points_regular(&self, value: bool) {
461        StackPanel::set_are_vertical_snap_points_regular(self, value)
462    }
463
464    fn get_irregular_snap_points(
465        &self,
466        orientation: Orientation,
467        snap_points_alignment: SnapPointsAlignment,
468    ) -> Vec<f64> {
469        StackPanel::get_irregular_snap_points(self, orientation, snap_points_alignment)
470    }
471
472    fn get_regular_snap_points(
473        &self,
474        orientation: Orientation,
475        snap_points_alignment: SnapPointsAlignment,
476    ) -> (f64, f64) {
477        StackPanel::get_regular_snap_points(self, orientation, snap_points_alignment)
478    }
479
480    fn horizontal_snap_points_changed(&self, handler: SnapPointsChangedHandler) -> Rc<dyn IDisposable> {
481        self.add_disposable_handler(StackPanel::horizontal_snap_points_changed_event(), move |sender, e| {
482            handler(sender, e)
483        })
484    }
485
486    fn vertical_snap_points_changed(&self, handler: SnapPointsChangedHandler) -> Rc<dyn IDisposable> {
487        self.add_disposable_handler(StackPanel::vertical_snap_points_changed_event(), move |sender, e| {
488            handler(sender, e)
489        })
490    }
491}