Skip to main content

cranpose_ui/
nine_patch.rs

1//! Filling a box with an image that is bigger, or smaller, than the box.
2//!
3//! Two questions this answers that a plain scale cannot. A tiled fill repeats
4//! its source instead of stretching it, which is how a texture, a hatch or a
5//! skin's background covers an area of any size without going soft. And a
6//! nine-patch fill keeps the corners of a source at their own size while the
7//! edges and the middle grow, which is how a button, a panel or a scrollbar
8//! trough drawn once at one size is drawn correctly at every other.
9//!
10//! Both are expressed the same way: a list of source-rectangle to
11//! destination-rectangle pairs, each of which is one ordinary image draw. That
12//! keeps the answer identical on every backend and, unlike a repeating sampler,
13//! cannot bleed a neighbouring sprite in from a shared atlas — the sprite an
14//! application tiles is usually a region of an atlas, and a wrapped sampler
15//! reads whatever is next to it.
16
17use cranpose_ui_graphics::{Rect, Size};
18
19/// How a region larger than its source is filled.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub enum PatchFill {
22    /// Scale the source to fill the region.
23    #[default]
24    Stretch,
25    /// Repeat the source at its own size, clipping the last row and column.
26    Tile,
27}
28
29impl PatchFill {
30    /// Whether this fill repeats rather than scales.
31    pub fn is_tiled(self) -> bool {
32        matches!(self, PatchFill::Tile)
33    }
34}
35
36/// How far in from each edge of a source image the stretchable middle starts.
37///
38/// The four corners outside these insets are drawn at their own size at every
39/// destination size; everything inside grows.
40#[derive(Clone, Copy, Debug, Default, PartialEq)]
41pub struct NinePatchInsets {
42    pub left: f32,
43    pub top: f32,
44    pub right: f32,
45    pub bottom: f32,
46}
47
48impl NinePatchInsets {
49    /// Insets clamped to non-negative values.
50    pub fn new(left: f32, top: f32, right: f32, bottom: f32) -> Self {
51        Self {
52            left: sanitize(left),
53            top: sanitize(top),
54            right: sanitize(right),
55            bottom: sanitize(bottom),
56        }
57    }
58
59    /// The same inset on all four edges.
60    pub fn uniform(inset: f32) -> Self {
61        Self::new(inset, inset, inset, inset)
62    }
63
64    /// Insets scaled by `factor`, for a source measured in pixels drawn into a
65    /// destination measured in points.
66    pub fn scaled(self, factor: f32) -> Self {
67        if !factor.is_finite() || factor <= 0.0 {
68            return self;
69        }
70        Self::new(
71            self.left * factor,
72            self.top * factor,
73            self.right * factor,
74            self.bottom * factor,
75        )
76    }
77
78    /// Whether a source of this size has room for the corners plus a middle.
79    ///
80    /// Insets that meet or cross leave no stretchable middle, which is not a
81    /// nine-patch at all — the caller wanted a plain scale.
82    pub fn fit(self, source: Size) -> bool {
83        source.width > self.left + self.right && source.height > self.top + self.bottom
84    }
85}
86
87fn sanitize(value: f32) -> f32 {
88    if value.is_finite() && value > 0.0 {
89        value
90    } else {
91        0.0
92    }
93}
94
95/// One image draw: the part of the source to read and the part of the
96/// destination to fill.
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct PatchQuad {
99    pub source: Rect,
100    pub destination: Rect,
101}
102
103/// The draws that tile `source` across `destination` at the source's own size.
104///
105/// The last row and column are clipped, so a destination that is not a whole
106/// number of tiles ends mid-tile rather than being scaled to fit.
107pub fn tile_quads(source: Rect, destination: Rect) -> Vec<PatchQuad> {
108    let mut quads = Vec::new();
109    push_tiles(&mut quads, source, destination);
110    quads
111}
112
113/// How many tiles [`tile_quads`] would produce, without building them.
114///
115/// A caller filling a large area with a small source can check the cost before
116/// paying it: each tile is one draw.
117pub fn tile_count(source: Rect, destination: Rect) -> usize {
118    if !usable(source) || !usable(destination) {
119        return 0;
120    }
121    let columns = (destination.width / source.width).ceil().max(0.0) as usize;
122    let rows = (destination.height / source.height).ceil().max(0.0) as usize;
123    columns.saturating_mul(rows)
124}
125
126/// The draws that fill `destination` with `source` as a nine-patch.
127///
128/// The four corners keep their source size. The four edges grow along one axis
129/// and keep their size across it. The middle grows along both. `center` and
130/// `edges` decide whether the growing parts are stretched or tiled.
131///
132/// Returns a single stretched quad when the insets leave no middle, or when the
133/// destination is too small to hold the corners — the picture stays sensible
134/// instead of drawing corners on top of each other.
135#[inline(never)]
136pub fn nine_patch_quads(
137    source: Rect,
138    destination: Rect,
139    insets: NinePatchInsets,
140    center: PatchFill,
141    edges: PatchFill,
142) -> Vec<PatchQuad> {
143    if !usable(source) || !usable(destination) {
144        return Vec::new();
145    }
146    let source_size = Size::new(source.width, source.height);
147    let corners_fit = destination.width > insets.left + insets.right
148        && destination.height > insets.top + insets.bottom;
149    if !insets.fit(source_size) || !corners_fit {
150        return vec![PatchQuad {
151            source,
152            destination,
153        }];
154    }
155
156    let source_columns = [
157        (source.x, insets.left),
158        (
159            source.x + insets.left,
160            source.width - insets.left - insets.right,
161        ),
162        (source.x + source.width - insets.right, insets.right),
163    ];
164    let source_rows = [
165        (source.y, insets.top),
166        (
167            source.y + insets.top,
168            source.height - insets.top - insets.bottom,
169        ),
170        (source.y + source.height - insets.bottom, insets.bottom),
171    ];
172    let destination_columns = [
173        (destination.x, insets.left),
174        (
175            destination.x + insets.left,
176            destination.width - insets.left - insets.right,
177        ),
178        (
179            destination.x + destination.width - insets.right,
180            insets.right,
181        ),
182    ];
183    let destination_rows = [
184        (destination.y, insets.top),
185        (
186            destination.y + insets.top,
187            destination.height - insets.top - insets.bottom,
188        ),
189        (
190            destination.y + destination.height - insets.bottom,
191            insets.bottom,
192        ),
193    ];
194
195    let mut quads = Vec::new();
196    for row in 0..3 {
197        for column in 0..3 {
198            let (source_x, source_width) = source_columns[column];
199            let (source_y, source_height) = source_rows[row];
200            let (destination_x, destination_width) = destination_columns[column];
201            let (destination_y, destination_height) = destination_rows[row];
202            if source_width <= 0.0
203                || source_height <= 0.0
204                || destination_width <= 0.0
205                || destination_height <= 0.0
206            {
207                continue;
208            }
209            let patch_source = Rect {
210                x: source_x,
211                y: source_y,
212                width: source_width,
213                height: source_height,
214            };
215            let patch_destination = Rect {
216                x: destination_x,
217                y: destination_y,
218                width: destination_width,
219                height: destination_height,
220            };
221            let stretched = column == 1 || row == 1;
222            let fill = match (column, row) {
223                (1, 1) => center,
224                _ if stretched => edges,
225                _ => PatchFill::Stretch,
226            };
227            if fill.is_tiled() {
228                push_tiles(&mut quads, patch_source, patch_destination);
229            } else {
230                quads.push(PatchQuad {
231                    source: patch_source,
232                    destination: patch_destination,
233                });
234            }
235        }
236    }
237    quads
238}
239
240fn usable(rect: Rect) -> bool {
241    rect.x.is_finite()
242        && rect.y.is_finite()
243        && rect.width.is_finite()
244        && rect.height.is_finite()
245        && rect.width > 0.0
246        && rect.height > 0.0
247}
248
249fn push_tiles(quads: &mut Vec<PatchQuad>, source: Rect, destination: Rect) {
250    if !usable(source) || !usable(destination) {
251        return;
252    }
253    let mut y = destination.y;
254    let bottom = destination.y + destination.height;
255    while y < bottom {
256        let height = source.height.min(bottom - y);
257        let mut x = destination.x;
258        let right = destination.x + destination.width;
259        while x < right {
260            let width = source.width.min(right - x);
261            quads.push(PatchQuad {
262                source: Rect {
263                    x: source.x,
264                    y: source.y,
265                    width,
266                    height,
267                },
268                destination: Rect {
269                    x,
270                    y,
271                    width,
272                    height,
273                },
274            });
275            x += source.width;
276        }
277        y += source.height;
278    }
279}
280
281#[cfg(test)]
282#[path = "tests/nine_patch_tests.rs"]
283mod tests;