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
126pub fn nine_patch_quads(
136 source: Rect,
137 destination: Rect,
138 insets: NinePatchInsets,
139 center: PatchFill,
140 edges: PatchFill,
141) -> Vec<PatchQuad> {
142 if !usable(source) || !usable(destination) {
143 return Vec::new();
144 }
145 let source_size = Size::new(source.width, source.height);
146 let corners_fit = destination.width > insets.left + insets.right
147 && destination.height > insets.top + insets.bottom;
148 if !insets.fit(source_size) || !corners_fit {
149 return vec![PatchQuad {
150 source,
151 destination,
152 }];
153 }
154
155 let source_columns = [
156 (source.x, insets.left),
157 (
158 source.x + insets.left,
159 source.width - insets.left - insets.right,
160 ),
161 (source.x + source.width - insets.right, insets.right),
162 ];
163 let source_rows = [
164 (source.y, insets.top),
165 (
166 source.y + insets.top,
167 source.height - insets.top - insets.bottom,
168 ),
169 (source.y + source.height - insets.bottom, insets.bottom),
170 ];
171 let destination_columns = [
172 (destination.x, insets.left),
173 (
174 destination.x + insets.left,
175 destination.width - insets.left - insets.right,
176 ),
177 (
178 destination.x + destination.width - insets.right,
179 insets.right,
180 ),
181 ];
182 let destination_rows = [
183 (destination.y, insets.top),
184 (
185 destination.y + insets.top,
186 destination.height - insets.top - insets.bottom,
187 ),
188 (
189 destination.y + destination.height - insets.bottom,
190 insets.bottom,
191 ),
192 ];
193
194 let mut quads = Vec::new();
195 for row in 0..3 {
196 for column in 0..3 {
197 let (source_x, source_width) = source_columns[column];
198 let (source_y, source_height) = source_rows[row];
199 let (destination_x, destination_width) = destination_columns[column];
200 let (destination_y, destination_height) = destination_rows[row];
201 if source_width <= 0.0
202 || source_height <= 0.0
203 || destination_width <= 0.0
204 || destination_height <= 0.0
205 {
206 continue;
207 }
208 let patch_source = Rect {
209 x: source_x,
210 y: source_y,
211 width: source_width,
212 height: source_height,
213 };
214 let patch_destination = Rect {
215 x: destination_x,
216 y: destination_y,
217 width: destination_width,
218 height: destination_height,
219 };
220 let stretched = column == 1 || row == 1;
221 let fill = match (column, row) {
222 (1, 1) => center,
223 _ if stretched => edges,
224 _ => PatchFill::Stretch,
225 };
226 if fill.is_tiled() {
227 push_tiles(&mut quads, patch_source, patch_destination);
228 } else {
229 quads.push(PatchQuad {
230 source: patch_source,
231 destination: patch_destination,
232 });
233 }
234 }
235 }
236 quads
237}
238
239fn usable(rect: Rect) -> bool {
240 rect.x.is_finite()
241 && rect.y.is_finite()
242 && rect.width.is_finite()
243 && rect.height.is_finite()
244 && rect.width > 0.0
245 && rect.height > 0.0
246}
247
248fn push_tiles(quads: &mut Vec<PatchQuad>, source: Rect, destination: Rect) {
249 if !usable(source) || !usable(destination) {
250 return;
251 }
252 let mut y = destination.y;
253 let bottom = destination.y + destination.height;
254 while y < bottom {
255 let height = source.height.min(bottom - y);
256 let mut x = destination.x;
257 let right = destination.x + destination.width;
258 while x < right {
259 let width = source.width.min(right - x);
260 quads.push(PatchQuad {
261 source: Rect {
262 x: source.x,
263 y: source.y,
264 width,
265 height,
266 },
267 destination: Rect {
268 x,
269 y,
270 width,
271 height,
272 },
273 });
274 x += source.width;
275 }
276 y += source.height;
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn only_a_tiled_fill_repeats() {
286 assert!(PatchFill::Tile.is_tiled());
287 assert!(!PatchFill::Stretch.is_tiled());
288 }
289
290 fn rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
291 Rect {
292 x,
293 y,
294 width,
295 height,
296 }
297 }
298
299 #[test]
300 fn insets_cannot_be_negative_or_unmeasurable() {
301 let insets = NinePatchInsets::new(-4.0, f32::NAN, 6.0, f32::INFINITY);
302 assert_eq!(insets.left, 0.0);
303 assert_eq!(insets.top, 0.0);
304 assert_eq!(insets.right, 6.0);
305 assert_eq!(insets.bottom, 0.0);
306 assert_eq!(NinePatchInsets::uniform(3.0).left, 3.0);
307 }
308
309 #[test]
310 fn insets_scale_with_the_source_they_were_measured_on() {
311 let insets = NinePatchInsets::uniform(4.0).scaled(2.0);
312 assert_eq!(insets, NinePatchInsets::uniform(8.0));
313 assert_eq!(
314 NinePatchInsets::uniform(4.0).scaled(0.0),
315 NinePatchInsets::uniform(4.0)
316 );
317 }
318
319 #[test]
320 fn insets_that_leave_no_middle_do_not_fit() {
321 let source = Size::new(20.0, 20.0);
322 assert!(NinePatchInsets::uniform(4.0).fit(source));
323 assert!(!NinePatchInsets::uniform(10.0).fit(source));
324 assert!(!NinePatchInsets::uniform(12.0).fit(source));
325 }
326
327 #[test]
328 fn a_whole_number_of_tiles_covers_the_destination_exactly() {
329 let quads = tile_quads(rect(0.0, 0.0, 10.0, 10.0), rect(0.0, 0.0, 20.0, 20.0));
330 assert_eq!(quads.len(), 4);
331 assert_eq!(
332 tile_count(rect(0.0, 0.0, 10.0, 10.0), rect(0.0, 0.0, 20.0, 20.0)),
333 4
334 );
335 assert_eq!(quads[0].destination, rect(0.0, 0.0, 10.0, 10.0));
336 assert_eq!(quads[3].destination, rect(10.0, 10.0, 10.0, 10.0));
337 assert!(quads.iter().all(|quad| quad.source.width == 10.0));
338 }
339
340 #[test]
341 fn a_partial_tile_is_clipped_rather_than_squeezed() {
342 let quads = tile_quads(rect(0.0, 0.0, 10.0, 10.0), rect(0.0, 0.0, 25.0, 10.0));
343 assert_eq!(quads.len(), 3);
344 let last = quads[2];
345 assert_eq!(last.destination, rect(20.0, 0.0, 5.0, 10.0));
346 assert_eq!(
347 last.source,
348 rect(0.0, 0.0, 5.0, 10.0),
349 "the clipped tile shows the leading part of the source at 1:1"
350 );
351 }
352
353 #[test]
354 fn tiling_reads_from_the_region_it_was_given_not_the_whole_atlas() {
355 let quads = tile_quads(rect(64.0, 32.0, 8.0, 8.0), rect(0.0, 0.0, 16.0, 8.0));
356 assert_eq!(quads.len(), 2);
357 assert!(
358 quads
359 .iter()
360 .all(|quad| quad.source.x == 64.0 && quad.source.y == 32.0)
361 );
362 }
363
364 #[test]
365 fn nothing_is_drawn_for_a_source_or_destination_with_no_area() {
366 assert!(tile_quads(rect(0.0, 0.0, 0.0, 10.0), rect(0.0, 0.0, 20.0, 20.0)).is_empty());
367 assert!(tile_quads(rect(0.0, 0.0, 10.0, 10.0), rect(0.0, 0.0, 20.0, 0.0)).is_empty());
368 assert_eq!(
369 tile_count(rect(0.0, 0.0, 0.0, 0.0), rect(0.0, 0.0, 8.0, 8.0)),
370 0
371 );
372 assert!(
373 nine_patch_quads(
374 rect(0.0, 0.0, 0.0, 0.0),
375 rect(0.0, 0.0, 20.0, 20.0),
376 NinePatchInsets::uniform(4.0),
377 PatchFill::Stretch,
378 PatchFill::Stretch,
379 )
380 .is_empty()
381 );
382 }
383
384 #[test]
385 fn a_stretched_nine_patch_keeps_its_corners_and_grows_the_rest() {
386 let quads = nine_patch_quads(
387 rect(0.0, 0.0, 30.0, 30.0),
388 rect(0.0, 0.0, 100.0, 60.0),
389 NinePatchInsets::uniform(10.0),
390 PatchFill::Stretch,
391 PatchFill::Stretch,
392 );
393 assert_eq!(quads.len(), 9);
394
395 let top_left = quads[0];
396 assert_eq!(top_left.source, rect(0.0, 0.0, 10.0, 10.0));
397 assert_eq!(
398 top_left.destination,
399 rect(0.0, 0.0, 10.0, 10.0),
400 "a corner is drawn at its own size"
401 );
402
403 let bottom_right = quads[8];
404 assert_eq!(bottom_right.source, rect(20.0, 20.0, 10.0, 10.0));
405 assert_eq!(bottom_right.destination, rect(90.0, 50.0, 10.0, 10.0));
406
407 let middle = quads[4];
408 assert_eq!(middle.source, rect(10.0, 10.0, 10.0, 10.0));
409 assert_eq!(middle.destination, rect(10.0, 10.0, 80.0, 40.0));
410 }
411
412 #[test]
413 fn the_patches_cover_the_destination_without_gaps_or_overlap() {
414 let destination = rect(5.0, 7.0, 100.0, 60.0);
415 let quads = nine_patch_quads(
416 rect(0.0, 0.0, 30.0, 30.0),
417 destination,
418 NinePatchInsets::new(10.0, 8.0, 6.0, 4.0),
419 PatchFill::Stretch,
420 PatchFill::Stretch,
421 );
422 let area: f32 = quads
423 .iter()
424 .map(|quad| quad.destination.width * quad.destination.height)
425 .sum();
426 assert!(
427 (area - destination.width * destination.height).abs() < 0.001,
428 "nine patches must tile the destination exactly, covered {area}"
429 );
430 }
431
432 #[test]
433 fn a_tiled_nine_patch_repeats_its_edges_and_middle() {
434 let quads = nine_patch_quads(
435 rect(0.0, 0.0, 30.0, 30.0),
436 rect(0.0, 0.0, 50.0, 30.0),
437 NinePatchInsets::uniform(10.0),
438 PatchFill::Tile,
439 PatchFill::Tile,
440 );
441 let corners = quads
442 .iter()
443 .filter(|quad| quad.destination.width == 10.0 && quad.destination.height == 10.0)
444 .count();
445 assert!(corners >= 4);
446 assert!(
447 quads
448 .iter()
449 .all(|quad| quad.source.width <= 10.0 && quad.source.height <= 10.0),
450 "a tiled patch never reads more than one source tile at a time"
451 );
452 assert!(
453 quads.len() > 9,
454 "tiling produces more draws than the nine stretched patches"
455 );
456 }
457
458 #[test]
459 fn a_destination_too_small_for_the_corners_falls_back_to_a_plain_scale() {
460 let quads = nine_patch_quads(
461 rect(0.0, 0.0, 30.0, 30.0),
462 rect(0.0, 0.0, 12.0, 12.0),
463 NinePatchInsets::uniform(10.0),
464 PatchFill::Stretch,
465 PatchFill::Stretch,
466 );
467 assert_eq!(quads.len(), 1);
468 assert_eq!(quads[0].destination, rect(0.0, 0.0, 12.0, 12.0));
469 assert_eq!(quads[0].source, rect(0.0, 0.0, 30.0, 30.0));
470 }
471
472 #[test]
473 fn insets_with_no_middle_left_fall_back_to_a_plain_scale() {
474 let quads = nine_patch_quads(
475 rect(0.0, 0.0, 20.0, 20.0),
476 rect(0.0, 0.0, 100.0, 100.0),
477 NinePatchInsets::uniform(10.0),
478 PatchFill::Stretch,
479 PatchFill::Stretch,
480 );
481 assert_eq!(quads.len(), 1);
482 }
483}