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 ring_geometry(
166 &mut vertices,
167 &mut indices,
168 [x0, y0, x1, y1],
169 border,
170 ba,
171 border_v,
172 );
173 } else {
174 rect_geometry(
178 &mut vertices,
179 &mut indices,
180 [x0, y0, x1, y1],
181 radius,
182 ba,
183 border_v,
184 );
185 rect_geometry(
186 &mut vertices,
187 &mut indices,
188 [x0 + border, y0 + border, x1 - border, y1 - border],
189 (radius - border).max(0.0),
190 a,
191 v,
192 );
193 }
194 } else {
195 rect_geometry(&mut vertices, &mut indices, [x0, y0, x1, y1], radius, a, v);
196 }
197 out.calls.push(TextDrawCall {
198 vertices,
199 indices,
200 atlas_slot: texture_slot.unwrap_or(fill_slot),
201 clip_rect: clips
202 .get(&s.asset_id)
203 .map(|b| crate::render::text::band_to_window(&overlay, *b)),
204 layer: layers.get(&s.asset_id).copied().unwrap_or(0),
205 });
206 }
207}
208
209fn rect_geometry(
214 vertices: &mut Vec<TextVertex>,
215 indices: &mut Vec<u16>,
216 rect: [f32; 4],
217 radius: f32,
218 alpha: f32,
219 mut v: impl FnMut(f32, f32, f32) -> TextVertex,
220) {
221 let [x0, y0, x1, y1] = rect;
222 if radius > 0.5 {
223 rounded_rect_geometry(vertices, indices, rect, radius, alpha, v);
224 } else {
225 let base = vertices.len() as u16;
226 vertices.extend_from_slice(&[
227 v(x0, y0, alpha),
228 v(x1, y0, alpha),
229 v(x1, y1, alpha),
230 v(x0, y1, alpha),
231 ]);
232 indices.extend([0, 1, 2, 0, 2, 3].map(|i| base + i));
233 }
234}
235
236fn ring_geometry(
240 vertices: &mut Vec<TextVertex>,
241 indices: &mut Vec<u16>,
242 rect: [f32; 4],
243 width: f32,
244 alpha: f32,
245 mut v: impl FnMut(f32, f32, f32) -> TextVertex,
246) {
247 let [x0, y0, x1, y1] = rect;
248 let strips = [
249 [x0, y0, x1, y0 + width],
250 [x0, y1 - width, x1, y1],
251 [x0, y0 + width, x0 + width, y1 - width],
252 [x1 - width, y0 + width, x1, y1 - width],
253 ];
254 for strip in strips {
255 rect_geometry(vertices, indices, strip, 0.0, alpha, &mut v);
256 }
257}
258
259const CORNER_SEGMENTS: usize = 6;
262const EDGE_FEATHER: f32 = 1.25;
266
267fn rounded_rect_geometry(
272 vertices: &mut Vec<TextVertex>,
273 indices: &mut Vec<u16>,
274 rect: [f32; 4],
275 radius: f32,
276 alpha: f32,
277 mut v: impl FnMut(f32, f32, f32) -> TextVertex,
278) {
279 use core::f32::consts::FRAC_PI_2;
280 let [x0, y0, x1, y1] = rect;
281 let mut arc = [(0.0_f32, 0.0_f32); CORNER_SEGMENTS + 1];
284 for (i, slot) in arc.iter_mut().enumerate() {
285 let (s, c) = sin_cos((i as f32 / CORNER_SEGMENTS as f32) * FRAC_PI_2);
286 *slot = (c, s);
287 }
288 let corners = [
291 (x0 + radius, y0 + radius, 2),
292 (x1 - radius, y0 + radius, 3),
293 (x1 - radius, y1 - radius, 0),
294 (x0 + radius, y1 - radius, 1),
295 ];
296 let mut boundary = [(0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32); 4 * (CORNER_SEGMENTS + 1)];
297 for (c, &(cx, cy, quadrant)) in corners.iter().enumerate() {
298 for (i, &(uc, us)) in arc.iter().enumerate() {
299 let (rc, rs) = match quadrant {
300 0 => (uc, us),
301 1 => (-us, uc),
302 2 => (-uc, -us),
303 _ => (us, -uc),
304 };
305 boundary[c * (CORNER_SEGMENTS + 1) + i] = (cx, cy, rc, rs);
306 }
307 }
308 let m = boundary.len();
309 let base = vertices.len() as u16;
310 let inner_r = (radius - EDGE_FEATHER).max(0.0);
311 vertices.reserve(2 * m);
312 for &(cx, cy, cos, sin) in &boundary {
313 vertices.push(v(cx + inner_r * cos, cy + inner_r * sin, alpha));
314 }
315 for &(cx, cy, cos, sin) in &boundary {
316 vertices.push(v(cx + radius * cos, cy + radius * sin, 0.0));
317 }
318 indices.reserve(3 * (m - 2) + 6 * m);
319 for i in 1..m - 1 {
320 indices.extend([base, base + i as u16, base + (i + 1) as u16]);
321 }
322 for i in 0..m {
323 let j = (i + 1) % m;
324 let (i, j, m) = (i as u16, j as u16, m as u16);
325 indices.extend([i, j, m + j, i, m + j, m + i].map(|k| base + k));
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::ecs::TextureHandle;
333 use crate::ecs::asset_id::AssetId;
334
335 use alloc::vec;
336 fn no_clips() -> ClipRects {
337 ClipRects::new()
338 }
339 fn no_layers() -> OverlayLayers {
340 OverlayLayers::new()
341 }
342
343 fn no_slots() -> TextureSlots {
344 TextureSlots::new()
345 }
346
347 fn sprite(x: f32, y: f32, w: f32, h: f32, tint: [f32; 4]) -> Sprite {
348 Sprite {
349 asset_id: AssetId::default(),
350 x,
351 y,
352 width: w,
353 height: h,
354 texture: None,
355 tint,
356 follow_cursor: false,
357 visible: true,
358 screen: None,
359 fit: SpriteFit::Fit,
360 corner_radius: 0.0,
361 border_width: 0.0,
362 border_color: [0.0, 0.0, 0.0, 1.0],
363 }
364 }
365
366 #[test]
367 fn no_fonts_means_no_calls() {
368 let s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 0.0, 0.0, 1.0]);
369 assert!(
370 build_sprite_calls(
371 core::slice::from_ref(&s),
372 None,
373 &no_slots(),
374 [0.0, 0.0],
375 &no_clips(),
376 &no_layers()
377 )
378 .is_empty()
379 );
380 }
381
382 #[test]
383 fn visible_sprite_emits_quad_with_sentinel_uv() {
384 let s = sprite(10.0, 20.0, 100.0, 50.0, [0.5, 0.5, 0.5, 0.75]);
385 let calls = build_sprite_calls(
386 core::slice::from_ref(&s),
387 Some(0),
388 &no_slots(),
389 [0.0, 0.0],
390 &no_clips(),
391 &no_layers(),
392 );
393 assert_eq!(calls.len(), 1);
394 assert_eq!(calls[0].vertices.len(), 4);
395 assert_eq!(calls[0].indices, vec![0, 1, 2, 0, 2, 3]);
396 for v in &calls[0].vertices {
397 assert!(v.uv[0] < 0.0, "sentinel u should be negative");
398 assert!((v.uv[1] - 0.75).abs() < 1e-5, "alpha carried in v");
399 assert_eq!(v.color, [0.5, 0.5, 0.5]);
400 }
401 assert_eq!(calls[0].vertices[0].pos, [10.0, 20.0]);
402 assert_eq!(calls[0].vertices[2].pos, [110.0, 70.0]);
403 }
404
405 #[test]
408 fn transparent_fill_draws_when_a_border_is_set() {
409 let invisible = sprite(0.0, 0.0, 100.0, 50.0, [0.0, 0.0, 0.0, 0.0]);
410 assert!(
411 build_sprite_calls(
412 core::slice::from_ref(&invisible),
413 Some(0),
414 &no_slots(),
415 [0.0, 0.0],
416 &no_clips(),
417 &no_layers()
418 )
419 .is_empty(),
420 "borderless transparent fill still skips"
421 );
422
423 let mut outline = sprite(0.0, 0.0, 100.0, 50.0, [0.0, 0.0, 0.0, 0.0]);
424 outline.border_width = 2.0;
425 outline.border_color = [0.2, 0.4, 0.9, 1.0];
426 let calls = build_sprite_calls(
427 core::slice::from_ref(&outline),
428 Some(0),
429 &no_slots(),
430 [0.0, 0.0],
431 &no_clips(),
432 &no_layers(),
433 );
434 assert_eq!(calls.len(), 1, "the border ring draws");
435 let verts = &calls[0].vertices;
438 assert_eq!(verts.len(), 16);
439 assert!(
440 verts
441 .iter()
442 .all(|v| v.color == [0.2, 0.4, 0.9] && v.uv == [-1.0, 1.0])
443 );
444 let inside = |x: f32, y: f32| {
445 calls[0].indices.chunks(3).any(|t| {
446 let p: Vec<[f32; 2]> = t.iter().map(|&i| verts[i as usize].pos).collect();
447 point_in_triangle([x, y], p[0], p[1], p[2])
448 })
449 };
450 assert!(inside(50.0, 1.0), "the top strip covers the edge");
451 assert!(inside(1.0, 25.0), "the left strip covers the edge");
452 assert!(!inside(50.0, 25.0), "the interior is empty");
453
454 let mut panel = sprite(0.0, 0.0, 100.0, 50.0, [0.1, 0.1, 0.1, 1.0]);
457 panel.border_width = 2.0;
458 panel.border_color = [0.2, 0.4, 0.9, 1.0];
459 let calls = build_sprite_calls(
460 core::slice::from_ref(&panel),
461 Some(0),
462 &no_slots(),
463 [0.0, 0.0],
464 &no_clips(),
465 &no_layers(),
466 );
467 assert_eq!(calls[0].vertices.len(), 8);
468 }
469
470 fn point_in_triangle(p: [f32; 2], a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
471 let sign = |p: [f32; 2], q: [f32; 2], r: [f32; 2]| {
472 (p[0] - r[0]) * (q[1] - r[1]) - (q[0] - r[0]) * (p[1] - r[1])
473 };
474 let (d1, d2, d3) = (sign(p, a, b), sign(p, b, c), sign(p, c, a));
475 let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
476 let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
477 !(neg && pos)
478 }
479
480 #[test]
481 fn textured_sprite_emits_real_uvs_and_its_slot() {
482 let mut s = sprite(10.0, 20.0, 100.0, 50.0, [1.0, 0.9, 0.8, 0.75]);
483 s.texture = Some(TextureHandle(42));
484 let mut slots = no_slots();
485 slots.insert(TextureHandle(42), 3);
486 let calls = build_sprite_calls(
487 core::slice::from_ref(&s),
488 Some(0),
489 &slots,
490 [0.0, 0.0],
491 &no_clips(),
492 &no_layers(),
493 );
494 assert_eq!(calls.len(), 1);
495 assert_eq!(calls[0].atlas_slot, 3);
497 let vs = &calls[0].vertices;
498 assert_eq!(vs[0].uv, [0.0, 0.0]);
499 assert_eq!(vs[1].uv, [1.0, 0.0]);
500 assert_eq!(vs[2].uv, [1.0, 1.0]);
501 assert_eq!(vs[3].uv, [0.0, 1.0]);
502 for v in vs {
503 assert_eq!(v.color, [1.0, 0.9, 0.8]);
505 assert!((v.mode - 0.75).abs() < 1e-5);
506 }
507 }
508
509 #[test]
510 fn rounded_sprite_tessellates_with_a_feathered_edge() {
511 let mut s = sprite(100.0, 100.0, 400.0, 200.0, [0.1, 0.2, 0.3, 0.9]);
512 s.corner_radius = 20.0;
513 let calls = build_sprite_calls(
514 core::slice::from_ref(&s),
515 Some(0),
516 &no_slots(),
517 [0.0, 0.0],
518 &no_clips(),
519 &no_layers(),
520 );
521 assert_eq!(calls.len(), 1);
522 let vs = &calls[0].vertices;
523 let ring = 4 * (CORNER_SEGMENTS + 1);
526 assert_eq!(vs.len(), 2 * ring);
527 for v in &vs[..ring] {
528 assert!(
529 (v.uv[1] - 0.9).abs() < 1e-5,
530 "inner ring carries the tint alpha"
531 );
532 }
533 for v in &vs[ring..] {
534 assert!(v.uv[1].abs() < 1e-5, "outer ring fades to transparent");
535 }
536 for v in vs {
539 assert!(v.pos[0] >= 100.0 - 1e-3 && v.pos[0] <= 500.0 + 1e-3);
540 assert!(v.pos[1] >= 100.0 - 1e-3 && v.pos[1] <= 300.0 + 1e-3);
541 }
542 let min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
543 assert!((min_x - 100.0).abs() < 1e-3);
544 assert!(!vs.iter().any(|v| v.pos == [100.0, 100.0]));
546 }
547
548 #[test]
549 fn bordered_sprite_emits_a_border_ring_and_an_inset_fill() {
550 let mut s = sprite(100.0, 100.0, 200.0, 120.0, [0.1, 0.2, 0.3, 1.0]);
551 s.border_width = 2.0;
552 s.border_color = [0.8, 0.4, 0.2, 1.0];
553 let calls = build_sprite_calls(
554 core::slice::from_ref(&s),
555 Some(0),
556 &no_slots(),
557 [0.0, 0.0],
558 &no_clips(),
559 &no_layers(),
560 );
561 assert_eq!(calls.len(), 1);
562 let vs = &calls[0].vertices;
563 assert!(
565 vs.iter().any(|v| v.color == [0.8, 0.4, 0.2]),
566 "border colour present"
567 );
568 assert!(
569 vs.iter().any(|v| v.color == [0.1, 0.2, 0.3]),
570 "fill colour present"
571 );
572 let outer_min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
575 let outer_max_x = vs.iter().map(|v| v.pos[0]).fold(f32::MIN, f32::max);
576 assert!(
577 (outer_min_x - 100.0).abs() < 1e-3,
578 "border at the left edge"
579 );
580 assert!(
581 (outer_max_x - 300.0).abs() < 1e-3,
582 "border at the right edge"
583 );
584 let fill_min_x = vs
585 .iter()
586 .filter(|v| v.color == [0.1, 0.2, 0.3])
587 .map(|v| v.pos[0])
588 .fold(f32::MAX, f32::min);
589 assert!(
590 (fill_min_x - 102.0).abs() < 1e-3,
591 "fill inset by the stroke width"
592 );
593 }
594
595 #[test]
596 fn zero_border_stays_a_single_layer() {
597 let mut s = sprite(0.0, 0.0, 100.0, 100.0, [0.2, 0.3, 0.4, 1.0]);
598 s.border_width = 0.0;
600 s.border_color = [1.0, 0.0, 0.0, 1.0];
601 let calls = build_sprite_calls(
602 core::slice::from_ref(&s),
603 Some(0),
604 &no_slots(),
605 [0.0, 0.0],
606 &no_clips(),
607 &no_layers(),
608 );
609 assert_eq!(
610 calls[0].vertices.len(),
611 4,
612 "one plain quad, no border layer"
613 );
614 assert!(calls[0].vertices.iter().all(|v| v.color == [0.2, 0.3, 0.4]));
615 }
616
617 #[test]
618 fn rounded_view_sprite_scales_its_radius_with_the_window() {
619 let mut s = sprite(100.0, 100.0, 400.0, 200.0, [0.1, 0.2, 0.3, 0.9]);
623 s.screen = Some(AssetId(7));
624 s.corner_radius = 20.0;
625 let calls = build_sprite_calls(
626 core::slice::from_ref(&s),
627 Some(0),
628 &no_slots(),
629 [2.0 * UI_REFERENCE_SIZE[0], 2.0 * UI_REFERENCE_SIZE[1]],
630 &no_clips(),
631 &no_layers(),
632 );
633 let vs = &calls[0].vertices;
634 let min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
635 let top_left_arc_y = vs
636 .iter()
637 .filter(|v| (v.pos[0] - min_x).abs() < 1e-3)
638 .map(|v| v.pos[1])
639 .fold(f32::MAX, f32::min);
640 assert!((min_x - 200.0).abs() < 1e-3);
641 assert!((top_left_arc_y - (200.0 + 40.0)).abs() < 1e-3);
642 }
643
644 #[test]
645 fn textured_sprite_without_a_loaded_texture_falls_back_to_fill() {
646 let mut s = sprite(0.0, 0.0, 10.0, 10.0, [0.2, 0.3, 0.4, 1.0]);
647 s.texture = Some(TextureHandle(42));
648 let calls = build_sprite_calls(
650 core::slice::from_ref(&s),
651 Some(5),
652 &no_slots(),
653 [0.0, 0.0],
654 &no_clips(),
655 &no_layers(),
656 );
657 assert_eq!(calls[0].atlas_slot, 5);
658 assert!(calls[0].vertices[0].uv[0] < 0.0);
659 assert_eq!(calls[0].vertices[0].mode, 0.0);
660 }
661
662 #[test]
663 fn invisible_sprite_is_skipped() {
664 let mut s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 1.0, 1.0, 1.0]);
665 s.visible = false;
666 assert!(
667 build_sprite_calls(
668 core::slice::from_ref(&s),
669 Some(0),
670 &no_slots(),
671 [0.0, 0.0],
672 &no_clips(),
673 &no_layers()
674 )
675 .is_empty()
676 );
677 }
678
679 #[test]
680 fn zero_alpha_sprite_is_skipped() {
681 let s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 1.0, 1.0, 0.0]);
682 assert!(
683 build_sprite_calls(
684 core::slice::from_ref(&s),
685 Some(0),
686 &no_slots(),
687 [0.0, 0.0],
688 &no_clips(),
689 &no_layers()
690 )
691 .is_empty()
692 );
693 }
694
695 #[test]
696 fn view_owned_sprite_scales_to_window() {
697 let mut s = sprite(100.0, 100.0, 200.0, 100.0, [1.0, 1.0, 1.0, 1.0]);
701 s.screen = Some(AssetId(7));
702 let calls = build_sprite_calls(
703 core::slice::from_ref(&s),
704 Some(0),
705 &no_slots(),
706 [2560.0, 1440.0],
707 &no_clips(),
708 &no_layers(),
709 );
710 assert_eq!(calls.len(), 1);
711 assert_eq!(calls[0].vertices[0].pos, [200.0, 200.0]);
712 assert_eq!(calls[0].vertices[2].pos, [600.0, 400.0]);
713 }
714
715 #[test]
716 fn view_owned_full_canvas_backdrop_fills_window() {
717 let mut s = sprite(0.0, 0.0, 1280.0, 720.0, [0.0, 0.0, 0.0, 0.5]);
720 s.screen = Some(AssetId(7));
721 let calls = build_sprite_calls(
722 core::slice::from_ref(&s),
723 Some(0),
724 &no_slots(),
725 [2560.0, 1440.0],
726 &no_clips(),
727 &no_layers(),
728 );
729 assert_eq!(calls.len(), 1);
730 assert_eq!(calls[0].vertices[0].pos, [0.0, 0.0]);
731 assert_eq!(calls[0].vertices[2].pos, [2560.0, 1440.0]);
732 }
733
734 #[test]
735 fn cover_sprite_fills_the_window_and_crops_the_overflow() {
736 let mut s = sprite(0.0, 0.0, 1280.0, 720.0, [1.0, 1.0, 1.0, 1.0]);
740 s.screen = Some(AssetId(7));
741 s.fit = SpriteFit::Cover;
742 let calls = build_sprite_calls(
743 core::slice::from_ref(&s),
744 Some(0),
745 &no_slots(),
746 [1024.0, 768.0],
747 &no_clips(),
748 &no_layers(),
749 );
750 let scale = 768.0 / 720.0;
751 let overflow = (1280.0 * scale - 1024.0) / 2.0;
752 let vs = &calls[0].vertices;
753 assert!(
754 (vs[0].pos[0] - -overflow).abs() < 1e-3,
755 "x0={}",
756 vs[0].pos[0]
757 );
758 assert!((vs[0].pos[1]).abs() < 1e-3, "y0={}", vs[0].pos[1]);
759 assert!(
760 (vs[2].pos[0] - (1024.0 + overflow)).abs() < 1e-3,
761 "x1={}",
762 vs[2].pos[0]
763 );
764 assert!((vs[2].pos[1] - 768.0).abs() < 1e-3, "y1={}", vs[2].pos[1]);
765 }
766
767 #[test]
768 fn cover_sprite_anchored_to_the_canvas_bottom_stays_flush() {
769 let mut s = sprite(400.0, 100.0, 480.0, 620.0, [1.0, 1.0, 1.0, 1.0]);
773 s.screen = Some(AssetId(7));
774 s.fit = SpriteFit::Cover;
775 let calls = build_sprite_calls(
776 core::slice::from_ref(&s),
777 Some(0),
778 &no_slots(),
779 [1024.0, 768.0],
780 &no_clips(),
781 &no_layers(),
782 );
783 let bottom = calls[0].vertices[2].pos[1];
784 assert!((bottom - 768.0).abs() < 1e-3, "bottom={bottom}");
785 }
786
787 #[test]
788 fn view_less_sprite_keeps_literal_pixels() {
789 let s = sprite(10.0, 20.0, 100.0, 50.0, [0.5, 0.5, 0.5, 1.0]);
791 let calls = build_sprite_calls(
792 core::slice::from_ref(&s),
793 Some(0),
794 &no_slots(),
795 [2560.0, 1440.0],
796 &no_clips(),
797 &no_layers(),
798 );
799 assert_eq!(calls[0].vertices[0].pos, [10.0, 20.0]);
800 assert_eq!(calls[0].vertices[2].pos, [110.0, 70.0]);
801 }
802
803 #[test]
804 fn clipped_element_carries_window_space_clip_rect() {
805 let mut s = sprite(100.0, 100.0, 50.0, 50.0, [1.0, 1.0, 1.0, 1.0]);
809 s.asset_id = AssetId(7);
810 s.screen = Some(AssetId(1));
811 let mut clips = no_clips();
812 clips.insert(AssetId(7), [200.0, 200.0, 200.0, 60.0]);
816 let calls = build_sprite_calls(
817 core::slice::from_ref(&s),
818 Some(0),
819 &no_slots(),
820 [2560.0, 1440.0],
821 &clips,
822 &no_layers(),
823 );
824 let clip = calls[0].clip_rect.expect("clipped sprite has a clip rect");
825 assert!((clip[0] - 400.0).abs() < 1e-3, "x={}", clip[0]);
826 assert!((clip[1] - 400.0).abs() < 1e-3, "y={}", clip[1]);
827 assert!((clip[2] - 400.0).abs() < 1e-3, "w={}", clip[2]);
828 assert!((clip[3] - 120.0).abs() < 1e-3, "h={}", clip[3]);
829
830 let mut other = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
832 other.asset_id = AssetId(9);
833 other.screen = Some(AssetId(1));
834 let calls = build_sprite_calls(
835 core::slice::from_ref(&other),
836 Some(0),
837 &no_slots(),
838 [2560.0, 1440.0],
839 &clips,
840 &no_layers(),
841 );
842 assert!(calls[0].clip_rect.is_none());
843 }
844
845 #[test]
848 fn sprite_call_takes_its_layer_from_the_map() {
849 let mut mapped = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
850 mapped.asset_id = AssetId(42);
851 let mut layers = OverlayLayers::new();
852 layers.insert(AssetId(42), 7);
853 let calls = build_sprite_calls(
854 core::slice::from_ref(&mapped),
855 Some(0),
856 &no_slots(),
857 [100.0, 100.0],
858 &no_clips(),
859 &layers,
860 );
861 assert_eq!(calls[0].layer, 7);
862
863 let mut unmapped = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
864 unmapped.asset_id = AssetId(99);
865 let calls = build_sprite_calls(
866 core::slice::from_ref(&unmapped),
867 Some(0),
868 &no_slots(),
869 [100.0, 100.0],
870 &no_clips(),
871 &layers,
872 );
873 assert_eq!(calls[0].layer, 0, "an unmapped id is layer 0");
874 }
875
876 #[test]
879 fn follow_cursor_sprites_are_skipped() {
880 let mut s = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
881 s.follow_cursor = true;
882 assert!(
883 build_sprite_calls(
884 core::slice::from_ref(&s),
885 Some(0),
886 &no_slots(),
887 [0.0, 0.0],
888 &no_clips(),
889 &no_layers()
890 )
891 .is_empty()
892 );
893 }
894}