Skip to main content

ferroui_controls/
wrap_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::{Control, ControlImpl, Panel, PanelImpl};
6use ferroui_base::input::{INavigableContainer, InputElement, InputElementImpl, NavigationDirection};
7use ferroui_base::interactivity::InteractiveImpl;
8use ferroui_base::layout::{LayoutHelper, Layoutable, LayoutableImpl, Orientation};
9use ferroui_base::utilities::math_utilities::max;
10use ferroui_base::utilities::MathUtilities;
11use ferroui_base::{
12    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroProperty,
13    Rect, Ref, Size, StyledElementImpl, StyledProperty, VisualImpl,
14};
15
16/// Defines how items are aligned and spaced in a [`WrapPanel`].
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18#[repr(i32)]
19pub enum WrapPanelItemsAlignment {
20    /// Items are laid out so the first one in each column/row touches the
21    /// top/left of the panel.
22    #[default]
23    Start = 0,
24    /// Items are laid out so that each column/row is centred vertically/
25    /// horizontally within the panel.
26    Center = 1,
27    /// Items are laid out so the last one in each column/row touches the
28    /// bottom/right of the panel.
29    End = 2,
30    /// Items are laid out with equal spacing between them within each
31    /// column/row.
32    Justify = 3,
33    /// Items are stretched evenly to fill the entire height/width of each
34    /// column/row (the last line excluded).
35    Stretch = 4,
36    /// Items are stretched evenly to fill the entire height/width of each
37    /// column/row (the last line included).
38    StretchAll = 5,
39}
40
41/// Positions child elements in sequential position from left to right,
42/// breaking content to the next line at the edge of the containing box.
43/// Subsequent ordering happens sequentially from top to bottom or from right
44/// to left, depending on the value of the `Orientation` property.
45#[repr(C)]
46pub struct WrapPanel {
47    base: Panel,
48}
49
50ferro_class!(WrapPanel: Panel);
51ferroui_base::ferro_class_info!(WrapPanel { new: WrapPanel::new });
52ferro_impl_classes!(
53    WrapPanel: StyledElementImpl,
54    VisualImpl,
55    InteractiveImpl,
56    InputElementImpl,
57    ControlImpl,
58    PanelImpl
59);
60
61ferroui_base::ferro_impl_classes!(WrapPanel: FerroObjectImpl);
62
63impl LayoutableImpl for WrapPanel {
64    fn measure_override(this: &Self, constraint: Size) -> Size {
65        let item_width = this.item_width();
66        let item_height = this.item_height();
67        let item_spacing = this.item_spacing();
68        let line_spacing = this.line_spacing();
69        let orientation = this.orientation();
70        let children = this.children().snapshot();
71        let mut cur_line_size = UVSize::new(orientation);
72        let mut panel_size = UVSize::new(orientation);
73        let uv_constraint = UVSize::with_size(orientation, constraint.width, constraint.height);
74        let item_width_set = !item_width.is_nan();
75        let item_height_set = !item_height.is_nan();
76        let mut item_exists = false;
77        let mut line_exists = false;
78        let mut items_alignment = this.items_alignment();
79        let use_layout_rounding = this.use_layout_rounding();
80        // If we have infinite space on the U axis, we always use Start
81        // alignment to avoid strange behavior
82        if uv_constraint.u == f64::INFINITY {
83            items_alignment = WrapPanelItemsAlignment::Start;
84        }
85        // Justify/StretchAll need to measure with the full constraint on the
86        // U axis
87        if matches!(items_alignment, WrapPanelItemsAlignment::Justify | WrapPanelItemsAlignment::StretchAll) {
88            panel_size.u = uv_constraint.u;
89        }
90
91        let child_constraint = Size::new(
92            if item_width_set { item_width } else { constraint.width },
93            if item_height_set { item_height } else { constraint.height },
94        );
95
96        for child in children.iter() {
97            // Flow passes its own constraint to children
98            child.measure(child_constraint);
99
100            let child_size = UVSize::with_size(
101                orientation,
102                if item_width_set { item_width } else { child.desired_size().width },
103                if item_height_set { item_height } else { child.desired_size().height },
104            );
105
106            let next_spacing = if item_exists && child.is_visible() { item_spacing } else { 0.0 };
107            if greater_than(use_layout_rounding, cur_line_size.u + child_size.u + next_spacing, uv_constraint.u) {
108                // Need to switch to another line
109                panel_size.u = max(cur_line_size.u, panel_size.u);
110                panel_size.v += cur_line_size.v + (if line_exists { line_spacing } else { 0.0 });
111                cur_line_size = child_size;
112
113                item_exists = child.is_visible();
114                line_exists = true;
115            } else {
116                // Continue to accumulate a line
117                cur_line_size.u += child_size.u + next_spacing;
118                cur_line_size.v = max(child_size.v, cur_line_size.v);
119
120                item_exists |= child.is_visible(); // keep true
121            }
122        }
123
124        // Stretch needs to measure with the full constraint on the U axis if
125        // there are multiple lines
126        if line_exists && items_alignment == WrapPanelItemsAlignment::Stretch {
127            panel_size.u = uv_constraint.u;
128        }
129
130        // The last line size, if any should be added
131        panel_size.u = max(cur_line_size.u, panel_size.u);
132        panel_size.v += cur_line_size.v + (if line_exists { line_spacing } else { 0.0 });
133
134        Size::new(panel_size.width(), panel_size.height())
135    }
136
137    fn arrange_override(this: &Self, final_size: Size) -> Size {
138        let item_width = this.item_width();
139        let item_height = this.item_height();
140        let item_spacing = this.item_spacing();
141        let line_spacing = this.line_spacing();
142        let orientation = this.orientation();
143        let is_horizontal = orientation == Orientation::Horizontal;
144        let children = this.children().snapshot();
145        let mut first_in_line = 0;
146        let mut accumulated_v = 0.0;
147        let mut cur_line_size = UVSize::new(orientation);
148        let uv_final_size = UVSize::with_size(orientation, final_size.width, final_size.height);
149        let item_width_set = !item_width.is_nan();
150        let item_height_set = !item_height.is_nan();
151        let mut item_exists = false;
152        let mut line_exists = false;
153        let mut items_alignment = this.items_alignment();
154        let use_layout_rounding = this.use_layout_rounding();
155        // If we have infinite space on the U axis, we always use Start
156        // alignment to avoid strange behavior
157        if uv_final_size.u == f64::INFINITY {
158            items_alignment = WrapPanelItemsAlignment::Start;
159        }
160
161        let line = LineArrangement {
162            children: &children,
163            is_horizontal,
164            use_item_u: if is_horizontal { item_width_set } else { item_height_set },
165            item_u: if is_horizontal { item_width } else { item_height },
166            item_spacing,
167            final_u: uv_final_size.u,
168            items_alignment,
169        };
170
171        for (i, child) in children.iter().enumerate() {
172            let child_size = UVSize::with_size(
173                orientation,
174                if item_width_set { item_width } else { child.desired_size().width },
175                if item_height_set { item_height } else { child.desired_size().height },
176            );
177
178            let next_spacing = if item_exists && child.is_visible() { item_spacing } else { 0.0 };
179            if greater_than(use_layout_rounding, cur_line_size.u + child_size.u + next_spacing, uv_final_size.u) {
180                // Need to switch to another line
181                accumulated_v += if line_exists { line_spacing } else { 0.0 }; // add spacing to arrange line first
182                line.arrange(accumulated_v, cur_line_size.v, first_in_line, i);
183                accumulated_v += cur_line_size.v; // add the height of the line just arranged
184                cur_line_size = child_size;
185
186                first_in_line = i;
187
188                item_exists = child.is_visible();
189                line_exists = true;
190            } else {
191                // Continue to accumulate a line
192                cur_line_size.u += child_size.u + next_spacing;
193                cur_line_size.v = max(child_size.v, cur_line_size.v);
194
195                item_exists |= child.is_visible(); // keep true
196            }
197        }
198
199        // Arrange the last line, if any
200        if first_in_line < children.len() {
201            accumulated_v += if line_exists { line_spacing } else { 0.0 }; // add spacing to arrange line first
202            line.arrange(accumulated_v, cur_line_size.v, first_in_line, children.len());
203        }
204
205        final_size
206    }
207}
208
209/// The state shared by the arrangement of every line of a [`WrapPanel`].
210struct LineArrangement<'a> {
211    children: &'a [Ref<Control>],
212    is_horizontal: bool,
213    use_item_u: bool,
214    item_u: f64,
215    item_spacing: f64,
216    final_u: f64,
217    items_alignment: WrapPanelItemsAlignment,
218}
219
220impl LineArrangement<'_> {
221    fn child_u(&self, child: &Control) -> f64 {
222        if self.use_item_u {
223            self.item_u
224        } else if self.is_horizontal {
225            child.desired_size().width
226        } else {
227            child.desired_size().height
228        }
229    }
230
231    fn arrange(&self, accumulated_v: f64, line_v: f64, start: usize, end_excluded: usize) {
232        let children = self.children;
233        let mut u = 0.0;
234        let mut spacing = self.item_spacing;
235        // Count of spacings between items
236        let mut spacing_count = -1;
237        let mut total_u = 0.0;
238        let mut stretch_ratio = 1.0;
239        let mut line_items_alignment = self.items_alignment;
240        if self.items_alignment == WrapPanelItemsAlignment::Stretch && end_excluded == children.len() {
241            // Don't stretch the last line
242            line_items_alignment = WrapPanelItemsAlignment::Start;
243        }
244
245        if line_items_alignment != WrapPanelItemsAlignment::Start {
246            for child in &children[start..end_excluded] {
247                total_u += self.child_u(child);
248                if child.is_visible() {
249                    spacing_count += 1;
250                }
251            }
252        }
253
254        match line_items_alignment {
255            WrapPanelItemsAlignment::Start => {}
256            WrapPanelItemsAlignment::Center => {
257                total_u += spacing * spacing_count as f64;
258                u = (self.final_u - total_u) / 2.0;
259            }
260            WrapPanelItemsAlignment::End => {
261                total_u += spacing * spacing_count as f64;
262                u = self.final_u - total_u;
263            }
264            WrapPanelItemsAlignment::Justify => {
265                let total_spacing = max(self.final_u - total_u, 0.0);
266                if spacing_count > 0 {
267                    spacing = total_spacing / spacing_count as f64;
268                }
269            }
270            WrapPanelItemsAlignment::Stretch | WrapPanelItemsAlignment::StretchAll => {
271                if !MathUtilities::is_zero(total_u) {
272                    let final_u_without_spacing = max(self.final_u - spacing * spacing_count as f64, 0.0);
273                    stretch_ratio = final_u_without_spacing / total_u;
274                }
275            }
276        }
277
278        for child in &children[start..end_excluded] {
279            let layout_slot_u = self.child_u(child) * stretch_ratio;
280            child.arrange(if self.is_horizontal {
281                Rect::new(u, accumulated_v, layout_slot_u, line_v)
282            } else {
283                Rect::new(accumulated_v, u, line_v, layout_slot_u)
284            });
285            u += layout_slot_u + (if child.is_visible() { spacing } else { 0.0 });
286        }
287    }
288}
289
290fn greater_than(use_layout_rounding: bool, value1: f64, value2: f64) -> bool {
291    if use_layout_rounding {
292        value1 > value2 && value1 - value2 > LayoutHelper::LAYOUT_EPSILON
293    } else {
294        MathUtilities::greater_than(value1, value2)
295    }
296}
297
298/// A size expressed along (`u`) and across (`v`) the panel's orientation.
299#[derive(Clone, Copy)]
300struct UVSize {
301    u: f64,
302    v: f64,
303    orientation: Orientation,
304}
305
306impl UVSize {
307    fn with_size(orientation: Orientation, width: f64, height: f64) -> Self {
308        if orientation == Orientation::Horizontal {
309            Self { u: width, v: height, orientation }
310        } else {
311            Self { u: height, v: width, orientation }
312        }
313    }
314
315    fn new(orientation: Orientation) -> Self {
316        Self { u: 0.0, v: 0.0, orientation }
317    }
318
319    fn width(&self) -> f64 {
320        if self.orientation == Orientation::Horizontal {
321            self.u
322        } else {
323            self.v
324        }
325    }
326
327    fn height(&self) -> f64 {
328        if self.orientation == Orientation::Horizontal {
329            self.v
330        } else {
331            self.u
332        }
333    }
334}
335
336ferroui_base::ferro_properties! { impl WrapPanel {
337    ferro_property!(
338        /// Defines the `ItemSpacing` property.
339        pub fn item_spacing_property() -> StyledProperty<f64> {
340            FerroProperty::register::<WrapPanel, _>("ItemSpacing", 0.0)
341        }
342    );
343
344    ferro_property!(
345        /// Defines the `LineSpacing` property.
346        pub fn line_spacing_property() -> StyledProperty<f64> {
347            FerroProperty::register::<WrapPanel, _>("LineSpacing", 0.0)
348        }
349    );
350
351    ferro_property!(
352        /// Defines the `Orientation` property.
353        pub fn orientation_property() -> StyledProperty<Orientation> {
354            FerroProperty::register::<WrapPanel, _>("Orientation", Orientation::Horizontal)
355        }
356    );
357
358    ferro_property!(
359        /// Defines the `ItemsAlignment` property.
360        pub fn items_alignment_property() -> StyledProperty<WrapPanelItemsAlignment> {
361            FerroProperty::register::<WrapPanel, _>("ItemsAlignment", WrapPanelItemsAlignment::Start)
362        }
363    );
364
365    ferro_property!(
366        /// Defines the `ItemWidth` property.
367        pub fn item_width_property() -> StyledProperty<f64> {
368            FerroProperty::register::<WrapPanel, _>("ItemWidth", f64::NAN)
369        }
370    );
371
372    ferro_property!(
373        /// Defines the `ItemHeight` property.
374        pub fn item_height_property() -> StyledProperty<f64> {
375            FerroProperty::register::<WrapPanel, _>("ItemHeight", f64::NAN)
376        }
377    );
378} }
379
380impl WrapPanel {
381    fn static_constructor() {
382        Layoutable::affects_measure::<WrapPanel>(&[
383            Self::item_spacing_property().as_property(),
384            Self::line_spacing_property().as_property(),
385            Self::orientation_property().as_property(),
386            Self::item_width_property().as_property(),
387            Self::item_height_property().as_property(),
388        ]);
389        Layoutable::affects_arrange::<WrapPanel>(&[Self::items_alignment_property().as_property()]);
390    }
391
392    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
393    pub fn construct() -> Self {
394        Self { base: Panel::construct() }
395    }
396
397    pub fn new() -> Ref<Self> {
398        instantiate(Self::construct())
399    }
400
401    /// The spacing between items.
402    pub fn item_spacing(&self) -> f64 {
403        self.get_value(Self::item_spacing_property())
404    }
405
406    pub fn set_item_spacing(&self, value: f64) {
407        self.set_value(Self::item_spacing_property(), value)
408    }
409
410    /// The spacing between lines.
411    pub fn line_spacing(&self) -> f64 {
412        self.get_value(Self::line_spacing_property())
413    }
414
415    pub fn set_line_spacing(&self, value: f64) {
416        self.set_value(Self::line_spacing_property(), value)
417    }
418
419    /// The orientation in which child controls will be layed out.
420    pub fn orientation(&self) -> Orientation {
421        self.get_value(Self::orientation_property())
422    }
423
424    pub fn set_orientation(&self, value: Orientation) {
425        self.set_value(Self::orientation_property(), value)
426    }
427
428    /// The alignment of items in the panel.
429    pub fn items_alignment(&self) -> WrapPanelItemsAlignment {
430        self.get_value(Self::items_alignment_property())
431    }
432
433    pub fn set_items_alignment(&self, value: WrapPanelItemsAlignment) {
434        self.set_value(Self::items_alignment_property(), value)
435    }
436
437    /// The width of all items in the panel.
438    pub fn item_width(&self) -> f64 {
439        self.get_value(Self::item_width_property())
440    }
441
442    pub fn set_item_width(&self, value: f64) {
443        self.set_value(Self::item_width_property(), value)
444    }
445
446    /// The height of all items in the panel.
447    pub fn item_height(&self) -> f64 {
448        self.get_value(Self::item_height_property())
449    }
450
451    pub fn set_item_height(&self, value: f64) {
452        self.set_value(Self::item_height_property(), value)
453    }
454}
455
456impl INavigableContainer for WrapPanel {
457    /// Gets the next control in the specified direction.
458    fn get_control(
459        &self,
460        direction: NavigationDirection,
461        from: Option<&Ref<InputElement>>,
462        _wrap: bool,
463    ) -> Option<Ref<InputElement>> {
464        let children = self.children();
465        let horiz = self.orientation() == Orientation::Horizontal;
466        let mut index: i64 = from
467            .and_then(|from| from.cast::<Control>())
468            .and_then(|from| children.index_of(&from))
469            .map_or(-1, |index| index as i64);
470
471        match direction {
472            NavigationDirection::First => index = 0,
473            NavigationDirection::Last => index = children.count() as i64 - 1,
474            NavigationDirection::Next => index += 1,
475            NavigationDirection::Previous => index -= 1,
476            NavigationDirection::Left => index = if horiz { index - 1 } else { -1 },
477            NavigationDirection::Right => index = if horiz { index + 1 } else { -1 },
478            NavigationDirection::Up => index = if horiz { -1 } else { index - 1 },
479            NavigationDirection::Down => index = if horiz { -1 } else { index + 1 },
480            _ => {}
481        }
482
483        if index >= 0 && (index as usize) < children.count() {
484            return Some(children.get(index as usize).upcast());
485        }
486
487        None
488    }
489}