Skip to main content

ferroui_base/rendering/utilities/
tile_brush_calculator.rs

1use crate::media::{AlignmentX, AlignmentY, ITileBrush, MediaExtensions, Stretch, StretchDirection, TileMode};
2use crate::{Matrix, Point, Rect, RelativeRect, Size, Vector};
3
4/// Calculates the rectangles and transform needed to render a tile brush.
5pub struct TileBrushCalculator {
6    image_size: Size,
7    draw_rect: Rect,
8    is_valid: bool,
9    destination_rect: Rect,
10    intermediate_size: Size,
11    intermediate_transform: Matrix,
12    source_rect: Rect,
13}
14
15impl TileBrushCalculator {
16    /// Creates a calculator for a tile brush.
17    ///
18    /// * `brush` - The brush to be rendered.
19    /// * `content_size` - The size of the content of the tile brush.
20    /// * `target_size` - The size of the control to which the brush is being rendered.
21    pub fn from_brush(brush: &dyn ITileBrush, content_size: Size, target_size: Size) -> Self {
22        Self::new(
23            brush.tile_mode(),
24            brush.stretch(),
25            brush.alignment_x(),
26            brush.alignment_y(),
27            brush.source_rect(),
28            brush.destination_rect(),
29            content_size,
30            target_size,
31        )
32    }
33
34    /// Initializes a new instance of the [`TileBrushCalculator`] type.
35    ///
36    /// * `tile_mode` - The brush's tile mode.
37    /// * `stretch` - The brush's stretch.
38    /// * `alignment_x` - The brush's horizontal alignment.
39    /// * `alignment_y` - The brush's vertical alignment.
40    /// * `source_rect` - The brush's source rect
41    /// * `destination_rect` - The brush's destination rect.
42    /// * `content_size` - The size of the content of the tile brush.
43    /// * `target_size` - The size of the control to which the brush is being rendered.
44    #[allow(clippy::too_many_arguments)]
45    pub fn new(
46        tile_mode: TileMode,
47        stretch: Stretch,
48        alignment_x: AlignmentX,
49        alignment_y: AlignmentY,
50        source_rect: RelativeRect,
51        destination_rect: RelativeRect,
52        content_size: Size,
53        target_size: Size,
54    ) -> Self {
55        let image_size = content_size;
56
57        let source_rect = source_rect.to_pixels(image_size);
58        let destination_rect = destination_rect.to_pixels(target_size);
59
60        let scale = MediaExtensions::calculate_scaling(
61            stretch,
62            destination_rect.size(),
63            source_rect.size(),
64            StretchDirection::Both,
65        );
66        let translate = Self::calculate_translate(alignment_x, alignment_y, source_rect, destination_rect, scale);
67
68        let intermediate_size = if tile_mode == TileMode::None { target_size } else { destination_rect.size() };
69        let (intermediate_transform, draw_rect) =
70            Self::calculate_intermediate_transform(tile_mode, source_rect, destination_rect, scale, translate);
71
72        Self {
73            image_size,
74            draw_rect,
75            is_valid: false,
76            destination_rect,
77            intermediate_size,
78            intermediate_transform,
79            source_rect,
80        }
81    }
82
83    /// Upstream declares this get-only property and never assigns it, so it is always `false`.
84    pub fn is_valid(&self) -> bool {
85        self.is_valid
86    }
87
88    /// Gets the rectangle on the destination control to which content should be rendered.
89    ///
90    /// If the tile mode of the brush is repeating then this is describes rectangle
91    /// of a single repeat of the tiled content.
92    pub fn destination_rect(&self) -> Rect {
93        self.destination_rect
94    }
95
96    /// Gets the clip rectangle on the intermediate image with which the brush content should be
97    /// drawn when [`needs_intermediate`](Self::needs_intermediate) is true.
98    pub fn intermediate_clip(&self) -> Rect {
99        self.draw_rect
100    }
101
102    /// Gets the size of the intermediate image that should be created when
103    /// [`needs_intermediate`](Self::needs_intermediate) is true.
104    pub fn intermediate_size(&self) -> Size {
105        self.intermediate_size
106    }
107
108    /// Gets the transform to be used when rendering to the intermediate image when
109    /// [`needs_intermediate`](Self::needs_intermediate) is true.
110    pub fn intermediate_transform(&self) -> Matrix {
111        self.intermediate_transform
112    }
113
114    /// Gets a value indicating whether an intermediate image should be created in order to
115    /// render the tile brush.
116    ///
117    /// Intermediate images are required when a brush's tile mode is not repeating
118    /// but the source and destination aspect ratios are unequal, as all of the currently
119    /// supported rendering backends do not support non-tiled image brushes.
120    pub fn needs_intermediate(&self) -> bool {
121        if self.intermediate_transform != Matrix::IDENTITY {
122            return true;
123        }
124        if self.source_rect.position() != Point::default() {
125            return true;
126        }
127        if self.source_rect.size().aspect_ratio() == self.image_size.aspect_ratio() {
128            return false;
129        }
130        if self.source_rect.width != self.image_size.width || self.source_rect.height != self.image_size.height {
131            return true;
132        }
133        false
134    }
135
136    /// Gets the area of the source content to be rendered.
137    pub fn source_rect(&self) -> Rect {
138        self.source_rect
139    }
140
141    pub fn calculate_translate(
142        alignment_x: AlignmentX,
143        alignment_y: AlignmentY,
144        source_rect: Rect,
145        destination_rect: Rect,
146        scale: Vector,
147    ) -> Vector {
148        Self::calculate_translate_sizes(alignment_x, alignment_y, source_rect.size() * scale, destination_rect.size())
149    }
150
151    pub fn calculate_translate_sizes(
152        alignment_x: AlignmentX,
153        alignment_y: AlignmentY,
154        source_size: Size,
155        destination_size: Size,
156    ) -> Vector {
157        let mut x = 0.0;
158        let mut y = 0.0;
159
160        match alignment_x {
161            AlignmentX::Center => x += (destination_size.width - source_size.width) / 2.0,
162            AlignmentX::Right => x += destination_size.width - source_size.width,
163            AlignmentX::Left => {}
164        }
165
166        match alignment_y {
167            AlignmentY::Center => y += (destination_size.height - source_size.height) / 2.0,
168            AlignmentY::Bottom => y += destination_size.height - source_size.height,
169            AlignmentY::Top => {}
170        }
171
172        Vector::new(x, y)
173    }
174
175    /// Returns the transform and the draw rectangle (upstream's `out drawRect`).
176    pub fn calculate_intermediate_transform(
177        tile_mode: TileMode,
178        source_rect: Rect,
179        destination_rect: Rect,
180        scale: Vector,
181        translate: Vector,
182    ) -> (Matrix, Rect) {
183        let mut transform = Matrix::create_translation_vector((-source_rect.position()).into())
184            * Matrix::create_scale_vector(scale)
185            * Matrix::create_translation_vector(translate);
186        let dr;
187
188        if tile_mode == TileMode::None {
189            dr = destination_rect;
190            transform *= Matrix::create_translation_vector(destination_rect.position().into());
191        } else {
192            dr = Rect::from_size(destination_rect.size());
193        }
194
195        (transform, dr)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::RelativeUnit;
203
204    #[test]
205    fn no_tile_fill_1x() {
206        let result = TileBrushCalculator::new(
207            TileMode::None,
208            Stretch::Fill,
209            AlignmentX::Center,
210            AlignmentY::Center,
211            RelativeRect::FILL,
212            RelativeRect::FILL,
213            Size::new(100.0, 100.0),
214            Size::new(100.0, 100.0),
215        );
216
217        assert!(!result.needs_intermediate());
218        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), result.source_rect());
219        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), result.destination_rect());
220    }
221
222    #[test]
223    fn no_tile_fill_2x() {
224        let result = TileBrushCalculator::new(
225            TileMode::None,
226            Stretch::Fill,
227            AlignmentX::Center,
228            AlignmentY::Center,
229            RelativeRect::FILL,
230            RelativeRect::FILL,
231            Size::new(100.0, 100.0),
232            Size::new(200.0, 200.0),
233        );
234
235        // This doesn't need an intermediate render target (upstream keeps the same note).
236        assert!(result.needs_intermediate());
237        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), result.source_rect());
238        assert_eq!(Rect::new(0.0, 0.0, 200.0, 200.0), result.destination_rect());
239    }
240
241    #[test]
242    fn no_tile_uniform_center_horiz() {
243        let result = TileBrushCalculator::new(
244            TileMode::None,
245            Stretch::Uniform,
246            AlignmentX::Center,
247            AlignmentY::Center,
248            RelativeRect::FILL,
249            RelativeRect::FILL,
250            Size::new(100.0, 100.0),
251            Size::new(200.0, 100.0),
252        );
253
254        assert!(result.needs_intermediate());
255        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), result.source_rect());
256        assert_eq!(Rect::new(0.0, 0.0, 200.0, 100.0), result.destination_rect());
257        assert_eq!(Size::new(200.0, 100.0), result.intermediate_size());
258        assert_eq!(Matrix::create_translation(50.0, 0.0), result.intermediate_transform());
259    }
260
261    #[test]
262    fn no_tile_uniform_center_vert() {
263        let result = TileBrushCalculator::new(
264            TileMode::None,
265            Stretch::Uniform,
266            AlignmentX::Center,
267            AlignmentY::Center,
268            RelativeRect::FILL,
269            RelativeRect::FILL,
270            Size::new(100.0, 100.0),
271            Size::new(100.0, 200.0),
272        );
273
274        assert!(result.needs_intermediate());
275        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), result.source_rect());
276        assert_eq!(Rect::new(0.0, 0.0, 100.0, 200.0), result.destination_rect());
277        assert_eq!(Size::new(100.0, 200.0), result.intermediate_size());
278        assert_eq!(Matrix::create_translation(0.0, 50.0), result.intermediate_transform());
279    }
280
281    #[test]
282    fn no_tile_no_stretch_bottom_right_quarter_dest() {
283        let result = TileBrushCalculator::new(
284            TileMode::None,
285            Stretch::None,
286            AlignmentX::Center,
287            AlignmentY::Center,
288            RelativeRect::FILL,
289            RelativeRect::new(0.5, 0.5, 0.5, 0.5, RelativeUnit::Relative),
290            Size::new(800.0, 800.0),
291            Size::new(400.0, 400.0),
292        );
293
294        assert!(result.needs_intermediate());
295        assert_eq!(Rect::new(0.0, 0.0, 800.0, 800.0), result.source_rect());
296        assert_eq!(Rect::new(200.0, 200.0, 200.0, 200.0), result.destination_rect());
297        assert_eq!(Size::new(400.0, 400.0), result.intermediate_size());
298        assert_eq!(Rect::new(200.0, 200.0, 200.0, 200.0), result.intermediate_clip());
299        assert_eq!(Matrix::create_translation(-100.0, -100.0), result.intermediate_transform());
300    }
301
302    #[test]
303    fn tile_no_stretch_bottom_right_quarter_source_center_quarter_dest() {
304        let result = TileBrushCalculator::new(
305            TileMode::Tile,
306            Stretch::None,
307            AlignmentX::Center,
308            AlignmentY::Center,
309            RelativeRect::new(0.5, 0.5, 0.5, 0.5, RelativeUnit::Relative),
310            RelativeRect::new(0.25, 0.25, 0.5, 0.5, RelativeUnit::Relative),
311            Size::new(800.0, 800.0),
312            Size::new(400.0, 400.0),
313        );
314
315        assert!(result.needs_intermediate());
316        assert_eq!(Rect::new(400.0, 400.0, 400.0, 400.0), result.source_rect());
317        assert_eq!(Rect::new(100.0, 100.0, 200.0, 200.0), result.destination_rect());
318        assert_eq!(Size::new(200.0, 200.0), result.intermediate_size());
319        assert_eq!(Rect::new(0.0, 0.0, 200.0, 200.0), result.intermediate_clip());
320        assert_eq!(Matrix::create_translation(-500.0, -500.0), result.intermediate_transform());
321    }
322
323    // --- Not upstream tests: expected values computed by hand from the upstream algorithm.
324
325    #[test]
326    fn calculate_translate_follows_the_alignment() {
327        let source = Size::new(40.0, 20.0);
328        let destination = Size::new(100.0, 100.0);
329
330        for (alignment_x, alignment_y, expected) in [
331            (AlignmentX::Left, AlignmentY::Top, Vector::new(0.0, 0.0)),
332            (AlignmentX::Center, AlignmentY::Center, Vector::new(30.0, 40.0)),
333            (AlignmentX::Right, AlignmentY::Bottom, Vector::new(60.0, 80.0)),
334        ] {
335            assert_eq!(
336                expected,
337                TileBrushCalculator::calculate_translate_sizes(alignment_x, alignment_y, source, destination)
338            );
339        }
340
341        assert_eq!(
342            Vector::new(10.0, 60.0),
343            TileBrushCalculator::calculate_translate(
344                AlignmentX::Center,
345                AlignmentY::Bottom,
346                Rect::new(5.0, 5.0, 40.0, 20.0),
347                Rect::new(7.0, 7.0, 100.0, 100.0),
348                Vector::new(2.0, 2.0),
349            )
350        );
351    }
352
353    #[test]
354    fn calculate_intermediate_transform_offsets_by_destination_only_when_not_tiled() {
355        let source = Rect::new(10.0, 20.0, 50.0, 50.0);
356        let destination = Rect::new(5.0, 6.0, 100.0, 100.0);
357        let scale = Vector::new(2.0, 2.0);
358        let translate = Vector::new(1.0, 2.0);
359
360        let (transform, draw_rect) =
361            TileBrushCalculator::calculate_intermediate_transform(TileMode::None, source, destination, scale, translate);
362        assert_eq!(Matrix::new(2.0, 0.0, 0.0, 2.0, -20.0 + 1.0 + 5.0, -40.0 + 2.0 + 6.0), transform);
363        assert_eq!(destination, draw_rect);
364
365        let (transform, draw_rect) =
366            TileBrushCalculator::calculate_intermediate_transform(TileMode::FlipXY, source, destination, scale, translate);
367        assert_eq!(Matrix::new(2.0, 0.0, 0.0, 2.0, -19.0, -38.0), transform);
368        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), draw_rect);
369    }
370
371    #[test]
372    fn is_valid_is_never_set() {
373        let result = TileBrushCalculator::new(
374            TileMode::None,
375            Stretch::Fill,
376            AlignmentX::Center,
377            AlignmentY::Center,
378            RelativeRect::FILL,
379            RelativeRect::FILL,
380            Size::new(100.0, 100.0),
381            Size::new(100.0, 100.0),
382        );
383        assert!(!result.is_valid());
384    }
385
386    // Not from upstream.
387    #[test]
388    fn from_brush_uses_the_parameters_of_the_brush() {
389        let brush = crate::media::ImageBrush::new();
390        brush.set_tile_mode(TileMode::Tile);
391        brush.set_stretch(Stretch::Fill);
392        brush.set_destination_rect(RelativeRect::new(0.0, 0.0, 0.5, 0.5, RelativeUnit::Relative));
393        let handle: std::rc::Rc<dyn crate::media::IBrush> = brush.into();
394
395        let result =
396            TileBrushCalculator::from_brush(handle.as_tile_brush().unwrap(), Size::new(10.0, 10.0), Size::new(40.0, 40.0));
397        let expected = TileBrushCalculator::new(
398            TileMode::Tile,
399            Stretch::Fill,
400            AlignmentX::Center,
401            AlignmentY::Center,
402            RelativeRect::FILL,
403            RelativeRect::new(0.0, 0.0, 0.5, 0.5, RelativeUnit::Relative),
404            Size::new(10.0, 10.0),
405            Size::new(40.0, 40.0),
406        );
407
408        assert_eq!(Rect::new(0.0, 0.0, 20.0, 20.0), result.destination_rect());
409        assert_eq!(expected.destination_rect(), result.destination_rect());
410        assert_eq!(expected.intermediate_size(), result.intermediate_size());
411        assert_eq!(expected.intermediate_transform(), result.intermediate_transform());
412        assert_eq!(expected.source_rect(), result.source_rect());
413    }
414}