1use crate::math::sin_cos;
10use alloc::vec::Vec;
11
12use crate::components::{Sprite, SpriteFit};
13use crate::gfx::overlay::{OverlayTransform, UI_REFERENCE_SIZE};
14use crate::gfx::render_types::{TextDrawCall, TextVertex};
15use crate::render::overlay_maps::{ClipRects, OverlayLayers, TextureSlots};
16
17pub fn covers_canvas(s: &Sprite) -> bool {
21 let [ref_w, ref_h] = UI_REFERENCE_SIZE;
22 s.screen.is_some()
23 && s.x <= 0.0
24 && s.y <= 0.0
25 && s.x + s.width >= ref_w
26 && s.y + s.height >= ref_h
27}
28
29#[cfg(test)]
40pub(crate) fn build_sprite_calls(
41 sprites: &[Sprite],
42 default_atlas_slot: Option<usize>,
43 texture_slots: &TextureSlots,
44 viewport: [f32; 2],
45 clips: &ClipRects,
46 layers: &OverlayLayers,
47) -> Vec<TextDrawCall> {
48 let mut out = crate::render::call_buffer::TextCallBuffer::default();
49 build_sprite_calls_into(
50 &mut out,
51 sprites,
52 default_atlas_slot,
53 texture_slots,
54 viewport,
55 clips,
56 layers,
57 );
58 out.take()
59}
60
61pub fn build_sprite_calls_into(
67 out: &mut crate::render::call_buffer::TextCallBuffer,
68 sprites: &[Sprite],
69 default_atlas_slot: Option<usize>,
70 texture_slots: &TextureSlots,
71 viewport: [f32; 2],
72 clips: &ClipRects,
73 layers: &OverlayLayers,
74) {
75 let fill_slot = match default_atlas_slot {
76 Some(s) => s,
77 None => return,
78 };
79 let overlay = OverlayTransform::from_viewport(viewport);
80 let cover = OverlayTransform::cover_from_viewport(viewport);
81 let bottom = OverlayTransform::bottom_anchored_from_viewport(viewport);
82 let [vw, vh] = viewport;
83 for s in sprites {
84 if !s.visible || s.follow_cursor {
85 continue;
86 }
87 let [r, g, b, a] = s.tint;
88 let border_only = s.border_width > 0.0 && s.border_color[3] > 0.0;
91 if a <= 0.0 && !border_only {
92 continue;
93 }
94 let (x0, y0, x1, y1) = if s.screen.is_some() {
95 if s.fit == SpriteFit::Cover {
96 let (ax, ay) = cover.forward(s.x, s.y);
100 let (bx, by) = cover.forward(s.x + s.width, s.y + s.height);
101 (ax, ay, bx, by)
102 } else if s.fit == SpriteFit::Bottom {
103 let (ax, ay) = bottom.forward(s.x, s.y);
106 let (bx, by) = bottom.forward(s.x + s.width, s.y + s.height);
107 (ax, ay, bx, by)
108 } else if covers_canvas(s) && vw > 0.0 && vh > 0.0 {
109 (0.0, 0.0, vw, vh)
113 } else {
114 let (ax, ay) = overlay.forward(s.x, s.y);
115 let (bx, by) = overlay.forward(s.x + s.width, s.y + s.height);
116 (ax, ay, bx, by)
117 }
118 } else {
119 (s.x, s.y, s.x + s.width, s.y + s.height)
120 };
121 let texture_slot = s.texture.and_then(|t| texture_slots.get(&t).copied());
122 let (w, h) = ((x1 - x0).max(f32::EPSILON), (y1 - y0).max(f32::EPSILON));
126 let v = |x: f32, y: f32, alpha: f32| match texture_slot {
127 Some(_) => TextVertex {
129 pos: [x, y],
130 uv: [(x - x0) / w, (y - y0) / h],
131 color: [r, g, b],
132 mode: alpha,
133 },
134 None => TextVertex {
136 pos: [x, y],
137 uv: [-1.0, alpha],
138 color: [r, g, b],
139 mode: 0.0,
140 },
141 };
142 let scale = if s.width > 0.0 && s.height > 0.0 {
146 ((x1 - x0) / s.width).min((y1 - y0) / s.height)
147 } else {
148 1.0
149 };
150 let radius = (s.corner_radius * scale).min(w / 2.0).min(h / 2.0);
151 let border = (s.border_width * scale).min(w / 2.0).min(h / 2.0);
152 let (mut vertices, mut indices) = out.geometry();
153 if border > 0.5 && s.border_color[3] > 0.0 {
154 let [br, bg, bb, ba] = s.border_color;
155 let border_v = |x: f32, y: f32, alpha: f32| TextVertex {
156 pos: [x, y],
157 uv: [-1.0, alpha],
158 color: [br, bg, bb],
159 mode: 0.0,
160 };
161 if a <= 0.0 {
162 stroke_geometry(
166 &mut vertices,
167 &mut indices,
168 [x0, y0, x1, y1],
169 radius,
170 border,
171 ba,
172 border_v,
173 );
174 } else if a < 1.0 {
175 rect_geometry(&mut vertices, &mut indices, [x0, y0, x1, y1], radius, a, v);
180 stroke_geometry(
181 &mut vertices,
182 &mut indices,
183 [x0, y0, x1, y1],
184 radius,
185 border,
186 ba,
187 border_v,
188 );
189 } else {
190 rect_geometry(
194 &mut vertices,
195 &mut indices,
196 [x0, y0, x1, y1],
197 radius,
198 ba,
199 border_v,
200 );
201 rect_geometry(
202 &mut vertices,
203 &mut indices,
204 [x0 + border, y0 + border, x1 - border, y1 - border],
205 (radius - border).max(0.0),
206 a,
207 v,
208 );
209 }
210 } else {
211 rect_geometry(&mut vertices, &mut indices, [x0, y0, x1, y1], radius, a, v);
212 }
213 out.calls.push(TextDrawCall {
214 vertices,
215 indices,
216 atlas_slot: texture_slot.unwrap_or(fill_slot),
217 clip_rect: clips
218 .get(&s.asset_id)
219 .map(|b| crate::render::text::band_to_window(&overlay, *b)),
220 layer: layers.get(&s.asset_id).copied().unwrap_or(0),
221 });
222 }
223}
224
225fn rect_geometry(
230 vertices: &mut Vec<TextVertex>,
231 indices: &mut Vec<u16>,
232 rect: [f32; 4],
233 radius: f32,
234 alpha: f32,
235 mut v: impl FnMut(f32, f32, f32) -> TextVertex,
236) {
237 let [x0, y0, x1, y1] = rect;
238 if radius > 0.5 {
239 rounded_rect_geometry(vertices, indices, rect, radius, alpha, v);
240 } else {
241 let base = vertices.len() as u16;
242 vertices.extend_from_slice(&[
243 v(x0, y0, alpha),
244 v(x1, y0, alpha),
245 v(x1, y1, alpha),
246 v(x0, y1, alpha),
247 ]);
248 indices.extend([0, 1, 2, 0, 2, 3].map(|i| base + i));
249 }
250}
251
252fn stroke_geometry(
256 vertices: &mut Vec<TextVertex>,
257 indices: &mut Vec<u16>,
258 rect: [f32; 4],
259 radius: f32,
260 width: f32,
261 alpha: f32,
262 v: impl FnMut(f32, f32, f32) -> TextVertex,
263) {
264 match radius > 0.5 {
265 true => rounded_ring_geometry(vertices, indices, rect, radius, width, alpha, v),
266 false => ring_geometry(vertices, indices, rect, width, alpha, v),
267 }
268}
269
270fn ring_geometry(
274 vertices: &mut Vec<TextVertex>,
275 indices: &mut Vec<u16>,
276 rect: [f32; 4],
277 width: f32,
278 alpha: f32,
279 mut v: impl FnMut(f32, f32, f32) -> TextVertex,
280) {
281 let [x0, y0, x1, y1] = rect;
282 let strips = [
283 [x0, y0, x1, y0 + width],
284 [x0, y1 - width, x1, y1],
285 [x0, y0 + width, x0 + width, y1 - width],
286 [x1 - width, y0 + width, x1, y1 - width],
287 ];
288 for strip in strips {
289 rect_geometry(vertices, indices, strip, 0.0, alpha, &mut v);
290 }
291}
292
293const CORNER_SEGMENTS: usize = 6;
296const EDGE_FEATHER: f32 = 1.25;
300
301fn rounded_boundary(
308 rect: [f32; 4],
309 radius: f32,
310) -> [(f32, f32, f32, f32); 4 * (CORNER_SEGMENTS + 1)] {
311 use core::f32::consts::FRAC_PI_2;
312 let [x0, y0, x1, y1] = rect;
313 let mut arc = [(0.0_f32, 0.0_f32); CORNER_SEGMENTS + 1];
316 for (i, slot) in arc.iter_mut().enumerate() {
317 let (s, c) = sin_cos((i as f32 / CORNER_SEGMENTS as f32) * FRAC_PI_2);
318 *slot = (c, s);
319 }
320 let corners = [
323 (x0 + radius, y0 + radius, 2),
324 (x1 - radius, y0 + radius, 3),
325 (x1 - radius, y1 - radius, 0),
326 (x0 + radius, y1 - radius, 1),
327 ];
328 let mut boundary = [(0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32); 4 * (CORNER_SEGMENTS + 1)];
329 for (c, &(cx, cy, quadrant)) in corners.iter().enumerate() {
330 for (i, &(uc, us)) in arc.iter().enumerate() {
331 let (rc, rs) = match quadrant {
332 0 => (uc, us),
333 1 => (-us, uc),
334 2 => (-uc, -us),
335 _ => (us, -uc),
336 };
337 boundary[c * (CORNER_SEGMENTS + 1) + i] = (cx, cy, rc, rs);
338 }
339 }
340 boundary
341}
342
343fn rounded_ring_geometry(
349 vertices: &mut Vec<TextVertex>,
350 indices: &mut Vec<u16>,
351 rect: [f32; 4],
352 radius: f32,
353 width: f32,
354 alpha: f32,
355 mut v: impl FnMut(f32, f32, f32) -> TextVertex,
356) {
357 let [x0, y0, x1, y1] = rect;
358 let inner_r = (radius - width).max(0.0);
359 let solid_r = (radius - EDGE_FEATHER).max(inner_r);
360 let outer = rounded_boundary(rect, radius);
361 let inner = rounded_boundary([x0 + width, y0 + width, x1 - width, y1 - width], inner_r);
362 let m = outer.len();
363 let base = vertices.len() as u16;
364 vertices.reserve(3 * m);
365 for &(cx, cy, cos, sin) in &outer {
366 vertices.push(v(cx + radius * cos, cy + radius * sin, 0.0));
367 }
368 for &(cx, cy, cos, sin) in &outer {
369 vertices.push(v(cx + solid_r * cos, cy + solid_r * sin, alpha));
370 }
371 for &(cx, cy, cos, sin) in &inner {
372 vertices.push(v(cx + inner_r * cos, cy + inner_r * sin, alpha));
373 }
374 indices.reserve(12 * m);
375 for loop_start in [0, m] {
376 for i in 0..m {
377 let j = (i + 1) % m;
378 let (i, j, m) = ((loop_start + i) as u16, (loop_start + j) as u16, m as u16);
379 indices.extend([i, j, m + j, i, m + j, m + i].map(|k| base + k));
380 }
381 }
382}
383
384fn rounded_rect_geometry(
389 vertices: &mut Vec<TextVertex>,
390 indices: &mut Vec<u16>,
391 rect: [f32; 4],
392 radius: f32,
393 alpha: f32,
394 mut v: impl FnMut(f32, f32, f32) -> TextVertex,
395) {
396 let boundary = rounded_boundary(rect, radius);
397 let m = boundary.len();
398 let base = vertices.len() as u16;
399 let inner_r = (radius - EDGE_FEATHER).max(0.0);
400 vertices.reserve(2 * m);
401 for &(cx, cy, cos, sin) in &boundary {
402 vertices.push(v(cx + inner_r * cos, cy + inner_r * sin, alpha));
403 }
404 for &(cx, cy, cos, sin) in &boundary {
405 vertices.push(v(cx + radius * cos, cy + radius * sin, 0.0));
406 }
407 indices.reserve(3 * (m - 2) + 6 * m);
408 for i in 1..m - 1 {
409 indices.extend([base, base + i as u16, base + (i + 1) as u16]);
410 }
411 for i in 0..m {
412 let j = (i + 1) % m;
413 let (i, j, m) = (i as u16, j as u16, m as u16);
414 indices.extend([i, j, m + j, i, m + j, m + i].map(|k| base + k));
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::ecs::TextureHandle;
422 use crate::ecs::asset_id::AssetId;
423
424 use alloc::vec;
425 fn no_clips() -> ClipRects {
426 ClipRects::new()
427 }
428 fn no_layers() -> OverlayLayers {
429 OverlayLayers::new()
430 }
431
432 fn no_slots() -> TextureSlots {
433 TextureSlots::new()
434 }
435
436 fn sprite(x: f32, y: f32, w: f32, h: f32, tint: [f32; 4]) -> Sprite {
437 Sprite {
438 asset_id: AssetId::default(),
439 x,
440 y,
441 width: w,
442 height: h,
443 texture: None,
444 tint,
445 follow_cursor: false,
446 visible: true,
447 screen: None,
448 fit: SpriteFit::Fit,
449 corner_radius: 0.0,
450 border_width: 0.0,
451 border_color: [0.0, 0.0, 0.0, 1.0],
452 }
453 }
454
455 #[test]
456 fn no_fonts_means_no_calls() {
457 let s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 0.0, 0.0, 1.0]);
458 assert!(
459 build_sprite_calls(
460 core::slice::from_ref(&s),
461 None,
462 &no_slots(),
463 [0.0, 0.0],
464 &no_clips(),
465 &no_layers()
466 )
467 .is_empty()
468 );
469 }
470
471 #[test]
472 fn visible_sprite_emits_quad_with_sentinel_uv() {
473 let s = sprite(10.0, 20.0, 100.0, 50.0, [0.5, 0.5, 0.5, 0.75]);
474 let calls = build_sprite_calls(
475 core::slice::from_ref(&s),
476 Some(0),
477 &no_slots(),
478 [0.0, 0.0],
479 &no_clips(),
480 &no_layers(),
481 );
482 assert_eq!(calls.len(), 1);
483 assert_eq!(calls[0].vertices.len(), 4);
484 assert_eq!(calls[0].indices, vec![0, 1, 2, 0, 2, 3]);
485 for v in &calls[0].vertices {
486 assert!(v.uv[0] < 0.0, "sentinel u should be negative");
487 assert!((v.uv[1] - 0.75).abs() < 1e-5, "alpha carried in v");
488 assert_eq!(v.color, [0.5, 0.5, 0.5]);
489 }
490 assert_eq!(calls[0].vertices[0].pos, [10.0, 20.0]);
491 assert_eq!(calls[0].vertices[2].pos, [110.0, 70.0]);
492 }
493
494 #[test]
497 fn transparent_fill_draws_when_a_border_is_set() {
498 let invisible = sprite(0.0, 0.0, 100.0, 50.0, [0.0, 0.0, 0.0, 0.0]);
499 assert!(
500 build_sprite_calls(
501 core::slice::from_ref(&invisible),
502 Some(0),
503 &no_slots(),
504 [0.0, 0.0],
505 &no_clips(),
506 &no_layers()
507 )
508 .is_empty(),
509 "borderless transparent fill still skips"
510 );
511
512 let mut outline = sprite(0.0, 0.0, 100.0, 50.0, [0.0, 0.0, 0.0, 0.0]);
513 outline.border_width = 2.0;
514 outline.border_color = [0.2, 0.4, 0.9, 1.0];
515 let calls = build_sprite_calls(
516 core::slice::from_ref(&outline),
517 Some(0),
518 &no_slots(),
519 [0.0, 0.0],
520 &no_clips(),
521 &no_layers(),
522 );
523 assert_eq!(calls.len(), 1, "the border ring draws");
524 let verts = &calls[0].vertices;
527 assert_eq!(verts.len(), 16);
528 assert!(
529 verts
530 .iter()
531 .all(|v| v.color == [0.2, 0.4, 0.9] && v.uv == [-1.0, 1.0])
532 );
533 let inside = |x: f32, y: f32| {
534 calls[0].indices.chunks(3).any(|t| {
535 let p: Vec<[f32; 2]> = t.iter().map(|&i| verts[i as usize].pos).collect();
536 point_in_triangle([x, y], p[0], p[1], p[2])
537 })
538 };
539 assert!(inside(50.0, 1.0), "the top strip covers the edge");
540 assert!(inside(1.0, 25.0), "the left strip covers the edge");
541 assert!(!inside(50.0, 25.0), "the interior is empty");
542
543 let mut panel = sprite(0.0, 0.0, 100.0, 50.0, [0.1, 0.1, 0.1, 1.0]);
546 panel.border_width = 2.0;
547 panel.border_color = [0.2, 0.4, 0.9, 1.0];
548 let calls = build_sprite_calls(
549 core::slice::from_ref(&panel),
550 Some(0),
551 &no_slots(),
552 [0.0, 0.0],
553 &no_clips(),
554 &no_layers(),
555 );
556 assert_eq!(calls[0].vertices.len(), 8);
557 }
558
559 #[test]
563 fn a_translucent_fill_is_not_backed_by_its_border() {
564 let mut panel = sprite(0.0, 0.0, 100.0, 50.0, [0.1, 0.1, 0.12, 0.5]);
565 panel.border_width = 2.0;
566 panel.border_color = [0.3, 0.32, 0.4, 1.0];
567 panel.corner_radius = 8.0;
568 let calls = build_sprite_calls(
569 core::slice::from_ref(&panel),
570 Some(0),
571 &no_slots(),
572 [0.0, 0.0],
573 &no_clips(),
574 &no_layers(),
575 );
576 assert_eq!(calls.len(), 1);
577 let verts = &calls[0].vertices;
578 let covered = |x: f32, y: f32, color: [f32; 3]| {
580 calls[0].indices.chunks(3).any(|t| {
581 let p: Vec<[f32; 2]> = t.iter().map(|&i| verts[i as usize].pos).collect();
582 t.iter().all(|&i| verts[i as usize].color == color)
583 && point_in_triangle([x, y], p[0], p[1], p[2])
584 })
585 };
586 let border = [0.3, 0.32, 0.4];
587 let fill = [0.1, 0.1, 0.12];
588 assert!(covered(50.0, 1.0, border), "the stroke covers the edge");
589 assert!(
590 !covered(50.0, 25.0, border),
591 "and leaves the middle to the wash"
592 );
593 assert!(covered(50.0, 25.0, fill), "which draws there at its alpha");
594 assert!(
595 verts
596 .iter()
597 .filter(|v| v.color == fill)
598 .any(|v| (v.uv[1] - 0.5).abs() < 1e-5),
599 "the wash keeps its own alpha"
600 );
601
602 let border_alphas: Vec<f32> = verts
604 .iter()
605 .filter(|v| v.color == border)
606 .map(|v| v.uv[1])
607 .collect();
608 assert!(border_alphas.contains(&0.0));
609 assert!(border_alphas.iter().any(|&a| (a - 1.0).abs() < 1e-5));
610
611 panel.corner_radius = 2.0;
613 panel.border_width = 6.0;
614 let calls = build_sprite_calls(
615 core::slice::from_ref(&panel),
616 Some(0),
617 &no_slots(),
618 [0.0, 0.0],
619 &no_clips(),
620 &no_layers(),
621 );
622 let verts = &calls[0].vertices;
623 let covered = |x: f32, y: f32, color: [f32; 3]| {
624 calls[0].indices.chunks(3).any(|t| {
625 let p: Vec<[f32; 2]> = t.iter().map(|&i| verts[i as usize].pos).collect();
626 t.iter().all(|&i| verts[i as usize].color == color)
627 && point_in_triangle([x, y], p[0], p[1], p[2])
628 })
629 };
630 assert!(covered(50.0, 5.9, border));
631 assert!(!covered(50.0, 6.1, border));
632
633 panel.border_width = 2.0;
635 panel.corner_radius = 0.0;
636 let calls = build_sprite_calls(
637 core::slice::from_ref(&panel),
638 Some(0),
639 &no_slots(),
640 [0.0, 0.0],
641 &no_clips(),
642 &no_layers(),
643 );
644 let verts = &calls[0].vertices;
645 assert_eq!(verts.len(), 4 + 16, "one quad of fill, four edge strips");
646 }
647
648 fn point_in_triangle(p: [f32; 2], a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
649 let sign = |p: [f32; 2], q: [f32; 2], r: [f32; 2]| {
650 (p[0] - r[0]) * (q[1] - r[1]) - (q[0] - r[0]) * (p[1] - r[1])
651 };
652 let (d1, d2, d3) = (sign(p, a, b), sign(p, b, c), sign(p, c, a));
653 let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
654 let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
655 !(neg && pos)
656 }
657
658 #[test]
659 fn textured_sprite_emits_real_uvs_and_its_slot() {
660 let mut s = sprite(10.0, 20.0, 100.0, 50.0, [1.0, 0.9, 0.8, 0.75]);
661 s.texture = Some(TextureHandle(42));
662 let mut slots = no_slots();
663 slots.insert(TextureHandle(42), 3);
664 let calls = build_sprite_calls(
665 core::slice::from_ref(&s),
666 Some(0),
667 &slots,
668 [0.0, 0.0],
669 &no_clips(),
670 &no_layers(),
671 );
672 assert_eq!(calls.len(), 1);
673 assert_eq!(calls[0].atlas_slot, 3);
675 let vs = &calls[0].vertices;
676 assert_eq!(vs[0].uv, [0.0, 0.0]);
677 assert_eq!(vs[1].uv, [1.0, 0.0]);
678 assert_eq!(vs[2].uv, [1.0, 1.0]);
679 assert_eq!(vs[3].uv, [0.0, 1.0]);
680 for v in vs {
681 assert_eq!(v.color, [1.0, 0.9, 0.8]);
683 assert!((v.mode - 0.75).abs() < 1e-5);
684 }
685 }
686
687 #[test]
688 fn rounded_sprite_tessellates_with_a_feathered_edge() {
689 let mut s = sprite(100.0, 100.0, 400.0, 200.0, [0.1, 0.2, 0.3, 0.9]);
690 s.corner_radius = 20.0;
691 let calls = build_sprite_calls(
692 core::slice::from_ref(&s),
693 Some(0),
694 &no_slots(),
695 [0.0, 0.0],
696 &no_clips(),
697 &no_layers(),
698 );
699 assert_eq!(calls.len(), 1);
700 let vs = &calls[0].vertices;
701 let ring = 4 * (CORNER_SEGMENTS + 1);
704 assert_eq!(vs.len(), 2 * ring);
705 for v in &vs[..ring] {
706 assert!(
707 (v.uv[1] - 0.9).abs() < 1e-5,
708 "inner ring carries the tint alpha"
709 );
710 }
711 for v in &vs[ring..] {
712 assert!(v.uv[1].abs() < 1e-5, "outer ring fades to transparent");
713 }
714 for v in vs {
717 assert!(v.pos[0] >= 100.0 - 1e-3 && v.pos[0] <= 500.0 + 1e-3);
718 assert!(v.pos[1] >= 100.0 - 1e-3 && v.pos[1] <= 300.0 + 1e-3);
719 }
720 let min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
721 assert!((min_x - 100.0).abs() < 1e-3);
722 assert!(!vs.iter().any(|v| v.pos == [100.0, 100.0]));
724 }
725
726 #[test]
727 fn bordered_sprite_emits_a_border_ring_and_an_inset_fill() {
728 let mut s = sprite(100.0, 100.0, 200.0, 120.0, [0.1, 0.2, 0.3, 1.0]);
729 s.border_width = 2.0;
730 s.border_color = [0.8, 0.4, 0.2, 1.0];
731 let calls = build_sprite_calls(
732 core::slice::from_ref(&s),
733 Some(0),
734 &no_slots(),
735 [0.0, 0.0],
736 &no_clips(),
737 &no_layers(),
738 );
739 assert_eq!(calls.len(), 1);
740 let vs = &calls[0].vertices;
741 assert!(
743 vs.iter().any(|v| v.color == [0.8, 0.4, 0.2]),
744 "border colour present"
745 );
746 assert!(
747 vs.iter().any(|v| v.color == [0.1, 0.2, 0.3]),
748 "fill colour present"
749 );
750 let outer_min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
753 let outer_max_x = vs.iter().map(|v| v.pos[0]).fold(f32::MIN, f32::max);
754 assert!(
755 (outer_min_x - 100.0).abs() < 1e-3,
756 "border at the left edge"
757 );
758 assert!(
759 (outer_max_x - 300.0).abs() < 1e-3,
760 "border at the right edge"
761 );
762 let fill_min_x = vs
763 .iter()
764 .filter(|v| v.color == [0.1, 0.2, 0.3])
765 .map(|v| v.pos[0])
766 .fold(f32::MAX, f32::min);
767 assert!(
768 (fill_min_x - 102.0).abs() < 1e-3,
769 "fill inset by the stroke width"
770 );
771 }
772
773 #[test]
774 fn zero_border_stays_a_single_layer() {
775 let mut s = sprite(0.0, 0.0, 100.0, 100.0, [0.2, 0.3, 0.4, 1.0]);
776 s.border_width = 0.0;
778 s.border_color = [1.0, 0.0, 0.0, 1.0];
779 let calls = build_sprite_calls(
780 core::slice::from_ref(&s),
781 Some(0),
782 &no_slots(),
783 [0.0, 0.0],
784 &no_clips(),
785 &no_layers(),
786 );
787 assert_eq!(
788 calls[0].vertices.len(),
789 4,
790 "one plain quad, no border layer"
791 );
792 assert!(calls[0].vertices.iter().all(|v| v.color == [0.2, 0.3, 0.4]));
793 }
794
795 #[test]
796 fn rounded_view_sprite_scales_its_radius_with_the_window() {
797 let mut s = sprite(100.0, 100.0, 400.0, 200.0, [0.1, 0.2, 0.3, 0.9]);
801 s.screen = Some(AssetId(7));
802 s.corner_radius = 20.0;
803 let calls = build_sprite_calls(
804 core::slice::from_ref(&s),
805 Some(0),
806 &no_slots(),
807 [2.0 * UI_REFERENCE_SIZE[0], 2.0 * UI_REFERENCE_SIZE[1]],
808 &no_clips(),
809 &no_layers(),
810 );
811 let vs = &calls[0].vertices;
812 let min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
813 let top_left_arc_y = vs
814 .iter()
815 .filter(|v| (v.pos[0] - min_x).abs() < 1e-3)
816 .map(|v| v.pos[1])
817 .fold(f32::MAX, f32::min);
818 assert!((min_x - 200.0).abs() < 1e-3);
819 assert!((top_left_arc_y - (200.0 + 40.0)).abs() < 1e-3);
820 }
821
822 #[test]
823 fn textured_sprite_without_a_loaded_texture_falls_back_to_fill() {
824 let mut s = sprite(0.0, 0.0, 10.0, 10.0, [0.2, 0.3, 0.4, 1.0]);
825 s.texture = Some(TextureHandle(42));
826 let calls = build_sprite_calls(
828 core::slice::from_ref(&s),
829 Some(5),
830 &no_slots(),
831 [0.0, 0.0],
832 &no_clips(),
833 &no_layers(),
834 );
835 assert_eq!(calls[0].atlas_slot, 5);
836 assert!(calls[0].vertices[0].uv[0] < 0.0);
837 assert_eq!(calls[0].vertices[0].mode, 0.0);
838 }
839
840 #[test]
841 fn invisible_sprite_is_skipped() {
842 let mut s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 1.0, 1.0, 1.0]);
843 s.visible = false;
844 assert!(
845 build_sprite_calls(
846 core::slice::from_ref(&s),
847 Some(0),
848 &no_slots(),
849 [0.0, 0.0],
850 &no_clips(),
851 &no_layers()
852 )
853 .is_empty()
854 );
855 }
856
857 #[test]
858 fn zero_alpha_sprite_is_skipped() {
859 let s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 1.0, 1.0, 0.0]);
860 assert!(
861 build_sprite_calls(
862 core::slice::from_ref(&s),
863 Some(0),
864 &no_slots(),
865 [0.0, 0.0],
866 &no_clips(),
867 &no_layers()
868 )
869 .is_empty()
870 );
871 }
872
873 #[test]
874 fn view_owned_sprite_scales_to_window() {
875 let mut s = sprite(100.0, 100.0, 200.0, 100.0, [1.0, 1.0, 1.0, 1.0]);
879 s.screen = Some(AssetId(7));
880 let calls = build_sprite_calls(
881 core::slice::from_ref(&s),
882 Some(0),
883 &no_slots(),
884 [2560.0, 1440.0],
885 &no_clips(),
886 &no_layers(),
887 );
888 assert_eq!(calls.len(), 1);
889 assert_eq!(calls[0].vertices[0].pos, [200.0, 200.0]);
890 assert_eq!(calls[0].vertices[2].pos, [600.0, 400.0]);
891 }
892
893 #[test]
894 fn view_owned_full_canvas_backdrop_fills_window() {
895 let mut s = sprite(0.0, 0.0, 1280.0, 720.0, [0.0, 0.0, 0.0, 0.5]);
898 s.screen = Some(AssetId(7));
899 let calls = build_sprite_calls(
900 core::slice::from_ref(&s),
901 Some(0),
902 &no_slots(),
903 [2560.0, 1440.0],
904 &no_clips(),
905 &no_layers(),
906 );
907 assert_eq!(calls.len(), 1);
908 assert_eq!(calls[0].vertices[0].pos, [0.0, 0.0]);
909 assert_eq!(calls[0].vertices[2].pos, [2560.0, 1440.0]);
910 }
911
912 #[test]
913 fn cover_sprite_fills_the_window_and_crops_the_overflow() {
914 let mut s = sprite(0.0, 0.0, 1280.0, 720.0, [1.0, 1.0, 1.0, 1.0]);
918 s.screen = Some(AssetId(7));
919 s.fit = SpriteFit::Cover;
920 let calls = build_sprite_calls(
921 core::slice::from_ref(&s),
922 Some(0),
923 &no_slots(),
924 [1024.0, 768.0],
925 &no_clips(),
926 &no_layers(),
927 );
928 let scale = 768.0 / 720.0;
929 let overflow = (1280.0 * scale - 1024.0) / 2.0;
930 let vs = &calls[0].vertices;
931 assert!(
932 (vs[0].pos[0] - -overflow).abs() < 1e-3,
933 "x0={}",
934 vs[0].pos[0]
935 );
936 assert!((vs[0].pos[1]).abs() < 1e-3, "y0={}", vs[0].pos[1]);
937 assert!(
938 (vs[2].pos[0] - (1024.0 + overflow)).abs() < 1e-3,
939 "x1={}",
940 vs[2].pos[0]
941 );
942 assert!((vs[2].pos[1] - 768.0).abs() < 1e-3, "y1={}", vs[2].pos[1]);
943 }
944
945 #[test]
946 fn cover_sprite_anchored_to_the_canvas_bottom_stays_flush() {
947 let mut s = sprite(400.0, 100.0, 480.0, 620.0, [1.0, 1.0, 1.0, 1.0]);
951 s.screen = Some(AssetId(7));
952 s.fit = SpriteFit::Cover;
953 let calls = build_sprite_calls(
954 core::slice::from_ref(&s),
955 Some(0),
956 &no_slots(),
957 [1024.0, 768.0],
958 &no_clips(),
959 &no_layers(),
960 );
961 let bottom = calls[0].vertices[2].pos[1];
962 assert!((bottom - 768.0).abs() < 1e-3, "bottom={bottom}");
963 }
964
965 #[test]
966 fn view_less_sprite_keeps_literal_pixels() {
967 let s = sprite(10.0, 20.0, 100.0, 50.0, [0.5, 0.5, 0.5, 1.0]);
969 let calls = build_sprite_calls(
970 core::slice::from_ref(&s),
971 Some(0),
972 &no_slots(),
973 [2560.0, 1440.0],
974 &no_clips(),
975 &no_layers(),
976 );
977 assert_eq!(calls[0].vertices[0].pos, [10.0, 20.0]);
978 assert_eq!(calls[0].vertices[2].pos, [110.0, 70.0]);
979 }
980
981 #[test]
982 fn clipped_element_carries_window_space_clip_rect() {
983 let mut s = sprite(100.0, 100.0, 50.0, 50.0, [1.0, 1.0, 1.0, 1.0]);
987 s.asset_id = AssetId(7);
988 s.screen = Some(AssetId(1));
989 let mut clips = no_clips();
990 clips.insert(AssetId(7), [200.0, 200.0, 200.0, 60.0]);
994 let calls = build_sprite_calls(
995 core::slice::from_ref(&s),
996 Some(0),
997 &no_slots(),
998 [2560.0, 1440.0],
999 &clips,
1000 &no_layers(),
1001 );
1002 let clip = calls[0].clip_rect.expect("clipped sprite has a clip rect");
1003 assert!((clip[0] - 400.0).abs() < 1e-3, "x={}", clip[0]);
1004 assert!((clip[1] - 400.0).abs() < 1e-3, "y={}", clip[1]);
1005 assert!((clip[2] - 400.0).abs() < 1e-3, "w={}", clip[2]);
1006 assert!((clip[3] - 120.0).abs() < 1e-3, "h={}", clip[3]);
1007
1008 let mut other = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
1010 other.asset_id = AssetId(9);
1011 other.screen = Some(AssetId(1));
1012 let calls = build_sprite_calls(
1013 core::slice::from_ref(&other),
1014 Some(0),
1015 &no_slots(),
1016 [2560.0, 1440.0],
1017 &clips,
1018 &no_layers(),
1019 );
1020 assert!(calls[0].clip_rect.is_none());
1021 }
1022
1023 #[test]
1026 fn sprite_call_takes_its_layer_from_the_map() {
1027 let mut mapped = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
1028 mapped.asset_id = AssetId(42);
1029 let mut layers = OverlayLayers::new();
1030 layers.insert(AssetId(42), 7);
1031 let calls = build_sprite_calls(
1032 core::slice::from_ref(&mapped),
1033 Some(0),
1034 &no_slots(),
1035 [100.0, 100.0],
1036 &no_clips(),
1037 &layers,
1038 );
1039 assert_eq!(calls[0].layer, 7);
1040
1041 let mut unmapped = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
1042 unmapped.asset_id = AssetId(99);
1043 let calls = build_sprite_calls(
1044 core::slice::from_ref(&unmapped),
1045 Some(0),
1046 &no_slots(),
1047 [100.0, 100.0],
1048 &no_clips(),
1049 &layers,
1050 );
1051 assert_eq!(calls[0].layer, 0, "an unmapped id is layer 0");
1052 }
1053
1054 #[test]
1057 fn follow_cursor_sprites_are_skipped() {
1058 let mut s = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
1059 s.follow_cursor = true;
1060 assert!(
1061 build_sprite_calls(
1062 core::slice::from_ref(&s),
1063 Some(0),
1064 &no_slots(),
1065 [0.0, 0.0],
1066 &no_clips(),
1067 &no_layers()
1068 )
1069 .is_empty()
1070 );
1071 }
1072}