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)]
281#[path = "tests/nine_patch_tests.rs"]
282mod tests;