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