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!(quads
367 .iter()
368 .all(|quad| quad.source.x == 64.0 && quad.source.y == 32.0));
369 }
370
371 #[test]
372 fn nothing_is_drawn_for_a_source_or_destination_with_no_area() {
373 assert!(tile_quads(rect(0.0, 0.0, 0.0, 10.0), rect(0.0, 0.0, 20.0, 20.0)).is_empty());
374 assert!(tile_quads(rect(0.0, 0.0, 10.0, 10.0), rect(0.0, 0.0, 20.0, 0.0)).is_empty());
375 assert_eq!(
376 tile_count(rect(0.0, 0.0, 0.0, 0.0), rect(0.0, 0.0, 8.0, 8.0)),
377 0
378 );
379 assert!(nine_patch_quads(
380 rect(0.0, 0.0, 0.0, 0.0),
381 rect(0.0, 0.0, 20.0, 20.0),
382 NinePatchInsets::uniform(4.0),
383 PatchFill::Stretch,
384 PatchFill::Stretch,
385 )
386 .is_empty());
387 }
388
389 #[test]
390 fn a_stretched_nine_patch_keeps_its_corners_and_grows_the_rest() {
391 let quads = nine_patch_quads(
392 rect(0.0, 0.0, 30.0, 30.0),
393 rect(0.0, 0.0, 100.0, 60.0),
394 NinePatchInsets::uniform(10.0),
395 PatchFill::Stretch,
396 PatchFill::Stretch,
397 );
398 assert_eq!(quads.len(), 9);
399
400 let top_left = quads[0];
401 assert_eq!(top_left.source, rect(0.0, 0.0, 10.0, 10.0));
402 assert_eq!(
403 top_left.destination,
404 rect(0.0, 0.0, 10.0, 10.0),
405 "a corner is drawn at its own size"
406 );
407
408 let bottom_right = quads[8];
409 assert_eq!(bottom_right.source, rect(20.0, 20.0, 10.0, 10.0));
410 assert_eq!(bottom_right.destination, rect(90.0, 50.0, 10.0, 10.0));
411
412 let middle = quads[4];
413 assert_eq!(middle.source, rect(10.0, 10.0, 10.0, 10.0));
414 assert_eq!(middle.destination, rect(10.0, 10.0, 80.0, 40.0));
415 }
416
417 #[test]
418 fn the_patches_cover_the_destination_without_gaps_or_overlap() {
419 let destination = rect(5.0, 7.0, 100.0, 60.0);
420 let quads = nine_patch_quads(
421 rect(0.0, 0.0, 30.0, 30.0),
422 destination,
423 NinePatchInsets::new(10.0, 8.0, 6.0, 4.0),
424 PatchFill::Stretch,
425 PatchFill::Stretch,
426 );
427 let area: f32 = quads
428 .iter()
429 .map(|quad| quad.destination.width * quad.destination.height)
430 .sum();
431 assert!(
432 (area - destination.width * destination.height).abs() < 0.001,
433 "nine patches must tile the destination exactly, covered {area}"
434 );
435 }
436
437 #[test]
438 fn a_tiled_nine_patch_repeats_its_edges_and_middle() {
439 let quads = nine_patch_quads(
440 rect(0.0, 0.0, 30.0, 30.0),
441 rect(0.0, 0.0, 50.0, 30.0),
442 NinePatchInsets::uniform(10.0),
443 PatchFill::Tile,
444 PatchFill::Tile,
445 );
446 let corners = quads
449 .iter()
450 .filter(|quad| quad.destination.width == 10.0 && quad.destination.height == 10.0)
451 .count();
452 assert!(corners >= 4);
453 assert!(
454 quads
455 .iter()
456 .all(|quad| quad.source.width <= 10.0 && quad.source.height <= 10.0),
457 "a tiled patch never reads more than one source tile at a time"
458 );
459 assert!(
460 quads.len() > 9,
461 "tiling produces more draws than the nine stretched patches"
462 );
463 }
464
465 #[test]
466 fn a_destination_too_small_for_the_corners_falls_back_to_a_plain_scale() {
467 let quads = nine_patch_quads(
468 rect(0.0, 0.0, 30.0, 30.0),
469 rect(0.0, 0.0, 12.0, 12.0),
470 NinePatchInsets::uniform(10.0),
471 PatchFill::Stretch,
472 PatchFill::Stretch,
473 );
474 assert_eq!(quads.len(), 1);
475 assert_eq!(quads[0].destination, rect(0.0, 0.0, 12.0, 12.0));
476 assert_eq!(quads[0].source, rect(0.0, 0.0, 30.0, 30.0));
477 }
478
479 #[test]
480 fn insets_with_no_middle_left_fall_back_to_a_plain_scale() {
481 let quads = nine_patch_quads(
482 rect(0.0, 0.0, 20.0, 20.0),
483 rect(0.0, 0.0, 100.0, 100.0),
484 NinePatchInsets::uniform(10.0),
485 PatchFill::Stretch,
486 PatchFill::Stretch,
487 );
488 assert_eq!(quads.len(), 1);
489 }
490}