1use cranpose_ui_graphics::{Rect, Size};
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub enum PatchFill {
22 #[default]
24 Stretch,
25 Tile,
27}
28
29impl PatchFill {
30 pub fn is_tiled(self) -> bool {
32 matches!(self, PatchFill::Tile)
33 }
34}
35
36#[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 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 pub fn uniform(inset: f32) -> Self {
61 Self::new(inset, inset, inset, inset)
62 }
63
64 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 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#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct PatchQuad {
99 pub source: Rect,
100 pub destination: Rect,
101}
102
103pub 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
113pub 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#[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;