Skip to main content

ferroui_base/layout/
layout_helper.rs

1use super::{Layoutable, MinMax};
2use crate::utilities::MathUtilities;
3use crate::{Point, Rect, Size, Thickness, Visual};
4
5/// Provides helper methods needed for layout.
6pub struct LayoutHelper;
7
8impl LayoutHelper {
9    /// Epsilon value used for certain layout calculations.
10    /// Based on the value in WPF LayoutDoubleUtil.
11    pub const LAYOUT_EPSILON: f64 = 0.00000153;
12
13    /// Calculates a control's size based on its width, height, min-width,
14    /// min-height, max-width and max-height.
15    pub fn apply_layout_constraints(control: &Layoutable, constraints: Size) -> Size {
16        Self::apply_layout_constraints_min_max(MinMax::new(control), constraints)
17    }
18
19    pub(crate) fn apply_layout_constraints_min_max(min_max: MinMax, constraints: Size) -> Size {
20        Size::new(
21            MathUtilities::clamp(constraints.width, min_max.min_width, min_max.max_width),
22            MathUtilities::clamp(constraints.height, min_max.min_height, min_max.max_height),
23        )
24    }
25
26    /// Measures a child with padding and a border, returning the desired
27    /// size including them.
28    pub fn measure_child_with_border(
29        control: Option<&Layoutable>,
30        available_size: Size,
31        mut padding: Thickness,
32        mut border_thickness: Thickness,
33    ) -> Size {
34        if let Some(scale) = Self::is_parent_layout_rounded(control) {
35            padding = Self::round_layout_thickness(padding, scale);
36            border_thickness = Self::round_layout_thickness(border_thickness, scale);
37        }
38
39        match control {
40            Some(control) => {
41                control.measure(available_size.deflate(padding + border_thickness));
42                control.desired_size().inflate(padding + border_thickness)
43            }
44            None => Size::default().inflate(padding + border_thickness),
45        }
46    }
47
48    /// Measures a child with padding, returning the desired size including
49    /// the padding.
50    pub fn measure_child(control: Option<&Layoutable>, available_size: Size, mut padding: Thickness) -> Size {
51        if let Some(scale) = Self::is_parent_layout_rounded(control) {
52            padding = Self::round_layout_thickness(padding, scale);
53        }
54
55        match control {
56            Some(control) => {
57                control.measure(available_size.deflate(padding));
58                control.desired_size().inflate(padding)
59            }
60            None => Size::new(padding.left + padding.right, padding.bottom + padding.top),
61        }
62    }
63
64    /// Arranges a child inside padding and a border.
65    pub fn arrange_child_with_border(
66        child: Option<&Layoutable>,
67        available_size: Size,
68        mut padding: Thickness,
69        mut border_thickness: Thickness,
70    ) -> Size {
71        if let Some(scale) = Self::is_parent_layout_rounded(child) {
72            padding = Self::round_layout_thickness(padding, scale);
73            border_thickness = Self::round_layout_thickness(border_thickness, scale);
74        }
75        Self::arrange_child_internal(child, available_size, padding + border_thickness)
76    }
77
78    /// Arranges a child inside padding.
79    pub fn arrange_child(child: Option<&Layoutable>, available_size: Size, mut padding: Thickness) -> Size {
80        if let Some(scale) = Self::is_parent_layout_rounded(child) {
81            padding = Self::round_layout_thickness(padding, scale);
82        }
83        Self::arrange_child_internal(child, available_size, padding)
84    }
85
86    fn arrange_child_internal(child: Option<&Layoutable>, available_size: Size, padding: Thickness) -> Size {
87        if let Some(child) = child {
88            child.arrange(Rect::from_size(available_size).deflate_thickness(padding));
89        }
90        available_size
91    }
92
93    /// Returns the layout scale of the child's parent if the parent uses
94    /// layout rounding.
95    fn is_parent_layout_rounded(child: Option<&Layoutable>) -> Option<f64> {
96        let parent = child?.visual_parent()?;
97        let parent = parent.downcast_ref::<Layoutable>()?;
98        if !parent.use_layout_rounding() {
99            return None;
100        }
101        Some(Self::get_layout_scale(parent))
102    }
103
104    /// Invalidates measure for the control and all of its visual descendants.
105    pub fn invalidate_self_and_children_measure(control: &Layoutable) {
106        fn inner(target: &Visual) {
107            if let Some(layoutable) = target.downcast_ref::<Layoutable>() {
108                layoutable.invalidate_measure();
109            }
110            if let Some(children) = target.visual_children_snapshot() {
111                for child in children.iter() {
112                    inner(child);
113                }
114            }
115        }
116        inner(control);
117    }
118
119    /// Obtains the layout scale of the given control, based on its layout
120    /// root.
121    pub fn get_layout_scale(control: &Layoutable) -> f64 {
122        control.get_layout_root().map_or(1.0, |root| root.layout_scaling())
123    }
124
125    /// Rounds a size to an integer number of device pixels, rounding up.
126    pub fn round_layout_size_up(size: Size, dpi_scale: f64) -> Size {
127        // If DPI == 1, don't use DPI-aware rounding.
128        if dpi_scale == 1.0 {
129            Size::new(size.width.ceil(), size.height.ceil())
130        } else {
131            Size::new(
132                (Self::round_to_8_digits(size.width) * dpi_scale).ceil() / dpi_scale,
133                (Self::round_to_8_digits(size.height) * dpi_scale).ceil() / dpi_scale,
134            )
135        }
136    }
137
138    /// Rounds a thickness to integer numbers of device pixels.
139    pub fn round_layout_thickness(thickness: Thickness, dpi_scale: f64) -> Thickness {
140        // If DPI == 1, don't use DPI-aware rounding.
141        if dpi_scale == 1.0 {
142            Thickness::new(
143                thickness.left.round_ties_even(),
144                thickness.top.round_ties_even(),
145                thickness.right.round_ties_even(),
146                thickness.bottom.round_ties_even(),
147            )
148        } else {
149            Thickness::new(
150                (thickness.left * dpi_scale).round_ties_even() / dpi_scale,
151                (thickness.top * dpi_scale).round_ties_even() / dpi_scale,
152                (thickness.right * dpi_scale).round_ties_even() / dpi_scale,
153                (thickness.bottom * dpi_scale).round_ties_even() / dpi_scale,
154            )
155        }
156    }
157
158    /// Rounds a point to integer device pixels.
159    #[inline]
160    pub fn round_layout_point(point: Point, dpi_scale: f64) -> Point {
161        // If DPI == 1, don't use DPI-aware rounding.
162        if dpi_scale == 1.0 {
163            Point::new(point.x.round_ties_even(), point.y.round_ties_even())
164        } else {
165            Point::new(
166                (point.x * dpi_scale).round_ties_even() / dpi_scale,
167                (point.y * dpi_scale).round_ties_even() / dpi_scale,
168            )
169        }
170    }
171
172    /// Calculates the value to be used for layout rounding at high DPI by
173    /// rounding the value up or down to the nearest pixel.
174    pub fn round_layout_value(value: f64, dpi_scale: f64) -> f64 {
175        // If DPI == 1, don't use DPI-aware rounding.
176        if dpi_scale == 1.0 {
177            value.round_ties_even()
178        } else {
179            (value * dpi_scale).round_ties_even() / dpi_scale
180        }
181    }
182
183    /// Calculates the value to be used for layout rounding at high DPI by
184    /// rounding the value up to the nearest pixel.
185    pub fn round_layout_value_up(value: f64, dpi_scale: f64) -> f64 {
186        // If DPI == 1, don't use DPI-aware rounding.
187        if dpi_scale == 1.0 {
188            value.ceil()
189        } else {
190            (Self::round_to_8_digits(value) * dpi_scale).ceil() / dpi_scale
191        }
192    }
193
194    #[inline]
195    fn round_to_8_digits(value: f64) -> f64 {
196        // Round the value (towards zero) to avoid FP errors. This is needed
197        // because if `value` has a floating point precision error (e.g.
198        // 79.333333333333343) then when it's multiplied by `dpi_scale` and
199        // rounded up, it will be rounded up to a value one greater than it
200        // should be.
201        if !value.is_finite() {
202            return value;
203        }
204        (value * 1e8).trunc() / 1e8
205    }
206
207    /// Validates a render/layout scaling value, normalizing values close to
208    /// one to exactly one. Panics on invalid values.
209    pub fn validate_scaling(scaling: f64) -> f64 {
210        if MathUtilities::is_negative_or_non_finite(scaling) || MathUtilities::is_zero(scaling) {
211            panic!("Invalid render scaling value {scaling}");
212        }
213        if MathUtilities::is_one(scaling) {
214            // Ensure we've got exactly 1.0 and not an approximation, so that
215            // layout hot paths can compare exactly.
216            return 1.0;
217        }
218        scaling
219    }
220}