Skip to main content

ferroui_base/media/
media_extensions.rs

1use crate::media::{Stretch, StretchDirection};
2use crate::utilities::MathUtilities;
3use crate::{Size, Vector};
4
5/// Provides extension methods for media types.
6pub struct MediaExtensions;
7
8impl MediaExtensions {
9    /// Calculates scaling based on a [`Stretch`] value.
10    ///
11    /// * `stretch` - The stretch mode.
12    /// * `destination_size` - The size of the destination viewport.
13    /// * `source_size` - The size of the source.
14    /// * `stretch_direction` - The stretch direction (upstream default:
15    ///   [`StretchDirection::Both`]).
16    ///
17    /// Returns a vector with the X and Y scaling factors.
18    pub fn calculate_scaling(
19        stretch: Stretch,
20        destination_size: Size,
21        source_size: Size,
22        stretch_direction: StretchDirection,
23    ) -> Vector {
24        let mut scale_x = 1.0;
25        let mut scale_y = 1.0;
26
27        let is_constrained_width = destination_size.width != f64::INFINITY;
28        let is_constrained_height = destination_size.height != f64::INFINITY;
29
30        if (stretch == Stretch::Uniform || stretch == Stretch::UniformToFill || stretch == Stretch::Fill)
31            && (is_constrained_width || is_constrained_height)
32        {
33            // Compute scaling factors for both axes
34            scale_x =
35                if MathUtilities::is_zero(source_size.width) { 0.0 } else { destination_size.width / source_size.width };
36            scale_y = if MathUtilities::is_zero(source_size.height) {
37                0.0
38            } else {
39                destination_size.height / source_size.height
40            };
41
42            if !is_constrained_width {
43                scale_x = scale_y;
44            } else if !is_constrained_height {
45                scale_y = scale_x;
46            } else {
47                // If not preserving aspect ratio, then just apply transform to fit
48                match stretch {
49                    Stretch::Uniform => {
50                        // Find minimum scale that we use for both axes
51                        let minscale = if scale_x < scale_y { scale_x } else { scale_y };
52                        scale_x = minscale;
53                        scale_y = minscale;
54                    }
55                    Stretch::UniformToFill => {
56                        // Find maximum scale that we use for both axes
57                        let maxscale = if scale_x > scale_y { scale_x } else { scale_y };
58                        scale_x = maxscale;
59                        scale_y = maxscale;
60                    }
61                    Stretch::Fill => {
62                        // We already computed the fill scale factors above, so just use them
63                    }
64                    Stretch::None => {}
65                }
66            }
67
68            // Apply stretch direction by bounding scales.
69            // In the uniform case, scale_x=scale_y, so this sort of clamping will maintain aspect ratio
70            // In the uniform fill case, we have the same result too.
71            // In the fill case, note that we change aspect ratio, but that is okay
72            match stretch_direction {
73                StretchDirection::UpOnly => {
74                    if scale_x < 1.0 {
75                        scale_x = 1.0;
76                    }
77                    if scale_y < 1.0 {
78                        scale_y = 1.0;
79                    }
80                }
81                StretchDirection::DownOnly => {
82                    if scale_x > 1.0 {
83                        scale_x = 1.0;
84                    }
85                    if scale_y > 1.0 {
86                        scale_y = 1.0;
87                    }
88                }
89                StretchDirection::Both => {}
90            }
91        }
92
93        Vector::new(scale_x, scale_y)
94    }
95
96    /// Calculates a scaled size based on a [`Stretch`] value.
97    ///
98    /// * `stretch` - The stretch mode.
99    /// * `destination_size` - The size of the destination viewport.
100    /// * `source_size` - The size of the source.
101    /// * `stretch_direction` - The stretch direction (upstream default:
102    ///   [`StretchDirection::Both`]).
103    ///
104    /// Returns the size of the stretched source.
105    pub fn calculate_size(
106        stretch: Stretch,
107        destination_size: Size,
108        source_size: Size,
109        stretch_direction: StretchDirection,
110    ) -> Size {
111        source_size * Self::calculate_scaling(stretch, destination_size, source_size, stretch_direction)
112    }
113}
114
115// Upstream has no dedicated tests for these helpers; the tests below are not upstream tests.
116// The expected values are computed by hand from the upstream algorithm.
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    const BOTH: StretchDirection = StretchDirection::Both;
122
123    #[test]
124    fn none_never_scales() {
125        let scale = MediaExtensions::calculate_scaling(Stretch::None, Size::new(200.0, 50.0), Size::new(100.0, 100.0), BOTH);
126        assert_eq!(Vector::new(1.0, 1.0), scale);
127    }
128
129    #[test]
130    fn fill_scales_each_axis_independently() {
131        let scale = MediaExtensions::calculate_scaling(Stretch::Fill, Size::new(200.0, 50.0), Size::new(100.0, 100.0), BOTH);
132        assert_eq!(Vector::new(2.0, 0.5), scale);
133    }
134
135    #[test]
136    fn uniform_uses_the_smaller_scale() {
137        let scale =
138            MediaExtensions::calculate_scaling(Stretch::Uniform, Size::new(200.0, 50.0), Size::new(100.0, 100.0), BOTH);
139        assert_eq!(Vector::new(0.5, 0.5), scale);
140    }
141
142    #[test]
143    fn uniform_to_fill_uses_the_larger_scale() {
144        let scale = MediaExtensions::calculate_scaling(
145            Stretch::UniformToFill,
146            Size::new(200.0, 50.0),
147            Size::new(100.0, 100.0),
148            BOTH,
149        );
150        assert_eq!(Vector::new(2.0, 2.0), scale);
151    }
152
153    #[test]
154    fn unconstrained_axis_copies_the_other_scale() {
155        let source = Size::new(100.0, 50.0);
156        for stretch in [Stretch::Fill, Stretch::Uniform, Stretch::UniformToFill] {
157            assert_eq!(
158                Vector::new(3.0, 3.0),
159                MediaExtensions::calculate_scaling(stretch, Size::new(f64::INFINITY, 150.0), source, BOTH)
160            );
161            assert_eq!(
162                Vector::new(4.0, 4.0),
163                MediaExtensions::calculate_scaling(stretch, Size::new(400.0, f64::INFINITY), source, BOTH)
164            );
165            assert_eq!(
166                Vector::new(1.0, 1.0),
167                MediaExtensions::calculate_scaling(stretch, Size::INFINITY, source, BOTH)
168            );
169        }
170    }
171
172    #[test]
173    fn zero_source_gives_zero_scale() {
174        let scale = MediaExtensions::calculate_scaling(Stretch::Fill, Size::new(200.0, 50.0), Size::new(0.0, 100.0), BOTH);
175        assert_eq!(Vector::new(0.0, 0.5), scale);
176
177        let scale =
178            MediaExtensions::calculate_scaling(Stretch::Uniform, Size::new(200.0, 50.0), Size::new(100.0, 0.0), BOTH);
179        assert_eq!(Vector::new(0.0, 0.0), scale);
180    }
181
182    #[test]
183    fn stretch_direction_bounds_the_scales() {
184        let destination = Size::new(200.0, 50.0);
185        let source = Size::new(100.0, 100.0);
186
187        assert_eq!(
188            Vector::new(2.0, 1.0),
189            MediaExtensions::calculate_scaling(Stretch::Fill, destination, source, StretchDirection::UpOnly)
190        );
191        assert_eq!(
192            Vector::new(1.0, 0.5),
193            MediaExtensions::calculate_scaling(Stretch::Fill, destination, source, StretchDirection::DownOnly)
194        );
195        assert_eq!(
196            Vector::new(1.0, 1.0),
197            MediaExtensions::calculate_scaling(Stretch::Uniform, destination, source, StretchDirection::UpOnly)
198        );
199        assert_eq!(
200            Vector::new(1.0, 1.0),
201            MediaExtensions::calculate_scaling(Stretch::UniformToFill, destination, source, StretchDirection::DownOnly)
202        );
203    }
204
205    #[test]
206    fn calculate_size_scales_the_source() {
207        let destination = Size::new(200.0, 50.0);
208        let source = Size::new(100.0, 80.0);
209
210        assert_eq!(source, MediaExtensions::calculate_size(Stretch::None, destination, source, BOTH));
211        assert_eq!(destination, MediaExtensions::calculate_size(Stretch::Fill, destination, source, BOTH));
212        assert_eq!(Size::new(62.5, 50.0), MediaExtensions::calculate_size(Stretch::Uniform, destination, source, BOTH));
213        assert_eq!(
214            Size::new(200.0, 160.0),
215            MediaExtensions::calculate_size(Stretch::UniformToFill, destination, source, BOTH)
216        );
217    }
218}