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    /// Distance from the left edge of the source to the stretchable middle, in the unit of the
43    /// source.
44    pub left: f32,
45    /// Distance from the top edge of the source to the stretchable middle, in the unit of the
46    /// source.
47    pub top: f32,
48    /// Distance from the right edge of the source to the stretchable middle, in the unit of the
49    /// source.
50    pub right: f32,
51    /// Distance from the bottom edge of the source to the stretchable middle, in the unit of the
52    /// source.
53    pub bottom: f32,
54}
55
56impl NinePatchInsets {
57    /// Insets clamped to non-negative values.
58    pub fn new(left: f32, top: f32, right: f32, bottom: f32) -> Self {
59        Self {
60            left: sanitize(left),
61            top: sanitize(top),
62            right: sanitize(right),
63            bottom: sanitize(bottom),
64        }
65    }
66
67    /// The same inset on all four edges.
68    pub fn uniform(inset: f32) -> Self {
69        Self::new(inset, inset, inset, inset)
70    }
71
72    /// Insets scaled by `factor`, for a source measured in pixels drawn into a
73    /// destination measured in points.
74    pub fn scaled(self, factor: f32) -> Self {
75        if !factor.is_finite() || factor <= 0.0 {
76            return self;
77        }
78        Self::new(
79            self.left * factor,
80            self.top * factor,
81            self.right * factor,
82            self.bottom * factor,
83        )
84    }
85
86    /// Whether a source of this size has room for the corners plus a middle.
87    ///
88    /// Insets that meet or cross leave no stretchable middle, which is not a
89    /// nine-patch at all — the caller wanted a plain scale.
90    pub fn fit(self, source: Size) -> bool {
91        source.width > self.left + self.right && source.height > self.top + self.bottom
92    }
93}
94
95fn sanitize(value: f32) -> f32 {
96    if value.is_finite() && value > 0.0 {
97        value
98    } else {
99        0.0
100    }
101}
102
103/// One image draw: the part of the source to read and the part of the
104/// destination to fill.
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct PatchQuad {
107    /// The part of the source image to read.
108    pub source: Rect,
109    /// The part of the destination to fill with that source part.
110    pub destination: Rect,
111}
112
113/// The draws that tile `source` across `destination` at the source's own size.
114///
115/// The last row and column are clipped, so a destination that is not a whole
116/// number of tiles ends mid-tile rather than being scaled to fit.
117pub fn tile_quads(source: Rect, destination: Rect) -> Vec<PatchQuad> {
118    let mut quads = Vec::new();
119    push_tiles(&mut quads, source, destination);
120    quads
121}
122
123/// How many tiles [`tile_quads`] would produce, without building them.
124///
125/// A caller filling a large area with a small source can check the cost before
126/// paying it: each tile is one draw.
127pub fn tile_count(source: Rect, destination: Rect) -> usize {
128    if !usable(source) || !usable(destination) {
129        return 0;
130    }
131    let columns = (destination.width / source.width).ceil().max(0.0) as usize;
132    let rows = (destination.height / source.height).ceil().max(0.0) as usize;
133    columns.saturating_mul(rows)
134}
135
136/// The draws that fill `destination` with `source` as a nine-patch.
137///
138/// The four corners keep their source size. The four edges grow along one axis
139/// and keep their size across it. The middle grows along both. `center` and
140/// `edges` decide whether the growing parts are stretched or tiled.
141///
142/// Returns a single stretched quad when the insets leave no middle, or when the
143/// destination is too small to hold the corners — the picture stays sensible
144/// instead of drawing corners on top of each other.
145#[inline(never)]
146pub fn nine_patch_quads(
147    source: Rect,
148    destination: Rect,
149    insets: NinePatchInsets,
150    center: PatchFill,
151    edges: PatchFill,
152) -> Vec<PatchQuad> {
153    if !usable(source) || !usable(destination) {
154        return Vec::new();
155    }
156    let source_size = Size::new(source.width, source.height);
157    let corners_fit = destination.width > insets.left + insets.right
158        && destination.height > insets.top + insets.bottom;
159    if !insets.fit(source_size) || !corners_fit {
160        return vec![PatchQuad {
161            source,
162            destination,
163        }];
164    }
165
166    let source_columns = [
167        (source.x, insets.left),
168        (
169            source.x + insets.left,
170            source.width - insets.left - insets.right,
171        ),
172        (source.x + source.width - insets.right, insets.right),
173    ];
174    let source_rows = [
175        (source.y, insets.top),
176        (
177            source.y + insets.top,
178            source.height - insets.top - insets.bottom,
179        ),
180        (source.y + source.height - insets.bottom, insets.bottom),
181    ];
182    let destination_columns = [
183        (destination.x, insets.left),
184        (
185            destination.x + insets.left,
186            destination.width - insets.left - insets.right,
187        ),
188        (
189            destination.x + destination.width - insets.right,
190            insets.right,
191        ),
192    ];
193    let destination_rows = [
194        (destination.y, insets.top),
195        (
196            destination.y + insets.top,
197            destination.height - insets.top - insets.bottom,
198        ),
199        (
200            destination.y + destination.height - insets.bottom,
201            insets.bottom,
202        ),
203    ];
204
205    let mut quads = Vec::new();
206    for row in 0..3 {
207        for column in 0..3 {
208            let (source_x, source_width) = source_columns[column];
209            let (source_y, source_height) = source_rows[row];
210            let (destination_x, destination_width) = destination_columns[column];
211            let (destination_y, destination_height) = destination_rows[row];
212            if source_width <= 0.0
213                || source_height <= 0.0
214                || destination_width <= 0.0
215                || destination_height <= 0.0
216            {
217                continue;
218            }
219            let patch_source = Rect {
220                x: source_x,
221                y: source_y,
222                width: source_width,
223                height: source_height,
224            };
225            let patch_destination = Rect {
226                x: destination_x,
227                y: destination_y,
228                width: destination_width,
229                height: destination_height,
230            };
231            let stretched = column == 1 || row == 1;
232            let fill = match (column, row) {
233                (1, 1) => center,
234                _ if stretched => edges,
235                _ => PatchFill::Stretch,
236            };
237            if fill.is_tiled() {
238                push_tiles(&mut quads, patch_source, patch_destination);
239            } else {
240                quads.push(PatchQuad {
241                    source: patch_source,
242                    destination: patch_destination,
243                });
244            }
245        }
246    }
247    quads
248}
249
250fn usable(rect: Rect) -> bool {
251    rect.x.is_finite()
252        && rect.y.is_finite()
253        && rect.width.is_finite()
254        && rect.height.is_finite()
255        && rect.width > 0.0
256        && rect.height > 0.0
257}
258
259fn push_tiles(quads: &mut Vec<PatchQuad>, source: Rect, destination: Rect) {
260    if !usable(source) || !usable(destination) {
261        return;
262    }
263    let mut y = destination.y;
264    let bottom = destination.y + destination.height;
265    while y < bottom {
266        let height = source.height.min(bottom - y);
267        let mut x = destination.x;
268        let right = destination.x + destination.width;
269        while x < right {
270            let width = source.width.min(right - x);
271            quads.push(PatchQuad {
272                source: Rect {
273                    x: source.x,
274                    y: source.y,
275                    width,
276                    height,
277                },
278                destination: Rect {
279                    x,
280                    y,
281                    width,
282                    height,
283                },
284            });
285            x += source.width;
286        }
287        y += source.height;
288    }
289}
290
291#[cfg(test)]
292#[path = "tests/nine_patch_tests.rs"]
293mod tests;