1use alloc::vec::Vec;
10use concinnity_core::math::{cos, sin};
11
12use crate::components::{Sprite, SpriteFit};
13use crate::overlay_maps::{ClipRects, OverlayLayers, TextureSlots};
14use crate::render_types::{TextDrawCall, TextVertex};
15use concinnity_core::gfx::overlay::{OverlayTransform, UI_REFERENCE_SIZE};
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::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::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::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, PI};
280 let [x0, y0, x1, y1] = rect;
281 let corners = [
284 (x0 + radius, y0 + radius, PI),
285 (x1 - radius, y0 + radius, 1.5 * PI),
286 (x1 - radius, y1 - radius, 0.0),
287 (x0 + radius, y1 - radius, FRAC_PI_2),
288 ];
289 let mut boundary = [(0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32); 4 * (CORNER_SEGMENTS + 1)];
290 for (c, &(cx, cy, start)) in corners.iter().enumerate() {
291 for i in 0..=CORNER_SEGMENTS {
292 let t = start + (i as f32 / CORNER_SEGMENTS as f32) * FRAC_PI_2;
293 boundary[c * (CORNER_SEGMENTS + 1) + i] = (cx, cy, cos(t), sin(t));
294 }
295 }
296 let m = boundary.len();
297 let base = vertices.len() as u16;
298 let inner_r = (radius - EDGE_FEATHER).max(0.0);
299 vertices.reserve(2 * m);
300 for &(cx, cy, cos, sin) in &boundary {
301 vertices.push(v(cx + inner_r * cos, cy + inner_r * sin, alpha));
302 }
303 for &(cx, cy, cos, sin) in &boundary {
304 vertices.push(v(cx + radius * cos, cy + radius * sin, 0.0));
305 }
306 indices.reserve(3 * (m - 2) + 6 * m);
307 for i in 1..m - 1 {
308 indices.extend([base, base + i as u16, base + (i + 1) as u16]);
309 }
310 for i in 0..m {
311 let j = (i + 1) % m;
312 let (i, j, m) = (i as u16, j as u16, m as u16);
313 indices.extend([i, j, m + j, i, m + j, m + i].map(|k| base + k));
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::ecs::TextureHandle;
321 use crate::ecs::asset_id::AssetId;
322
323 use alloc::vec;
324 fn no_clips() -> ClipRects {
325 ClipRects::new()
326 }
327 fn no_layers() -> OverlayLayers {
328 OverlayLayers::new()
329 }
330
331 fn no_slots() -> TextureSlots {
332 TextureSlots::new()
333 }
334
335 fn sprite(x: f32, y: f32, w: f32, h: f32, tint: [f32; 4]) -> Sprite {
336 Sprite {
337 asset_id: AssetId::default(),
338 x,
339 y,
340 width: w,
341 height: h,
342 texture: None,
343 tint,
344 follow_cursor: false,
345 visible: true,
346 screen: None,
347 fit: SpriteFit::Fit,
348 corner_radius: 0.0,
349 border_width: 0.0,
350 border_color: [0.0, 0.0, 0.0, 1.0],
351 }
352 }
353
354 #[test]
355 fn no_fonts_means_no_calls() {
356 let s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 0.0, 0.0, 1.0]);
357 assert!(
358 build_sprite_calls(
359 core::slice::from_ref(&s),
360 None,
361 &no_slots(),
362 [0.0, 0.0],
363 &no_clips(),
364 &no_layers()
365 )
366 .is_empty()
367 );
368 }
369
370 #[test]
371 fn visible_sprite_emits_quad_with_sentinel_uv() {
372 let s = sprite(10.0, 20.0, 100.0, 50.0, [0.5, 0.5, 0.5, 0.75]);
373 let calls = build_sprite_calls(
374 core::slice::from_ref(&s),
375 Some(0),
376 &no_slots(),
377 [0.0, 0.0],
378 &no_clips(),
379 &no_layers(),
380 );
381 assert_eq!(calls.len(), 1);
382 assert_eq!(calls[0].vertices.len(), 4);
383 assert_eq!(calls[0].indices, vec![0, 1, 2, 0, 2, 3]);
384 for v in &calls[0].vertices {
385 assert!(v.uv[0] < 0.0, "sentinel u should be negative");
386 assert!((v.uv[1] - 0.75).abs() < 1e-5, "alpha carried in v");
387 assert_eq!(v.color, [0.5, 0.5, 0.5]);
388 }
389 assert_eq!(calls[0].vertices[0].pos, [10.0, 20.0]);
390 assert_eq!(calls[0].vertices[2].pos, [110.0, 70.0]);
391 }
392
393 #[test]
396 fn transparent_fill_draws_when_a_border_is_set() {
397 let invisible = sprite(0.0, 0.0, 100.0, 50.0, [0.0, 0.0, 0.0, 0.0]);
398 assert!(
399 build_sprite_calls(
400 core::slice::from_ref(&invisible),
401 Some(0),
402 &no_slots(),
403 [0.0, 0.0],
404 &no_clips(),
405 &no_layers()
406 )
407 .is_empty(),
408 "borderless transparent fill still skips"
409 );
410
411 let mut outline = sprite(0.0, 0.0, 100.0, 50.0, [0.0, 0.0, 0.0, 0.0]);
412 outline.border_width = 2.0;
413 outline.border_color = [0.2, 0.4, 0.9, 1.0];
414 let calls = build_sprite_calls(
415 core::slice::from_ref(&outline),
416 Some(0),
417 &no_slots(),
418 [0.0, 0.0],
419 &no_clips(),
420 &no_layers(),
421 );
422 assert_eq!(calls.len(), 1, "the border ring draws");
423 let verts = &calls[0].vertices;
426 assert_eq!(verts.len(), 16);
427 assert!(
428 verts
429 .iter()
430 .all(|v| v.color == [0.2, 0.4, 0.9] && v.uv == [-1.0, 1.0])
431 );
432 let inside = |x: f32, y: f32| {
433 calls[0].indices.chunks(3).any(|t| {
434 let p: Vec<[f32; 2]> = t.iter().map(|&i| verts[i as usize].pos).collect();
435 point_in_triangle([x, y], p[0], p[1], p[2])
436 })
437 };
438 assert!(inside(50.0, 1.0), "the top strip covers the edge");
439 assert!(inside(1.0, 25.0), "the left strip covers the edge");
440 assert!(!inside(50.0, 25.0), "the interior is empty");
441
442 let mut panel = sprite(0.0, 0.0, 100.0, 50.0, [0.1, 0.1, 0.1, 1.0]);
445 panel.border_width = 2.0;
446 panel.border_color = [0.2, 0.4, 0.9, 1.0];
447 let calls = build_sprite_calls(
448 core::slice::from_ref(&panel),
449 Some(0),
450 &no_slots(),
451 [0.0, 0.0],
452 &no_clips(),
453 &no_layers(),
454 );
455 assert_eq!(calls[0].vertices.len(), 8);
456 }
457
458 fn point_in_triangle(p: [f32; 2], a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
459 let sign = |p: [f32; 2], q: [f32; 2], r: [f32; 2]| {
460 (p[0] - r[0]) * (q[1] - r[1]) - (q[0] - r[0]) * (p[1] - r[1])
461 };
462 let (d1, d2, d3) = (sign(p, a, b), sign(p, b, c), sign(p, c, a));
463 let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
464 let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
465 !(neg && pos)
466 }
467
468 #[test]
469 fn textured_sprite_emits_real_uvs_and_its_slot() {
470 let mut s = sprite(10.0, 20.0, 100.0, 50.0, [1.0, 0.9, 0.8, 0.75]);
471 s.texture = Some(TextureHandle(42));
472 let mut slots = no_slots();
473 slots.insert(TextureHandle(42), 3);
474 let calls = build_sprite_calls(
475 core::slice::from_ref(&s),
476 Some(0),
477 &slots,
478 [0.0, 0.0],
479 &no_clips(),
480 &no_layers(),
481 );
482 assert_eq!(calls.len(), 1);
483 assert_eq!(calls[0].atlas_slot, 3);
485 let vs = &calls[0].vertices;
486 assert_eq!(vs[0].uv, [0.0, 0.0]);
487 assert_eq!(vs[1].uv, [1.0, 0.0]);
488 assert_eq!(vs[2].uv, [1.0, 1.0]);
489 assert_eq!(vs[3].uv, [0.0, 1.0]);
490 for v in vs {
491 assert_eq!(v.color, [1.0, 0.9, 0.8]);
493 assert!((v.mode - 0.75).abs() < 1e-5);
494 }
495 }
496
497 #[test]
498 fn rounded_sprite_tessellates_with_a_feathered_edge() {
499 let mut s = sprite(100.0, 100.0, 400.0, 200.0, [0.1, 0.2, 0.3, 0.9]);
500 s.corner_radius = 20.0;
501 let calls = build_sprite_calls(
502 core::slice::from_ref(&s),
503 Some(0),
504 &no_slots(),
505 [0.0, 0.0],
506 &no_clips(),
507 &no_layers(),
508 );
509 assert_eq!(calls.len(), 1);
510 let vs = &calls[0].vertices;
511 let ring = 4 * (CORNER_SEGMENTS + 1);
514 assert_eq!(vs.len(), 2 * ring);
515 for v in &vs[..ring] {
516 assert!(
517 (v.uv[1] - 0.9).abs() < 1e-5,
518 "inner ring carries the tint alpha"
519 );
520 }
521 for v in &vs[ring..] {
522 assert!(v.uv[1].abs() < 1e-5, "outer ring fades to transparent");
523 }
524 for v in vs {
527 assert!(v.pos[0] >= 100.0 - 1e-3 && v.pos[0] <= 500.0 + 1e-3);
528 assert!(v.pos[1] >= 100.0 - 1e-3 && v.pos[1] <= 300.0 + 1e-3);
529 }
530 let min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
531 assert!((min_x - 100.0).abs() < 1e-3);
532 assert!(!vs.iter().any(|v| v.pos == [100.0, 100.0]));
534 }
535
536 #[test]
537 fn bordered_sprite_emits_a_border_ring_and_an_inset_fill() {
538 let mut s = sprite(100.0, 100.0, 200.0, 120.0, [0.1, 0.2, 0.3, 1.0]);
539 s.border_width = 2.0;
540 s.border_color = [0.8, 0.4, 0.2, 1.0];
541 let calls = build_sprite_calls(
542 core::slice::from_ref(&s),
543 Some(0),
544 &no_slots(),
545 [0.0, 0.0],
546 &no_clips(),
547 &no_layers(),
548 );
549 assert_eq!(calls.len(), 1);
550 let vs = &calls[0].vertices;
551 assert!(
553 vs.iter().any(|v| v.color == [0.8, 0.4, 0.2]),
554 "border colour present"
555 );
556 assert!(
557 vs.iter().any(|v| v.color == [0.1, 0.2, 0.3]),
558 "fill colour present"
559 );
560 let outer_min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
563 let outer_max_x = vs.iter().map(|v| v.pos[0]).fold(f32::MIN, f32::max);
564 assert!(
565 (outer_min_x - 100.0).abs() < 1e-3,
566 "border at the left edge"
567 );
568 assert!(
569 (outer_max_x - 300.0).abs() < 1e-3,
570 "border at the right edge"
571 );
572 let fill_min_x = vs
573 .iter()
574 .filter(|v| v.color == [0.1, 0.2, 0.3])
575 .map(|v| v.pos[0])
576 .fold(f32::MAX, f32::min);
577 assert!(
578 (fill_min_x - 102.0).abs() < 1e-3,
579 "fill inset by the stroke width"
580 );
581 }
582
583 #[test]
584 fn zero_border_stays_a_single_layer() {
585 let mut s = sprite(0.0, 0.0, 100.0, 100.0, [0.2, 0.3, 0.4, 1.0]);
586 s.border_width = 0.0;
588 s.border_color = [1.0, 0.0, 0.0, 1.0];
589 let calls = build_sprite_calls(
590 core::slice::from_ref(&s),
591 Some(0),
592 &no_slots(),
593 [0.0, 0.0],
594 &no_clips(),
595 &no_layers(),
596 );
597 assert_eq!(
598 calls[0].vertices.len(),
599 4,
600 "one plain quad, no border layer"
601 );
602 assert!(calls[0].vertices.iter().all(|v| v.color == [0.2, 0.3, 0.4]));
603 }
604
605 #[test]
606 fn rounded_view_sprite_scales_its_radius_with_the_window() {
607 let mut s = sprite(100.0, 100.0, 400.0, 200.0, [0.1, 0.2, 0.3, 0.9]);
611 s.screen = Some(AssetId(7));
612 s.corner_radius = 20.0;
613 let calls = build_sprite_calls(
614 core::slice::from_ref(&s),
615 Some(0),
616 &no_slots(),
617 [2.0 * UI_REFERENCE_SIZE[0], 2.0 * UI_REFERENCE_SIZE[1]],
618 &no_clips(),
619 &no_layers(),
620 );
621 let vs = &calls[0].vertices;
622 let min_x = vs.iter().map(|v| v.pos[0]).fold(f32::MAX, f32::min);
623 let top_left_arc_y = vs
624 .iter()
625 .filter(|v| (v.pos[0] - min_x).abs() < 1e-3)
626 .map(|v| v.pos[1])
627 .fold(f32::MAX, f32::min);
628 assert!((min_x - 200.0).abs() < 1e-3);
629 assert!((top_left_arc_y - (200.0 + 40.0)).abs() < 1e-3);
630 }
631
632 #[test]
633 fn textured_sprite_without_a_loaded_texture_falls_back_to_fill() {
634 let mut s = sprite(0.0, 0.0, 10.0, 10.0, [0.2, 0.3, 0.4, 1.0]);
635 s.texture = Some(TextureHandle(42));
636 let calls = build_sprite_calls(
638 core::slice::from_ref(&s),
639 Some(5),
640 &no_slots(),
641 [0.0, 0.0],
642 &no_clips(),
643 &no_layers(),
644 );
645 assert_eq!(calls[0].atlas_slot, 5);
646 assert!(calls[0].vertices[0].uv[0] < 0.0);
647 assert_eq!(calls[0].vertices[0].mode, 0.0);
648 }
649
650 #[test]
651 fn invisible_sprite_is_skipped() {
652 let mut s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 1.0, 1.0, 1.0]);
653 s.visible = false;
654 assert!(
655 build_sprite_calls(
656 core::slice::from_ref(&s),
657 Some(0),
658 &no_slots(),
659 [0.0, 0.0],
660 &no_clips(),
661 &no_layers()
662 )
663 .is_empty()
664 );
665 }
666
667 #[test]
668 fn zero_alpha_sprite_is_skipped() {
669 let s = sprite(0.0, 0.0, 100.0, 100.0, [1.0, 1.0, 1.0, 0.0]);
670 assert!(
671 build_sprite_calls(
672 core::slice::from_ref(&s),
673 Some(0),
674 &no_slots(),
675 [0.0, 0.0],
676 &no_clips(),
677 &no_layers()
678 )
679 .is_empty()
680 );
681 }
682
683 #[test]
684 fn view_owned_sprite_scales_to_window() {
685 let mut s = sprite(100.0, 100.0, 200.0, 100.0, [1.0, 1.0, 1.0, 1.0]);
689 s.screen = Some(AssetId(7));
690 let calls = build_sprite_calls(
691 core::slice::from_ref(&s),
692 Some(0),
693 &no_slots(),
694 [2560.0, 1440.0],
695 &no_clips(),
696 &no_layers(),
697 );
698 assert_eq!(calls.len(), 1);
699 assert_eq!(calls[0].vertices[0].pos, [200.0, 200.0]);
700 assert_eq!(calls[0].vertices[2].pos, [600.0, 400.0]);
701 }
702
703 #[test]
704 fn view_owned_full_canvas_backdrop_fills_window() {
705 let mut s = sprite(0.0, 0.0, 1280.0, 720.0, [0.0, 0.0, 0.0, 0.5]);
708 s.screen = Some(AssetId(7));
709 let calls = build_sprite_calls(
710 core::slice::from_ref(&s),
711 Some(0),
712 &no_slots(),
713 [2560.0, 1440.0],
714 &no_clips(),
715 &no_layers(),
716 );
717 assert_eq!(calls.len(), 1);
718 assert_eq!(calls[0].vertices[0].pos, [0.0, 0.0]);
719 assert_eq!(calls[0].vertices[2].pos, [2560.0, 1440.0]);
720 }
721
722 #[test]
723 fn cover_sprite_fills_the_window_and_crops_the_overflow() {
724 let mut s = sprite(0.0, 0.0, 1280.0, 720.0, [1.0, 1.0, 1.0, 1.0]);
728 s.screen = Some(AssetId(7));
729 s.fit = SpriteFit::Cover;
730 let calls = build_sprite_calls(
731 core::slice::from_ref(&s),
732 Some(0),
733 &no_slots(),
734 [1024.0, 768.0],
735 &no_clips(),
736 &no_layers(),
737 );
738 let scale = 768.0 / 720.0;
739 let overflow = (1280.0 * scale - 1024.0) / 2.0;
740 let vs = &calls[0].vertices;
741 assert!(
742 (vs[0].pos[0] - -overflow).abs() < 1e-3,
743 "x0={}",
744 vs[0].pos[0]
745 );
746 assert!((vs[0].pos[1]).abs() < 1e-3, "y0={}", vs[0].pos[1]);
747 assert!(
748 (vs[2].pos[0] - (1024.0 + overflow)).abs() < 1e-3,
749 "x1={}",
750 vs[2].pos[0]
751 );
752 assert!((vs[2].pos[1] - 768.0).abs() < 1e-3, "y1={}", vs[2].pos[1]);
753 }
754
755 #[test]
756 fn cover_sprite_anchored_to_the_canvas_bottom_stays_flush() {
757 let mut s = sprite(400.0, 100.0, 480.0, 620.0, [1.0, 1.0, 1.0, 1.0]);
761 s.screen = Some(AssetId(7));
762 s.fit = SpriteFit::Cover;
763 let calls = build_sprite_calls(
764 core::slice::from_ref(&s),
765 Some(0),
766 &no_slots(),
767 [1024.0, 768.0],
768 &no_clips(),
769 &no_layers(),
770 );
771 let bottom = calls[0].vertices[2].pos[1];
772 assert!((bottom - 768.0).abs() < 1e-3, "bottom={bottom}");
773 }
774
775 #[test]
776 fn view_less_sprite_keeps_literal_pixels() {
777 let s = sprite(10.0, 20.0, 100.0, 50.0, [0.5, 0.5, 0.5, 1.0]);
779 let calls = build_sprite_calls(
780 core::slice::from_ref(&s),
781 Some(0),
782 &no_slots(),
783 [2560.0, 1440.0],
784 &no_clips(),
785 &no_layers(),
786 );
787 assert_eq!(calls[0].vertices[0].pos, [10.0, 20.0]);
788 assert_eq!(calls[0].vertices[2].pos, [110.0, 70.0]);
789 }
790
791 #[test]
792 fn clipped_element_carries_window_space_clip_rect() {
793 let mut s = sprite(100.0, 100.0, 50.0, 50.0, [1.0, 1.0, 1.0, 1.0]);
797 s.asset_id = AssetId(7);
798 s.screen = Some(AssetId(1));
799 let mut clips = no_clips();
800 clips.insert(AssetId(7), [200.0, 200.0, 200.0, 60.0]);
804 let calls = build_sprite_calls(
805 core::slice::from_ref(&s),
806 Some(0),
807 &no_slots(),
808 [2560.0, 1440.0],
809 &clips,
810 &no_layers(),
811 );
812 let clip = calls[0].clip_rect.expect("clipped sprite has a clip rect");
813 assert!((clip[0] - 400.0).abs() < 1e-3, "x={}", clip[0]);
814 assert!((clip[1] - 400.0).abs() < 1e-3, "y={}", clip[1]);
815 assert!((clip[2] - 400.0).abs() < 1e-3, "w={}", clip[2]);
816 assert!((clip[3] - 120.0).abs() < 1e-3, "h={}", clip[3]);
817
818 let mut other = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
820 other.asset_id = AssetId(9);
821 other.screen = Some(AssetId(1));
822 let calls = build_sprite_calls(
823 core::slice::from_ref(&other),
824 Some(0),
825 &no_slots(),
826 [2560.0, 1440.0],
827 &clips,
828 &no_layers(),
829 );
830 assert!(calls[0].clip_rect.is_none());
831 }
832
833 #[test]
836 fn sprite_call_takes_its_layer_from_the_map() {
837 let mut mapped = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
838 mapped.asset_id = AssetId(42);
839 let mut layers = OverlayLayers::new();
840 layers.insert(AssetId(42), 7);
841 let calls = build_sprite_calls(
842 core::slice::from_ref(&mapped),
843 Some(0),
844 &no_slots(),
845 [100.0, 100.0],
846 &no_clips(),
847 &layers,
848 );
849 assert_eq!(calls[0].layer, 7);
850
851 let mut unmapped = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
852 unmapped.asset_id = AssetId(99);
853 let calls = build_sprite_calls(
854 core::slice::from_ref(&unmapped),
855 Some(0),
856 &no_slots(),
857 [100.0, 100.0],
858 &no_clips(),
859 &layers,
860 );
861 assert_eq!(calls[0].layer, 0, "an unmapped id is layer 0");
862 }
863
864 #[test]
867 fn follow_cursor_sprites_are_skipped() {
868 let mut s = sprite(0.0, 0.0, 10.0, 10.0, [1.0, 1.0, 1.0, 1.0]);
869 s.follow_cursor = true;
870 assert!(
871 build_sprite_calls(
872 core::slice::from_ref(&s),
873 Some(0),
874 &no_slots(),
875 [0.0, 0.0],
876 &no_clips(),
877 &no_layers()
878 )
879 .is_empty()
880 );
881 }
882}