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,
45 pub top: f32,
48 pub right: f32,
51 pub bottom: f32,
54}
55
56impl NinePatchInsets {
57 pub fn new(left: f32, top: f32, right: f32, bottom: f32) -> Self {
59 Self {
60 left: sanitize(left),
61 top: sanitize(top),
62 right: sanitize(right),
63 bottom: sanitize(bottom),
64 }
65 }
66
67 pub fn uniform(inset: f32) -> Self {
69 Self::new(inset, inset, inset, inset)
70 }
71
72 pub fn scaled(self, factor: f32) -> Self {
75 if !factor.is_finite() || factor <= 0.0 {
76 return self;
77 }
78 Self::new(
79 self.left * factor,
80 self.top * factor,
81 self.right * factor,
82 self.bottom * factor,
83 )
84 }
85
86 pub fn fit(self, source: Size) -> bool {
91 source.width > self.left + self.right && source.height > self.top + self.bottom
92 }
93}
94
95fn sanitize(value: f32) -> f32 {
96 if value.is_finite() && value > 0.0 {
97 value
98 } else {
99 0.0
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct PatchQuad {
107 pub source: Rect,
109 pub destination: Rect,
111}
112
113pub fn tile_quads(source: Rect, destination: Rect) -> Vec<PatchQuad> {
118 let mut quads = Vec::new();
119 push_tiles(&mut quads, source, destination);
120 quads
121}
122
123pub fn tile_count(source: Rect, destination: Rect) -> usize {
128 if !usable(source) || !usable(destination) {
129 return 0;
130 }
131 let columns = (destination.width / source.width).ceil().max(0.0) as usize;
132 let rows = (destination.height / source.height).ceil().max(0.0) as usize;
133 columns.saturating_mul(rows)
134}
135
136#[inline(never)]
146pub fn nine_patch_quads(
147 source: Rect,
148 destination: Rect,
149 insets: NinePatchInsets,
150 center: PatchFill,
151 edges: PatchFill,
152) -> Vec<PatchQuad> {
153 if !usable(source) || !usable(destination) {
154 return Vec::new();
155 }
156 let source_size = Size::new(source.width, source.height);
157 let corners_fit = destination.width > insets.left + insets.right
158 && destination.height > insets.top + insets.bottom;
159 if !insets.fit(source_size) || !corners_fit {
160 return vec![PatchQuad {
161 source,
162 destination,
163 }];
164 }
165
166 let source_columns = [
167 (source.x, insets.left),
168 (
169 source.x + insets.left,
170 source.width - insets.left - insets.right,
171 ),
172 (source.x + source.width - insets.right, insets.right),
173 ];
174 let source_rows = [
175 (source.y, insets.top),
176 (
177 source.y + insets.top,
178 source.height - insets.top - insets.bottom,
179 ),
180 (source.y + source.height - insets.bottom, insets.bottom),
181 ];
182 let destination_columns = [
183 (destination.x, insets.left),
184 (
185 destination.x + insets.left,
186 destination.width - insets.left - insets.right,
187 ),
188 (
189 destination.x + destination.width - insets.right,
190 insets.right,
191 ),
192 ];
193 let destination_rows = [
194 (destination.y, insets.top),
195 (
196 destination.y + insets.top,
197 destination.height - insets.top - insets.bottom,
198 ),
199 (
200 destination.y + destination.height - insets.bottom,
201 insets.bottom,
202 ),
203 ];
204
205 let mut quads = Vec::new();
206 for row in 0..3 {
207 for column in 0..3 {
208 let (source_x, source_width) = source_columns[column];
209 let (source_y, source_height) = source_rows[row];
210 let (destination_x, destination_width) = destination_columns[column];
211 let (destination_y, destination_height) = destination_rows[row];
212 if source_width <= 0.0
213 || source_height <= 0.0
214 || destination_width <= 0.0
215 || destination_height <= 0.0
216 {
217 continue;
218 }
219 let patch_source = Rect {
220 x: source_x,
221 y: source_y,
222 width: source_width,
223 height: source_height,
224 };
225 let patch_destination = Rect {
226 x: destination_x,
227 y: destination_y,
228 width: destination_width,
229 height: destination_height,
230 };
231 let stretched = column == 1 || row == 1;
232 let fill = match (column, row) {
233 (1, 1) => center,
234 _ if stretched => edges,
235 _ => PatchFill::Stretch,
236 };
237 if fill.is_tiled() {
238 push_tiles(&mut quads, patch_source, patch_destination);
239 } else {
240 quads.push(PatchQuad {
241 source: patch_source,
242 destination: patch_destination,
243 });
244 }
245 }
246 }
247 quads
248}
249
250fn usable(rect: Rect) -> bool {
251 rect.x.is_finite()
252 && rect.y.is_finite()
253 && rect.width.is_finite()
254 && rect.height.is_finite()
255 && rect.width > 0.0
256 && rect.height > 0.0
257}
258
259fn push_tiles(quads: &mut Vec<PatchQuad>, source: Rect, destination: Rect) {
260 if !usable(source) || !usable(destination) {
261 return;
262 }
263 let mut y = destination.y;
264 let bottom = destination.y + destination.height;
265 while y < bottom {
266 let height = source.height.min(bottom - y);
267 let mut x = destination.x;
268 let right = destination.x + destination.width;
269 while x < right {
270 let width = source.width.min(right - x);
271 quads.push(PatchQuad {
272 source: Rect {
273 x: source.x,
274 y: source.y,
275 width,
276 height,
277 },
278 destination: Rect {
279 x,
280 y,
281 width,
282 height,
283 },
284 });
285 x += source.width;
286 }
287 y += source.height;
288 }
289}
290
291#[cfg(test)]
292#[path = "tests/nine_patch_tests.rs"]
293mod tests;