1use std::sync::Arc;
2
3use crate::geometry::{Rect, Size, Transform, Vec2};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct Color {
7 pub r: f32,
8
9 pub g: f32,
10
11 pub b: f32,
12
13 pub a: f32,
14}
15
16impl Color {
17 pub const WHITE: Self = Self {
18 r: 1.0,
19 g: 1.0,
20 b: 1.0,
21 a: 1.0,
22 };
23
24 pub const BLACK: Self = Self {
25 r: 0.0,
26 g: 0.0,
27 b: 0.0,
28 a: 1.0,
29 };
30
31 pub const RED: Self = Self {
32 r: 1.0,
33 g: 0.0,
34 b: 0.0,
35 a: 1.0,
36 };
37
38 pub const GREEN: Self = Self {
39 r: 0.0,
40 g: 1.0,
41 b: 0.0,
42 a: 1.0,
43 };
44
45 pub const BLUE: Self = Self {
46 r: 0.0,
47 g: 0.0,
48 b: 1.0,
49 a: 1.0,
50 };
51
52 pub const TRANSPARENT: Self = Self {
53 r: 0.0,
54 g: 0.0,
55 b: 0.0,
56 a: 0.0,
57 };
58
59 pub const ALICEBLUE: Self = Self::hex(0xF0F8FF);
60
61 pub const ANTIQUEWHITE: Self = Self::hex(0xFAEBD7);
62
63 pub const AQUA: Self = Self::hex(0x00FFFF);
64
65 pub const AQUAMARINE: Self = Self::hex(0x7FFFD4);
66
67 pub const AZURE: Self = Self::hex(0xF0FFFF);
68
69 pub const BEIGE: Self = Self::hex(0xF5F5DC);
70
71 pub const BISQUE: Self = Self::hex(0xFFE4C4);
72
73 pub const BLANCHEDALMOND: Self = Self::hex(0xFFEBCD);
74
75 pub const BLUEVIOLET: Self = Self::hex(0x8A2BE2);
76
77 pub const BROWN: Self = Self::hex(0xA52A2A);
78
79 pub const BURLYWOOD: Self = Self::hex(0xDEB887);
80
81 pub const CADETBLUE: Self = Self::hex(0x5F9EA0);
82
83 pub const CHARTREUSE: Self = Self::hex(0x7FFF00);
84
85 pub const CHOCOLATE: Self = Self::hex(0xD2691E);
86
87 pub const CORAL: Self = Self::hex(0xFF7F50);
88
89 pub const CORNFLOWERBLUE: Self = Self::hex(0x6495ED);
90
91 pub const CORNSILK: Self = Self::hex(0xFFF8DC);
92
93 pub const CRIMSON: Self = Self::hex(0xDC143C);
94
95 pub const CYAN: Self = Self::hex(0x00FFFF);
96
97 pub const DARKBLUE: Self = Self::hex(0x00008B);
98
99 pub const DARKCYAN: Self = Self::hex(0x008B8B);
100
101 pub const DARKGOLDENROD: Self = Self::hex(0xB8860B);
102
103 pub const DARKGRAY: Self = Self::hex(0xA9A9A9);
104
105 pub const DARKGREEN: Self = Self::hex(0x006400);
106
107 pub const DARKGREY: Self = Self::hex(0xA9A9A9);
108
109 pub const DARKKHAKI: Self = Self::hex(0xBDB76B);
110
111 pub const DARKMAGENTA: Self = Self::hex(0x8B008B);
112
113 pub const DARKOLIVEGREEN: Self = Self::hex(0x556B2F);
114
115 pub const DARKORANGE: Self = Self::hex(0xFF8C00);
116
117 pub const DARKORCHID: Self = Self::hex(0x9932CC);
118
119 pub const DARKRED: Self = Self::hex(0x8B0000);
120
121 pub const DARKSALMON: Self = Self::hex(0xE9967A);
122
123 pub const DARKSEAGREEN: Self = Self::hex(0x8FBC8F);
124
125 pub const DARKSLATEBLUE: Self = Self::hex(0x483D8B);
126
127 pub const DARKSLATEGRAY: Self = Self::hex(0x2F4F4F);
128
129 pub const DARKSLATEGREY: Self = Self::hex(0x2F4F4F);
130
131 pub const DARKTURQUOISE: Self = Self::hex(0x00CED1);
132
133 pub const DARKVIOLET: Self = Self::hex(0x9400D3);
134
135 pub const DEEPPINK: Self = Self::hex(0xFF1493);
136
137 pub const DEEPSKYBLUE: Self = Self::hex(0x00BFFF);
138
139 pub const DIMGRAY: Self = Self::hex(0x696969);
140
141 pub const DIMGREY: Self = Self::hex(0x696969);
142
143 pub const DODGERBLUE: Self = Self::hex(0x1E90FF);
144
145 pub const FIREBRICK: Self = Self::hex(0xB22222);
146
147 pub const FLORALWHITE: Self = Self::hex(0xFFFAF0);
148
149 pub const FORESTGREEN: Self = Self::hex(0x228B22);
150
151 pub const FUCHSIA: Self = Self::hex(0xFF00FF);
152
153 pub const GAINSBORO: Self = Self::hex(0xDCDCDC);
154
155 pub const GHOSTWHITE: Self = Self::hex(0xF8F8FF);
156
157 pub const GOLD: Self = Self::hex(0xFFD700);
158
159 pub const GOLDENROD: Self = Self::hex(0xDAA520);
160
161 pub const GRAY: Self = Self::hex(0x808080);
162
163 pub const GREENYELLOW: Self = Self::hex(0xADFF2F);
164
165 pub const GREY: Self = Self::hex(0x808080);
166
167 pub const HONEYDEW: Self = Self::hex(0xF0FFF0);
168
169 pub const HOTPINK: Self = Self::hex(0xFF69B4);
170
171 pub const INDIANRED: Self = Self::hex(0xCD5C5C);
172
173 pub const INDIGO: Self = Self::hex(0x4B0082);
174
175 pub const IVORY: Self = Self::hex(0xFFFFF0);
176
177 pub const KHAKI: Self = Self::hex(0xF0E68C);
178
179 pub const LAVENDER: Self = Self::hex(0xE6E6FA);
180
181 pub const LAVENDERBLUSH: Self = Self::hex(0xFFF0F5);
182
183 pub const LAWNGREEN: Self = Self::hex(0x7CFC00);
184
185 pub const LEMONCHIFFON: Self = Self::hex(0xFFFACD);
186
187 pub const LIGHTBLUE: Self = Self::hex(0xADD8E6);
188
189 pub const LIGHTCORAL: Self = Self::hex(0xF08080);
190
191 pub const LIGHTCYAN: Self = Self::hex(0xE0FFFF);
192
193 pub const LIGHTGOLDENRODYELLOW: Self = Self::hex(0xFAFAD2);
194
195 pub const LIGHTGRAY: Self = Self::hex(0xD3D3D3);
196
197 pub const LIGHTGREEN: Self = Self::hex(0x90EE90);
198
199 pub const LIGHTGREY: Self = Self::hex(0xD3D3D3);
200
201 pub const LIGHTPINK: Self = Self::hex(0xFFB6C1);
202
203 pub const LIGHTSALMON: Self = Self::hex(0xFFA07A);
204
205 pub const LIGHTSEAGREEN: Self = Self::hex(0x20B2AA);
206
207 pub const LIGHTSKYBLUE: Self = Self::hex(0x87CEFA);
208
209 pub const LIGHTSLATEGRAY: Self = Self::hex(0x778899);
210
211 pub const LIGHTSLATEGREY: Self = Self::hex(0x778899);
212
213 pub const LIGHTSTEELBLUE: Self = Self::hex(0xB0C4DE);
214
215 pub const LIGHTYELLOW: Self = Self::hex(0xFFFFE0);
216
217 pub const LIME: Self = Self::hex(0x00FF00);
218
219 pub const LIMEGREEN: Self = Self::hex(0x32CD32);
220
221 pub const LINEN: Self = Self::hex(0xFAF0E6);
222
223 pub const MAGENTA: Self = Self::hex(0xFF00FF);
224
225 pub const MAROON: Self = Self::hex(0x800000);
226
227 pub const MEDIUMAQUAMARINE: Self = Self::hex(0x66CDAA);
228
229 pub const MEDIUMBLUE: Self = Self::hex(0x0000CD);
230
231 pub const MEDIUMORCHID: Self = Self::hex(0xBA55D3);
232
233 pub const MEDIUMPURPLE: Self = Self::hex(0x9370DB);
234
235 pub const MEDIUMSEAGREEN: Self = Self::hex(0x3CB371);
236
237 pub const MEDIUMSLATEBLUE: Self = Self::hex(0x7B68EE);
238
239 pub const MEDIUMSPRINGGREEN: Self = Self::hex(0x00FA9A);
240
241 pub const MEDIUMTURQUOISE: Self = Self::hex(0x48D1CC);
242
243 pub const MEDIUMVIOLETRED: Self = Self::hex(0xC71585);
244
245 pub const MIDNIGHTBLUE: Self = Self::hex(0x191970);
246
247 pub const MINTCREAM: Self = Self::hex(0xF5FFFA);
248
249 pub const MISTYROSE: Self = Self::hex(0xFFE4E1);
250
251 pub const MOCCASIN: Self = Self::hex(0xFFE4B5);
252
253 pub const NAVAJOWHITE: Self = Self::hex(0xFFDEAD);
254
255 pub const NAVY: Self = Self::hex(0x000080);
256
257 pub const OLDLACE: Self = Self::hex(0xFDF5E6);
258
259 pub const OLIVE: Self = Self::hex(0x808000);
260
261 pub const OLIVEDRAB: Self = Self::hex(0x6B8E23);
262
263 pub const ORANGE: Self = Self::hex(0xFFA500);
264
265 pub const ORANGERED: Self = Self::hex(0xFF4500);
266
267 pub const ORCHID: Self = Self::hex(0xDA70D6);
268
269 pub const PALEGOLDENROD: Self = Self::hex(0xEEE8AA);
270
271 pub const PALEGREEN: Self = Self::hex(0x98FB98);
272
273 pub const PALETURQUOISE: Self = Self::hex(0xAFEEEE);
274
275 pub const PALEVIOLETRED: Self = Self::hex(0xDB7093);
276
277 pub const PAPAYAWHIP: Self = Self::hex(0xFFEFD5);
278
279 pub const PEACHPUFF: Self = Self::hex(0xFFDAB9);
280
281 pub const PERU: Self = Self::hex(0xCD853F);
282
283 pub const PINK: Self = Self::hex(0xFFC0CB);
284
285 pub const PLUM: Self = Self::hex(0xDDA0DD);
286
287 pub const POWDERBLUE: Self = Self::hex(0xB0E0E6);
288
289 pub const PURPLE: Self = Self::hex(0x800080);
290
291 pub const REBECCAPURPLE: Self = Self::hex(0x663399);
292
293 pub const ROSYBROWN: Self = Self::hex(0xBC8F8F);
294
295 pub const ROYALBLUE: Self = Self::hex(0x4169E1);
296
297 pub const SADDLEBROWN: Self = Self::hex(0x8B4513);
298
299 pub const SALMON: Self = Self::hex(0xFA8072);
300
301 pub const SANDYBROWN: Self = Self::hex(0xF4A460);
302
303 pub const SEAGREEN: Self = Self::hex(0x2E8B57);
304
305 pub const SEASHELL: Self = Self::hex(0xFFF5EE);
306
307 pub const SIENNA: Self = Self::hex(0xA0522D);
308
309 pub const SILVER: Self = Self::hex(0xC0C0C0);
310
311 pub const SKYBLUE: Self = Self::hex(0x87CEEB);
312
313 pub const SLATEBLUE: Self = Self::hex(0x6A5ACD);
314
315 pub const SLATEGRAY: Self = Self::hex(0x708090);
316
317 pub const SLATEGREY: Self = Self::hex(0x708090);
318
319 pub const SNOW: Self = Self::hex(0xFFFAFA);
320
321 pub const SPRINGGREEN: Self = Self::hex(0x00FF7F);
322
323 pub const STEELBLUE: Self = Self::hex(0x4682B4);
324
325 pub const TAN: Self = Self::hex(0xD2B48C);
326
327 pub const TEAL: Self = Self::hex(0x008080);
328
329 pub const THISTLE: Self = Self::hex(0xD8BFD8);
330
331 pub const TOMATO: Self = Self::hex(0xFF6347);
332
333 pub const TURQUOISE: Self = Self::hex(0x40E0D0);
334
335 pub const VIOLET: Self = Self::hex(0xEE82EE);
336
337 pub const WHEAT: Self = Self::hex(0xF5DEB3);
338
339 pub const WHITESMOKE: Self = Self::hex(0xF5F5F5);
340
341 pub const YELLOW: Self = Self::hex(0xFFFF00);
342
343 pub const YELLOWGREEN: Self = Self::hex(0x9ACD32);
344
345 pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
346 Self { r, g, b, a: 1.0 }
347 }
348
349 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
350 Self { r, g, b, a }
351 }
352
353 pub const fn rgb8(r: u8, g: u8, b: u8) -> Self {
354 Self {
355 r: r as f32 / 255.0,
356 g: g as f32 / 255.0,
357 b: b as f32 / 255.0,
358 a: 1.0,
359 }
360 }
361
362 pub const fn hex(hex: u32) -> Self {
363 Self::rgb8((hex >> 16) as u8, (hex >> 8) as u8, hex as u8)
364 }
365
366 pub const fn hexa(hex: u32) -> Self {
367 let mut c = Self::hex(hex >> 8);
368 c.a = (hex as u8) as f32 / 255.0;
369 c
370 }
371
372 pub fn to_linear(self) -> Self {
373 fn channel(c: f32) -> f32 {
374 if c <= 0.040_45 {
375 c / 12.92
376 } else {
377 ((c + 0.055) / 1.055).powf(2.4)
378 }
379 }
380 Self {
381 r: channel(self.r),
382 g: channel(self.g),
383 b: channel(self.b),
384 a: self.a,
385 }
386 }
387
388 pub fn from_linear(self) -> Self {
389 fn channel(c: f32) -> f32 {
390 if c <= 0.003_130_8 {
391 c * 12.92
392 } else {
393 1.055 * c.powf(1.0 / 2.4) - 0.055
394 }
395 }
396 Self {
397 r: channel(self.r),
398 g: channel(self.g),
399 b: channel(self.b),
400 a: self.a,
401 }
402 }
403
404 pub fn parse_hex(s: &str) -> Option<Self> {
405 let h = s.strip_prefix('#').unwrap_or(s);
406 let h = h.strip_prefix("0x").unwrap_or(h);
407 let d = |c: u8| (c as char).to_digit(16);
408 let bytes: [u8; 4] = match h.len() {
409 3 | 4 => {
410 let mut out = [0u8; 4];
411 let mut it = h.bytes();
412 for slot in out.iter_mut().take(h.len()) {
413 let v = d(it.next()?)? as u8;
414 *slot = v * 16 + v;
415 }
416 if h.len() == 3 {
417 out[3] = 0xFF;
418 }
419 out
420 }
421 6 | 8 => {
422 let b = h.as_bytes();
423 let pair =
424 |i: usize| d(b[i]).and_then(|hi| d(b[i + 1]).map(|lo| (hi * 16 + lo) as u8));
425 let mut out = [pair(0)?, pair(2)?, pair(4)?, 0xFF];
426 if h.len() == 8 {
427 out[3] = pair(6)?;
428 }
429 out
430 }
431 _ => return None,
432 };
433 Some(Self {
434 r: bytes[0] as f32 / 255.0,
435 g: bytes[1] as f32 / 255.0,
436 b: bytes[2] as f32 / 255.0,
437 a: bytes[3] as f32 / 255.0,
438 })
439 }
440
441 pub fn to_rgb8(self) -> (u8, u8, u8) {
442 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
443 (q(self.r), q(self.g), q(self.b))
444 }
445
446 pub fn to_rgba8(self) -> (u8, u8, u8, u8) {
447 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
448 (q(self.r), q(self.g), q(self.b), q(self.a))
449 }
450
451 pub fn to_hex_string(self) -> String {
452 let (r, g, b) = self.to_rgb8();
453 format!("#{r:02X}{g:02X}{b:02X}")
454 }
455
456 pub fn to_hex8_string(self) -> String {
457 let (r, g, b, a) = self.to_rgba8();
458 format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
459 }
460
461 pub fn with_alpha(self, alpha: f32) -> Self {
462 Self {
463 a: alpha.clamp(0.0, 1.0),
464 ..self
465 }
466 }
467
468 pub fn to_hsva(self) -> (f32, f32, f32, f32) {
469 let (r, g, b) = (
470 self.r.clamp(0.0, 1.0),
471 self.g.clamp(0.0, 1.0),
472 self.b.clamp(0.0, 1.0),
473 );
474 let max = r.max(g).max(b);
475 let min = r.min(g).min(b);
476 let d = max - min;
477 let v = max;
478 let s = if max <= f32::EPSILON { 0.0 } else { d / max };
479 let h = if d <= f32::EPSILON {
480 0.0
481 } else if max == r {
482 (((g - b) / d) % 6.0 + 6.0) % 6.0 / 6.0
483 } else if max == g {
484 ((b - r) / d + 2.0) / 6.0
485 } else {
486 ((r - g) / d + 4.0) / 6.0
487 };
488 (h, s, v, self.a.clamp(0.0, 1.0))
489 }
490
491 pub fn from_hsva(h: f32, s: f32, v: f32, a: f32) -> Self {
492 let h = if h.is_finite() {
493 h.rem_euclid(1.0)
494 } else {
495 0.0
496 };
497 let (s, v, a) = (s.clamp(0.0, 1.0), v.clamp(0.0, 1.0), a.clamp(0.0, 1.0));
498 if s <= f32::EPSILON {
499 return Self {
500 r: v,
501 g: v,
502 b: v,
503 a,
504 };
505 }
506 let sector = h * 6.0;
507 let i = sector.floor() as i32;
508 let f = sector - sector.floor();
509 let p = v * (1.0 - s);
510 let q = v * (1.0 - s * f);
511 let t = v * (1.0 - s * (1.0 - f));
512 let (r, g, b) = match i.rem_euclid(6) {
513 0 => (v, t, p),
514 1 => (q, v, p),
515 2 => (p, v, t),
516 3 => (p, q, v),
517 4 => (t, p, v),
518 _ => (v, p, q),
519 };
520 Self { r, g, b, a }
521 }
522
523 pub fn from_hsl(h_deg: f32, s: f32, l: f32) -> Self {
524 let h = if h_deg.is_finite() {
525 h_deg.rem_euclid(360.0)
526 } else {
527 0.0
528 };
529 let (s, l) = (s.clamp(0.0, 1.0), l.clamp(0.0, 1.0));
530 let a = s * l.min(1.0 - l);
531 let f = |n: f32| {
532 let k = (n + h / 30.0) % 12.0;
533 l - a * (-1.0f32).max((k - 3.0).min(9.0 - k).min(1.0))
534 };
535 Self {
536 r: f(0.0).clamp(0.0, 1.0),
537 g: f(8.0).clamp(0.0, 1.0),
538 b: f(4.0).clamp(0.0, 1.0),
539 a: 1.0,
540 }
541 }
542
543 pub fn to_hsl(self) -> (f32, f32, f32) {
544 let (r, g, b) = (
545 self.r.clamp(0.0, 1.0),
546 self.g.clamp(0.0, 1.0),
547 self.b.clamp(0.0, 1.0),
548 );
549 let mx = r.max(g).max(b);
550 let mn = r.min(g).min(b);
551 let d = mx - mn;
552 let l = ((mx + mn) * 0.5).clamp(0.0, 1.0);
553 if d <= f32::EPSILON {
554 return (0.0, 0.0, l);
555 }
556 let s = (d / (1.0 - (2.0 * l - 1.0).abs())).clamp(0.0, 1.0);
557 let h = if mx == r {
558 ((g - b) / d) % 6.0
559 } else if mx == g {
560 (b - r) / d + 2.0
561 } else {
562 (r - g) / d + 4.0
563 } * 60.0;
564 (h.rem_euclid(360.0), s, l)
565 }
566
567 pub fn to_hex6(self) -> String {
568 let (r, g, b) = self.to_rgb8();
569 format!("{r:02X}{g:02X}{b:02X}")
570 }
571}
572
573impl Default for Color {
574 fn default() -> Self {
575 Self::TRANSPARENT
576 }
577}
578
579#[derive(Clone, Copy, Debug, PartialEq)]
580pub struct Quad {
581 pub rect: Rect,
582
583 pub color: Color,
584
585 pub radius: f32,
586
587 pub border_color: Color,
588
589 pub border_width: f32,
590}
591
592impl Quad {
593 pub fn has_border(self) -> bool {
594 self.border_width > 0.0 && self.border_color.a > 0.0
595 }
596}
597
598#[derive(Clone, Copy, Debug, PartialEq)]
599pub struct Shadow {
600 pub rect: Rect,
601
602 pub color: Color,
603
604 pub radius: f32,
605
606 pub blur: f32,
607
608 pub spread: f32,
609
610 pub offset: Vec2,
611
612 pub inset: bool,
613}
614
615impl Shadow {
616 pub fn is_visible(&self) -> bool {
617 !self.rect.is_empty()
618 && self.color.a > 0.0
619 && self.blur >= 0.0
620 && self.blur.is_finite()
621 && (!self.inset || self.blur > 0.0)
622 }
623
624 pub fn bounds(&self) -> Rect {
625 if self.inset {
626 return self.rect;
627 }
628 let margin = self.blur * 0.5 + self.spread.max(0.0);
629 Rect::from_xywh(
630 self.rect.left() + self.offset.x - margin,
631 self.rect.top() + self.offset.y - margin,
632 self.rect.size.width + margin * 2.0,
633 self.rect.size.height + margin * 2.0,
634 )
635 }
636}
637
638#[derive(Clone, Copy, Debug, PartialEq)]
639pub struct GlyphQuad {
640 pub rect: Rect,
641
642 pub uv: [f32; 4],
643
644 pub color_glyph: bool,
645}
646
647#[derive(Clone, Debug, Default, PartialEq)]
648pub struct TextDraw {
649 pub color: Color,
650
651 pub glyphs: Vec<GlyphQuad>,
652}
653
654#[derive(Clone, Debug, PartialEq)]
655#[non_exhaustive]
656pub enum ImageSource {
657 File(String),
658
659 Rgba {
660 id: u64,
661
662 width: u32,
663
664 height: u32,
665
666 data: Arc<Vec<u8>>,
667 },
668
669 Dynamic {
670 id: u64,
671
672 width: u32,
673
674 height: u32,
675
676 data: Arc<Vec<u8>>,
677
678 version: u64,
679 },
680}
681
682impl ImageSource {
683 pub fn cache_key(&self) -> u64 {
684 match self {
685 Self::File(path) => {
686 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
687 for byte in path.as_bytes() {
688 hash ^= u64::from(*byte);
689 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
690 }
691 hash ^ 0x9e37_79b9_7f4a_7c15
692 }
693 Self::Rgba { id, .. } => *id,
694 Self::Dynamic { id, .. } => *id,
695 }
696 }
697
698 pub fn dynamic_version(&self) -> Option<u64> {
699 match self {
700 Self::Dynamic { version, .. } => Some(*version),
701 Self::File(_) | Self::Rgba { .. } => None,
702 }
703 }
704
705 pub fn extent(&self) -> Option<(u32, u32)> {
706 match self {
707 Self::File(_) => None,
708 Self::Rgba { width, height, .. } | Self::Dynamic { width, height, .. } => {
709 Some((*width, *height))
710 }
711 }
712 }
713}
714
715#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
716#[non_exhaustive]
717pub enum ImageSampling {
718 #[default]
719 Linear,
720
721 Nearest,
722}
723
724#[derive(Clone, Debug, PartialEq)]
725pub struct ImageDraw {
726 pub rect: Rect,
727
728 pub uv: [f32; 4],
729
730 pub source: ImageSource,
731
732 pub sampling: ImageSampling,
733}
734
735#[derive(Clone, Debug, PartialEq)]
736pub struct WaveformDraw {
737 pub bars: Vec<Rect>,
738
739 pub color: Color,
740}
741
742#[derive(Clone, Copy, Debug, PartialEq)]
743pub struct BackdropDraw {
744 pub rect: Rect,
745
746 pub radius: f32,
747
748 pub blur: f32,
749
750 pub dim: Color,
751
752 pub noise: f32,
753
754 pub mask: SpanMask,
755}
756
757impl BackdropDraw {
758 pub fn is_visible(self) -> bool {
759 !self.rect.is_empty()
760 && (self.blur > 0.0 && self.blur.is_finite() || self.dim.a > 0.0 || self.noise > 0.0)
761 }
762}
763
764#[derive(Clone, Copy, Debug, PartialEq)]
765#[non_exhaustive]
766#[derive(Default)]
767pub enum SpanMask {
768 #[default]
769 None,
770
771 Linear {
772 from: f32,
773 to: f32,
774 angle_rad: f32,
775 },
776
777 Uniform(f32),
778}
779
780impl SpanMask {
781 pub fn mask_vec(self) -> [f32; 4] {
782 match self {
783 SpanMask::None => [0.0, 0.0, 0.0, 0.0],
784 SpanMask::Linear {
785 from,
786 to,
787 angle_rad,
788 } => {
789 let angle = if angle_rad.is_finite() {
790 angle_rad
791 } else {
792 0.0
793 };
794 [1.0, from.clamp(0.0, 1.0), to.clamp(0.0, 1.0), angle]
795 }
796 SpanMask::Uniform(v) => [2.0, v.clamp(0.0, 1.0), 0.0, 0.0],
797 }
798 }
799
800 pub fn is_none(self) -> bool {
801 matches!(self, SpanMask::None)
802 }
803}
804
805#[derive(Clone, Copy, Debug, PartialEq)]
806pub struct BackdropSpan {
807 pub at: usize,
808
809 pub draw: BackdropDraw,
810
811 pub clip: Option<RoundedClip>,
812}
813
814#[derive(Clone, Copy, Debug, PartialEq)]
815pub struct FilterDraw {
816 pub rect: Rect,
817
818 pub radius: f32,
819
820 pub blur: f32,
821
822 pub mask: SpanMask,
823}
824
825impl FilterDraw {
826 pub fn is_visible(self) -> bool {
827 !self.rect.is_empty() && self.blur > 0.0 && self.blur.is_finite()
828 }
829}
830
831#[derive(Clone, Copy, Debug, PartialEq)]
832pub struct FilterSpan {
833 pub start: usize,
834
835 pub end: usize,
836
837 pub draw: FilterDraw,
838
839 pub clip: Option<RoundedClip>,
840}
841
842#[derive(Clone, Copy, Debug, PartialEq)]
847pub struct OpacitySpan {
848 pub start: usize,
849
850 pub end: usize,
851
852 pub opacity: f32,
853
854 pub depth: usize,
855}
856
857#[derive(Clone, Debug, PartialEq)]
859#[non_exhaustive]
860pub enum OpacityNode {
861 Segment { start: usize, end: usize },
863
864 Group {
866 opacity: f32,
867 children: Vec<OpacityNode>,
868 },
869}
870
871#[derive(Clone, Debug, PartialEq)]
872pub struct GradientDraw {
873 pub rect: Rect,
874
875 pub radius: f32,
876
877 pub kind: crate::brush::GradientKind,
878
879 pub stops: crate::brush::GradientStops,
880}
881
882impl GradientDraw {
883 pub fn is_visible(&self) -> bool {
884 !self.rect.is_empty() && !self.stops.is_empty()
885 }
886}
887
888#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
890pub struct ShaderId(pub u32);
891
892impl ShaderId {
893 #[must_use]
897 pub const fn new(raw: u32) -> Self {
898 Self(raw)
899 }
900
901 #[must_use]
903 pub const fn get(self) -> u32 {
904 self.0
905 }
906}
907
908#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
910pub struct RenderTargetId(pub u64);
911
912impl RenderTargetId {
913 #[must_use]
915 pub const fn new(raw: u64) -> Self {
916 Self(raw)
917 }
918
919 #[must_use]
921 pub const fn get(self) -> u64 {
922 self.0
923 }
924}
925
926#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
928#[non_exhaustive]
929pub enum BlendMode {
930 #[default]
932 Normal,
933
934 Add,
936
937 Multiply,
939
940 Screen,
942
943 Subtract,
945
946 Overlay,
948
949 Darken,
951
952 Lighten,
954
955 ColorDodge,
957
958 ColorBurn,
960
961 HardLight,
963
964 SoftLight,
966
967 Difference,
969
970 Exclusion,
972
973 Hue,
975
976 Saturation,
978
979 Color,
981
982 Luminosity,
984
985 AlphaMask,
987}
988
989impl BlendMode {
990 #[must_use]
992 pub const fn as_u32(self) -> u32 {
993 match self {
994 Self::Normal => 0,
995 Self::Add => 1,
996 Self::Multiply => 2,
997 Self::Screen => 3,
998 Self::Subtract => 4,
999 Self::Overlay => 5,
1000 Self::Darken => 6,
1001 Self::Lighten => 7,
1002 Self::ColorDodge => 8,
1003 Self::ColorBurn => 9,
1004 Self::HardLight => 10,
1005 Self::SoftLight => 11,
1006 Self::Difference => 12,
1007 Self::Exclusion => 13,
1008 Self::Hue => 14,
1009 Self::Saturation => 15,
1010 Self::Color => 16,
1011 Self::Luminosity => 17,
1012 Self::AlphaMask => 18,
1013 }
1014 }
1015}
1016
1017#[derive(Clone, Debug, PartialEq)]
1019#[non_exhaustive]
1020pub enum TexInput {
1021 Target(RenderTargetId),
1023
1024 Image(ImageSource),
1026}
1027
1028impl From<RenderTargetId> for TexInput {
1029 fn from(target: RenderTargetId) -> Self {
1030 Self::Target(target)
1031 }
1032}
1033
1034#[derive(Clone, Debug, PartialEq)]
1036pub struct ShaderDraw {
1037 pub rect: Rect,
1039
1040 pub uv: [f32; 4],
1042
1043 pub shader: ShaderId,
1045
1046 pub uniforms: Arc<[u8]>,
1051
1052 pub textures: Arc<[(u32, TexInput)]>,
1054
1055 pub blend: BlendMode,
1057
1058 pub opacity: f32,
1060}
1061
1062#[derive(Clone, Debug, PartialEq)]
1064#[non_exhaustive]
1065pub enum PassNode {
1066 Render {
1068 target: RenderTargetId,
1070
1071 size: Size,
1073
1074 blend: BlendMode,
1076
1077 items: std::ops::Range<usize>,
1079 },
1080
1081 Present {
1083 items: std::ops::Range<usize>,
1085 },
1086}
1087
1088#[derive(Clone, Copy, Debug, PartialEq)]
1090pub struct BlitDraw {
1091 pub rect: Rect,
1093
1094 pub uv: [f32; 4],
1096
1097 pub target: RenderTargetId,
1099
1100 pub opacity: f32,
1102}
1103
1104pub const MAX_SHADER_UNIFORM_BYTES: usize = 16 * 4 * 4;
1106
1107#[derive(Clone, Copy, Debug, PartialEq)]
1109pub struct TargetBegin {
1110 pub target: RenderTargetId,
1112
1113 pub size: Size,
1115}
1116
1117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1119pub struct TargetEnd {
1120 pub target: RenderTargetId,
1122}
1123
1124#[derive(Clone, Debug, PartialEq)]
1125#[non_exhaustive]
1126pub enum RenderItem {
1127 Quad(Quad),
1128
1129 Text(TextDraw),
1130
1131 Image(ImageDraw),
1132
1133 Shadow(Shadow),
1134
1135 Waveform(WaveformDraw),
1136
1137 Gradient(GradientDraw),
1138
1139 Shader(ShaderDraw),
1141
1142 Blit {
1144 rect: Rect,
1145 uv: [f32; 4],
1146 target: RenderTargetId,
1147 blend: BlendMode,
1148 },
1149
1150 TargetBegin {
1152 id: RenderTargetId,
1153 size: Size,
1154 blend: BlendMode,
1155 },
1156
1157 TargetEnd {
1159 id: RenderTargetId,
1160 },
1161}
1162
1163pub const MAX_WAVEFORM_BARS: usize = 8192;
1164
1165fn clip_uv(rect: Rect, uv: [f32; 4], clipped: Rect) -> [f32; 4] {
1166 let (w, h) = (rect.size.width, rect.size.height);
1167 if w <= 0.0 || h <= 0.0 {
1168 return uv;
1169 }
1170 let [u0, v0, u1, v1] = uv;
1171 let fx0 = (clipped.left() - rect.left()) / w;
1172 let fx1 = (clipped.right() - rect.left()) / w;
1173 let fy0 = (clipped.top() - rect.top()) / h;
1174 let fy1 = (clipped.bottom() - rect.top()) / h;
1175 [
1176 u0 + fx0 * (u1 - u0),
1177 v0 + fy0 * (v1 - v0),
1178 u0 + fx1 * (u1 - u0),
1179 v0 + fy1 * (v1 - v0),
1180 ]
1181}
1182
1183#[derive(Clone, Copy, Debug, PartialEq)]
1184pub struct RoundedClip {
1185 pub rect: Rect,
1186
1187 pub radius: f32,
1188}
1189
1190#[derive(Clone, Copy, Debug, PartialEq)]
1191struct ClipEntry {
1192 clip: RoundedClip,
1193
1194 under: Transform,
1195}
1196
1197impl RoundedClip {
1198 pub fn new(rect: Rect, radius: f32) -> Self {
1199 let radius = if radius.is_finite() && radius > 0.0 {
1200 radius
1201 } else {
1202 0.0
1203 };
1204 Self { rect, radius }
1205 }
1206
1207 pub fn is_rounded(self) -> bool {
1208 self.radius > 0.0 && !self.rect.is_empty()
1209 }
1210}
1211
1212fn push_opacity_segment(nodes: &mut Vec<OpacityNode>, start: usize, end: usize) {
1213 if start >= end {
1214 return;
1215 }
1216 if let Some(OpacityNode::Segment { end: prev_end, .. }) = nodes.last_mut()
1217 && *prev_end == start
1218 {
1219 *prev_end = end;
1220 return;
1221 }
1222 nodes.push(OpacityNode::Segment { start, end });
1223}
1224
1225fn build_opacity_nodes(
1226 spans: &[&OpacitySpan],
1227 mut index: usize,
1228 lo: usize,
1229 hi: usize,
1230) -> (Vec<OpacityNode>, usize) {
1231 let mut nodes = Vec::new();
1232 let mut cursor = lo;
1233 while index < spans.len() {
1234 let span = spans[index];
1235 if span.start >= hi {
1236 break;
1237 }
1238 if span.start < cursor || span.end > hi || span.end <= span.start {
1239 index += 1;
1240 continue;
1241 }
1242 if span.start > cursor {
1243 push_opacity_segment(&mut nodes, cursor, span.start);
1244 }
1245 let (children, next) = build_opacity_nodes(spans, index + 1, span.start, span.end);
1246 nodes.push(OpacityNode::Group {
1247 opacity: span.opacity,
1248 children,
1249 });
1250 cursor = span.end;
1251 index = next;
1252 if cursor >= hi {
1253 break;
1254 }
1255 }
1256 if cursor < hi {
1257 push_opacity_segment(&mut nodes, cursor, hi);
1258 }
1259 (nodes, index)
1260}
1261
1262#[derive(Clone, Copy, Debug, PartialEq)]
1263struct OpacityEntry {
1264 opacity: f32,
1265
1266 start: usize,
1267
1268 depth: usize,
1269}
1270
1271#[derive(Clone, Debug, Default)]
1272pub struct RenderList {
1273 items: Vec<RenderItem>,
1274 clip_stack: Vec<ClipEntry>,
1275 opacity_stack: Vec<OpacityEntry>,
1276 opacity_spans: Vec<OpacitySpan>,
1277 transform_stack: Vec<Transform>,
1278
1279 item_clips: Vec<Option<RoundedClip>>,
1280 item_transforms: Vec<Transform>,
1281
1282 backdrops: Vec<BackdropSpan>,
1283
1284 filters: Vec<FilterSpan>,
1285
1286 next_target: u64,
1287
1288 target_count: u32,
1289
1290 shader_count: u32,
1291}
1292
1293#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1295pub struct RenderMark {
1296 items: usize,
1297 item_clips: usize,
1298 item_transforms: usize,
1299 backdrops: usize,
1300 filters: usize,
1301 opacity_spans: usize,
1302 clip_depth: usize,
1303 opacity_depth: usize,
1304 transform_depth: usize,
1305 target_count: u32,
1306 shader_count: u32,
1307}
1308
1309impl RenderList {
1310 pub fn new() -> Self {
1311 Self::default()
1312 }
1313
1314 pub fn clear(&mut self) {
1315 self.items.clear();
1316 self.clip_stack.clear();
1317 self.opacity_stack.clear();
1318 self.opacity_spans.clear();
1319 self.transform_stack.clear();
1320 self.item_clips.clear();
1321 self.item_transforms.clear();
1322 self.backdrops.clear();
1323 self.filters.clear();
1324 self.next_target = 0;
1325 self.target_count = 0;
1326 self.shader_count = 0;
1327 }
1328
1329 pub fn push_opacity(&mut self, opacity: f32) {
1330 let o = if opacity.is_finite() {
1331 opacity.clamp(0.0, 1.0)
1332 } else {
1333 1.0
1334 };
1335 self.opacity_stack.push(OpacityEntry {
1336 opacity: o,
1337 start: self.items.len(),
1338 depth: self.opacity_stack.len(),
1339 });
1340 }
1341
1342 fn close_opacity(&mut self) {
1343 let Some(entry) = self.opacity_stack.pop() else {
1344 return;
1345 };
1346 if entry.opacity < 1.0 - f32::EPSILON && entry.start < self.items.len() {
1347 self.opacity_spans.push(OpacitySpan {
1348 start: entry.start,
1349 end: self.items.len(),
1350 opacity: entry.opacity,
1351 depth: entry.depth,
1352 });
1353 }
1354 }
1355
1356 pub fn pop_opacity(&mut self) {
1357 self.close_opacity();
1358 }
1359
1360 pub fn opacity_depth(&self) -> usize {
1361 self.opacity_stack.len()
1362 }
1363
1364 pub fn truncate_opacity(&mut self, depth: usize) {
1365 while self.opacity_stack.len() > depth {
1366 self.close_opacity();
1367 }
1368 }
1369
1370 pub fn active_opacity(&self) -> f32 {
1371 self.opacity_stack
1372 .iter()
1373 .map(|e| e.opacity)
1374 .fold(1.0, |a, b| a * b)
1375 }
1376
1377 fn opacity_visible(&self) -> bool {
1378 self.active_opacity() > 0.0
1379 }
1380
1381 pub fn opacity_spans(&self) -> &[OpacitySpan] {
1382 &self.opacity_spans
1383 }
1384
1385 pub fn has_opacity_groups(&self) -> bool {
1386 !self.opacity_spans.is_empty()
1387 }
1388
1389 pub fn opacity_at(&self, index: usize) -> f32 {
1393 let mut opacity = self
1394 .opacity_spans
1395 .iter()
1396 .filter(|span| span.start <= index && index < span.end)
1397 .map(|span| span.opacity)
1398 .fold(1.0, |a, b| a * b);
1399 for entry in &self.opacity_stack {
1400 if entry.start <= index {
1401 opacity *= entry.opacity;
1402 }
1403 }
1404 opacity
1405 }
1406
1407 pub fn opacity_plan(&self) -> Vec<OpacityNode> {
1412 let mut spans: Vec<&OpacitySpan> = self.opacity_spans.iter().collect();
1413 spans.sort_by(|a, b| {
1414 a.start
1415 .cmp(&b.start)
1416 .then_with(|| b.end.cmp(&a.end))
1417 .then_with(|| a.depth.cmp(&b.depth))
1418 });
1419 let (nodes, _) = build_opacity_nodes(&spans, 0, 0, self.items.len());
1420 nodes
1421 }
1422
1423 pub fn has_targets(&self) -> bool {
1425 self.target_count > 0
1426 }
1427
1428 pub fn has_shaders(&self) -> bool {
1430 self.shader_count > 0
1431 }
1432
1433 pub fn target_plan(&self) -> Vec<PassNode> {
1445 let mut nodes = Vec::new();
1446 let mut open: Vec<(RenderTargetId, Size, BlendMode, usize)> = Vec::new();
1447 let mut segment = 0usize;
1448 for (index, item) in self.items.iter().enumerate() {
1449 match item {
1450 RenderItem::TargetBegin { id, size, blend } => {
1451 if open.is_empty() && segment < index {
1452 nodes.push(PassNode::Present {
1453 items: segment..index,
1454 });
1455 }
1456 open.push((*id, *size, *blend, index + 1));
1457 }
1458 RenderItem::TargetEnd { id } => {
1459 let Some(position) = open.iter().rposition(|(open_id, ..)| open_id == id)
1460 else {
1461 continue;
1462 };
1463 let (target, size, blend, start) = open.remove(position);
1464 if start <= index {
1465 nodes.push(PassNode::Render {
1466 target,
1467 size,
1468 blend,
1469 items: start..index,
1470 });
1471 }
1472 if open.is_empty() {
1473 segment = index + 1;
1474 }
1475 }
1476 _ => {}
1477 }
1478 }
1479 if segment < self.items.len() {
1480 nodes.push(PassNode::Present {
1481 items: segment..self.items.len(),
1482 });
1483 }
1484 nodes
1485 }
1486
1487 pub fn push_transform(&mut self, t: Transform) {
1488 let active = self.active_transform().then(t);
1489 self.transform_stack.push(active);
1490 }
1491
1492 pub fn pop_transform(&mut self) {
1493 self.transform_stack.pop();
1494 }
1495
1496 pub fn transform_depth(&self) -> usize {
1497 self.transform_stack.len()
1498 }
1499
1500 pub fn truncate_transforms(&mut self, depth: usize) {
1501 while self.transform_stack.len() > depth {
1502 self.transform_stack.pop();
1503 }
1504 }
1505
1506 pub fn active_transform(&self) -> Transform {
1507 self.transform_stack.last().copied().unwrap_or_default()
1508 }
1509
1510 pub fn item_transform(&self, index: usize) -> Transform {
1511 self.item_transforms.get(index).copied().unwrap_or_default()
1512 }
1513
1514 pub fn item_transforms(&self) -> &[Transform] {
1515 &self.item_transforms
1516 }
1517
1518 pub fn push_clip(&mut self, rect: Rect) {
1519 self.clip_stack.push(ClipEntry {
1520 clip: RoundedClip::new(rect, 0.0),
1521 under: self.active_transform(),
1522 });
1523 }
1524
1525 pub fn push_rounded_clip(&mut self, rect: Rect, radius: f32) {
1526 self.clip_stack.push(ClipEntry {
1527 clip: RoundedClip::new(rect, radius),
1528 under: self.active_transform(),
1529 });
1530 }
1531
1532 pub fn clip_depth(&self) -> usize {
1533 self.clip_stack.len()
1534 }
1535
1536 pub fn active_clip(&self) -> Option<Rect> {
1537 let mut acc: Option<Rect> = None;
1538 for c in &self.clip_stack {
1539 acc = Some(match acc {
1540 Some(a) => a.intersect(c.clip.rect),
1541 None => c.clip.rect,
1542 });
1543 }
1544 acc
1545 }
1546
1547 pub fn active_clip_for(&self, t_item: Transform) -> Option<Rect> {
1548 if self.clip_stack.is_empty() {
1549 return None;
1550 }
1551 let inv = t_item.inverse()?;
1552 let mut acc: Option<Rect> = None;
1553 for c in &self.clip_stack {
1554 let mapped = c.under.then(inv).apply_rect_aabb(c.clip.rect);
1555 acc = Some(match acc {
1556 Some(a) => a.intersect(mapped),
1557 None => mapped,
1558 });
1559 }
1560 acc
1561 }
1562
1563 pub fn active_rounded_clip(&self) -> Option<RoundedClip> {
1564 self.clip_stack
1565 .iter()
1566 .rev()
1567 .find(|c| c.clip.is_rounded())
1568 .map(|c| c.clip)
1569 }
1570
1571 pub fn item_rounded_clip(&self, index: usize) -> Option<RoundedClip> {
1572 self.item_clips.get(index).copied().flatten()
1573 }
1574
1575 pub fn truncate_clips(&mut self, depth: usize) {
1576 while self.clip_stack.len() > depth {
1577 self.clip_stack.pop();
1578 }
1579 }
1580
1581 pub fn pop_clip(&mut self) {
1582 self.clip_stack.pop();
1583 }
1584
1585 fn clip_rect(&self, mut r: Rect) -> Option<Rect> {
1586 for c in &self.clip_stack {
1587 r = r.intersect(c.clip.rect);
1588 if r.is_empty() {
1589 return None;
1590 }
1591 }
1592 Some(r)
1593 }
1594
1595 pub fn push_quad(&mut self, rect: Rect, color: Color, radius: f32) {
1596 self.push_bordered_quad(rect, color, radius, 0.0, Color::TRANSPARENT);
1597 }
1598
1599 pub fn push_bordered_quad(
1600 &mut self,
1601 rect: Rect,
1602 color: Color,
1603 radius: f32,
1604 border_width: f32,
1605 border_color: Color,
1606 ) {
1607 if !self.opacity_visible() {
1608 return;
1609 }
1610 let t = self.active_transform();
1611
1612 if !t.is_identity() {
1613 if let Some(local) = self.active_clip_for(t)
1614 && !rect.intersects(local)
1615 {
1616 return;
1617 }
1618 let has_border = border_width > 0.0 && border_color.a > 0.0;
1619 if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1620 return;
1621 }
1622 let rounded = self.active_rounded_clip();
1623 self.items.push(RenderItem::Quad(Quad {
1624 rect,
1625 color,
1626 radius,
1627 border_color,
1628 border_width: if has_border { border_width } else { 0.0 },
1629 }));
1630 self.item_clips.push(rounded);
1631 self.item_transforms.push(t);
1632 return;
1633 }
1634 let has_border = border_width > 0.0 && border_color.a > 0.0;
1635 let plain = radius <= 0.0 && !has_border;
1641 let rect = if plain {
1642 match self.clip_rect(rect) {
1643 Some(cropped) => cropped,
1644 None => return,
1645 }
1646 } else {
1647 if let Some(clip) = self.active_clip()
1648 && !rect.intersects(clip)
1649 {
1650 return;
1651 }
1652 rect
1653 };
1654 if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1655 return;
1656 }
1657 let rounded = if plain {
1662 self.active_rounded_clip()
1663 } else {
1664 self.active_clip().map(|clip| {
1665 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1666 RoundedClip::new(clip, radius)
1667 })
1668 };
1669 self.items.push(RenderItem::Quad(Quad {
1670 rect,
1671 color,
1672 radius,
1673 border_color,
1674 border_width: if has_border { border_width } else { 0.0 },
1675 }));
1676 self.item_clips.push(rounded);
1677 self.item_transforms.push(t);
1678 }
1679
1680 pub fn push_shadow(&mut self, shadow: Shadow) {
1681 if !self.opacity_visible() {
1682 return;
1683 }
1684 let t = self.active_transform();
1685 if !shadow.is_visible() {
1686 return;
1687 }
1688 if !t.is_identity() {
1689 if let Some(local) = self.active_clip_for(t)
1690 && !shadow.bounds().intersects(local)
1691 {
1692 return;
1693 }
1694 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1695 let clip = self
1696 .active_clip()
1697 .map(|clip_rect| RoundedClip::new(clip_rect, radius));
1698 self.items.push(RenderItem::Shadow(shadow));
1699 self.item_clips.push(clip);
1700 self.item_transforms.push(t);
1701 return;
1702 }
1703 let Some(clip_rect) = self.active_clip() else {
1704 self.items.push(RenderItem::Shadow(shadow));
1705 self.item_clips.push(None);
1706 self.item_transforms.push(t);
1707 return;
1708 };
1709 if clip_rect.is_empty() {
1710 return;
1711 }
1712
1713 if !shadow.bounds().intersects(clip_rect) {
1714 return;
1715 }
1716
1717 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1718 self.items.push(RenderItem::Shadow(shadow));
1719 self.item_clips
1720 .push(Some(RoundedClip::new(clip_rect, radius)));
1721 self.item_transforms.push(self.active_transform());
1722 }
1723
1724 pub fn push_text(&mut self, mut draw: TextDraw) {
1725 if !self.opacity_visible() {
1726 return;
1727 }
1728 let t = self.active_transform();
1729 if draw.color.a <= 0.0 {
1730 return;
1731 }
1732 if !t.is_identity() {
1733 if let Some(local) = self.active_clip_for(t) {
1734 let keep = draw.glyphs.iter().any(|g| g.rect.intersects(local));
1735 if !keep {
1736 return;
1737 }
1738 }
1739 draw.glyphs.retain(|g| !g.rect.is_empty());
1740 if !draw.glyphs.is_empty() {
1741 let rounded = self.active_rounded_clip();
1742 self.items.push(RenderItem::Text(draw));
1743 self.item_clips.push(rounded);
1744 self.item_transforms.push(t);
1745 }
1746 return;
1747 }
1748 draw.glyphs.retain_mut(|g| {
1749 let Some(cr) = self.clip_rect(g.rect) else {
1750 return false;
1751 };
1752 g.uv = clip_uv(g.rect, g.uv, cr);
1753 g.rect = cr;
1754 true
1755 });
1756 if !draw.glyphs.is_empty() {
1757 let rounded = self.active_rounded_clip();
1758 self.items.push(RenderItem::Text(draw));
1759 self.item_clips.push(rounded);
1760 self.item_transforms.push(t);
1761 }
1762 }
1763
1764 pub fn push_image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) {
1765 self.push_image_sampled(rect, uv, source, ImageSampling::Linear);
1766 }
1767
1768 pub fn push_image_sampled(
1769 &mut self,
1770 rect: Rect,
1771 uv: [f32; 4],
1772 source: ImageSource,
1773 sampling: ImageSampling,
1774 ) {
1775 if !self.opacity_visible() {
1776 return;
1777 }
1778 let t = self.active_transform();
1779 if !t.is_identity() {
1780 if let Some(local) = self.active_clip_for(t)
1781 && !rect.intersects(local)
1782 {
1783 return;
1784 }
1785 if rect.is_empty() {
1786 return;
1787 }
1788 let rounded = self.active_rounded_clip();
1789 self.items.push(RenderItem::Image(ImageDraw {
1790 rect,
1791 uv,
1792 source,
1793 sampling,
1794 }));
1795 self.item_clips.push(rounded);
1796 self.item_transforms.push(t);
1797 return;
1798 }
1799 let Some(cr) = self.clip_rect(rect) else {
1800 return;
1801 };
1802 if cr.is_empty() {
1803 return;
1804 }
1805 let uv = clip_uv(rect, uv, cr);
1806 let rounded = self.active_rounded_clip();
1807 self.items.push(RenderItem::Image(ImageDraw {
1808 rect: cr,
1809 uv,
1810 source,
1811 sampling,
1812 }));
1813 self.item_clips.push(rounded);
1814 self.item_transforms.push(self.active_transform());
1815 }
1816
1817 pub fn push_waveform(&mut self, bars: &[Rect], color: Color) {
1818 if !self.opacity_visible() {
1819 return;
1820 }
1821 let t = self.active_transform();
1822 if bars.is_empty() || color.a <= 0.0 {
1823 return;
1824 }
1825 if !t.is_identity() {
1826 if let Some(local) = self.active_clip_for(t) {
1827 let keep = bars.iter().any(|r| !r.is_empty() && r.intersects(local));
1828 if !keep {
1829 return;
1830 }
1831 }
1832 let clipped: Vec<Rect> = bars
1833 .iter()
1834 .copied()
1835 .filter(|r| !r.is_empty())
1836 .take(MAX_WAVEFORM_BARS)
1837 .collect();
1838 if clipped.is_empty() {
1839 return;
1840 }
1841 let rounded = self.active_rounded_clip();
1842 self.items.push(RenderItem::Waveform(WaveformDraw {
1843 bars: clipped,
1844 color,
1845 }));
1846 self.item_clips.push(rounded);
1847 self.item_transforms.push(t);
1848 return;
1849 }
1850 let clipped: Vec<Rect> = match self.active_clip() {
1851 None => bars
1852 .iter()
1853 .copied()
1854 .filter(|r| !r.is_empty())
1855 .take(MAX_WAVEFORM_BARS)
1856 .collect(),
1857 Some(clip) if clip.is_empty() => return,
1858 Some(clip) => bars
1859 .iter()
1860 .filter_map(|r| {
1861 let c = r.intersect(clip);
1862 if c.is_empty() { None } else { Some(c) }
1863 })
1864 .take(MAX_WAVEFORM_BARS)
1865 .collect(),
1866 };
1867 if clipped.is_empty() {
1868 return;
1869 }
1870 let rounded = self.active_rounded_clip();
1871 self.items.push(RenderItem::Waveform(WaveformDraw {
1872 bars: clipped,
1873 color,
1874 }));
1875 self.item_clips.push(rounded);
1876 self.item_transforms.push(self.active_transform());
1877 }
1878
1879 pub fn items(&self) -> &[RenderItem] {
1880 &self.items
1881 }
1882
1883 pub fn push_shader(&mut self, mut draw: ShaderDraw) {
1888 if !self.opacity_visible() || draw.rect.is_empty() {
1889 return;
1890 }
1891 draw.opacity = if draw.opacity.is_finite() {
1892 draw.opacity.clamp(0.0, 1.0)
1893 } else {
1894 0.0
1895 };
1896 if draw.opacity <= 0.0 {
1897 return;
1898 }
1899 if draw.uniforms.len() > MAX_SHADER_UNIFORM_BYTES {
1900 draw.uniforms = Arc::from(&draw.uniforms[..MAX_SHADER_UNIFORM_BYTES]);
1901 }
1902 if !draw.textures.is_empty() {
1903 let mut textures = draw.textures.to_vec();
1904 textures.sort_by_key(|(binding, _)| *binding);
1905 let mut deduped: Vec<(u32, TexInput)> = Vec::with_capacity(textures.len());
1906 for pair in textures {
1907 if let Some(last) = deduped.last_mut()
1908 && last.0 == pair.0
1909 {
1910 *last = pair;
1911 } else {
1912 deduped.push(pair);
1913 }
1914 }
1915 draw.textures = Arc::from(deduped);
1916 }
1917 let t = self.active_transform();
1918 if let Some(local) = self.active_clip_for(t)
1919 && !draw.rect.intersects(local)
1920 {
1921 return;
1922 }
1923 let rounded = self.active_rounded_clip();
1924 self.items.push(RenderItem::Shader(draw));
1925 self.item_clips.push(rounded);
1926 self.item_transforms.push(t);
1927 self.shader_count += 1;
1928 }
1929
1930 pub fn push_blit(
1932 &mut self,
1933 rect: Rect,
1934 uv: [f32; 4],
1935 target: RenderTargetId,
1936 blend: BlendMode,
1937 ) {
1938 if !self.opacity_visible() || rect.is_empty() {
1939 return;
1940 }
1941 let t = self.active_transform();
1942 let (rect, uv) = if t.is_identity() {
1943 let Some(cropped) = self.clip_rect(rect) else {
1944 return;
1945 };
1946 if cropped.is_empty() {
1947 return;
1948 }
1949 let uv = clip_uv(rect, uv, cropped);
1950 (cropped, uv)
1951 } else {
1952 if let Some(local) = self.active_clip_for(t)
1953 && !rect.intersects(local)
1954 {
1955 return;
1956 }
1957 (rect, uv)
1958 };
1959 let rounded = self.active_rounded_clip();
1960 self.items.push(RenderItem::Blit {
1961 rect,
1962 uv,
1963 target,
1964 blend,
1965 });
1966 self.item_clips.push(rounded);
1967 self.item_transforms.push(t);
1968 }
1969
1970 #[must_use]
1976 pub fn begin_target(&mut self, size: Size, blend: BlendMode) -> RenderTargetId {
1977 let id = RenderTargetId::new(self.next_target);
1978 self.next_target += 1;
1979 self.items.push(RenderItem::TargetBegin { id, size, blend });
1980 self.item_clips.push(None);
1981 self.item_transforms.push(Transform::default());
1982 self.target_count += 1;
1983 id
1984 }
1985
1986 pub fn end_target(&mut self, id: RenderTargetId) {
1990 self.items.push(RenderItem::TargetEnd { id });
1991 self.item_clips.push(None);
1992 self.item_transforms.push(Transform::default());
1993 }
1994
1995 pub fn shaders(&self) -> impl Iterator<Item = &ShaderDraw> {
1997 self.items.iter().filter_map(|i| match i {
1998 RenderItem::Shader(s) => Some(s),
1999 _ => None,
2000 })
2001 }
2002
2003 pub fn push_gradient(
2004 &mut self,
2005 rect: Rect,
2006 radius: f32,
2007 kind: crate::brush::GradientKind,
2008 stops: crate::brush::GradientStops,
2009 ) {
2010 if !self.opacity_visible() {
2011 return;
2012 }
2013 if rect.is_empty() || stops.is_empty() {
2014 return;
2015 }
2016 let t = self.active_transform();
2017
2018 if !t.is_identity() {
2019 if let Some(local) = self.active_clip_for(t)
2020 && !rect.intersects(local)
2021 {
2022 return;
2023 }
2024 } else {
2025 let Some(clipped) = self.clip_rect(rect) else {
2026 return;
2027 };
2028 if clipped.is_empty() {
2029 return;
2030 }
2031
2032 return self.push_gradient_inner(clipped, radius, kind, stops, t);
2033 }
2034 self.push_gradient_inner(rect, radius, kind, stops, t)
2035 }
2036
2037 fn push_gradient_inner(
2038 &mut self,
2039 rect: Rect,
2040 radius: f32,
2041 kind: crate::brush::GradientKind,
2042 stops: crate::brush::GradientStops,
2043 t: Transform,
2044 ) {
2045 if rect.is_empty() || stops.is_empty() {
2046 return;
2047 }
2048
2049 if stops.stops().iter().all(|s| s.color.a <= 0.0) {
2050 return;
2051 }
2052 let radius = if radius.is_finite() {
2053 radius
2054 .clamp(0.0, rect.size.width.min(rect.size.height) * 0.5)
2055 .max(0.0)
2056 } else {
2057 0.0
2058 };
2059 let rounded = self.active_rounded_clip();
2060 self.items.push(RenderItem::Gradient(GradientDraw {
2061 rect,
2062 radius,
2063 kind,
2064 stops,
2065 }));
2066 self.item_clips.push(rounded);
2067 self.item_transforms.push(t);
2068 }
2069
2070 pub fn push_backdrop(
2071 &mut self,
2072 rect: Rect,
2073 radius: f32,
2074 blur: f32,
2075 dim: Color,
2076 noise: f32,
2077 mask: SpanMask,
2078 ) {
2079 if !self.opacity_visible() {
2080 return;
2081 }
2082 let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
2083 let noise = if noise.is_finite() {
2084 noise.clamp(0.0, 1.0)
2085 } else {
2086 0.0
2087 };
2088 let opacity = self.active_opacity();
2089 let dim = Color {
2090 a: dim.a * opacity,
2091 ..dim
2092 };
2093 if !(blur > 0.0 || dim.a > 0.0 || noise > 0.0) {
2094 return;
2095 }
2096 let Some(rect) = self.clip_rect(rect) else {
2097 return;
2098 };
2099 if rect.is_empty() {
2100 return;
2101 }
2102 let radius = if radius.is_finite() {
2103 radius.max(0.0)
2104 } else {
2105 0.0
2106 };
2107 let draw = BackdropDraw {
2108 rect,
2109 radius,
2110 blur,
2111 dim,
2112 noise: noise * opacity,
2113 mask,
2114 };
2115 if !draw.is_visible() {
2116 return;
2117 }
2118 self.backdrops.push(BackdropSpan {
2119 at: self.items.len(),
2120 draw,
2121 clip: self.active_rounded_clip(),
2122 });
2123 }
2124
2125 pub fn backdrops(&self) -> &[BackdropSpan] {
2126 &self.backdrops
2127 }
2128
2129 pub fn has_backdrops(&self) -> bool {
2130 !self.backdrops.is_empty()
2131 }
2132
2133 pub fn push_filter_begin(
2134 &mut self,
2135 rect: Rect,
2136 radius: f32,
2137 blur: f32,
2138 mask: SpanMask,
2139 ) -> Option<usize> {
2140 if !self.opacity_visible() {
2141 return None;
2142 }
2143 let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
2144 if !(blur > 0.0) {
2145 return None;
2146 }
2147 let rect = self.clip_rect(rect)?;
2148 if rect.is_empty() {
2149 return None;
2150 }
2151 let radius = if radius.is_finite() {
2152 radius.max(0.0)
2153 } else {
2154 0.0
2155 };
2156 let draw = FilterDraw {
2157 rect,
2158 radius,
2159 blur,
2160 mask,
2161 };
2162 if !draw.is_visible() {
2163 return None;
2164 }
2165 let start = self.items.len();
2166 self.filters.push(FilterSpan {
2167 start,
2168 end: start,
2169 draw,
2170 clip: self.active_rounded_clip(),
2171 });
2172 Some(self.filters.len() - 1)
2173 }
2174
2175 pub fn push_filter_end(&mut self, handle: usize) {
2176 let end = self.items.len();
2177 let Some(span) = self.filters.get_mut(handle) else {
2178 return;
2179 };
2180 span.end = end;
2181 if span.end <= span.start {
2182 self.filters.remove(handle);
2183 }
2184 }
2185
2186 pub fn filter_spans(&self) -> &[FilterSpan] {
2187 &self.filters
2188 }
2189
2190 pub fn has_filters(&self) -> bool {
2191 !self.filters.is_empty()
2192 }
2193
2194 pub fn item_clips(&self) -> &[Option<RoundedClip>] {
2195 &self.item_clips
2196 }
2197
2198 pub fn quads(&self) -> impl Iterator<Item = &Quad> {
2199 self.items.iter().filter_map(|i| match i {
2200 RenderItem::Quad(q) => Some(q),
2201 _ => None,
2202 })
2203 }
2204
2205 pub fn images(&self) -> impl Iterator<Item = &ImageDraw> {
2206 self.items.iter().filter_map(|i| match i {
2207 RenderItem::Image(d) => Some(d),
2208 _ => None,
2209 })
2210 }
2211
2212 pub fn shadows(&self) -> impl Iterator<Item = &Shadow> {
2213 self.items.iter().filter_map(|i| match i {
2214 RenderItem::Shadow(s) => Some(s),
2215 _ => None,
2216 })
2217 }
2218
2219 pub fn waveforms(&self) -> impl Iterator<Item = &WaveformDraw> {
2220 self.items.iter().filter_map(|i| match i {
2221 RenderItem::Waveform(w) => Some(w),
2222 _ => None,
2223 })
2224 }
2225
2226 pub fn gradients(&self) -> impl Iterator<Item = &GradientDraw> {
2227 self.items.iter().filter_map(|i| match i {
2228 RenderItem::Gradient(g) => Some(g),
2229 _ => None,
2230 })
2231 }
2232
2233 pub fn gradient_count(&self) -> usize {
2234 self.items
2235 .iter()
2236 .filter(|i| matches!(i, RenderItem::Gradient(_)))
2237 .count()
2238 }
2239
2240 pub fn waveform_bar_count(&self) -> usize {
2241 self.items
2242 .iter()
2243 .map(|i| match i {
2244 RenderItem::Waveform(w) => w.bars.len(),
2245 _ => 0,
2246 })
2247 .sum()
2248 }
2249
2250 pub fn len(&self) -> usize {
2251 self.items.len()
2252 }
2253
2254 pub fn is_empty(&self) -> bool {
2255 self.items.is_empty()
2256 }
2257
2258 pub fn mark(&self) -> RenderMark {
2260 RenderMark {
2261 items: self.items.len(),
2262 item_clips: self.item_clips.len(),
2263 item_transforms: self.item_transforms.len(),
2264 backdrops: self.backdrops.len(),
2265 filters: self.filters.len(),
2266 opacity_spans: self.opacity_spans.len(),
2267 clip_depth: self.clip_stack.len(),
2268 opacity_depth: self.opacity_stack.len(),
2269 transform_depth: self.transform_stack.len(),
2270 target_count: self.target_count,
2271 shader_count: self.shader_count,
2272 }
2273 }
2274
2275 pub fn suffix(&self, mark: RenderMark) -> Option<Self> {
2282 if mark.items > self.items.len()
2283 || mark.item_clips > self.item_clips.len()
2284 || mark.item_transforms > self.item_transforms.len()
2285 || mark.backdrops > self.backdrops.len()
2286 || mark.filters > self.filters.len()
2287 || mark.opacity_spans > self.opacity_spans.len()
2288 || mark.clip_depth != self.clip_stack.len()
2289 || mark.opacity_depth != self.opacity_stack.len()
2290 || mark.transform_depth != self.transform_stack.len()
2291 {
2292 return None;
2293 }
2294 let base = mark.items;
2295 Some(Self {
2296 items: self.items[mark.items..].to_vec(),
2297 clip_stack: Vec::new(),
2298 opacity_stack: Vec::new(),
2299 opacity_spans: self.opacity_spans[mark.opacity_spans..]
2300 .iter()
2301 .map(|span| OpacitySpan {
2302 start: span.start.saturating_sub(base),
2303 end: span.end.saturating_sub(base),
2304 ..*span
2305 })
2306 .collect(),
2307 transform_stack: Vec::new(),
2308 item_clips: self.item_clips[mark.item_clips..].to_vec(),
2309 item_transforms: self.item_transforms[mark.item_transforms..].to_vec(),
2310 backdrops: self.backdrops[mark.backdrops..]
2311 .iter()
2312 .map(|span| BackdropSpan {
2313 at: span.at.saturating_sub(base),
2314 ..*span
2315 })
2316 .collect(),
2317 filters: self.filters[mark.filters..]
2318 .iter()
2319 .map(|span| FilterSpan {
2320 start: span.start.saturating_sub(base),
2321 end: span.end.saturating_sub(base),
2322 ..*span
2323 })
2324 .collect(),
2325 next_target: self.next_target,
2326 target_count: self.target_count.saturating_sub(mark.target_count),
2327 shader_count: self.shader_count.saturating_sub(mark.shader_count),
2328 })
2329 }
2330
2331 pub fn append(&mut self, other: &Self) {
2337 debug_assert!(
2338 other.clip_stack.is_empty()
2339 && other.transform_stack.is_empty()
2340 && other.opacity_stack.is_empty(),
2341 "append expects a replayable layer suffix"
2342 );
2343 let base = self.items.len();
2344 self.items.extend_from_slice(&other.items);
2345 self.item_clips.extend_from_slice(&other.item_clips);
2346 self.item_transforms
2347 .extend_from_slice(&other.item_transforms);
2348 self.backdrops
2349 .extend(other.backdrops.iter().map(|span| BackdropSpan {
2350 at: span.at + base,
2351 ..*span
2352 }));
2353 self.filters
2354 .extend(other.filters.iter().map(|span| FilterSpan {
2355 start: span.start + base,
2356 end: span.end + base,
2357 ..*span
2358 }));
2359 self.opacity_spans
2360 .extend(other.opacity_spans.iter().map(|span| OpacitySpan {
2361 start: span.start + base,
2362 end: span.end + base,
2363 ..*span
2364 }));
2365 self.target_count = self.target_count.saturating_add(other.target_count);
2366 self.shader_count = self.shader_count.saturating_add(other.shader_count);
2367 }
2368}
2369
2370#[cfg(test)]
2371mod tests {
2372 use super::*;
2373
2374 fn tex(id: u64) -> TexInput {
2375 TexInput::Image(ImageSource::Rgba {
2376 id,
2377 width: 1,
2378 height: 1,
2379 data: Arc::new(vec![255; 4]),
2380 })
2381 }
2382
2383 #[test]
2384 fn color_construction() {
2385 assert_eq!(Color::rgb8(255, 0, 0), Color::RED);
2386 assert_eq!(Color::rgb(0.0, 1.0, 0.0).a, 1.0);
2387 assert_eq!(Color::TRANSPARENT.a, 0.0);
2388 }
2389
2390 #[test]
2391 fn hsl_roundtrips_through_rgb() {
2392 for (r, g, b) in [
2393 (255, 0, 0),
2394 (0, 255, 0),
2395 (0, 0, 255),
2396 (112, 143, 234),
2397 (185, 185, 185),
2398 ] {
2399 let c = Color::rgb8(r, g, b);
2400 let (h, s, l) = c.to_hsl();
2401 let back = Color::from_hsl(h, s, l).to_rgb8();
2402 assert!(
2403 (r as i16 - back.0 as i16).abs() <= 1
2404 && (g as i16 - back.1 as i16).abs() <= 1
2405 && (b as i16 - back.2 as i16).abs() <= 1,
2406 "#{r:02X}{g:02X}{b:02X} -> ({h:.1},{s:.3},{l:.3}) -> #{:02X}{:02X}{:02X}",
2407 back.0,
2408 back.1,
2409 back.2,
2410 );
2411 }
2412 assert_eq!(Color::from_hsl(0.0, 0.0, 0.5).to_rgb8(), (128, 128, 128));
2413
2414 assert_eq!(
2415 Color::from_hsl(720.0, 1.0, 0.5),
2416 Color::from_hsl(0.0, 1.0, 0.5)
2417 );
2418 assert_eq!(Color::from_hsl(0.0, 0.5, 0.5).to_hex6(), "BF4040");
2419 }
2420
2421 #[test]
2422 fn hex6_string_is_uppercase_without_hash() {
2423 assert_eq!(Color::rgb8(0x70, 0x8F, 0xEA).to_hex6(), "708FEA");
2424 }
2425
2426 #[test]
2427 fn hex_number_forms() {
2428 assert_eq!(Color::hex(0x0FA4FA), Color::rgb8(15, 164, 250));
2429 assert_eq!(Color::hex(0xFF0000), Color::RED);
2430 assert_eq!(Color::hex(0x000000), Color::BLACK);
2431
2432 assert_eq!(Color::hex(0x1_FF0000), Color::RED);
2433 let half = Color::hexa(0x0FA4FA80);
2434 assert_eq!(half.r, Color::hex(0x0FA4FA).r);
2435 assert_eq!(half.a, 128.0 / 255.0);
2436 assert_eq!(Color::hexa(0xFFFFFFFF).a, 1.0);
2437 }
2438
2439 #[test]
2440 fn parse_hex_forms() {
2441 assert_eq!(Color::parse_hex("#0FA4FA"), Some(Color::rgb8(15, 164, 250)));
2442 assert_eq!(Color::parse_hex("0fa4fa"), Some(Color::rgb8(15, 164, 250)));
2443 assert_eq!(
2444 Color::parse_hex("0x0FA4FA"),
2445 Some(Color::rgb8(15, 164, 250))
2446 );
2447 assert_eq!(Color::parse_hex("#fff"), Some(Color::WHITE));
2448 assert_eq!(Color::parse_hex("#0000"), Some(Color::TRANSPARENT));
2449 assert_eq!(Color::parse_hex("#0FA4FA80"), Some(Color::hexa(0x0FA4FA80)));
2450
2451 assert_eq!(Color::parse_hex("#0af"), Color::parse_hex("#00aaff"));
2452 }
2453
2454 #[test]
2455 fn parse_hex_rejects_garbage() {
2456 assert_eq!(Color::parse_hex(""), None);
2457 assert_eq!(Color::parse_hex("#"), None);
2458 assert_eq!(Color::parse_hex("bogus"), None);
2459 assert_eq!(Color::parse_hex("#12"), None);
2460 assert_eq!(Color::parse_hex("#12345"), None);
2461 assert_eq!(Color::parse_hex("#123456789"), None);
2462 assert_eq!(Color::parse_hex("#gggggg"), None);
2463 assert_eq!(Color::parse_hex(" #fff"), None, "no trimming");
2464 }
2465
2466 #[test]
2467 fn clip_balance_helpers() {
2468 let mut list = RenderList::new();
2469 assert_eq!(list.clip_depth(), 0);
2470 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2471 list.push_clip(Rect::from_xywh(0.0, 0.0, 5.0, 5.0));
2472 assert_eq!(list.clip_depth(), 2);
2473 list.truncate_clips(1);
2474 assert_eq!(list.clip_depth(), 1);
2475 list.truncate_clips(0);
2476 assert_eq!(list.clip_depth(), 0);
2477 }
2478
2479 #[test]
2480 fn opacity_stack_tracks_group_alpha() {
2481 let mut list = RenderList::new();
2482 assert_eq!(list.active_opacity(), 1.0);
2483 list.push_opacity(0.5);
2484 list.push_opacity(0.5);
2485 assert_eq!(list.active_opacity(), 0.25, "nestings multiply");
2486 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2487 let q = list.quads().next().expect("quad po push");
2488 assert_eq!(q.color.a, 1.0, "items keep their own alpha");
2489 assert!((list.opacity_at(0) - 0.25).abs() < 1e-6);
2490 list.pop_opacity();
2491 list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2492 assert_eq!(list.opacity_at(1), 0.5);
2493
2494 list.push_opacity(0.0);
2495 list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2496 assert_eq!(list.quads().count(), 2, "quad przy opacity 0 nie wchodzi");
2497 list.truncate_opacity(0);
2498 assert_eq!(list.opacity_depth(), 0);
2499 assert_eq!(list.active_opacity(), 1.0);
2500 assert_eq!(list.opacity_spans().len(), 2, "outer and inner span");
2501 assert_eq!(list.opacity_at(0), 0.25);
2502 assert_eq!(list.opacity_at(1), 0.5);
2503 assert_eq!(list.opacity_at(2), 1.0, "poza grupami");
2504
2505 list.push_opacity(f32::NAN);
2506 list.push_quad(Rect::from_xywh(60.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2507 let qs: Vec<_> = list.quads().collect();
2508 assert_eq!(qs[2].color.a, 1.0);
2509 assert_eq!(list.opacity_at(2), 1.0);
2510 list.clear();
2511 assert_eq!(list.opacity_depth(), 0, "clear also resets opacity");
2512 assert!(list.opacity_spans().is_empty());
2513 }
2514
2515 #[test]
2516 fn render_list_suffix_and_append_replay_items() {
2517 let mut list = RenderList::new();
2518 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2519 let mark = list.mark();
2520 list.push_clip(Rect::from_xywh(0.0, 0.0, 20.0, 20.0));
2521 list.push_quad(Rect::from_xywh(5.0, 5.0, 10.0, 10.0), Color::GREEN, 2.0);
2522 list.pop_clip();
2523
2524 let suffix = list.suffix(mark).expect("plain items are replayable");
2525 assert_eq!(suffix.len(), 1);
2526 assert_eq!(suffix.item_clips().len(), 1);
2527 assert_eq!(suffix.item_transforms().len(), 1);
2528
2529 let mut out = RenderList::new();
2530 out.append(&suffix);
2531 assert_eq!(out.len(), 1);
2532 assert_eq!(out.item_clips().len(), 1);
2533 assert_eq!(out.item_transforms().len(), 1);
2534 assert_eq!(
2535 out.quads().next().unwrap().rect,
2536 suffix.quads().next().unwrap().rect
2537 );
2538 }
2539
2540 #[test]
2541 fn render_list_suffix_rebases_composited_spans() {
2542 let mut list = RenderList::new();
2543 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2544 let mark = list.mark();
2545
2546 list.push_backdrop(
2547 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2548 0.0,
2549 4.0,
2550 Color::BLACK,
2551 0.0,
2552 SpanMask::None,
2553 );
2554 let filter = list
2555 .push_filter_begin(
2556 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2557 0.0,
2558 4.0,
2559 SpanMask::None,
2560 )
2561 .expect("visible filter opens a span");
2562 list.push_opacity(0.5);
2563 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
2564 list.pop_opacity();
2565 list.push_filter_end(filter);
2566
2567 let suffix = list.suffix(mark).expect("closed spans are replayable");
2568 assert_eq!(suffix.backdrops().len(), 1);
2569 assert_eq!(suffix.backdrops()[0].at, 0, "backdrop rebased to suffix");
2570 assert_eq!(suffix.filter_spans().len(), 1);
2571 assert_eq!(
2572 (suffix.filter_spans()[0].start, suffix.filter_spans()[0].end),
2573 (0, 1)
2574 );
2575 assert_eq!(suffix.opacity_spans().len(), 1);
2576 assert_eq!(
2577 (
2578 suffix.opacity_spans()[0].start,
2579 suffix.opacity_spans()[0].end
2580 ),
2581 (0, 1)
2582 );
2583
2584 let mut out = RenderList::new();
2585 out.push_quad(Rect::from_xywh(0.0, 0.0, 1.0, 1.0), Color::BLUE, 0.0);
2586 out.push_quad(Rect::from_xywh(0.0, 0.0, 1.0, 1.0), Color::BLUE, 0.0);
2587 out.append(&suffix);
2588 assert_eq!(out.backdrops()[0].at, 2);
2589 assert_eq!(out.filter_spans()[0].start, 2);
2590 assert_eq!(out.filter_spans()[0].end, 3);
2591 assert_eq!(out.opacity_spans()[0].start, 2);
2592 assert_eq!(out.opacity_spans()[0].end, 3);
2593 }
2594
2595 #[test]
2596 fn render_list_suffix_rejects_open_groups() {
2597 let mut list = RenderList::new();
2598 let mark = list.mark();
2599 list.push_opacity(0.5);
2600 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2601 assert!(
2602 list.suffix(mark).is_none(),
2603 "an open opacity group cannot be replayed"
2604 );
2605 list.pop_opacity();
2606
2607 let mark = list.mark();
2608 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2609 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2610 assert!(
2611 list.suffix(mark).is_none(),
2612 "an open clip cannot be replayed"
2613 );
2614 }
2615
2616 #[test]
2617 fn opacity_plan_nests_groups_and_merges_segments() {
2618 let mut list = RenderList::new();
2619 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2620 list.push_opacity(0.5);
2621 list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2622 list.push_opacity(0.5);
2623 list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2624 list.pop_opacity();
2625 list.push_quad(Rect::from_xywh(30.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2626 list.pop_opacity();
2627 list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2628
2629 assert_eq!(list.opacity_at(0), 1.0);
2630 assert_eq!(list.opacity_at(1), 0.5);
2631 assert_eq!(list.opacity_at(2), 0.25);
2632 assert_eq!(list.opacity_at(3), 0.5);
2633 assert_eq!(list.opacity_at(4), 1.0);
2634
2635 let plan = list.opacity_plan();
2636 assert_eq!(plan.len(), 3);
2637 assert_eq!(plan[0], OpacityNode::Segment { start: 0, end: 1 });
2638 assert_eq!(plan[2], OpacityNode::Segment { start: 4, end: 5 });
2639 let OpacityNode::Group { opacity, children } = &plan[1] else {
2640 panic!("expected outer group, got {:?}", plan[1]);
2641 };
2642 assert_eq!(*opacity, 0.5);
2643 assert_eq!(children.len(), 3);
2644 assert_eq!(children[0], OpacityNode::Segment { start: 1, end: 2 });
2645 assert_eq!(
2646 children[1],
2647 OpacityNode::Group {
2648 opacity: 0.5,
2649 children: vec![OpacityNode::Segment { start: 2, end: 3 }],
2650 }
2651 );
2652 assert_eq!(children[2], OpacityNode::Segment { start: 3, end: 4 });
2653 }
2654
2655 #[test]
2656 fn list_push_and_clear() {
2657 let mut list = RenderList::new();
2658 assert!(list.is_empty());
2659 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2660 assert_eq!(list.len(), 1);
2661 list.push_quad(Rect::ZERO, Color::RED, 0.0);
2662 list.push_quad(
2663 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2664 Color::TRANSPARENT,
2665 0.0,
2666 );
2667 assert_eq!(list.len(), 1, "empty-value filters");
2668
2669 list.push_text(TextDraw::default());
2670 assert_eq!(list.len(), 1, "empty TextDraw skipped");
2671
2672 list.push_text(TextDraw {
2673 color: Color::WHITE,
2674 glyphs: vec![GlyphQuad {
2675 rect: Rect::ZERO,
2676 uv: [0.0; 4],
2677 color_glyph: false,
2678 }],
2679 });
2680 assert_eq!(list.len(), 2);
2681 assert_eq!(list.quads().count(), 1);
2682
2683 list.clear();
2684 assert!(list.is_empty());
2685 }
2686
2687 #[test]
2688 fn bordered_quad_filters_and_flags() {
2689 let r = Rect::from_xywh(0.0, 0.0, 100.0, 60.0);
2690 let mut list = RenderList::new();
2691
2692 list.push_quad(r, Color::BLUE, 8.0);
2693 let q = list.quads().next().unwrap();
2694 assert!(!q.has_border());
2695 assert_eq!(q.border_width, 0.0);
2696
2697 let bg = Color::rgba(154.0 / 255.0, 214.0 / 255.0, 248.0 / 255.0, 24.0 / 255.0);
2698 list.push_bordered_quad(r, bg, 8.0, 1.0, Color::RED);
2699 let qs: Vec<_> = list.quads().collect();
2700 assert_eq!(qs.len(), 2, "background + border is one quad, not two");
2701 assert_eq!(qs[1].color, bg);
2702 assert_eq!(qs[1].border_color, Color::RED);
2703 assert_eq!(qs[1].border_width, 1.0);
2704 assert!(qs[1].has_border());
2705
2706 let mut list = RenderList::new();
2707 list.push_bordered_quad(r, Color::TRANSPARENT, 8.0, 2.0, Color::GREEN);
2708 assert_eq!(list.len(), 1);
2709
2710 let mut list = RenderList::new();
2711 list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 0.0, Color::RED);
2712 list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 2.0, Color::TRANSPARENT);
2713 list.push_bordered_quad(Rect::ZERO, Color::RED, 0.0, 1.0, Color::RED);
2714 assert!(list.is_empty());
2715 }
2716
2717 #[test]
2718 fn clip_intersects_and_filters() {
2719 let mut list = RenderList::new();
2720 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2721 list.push_quad(Rect::from_xywh(50.0, 50.0, 100.0, 100.0), Color::RED, 0.0);
2722 list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
2723 assert_eq!(list.len(), 1);
2724 let q = list.quads().next().unwrap();
2725 assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
2726
2727 list.pop_clip();
2728 list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
2729 assert_eq!(list.len(), 2, "unclipped after pop_clip");
2730 }
2731
2732 #[test]
2733 fn clipped_rounded_quad_keeps_its_geometry() {
2734 let clip = Rect::from_xywh(0.0, 0.0, 100.0, 100.0);
2735 let object = Rect::from_xywh(60.0, 20.0, 100.0, 80.0);
2736
2737 let mut list = RenderList::new();
2738 list.push_clip(clip);
2739 list.push_bordered_quad(object, Color::RED, 12.0, 2.0, Color::WHITE);
2740 assert_eq!(list.len(), 1);
2741 let q = list.quads().next().unwrap();
2742 assert_eq!(
2743 q.rect, object,
2744 "rounded geometry is cut by the clip, not resized to it"
2745 );
2746 assert_eq!(q.radius, 12.0, "corner radius survives the clip");
2747 assert_eq!(q.border_width, 2.0, "border stays on the real edge");
2748 let item_clip = list
2749 .item_rounded_clip(0)
2750 .expect("clip stored for the shader");
2751 assert_eq!(item_clip.rect, clip);
2752
2753 let mut outside = RenderList::new();
2754 outside.push_clip(clip);
2755 outside.push_bordered_quad(
2756 Rect::from_xywh(200.0, 200.0, 50.0, 50.0),
2757 Color::RED,
2758 8.0,
2759 1.0,
2760 Color::WHITE,
2761 );
2762 assert!(
2763 outside.is_empty(),
2764 "rounded quad fully outside the clip is still culled"
2765 );
2766
2767 let mut plain = RenderList::new();
2768 plain.push_clip(clip);
2769 plain.push_quad(Rect::from_xywh(50.0, 50.0, 100.0, 100.0), Color::RED, 0.0);
2770 assert_eq!(
2771 plain.quads().next().unwrap().rect,
2772 Rect::from_xywh(50.0, 50.0, 50.0, 50.0),
2773 "plain fills keep the cheap crop"
2774 );
2775 }
2776
2777 #[test]
2778 fn nested_clip_intersects() {
2779 let mut list = RenderList::new();
2780 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2781 list.push_clip(Rect::from_xywh(50.0, 50.0, 100.0, 100.0));
2782 list.push_quad(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), Color::RED, 0.0);
2783 assert_eq!(list.len(), 1);
2784 let q = list.quads().next().unwrap();
2785 assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
2786 }
2787
2788 #[test]
2789 fn rounded_clip_stored_per_item_for_shader() {
2790 let mut list = RenderList::new();
2791 let card = Rect::from_xywh(0.0, 0.0, 240.0, 280.0);
2792 list.push_rounded_clip(card, 8.0);
2793 list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
2794 assert_eq!(list.len(), 1);
2795 let clip = list.item_rounded_clip(0).expect("rounded clip stored");
2796 assert_eq!(clip.rect, card);
2797 assert_eq!(clip.radius, 8.0);
2798
2799 let mut list = RenderList::new();
2800 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2801 list.push_quad(Rect::from_xywh(10.0, 10.0, 20.0, 20.0), Color::RED, 0.0);
2802 assert_eq!(list.item_rounded_clip(0), None);
2803 }
2804
2805 #[test]
2806 fn rounded_clip_survives_rectangular_child_clip() {
2807 let mut list = RenderList::new();
2808 list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0), 8.0);
2809 list.push_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0));
2810 list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
2811 let clip = list.item_rounded_clip(0).expect("outer rounding kept");
2812 assert_eq!(clip.radius, 8.0);
2813 list.pop_clip();
2814 list.pop_clip();
2815 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2816 assert_eq!(list.item_rounded_clip(1), None, "no clip after pops");
2817 }
2818
2819 fn glyph(x: f32, y: f32, w: f32, h: f32) -> GlyphQuad {
2820 GlyphQuad {
2821 rect: Rect::from_xywh(x, y, w, h),
2822 uv: [0.0, 0.0, 1.0, 1.0],
2823 color_glyph: false,
2824 }
2825 }
2826
2827 fn pushed_glyphs(clip: Rect, g: GlyphQuad) -> Vec<GlyphQuad> {
2828 let mut list = RenderList::new();
2829 list.push_clip(clip);
2830 list.push_text(TextDraw {
2831 color: Color::WHITE,
2832 glyphs: vec![g],
2833 });
2834 match list.items() {
2835 [RenderItem::Text(t)] => t.glyphs.clone(),
2836 _ => panic!("expected a single TextDraw"),
2837 }
2838 }
2839
2840 #[test]
2841 fn clipped_glyph_keeps_proportional_uv() {
2842 let out = pushed_glyphs(
2843 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2844 glyph(90.0, 10.0, 20.0, 10.0),
2845 );
2846 assert_eq!(out.len(), 1);
2847 assert_eq!(out[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
2848 assert_eq!(out[0].uv, [0.0, 0.0, 0.5, 1.0]);
2849 }
2850
2851 #[test]
2852 fn fully_visible_glyph_keeps_uv() {
2853 let out = pushed_glyphs(
2854 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2855 glyph(10.0, 10.0, 20.0, 10.0),
2856 );
2857 assert_eq!(out.len(), 1);
2858 assert_eq!(out[0].rect, Rect::from_xywh(10.0, 10.0, 20.0, 10.0));
2859 assert_eq!(out[0].uv, [0.0, 0.0, 1.0, 1.0]);
2860 }
2861
2862 #[test]
2863 fn clipped_glyph_uv_scales_with_atlas_subrect() {
2864 let mut g = glyph(0.0, 0.0, 20.0, 20.0);
2865 g.uv = [0.25, 0.5, 0.75, 1.0];
2866 let out = pushed_glyphs(Rect::from_xywh(10.0, 0.0, 100.0, 100.0), g);
2867 assert_eq!(out.len(), 1);
2868 assert_eq!(out[0].rect, Rect::from_xywh(10.0, 0.0, 10.0, 20.0));
2869 assert_eq!(out[0].uv, [0.5, 0.5, 0.75, 1.0]);
2870 }
2871
2872 fn file_image() -> ImageSource {
2873 ImageSource::File("foto.png".into())
2874 }
2875
2876 #[test]
2877 fn image_push_clip_and_uv() {
2878 let mut list = RenderList::new();
2879 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2880 list.push_image(
2881 Rect::from_xywh(90.0, 10.0, 20.0, 10.0),
2882 [0.0, 0.0, 1.0, 1.0],
2883 file_image(),
2884 );
2885 list.push_image(
2886 Rect::from_xywh(200.0, 200.0, 10.0, 10.0),
2887 [0.0, 0.0, 1.0, 1.0],
2888 file_image(),
2889 );
2890 let images: Vec<_> = list.images().collect();
2891 assert_eq!(images.len(), 1, "outside the clip is dropped");
2892 assert_eq!(images[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
2893 assert_eq!(
2894 images[0].uv,
2895 [0.0, 0.0, 0.5, 1.0],
2896 "proportional UV, as for glyphs"
2897 );
2898 }
2899
2900 #[test]
2901 fn image_cache_keys() {
2902 assert_eq!(file_image().cache_key(), file_image().cache_key());
2903 assert_ne!(
2904 file_image().cache_key(),
2905 ImageSource::File("inne.png".into()).cache_key()
2906 );
2907 let rgba = |id| ImageSource::Rgba {
2908 id,
2909 width: 2,
2910 height: 2,
2911 data: Arc::new(vec![0u8; 16]),
2912 };
2913 assert_eq!(rgba(7).cache_key(), 7);
2914 assert_ne!(rgba(7).cache_key(), rgba(8).cache_key());
2915 }
2916
2917 fn shadow_spec() -> Shadow {
2918 Shadow {
2919 rect: Rect::from_xywh(10.0, 10.0, 100.0, 60.0),
2920 color: Color::BLACK,
2921 radius: 6.0,
2922 blur: 8.0,
2923 spread: 0.0,
2924 offset: Vec2::new(0.0, 2.0),
2925 inset: false,
2926 }
2927 }
2928
2929 #[test]
2930 fn shadow_push_allows_hard_outer_but_not_hard_inset_shadows() {
2931 let mut list = RenderList::new();
2932 list.push_shadow(Shadow {
2933 blur: 0.0,
2934 ..shadow_spec()
2935 });
2936 list.push_shadow(Shadow {
2937 blur: 0.0,
2938 inset: true,
2939 ..shadow_spec()
2940 });
2941 list.push_shadow(Shadow {
2942 blur: f32::NAN,
2943 ..shadow_spec()
2944 });
2945 list.push_shadow(Shadow {
2946 color: Color::TRANSPARENT,
2947 ..shadow_spec()
2948 });
2949 list.push_shadow(Shadow {
2950 rect: Rect::ZERO,
2951 ..shadow_spec()
2952 });
2953 assert_eq!(list.items().len(), 1, "only the hard outer shadow is kept");
2954 }
2955
2956 #[test]
2957 fn shadow_keeps_shape_rect_and_rides_the_clip() {
2958 let mut list = RenderList::new();
2959 list.push_clip(Rect::from_xywh(0.0, 0.0, 50.0, 50.0));
2960 list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), 12.0);
2961 list.push_shadow(shadow_spec());
2962 assert_eq!(list.len(), 1);
2963 assert_eq!(list.shadows().next().unwrap().rect, shadow_spec().rect);
2964 let clip = list.item_rounded_clip(0).expect("ancestor clip");
2965 assert_eq!(
2966 clip.rect,
2967 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2968 "przeciecie stosu clipow"
2969 );
2970 assert_eq!(clip.radius, 12.0, "rounding from inner clip");
2971 }
2972
2973 #[test]
2974 fn shadow_dropped_when_fully_clipped_out() {
2975 let mut list = RenderList::new();
2976 list.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2977 list.push_shadow(shadow_spec());
2978 assert!(list.is_empty(), "fully outside clip");
2979 }
2980
2981 #[test]
2982 fn shadow_bounds_grow_with_blur_spread_and_offset() {
2983 let s = Shadow {
2984 blur: 10.0,
2985 spread: 4.0,
2986 offset: Vec2::new(2.0, -3.0),
2987 ..shadow_spec()
2988 };
2989 let b = s.bounds();
2990 assert!((b.left() - 3.0).abs() < 1e-4, "{b:?}");
2991 assert!((b.top() - (-2.0)).abs() < 1e-4, "{b:?}");
2992 assert!((b.size.width - 118.0).abs() < 1e-4, "{b:?}");
2993 let inset = Shadow { inset: true, ..s };
2994 assert_eq!(inset.bounds(), inset.rect, "inset stays inside the shape");
2995 }
2996
2997 #[test]
2998 fn waveform_push_is_one_item_not_n() {
2999 let mut list = RenderList::new();
3000 let bars: Vec<Rect> = (0..100)
3001 .map(|i| Rect::from_xywh(i as f32 * 2.0, 0.0, 1.5, 20.0))
3002 .collect();
3003 list.push_waveform(&bars, Color::WHITE);
3004 assert_eq!(list.len(), 1, "whole waveform is 1 item");
3005 assert_eq!(list.waveform_bar_count(), 100);
3006 assert_eq!(list.waveforms().next().unwrap().bars.len(), 100);
3007 }
3008
3009 #[test]
3010 fn waveform_push_clips_and_filters() {
3011 let mut list = RenderList::new();
3012 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
3013 let bars = vec![
3014 Rect::from_xywh(0.0, 0.0, 5.0, 5.0),
3015 Rect::from_xywh(200.0, 200.0, 5.0, 5.0),
3016 Rect::ZERO,
3017 ];
3018 list.push_waveform(&bars, Color::WHITE);
3019 assert_eq!(list.len(), 1);
3020 assert_eq!(list.waveform_bar_count(), 1, "outside clip dropped");
3021
3022 let mut list = RenderList::new();
3023 list.push_waveform(&[], Color::WHITE);
3024 list.push_waveform(&[Rect::from_xywh(0.0, 0.0, 5.0, 5.0)], Color::TRANSPARENT);
3025 assert!(list.is_empty());
3026 }
3027
3028 #[test]
3029 fn dynamic_image_cache_key_is_id_and_versioned() {
3030 let a = ImageSource::Dynamic {
3031 id: 7,
3032 width: 4,
3033 height: 1,
3034 data: Arc::new(vec![0u8; 16]),
3035 version: 1,
3036 };
3037 let b = ImageSource::Dynamic {
3038 id: 7,
3039 width: 4,
3040 height: 1,
3041 data: Arc::new(vec![255u8; 16]),
3042 version: 2,
3043 };
3044 assert_eq!(a.cache_key(), 7);
3045 assert_eq!(a.cache_key(), b.cache_key(), "same texture slot");
3046 assert_ne!(a, b, "other version means other data");
3047 assert_eq!(a.dynamic_version(), Some(1));
3048 assert_eq!(
3049 ImageSource::Rgba {
3050 id: 7,
3051 width: 1,
3052 height: 1,
3053 data: Arc::new(vec![0u8; 4]),
3054 }
3055 .dynamic_version(),
3056 None
3057 );
3058 }
3059
3060 #[test]
3061 fn backdrop_side_channel_keeps_item_indices() {
3062 let mut list = RenderList::new();
3063 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3064 list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
3065 assert!(!list.has_backdrops());
3066 list.push_backdrop(
3067 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3068 8.0,
3069 16.0,
3070 Color::TRANSPARENT,
3071 0.0,
3072 SpanMask::None,
3073 );
3074 assert!(list.has_backdrops());
3075 assert_eq!(list.len(), 2, "marker does not add items");
3076 let span = list.backdrops()[0];
3077 assert_eq!(span.at, 2, "rozmywa dwa quady spod spodu");
3078 assert_eq!(span.draw.rect, Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3079 assert_eq!(span.draw.blur, 16.0);
3080 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
3081 assert_eq!(list.backdrops()[0].at, 2, "at frozen in push");
3082 }
3083
3084 #[test]
3085 fn filter_span_covers_subtree_range() {
3086 let mut list = RenderList::new();
3087 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3088 let h = list
3089 .push_filter_begin(
3090 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3091 8.0,
3092 16.0,
3093 SpanMask::None,
3094 )
3095 .expect("widoczny filtr otwiera zakres");
3096 assert_eq!(list.len(), 1, "begin does not add items");
3097 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
3098 list.push_quad(Rect::from_xywh(5.0, 5.0, 5.0, 5.0), Color::GREEN, 0.0);
3099 list.push_filter_end(h);
3100 assert!(list.has_filters());
3101 let span = list.filter_spans()[0];
3102 assert_eq!((span.start, span.end), (1, 3), "tylko poddrzewo");
3103 assert_eq!(span.draw.blur, 16.0);
3104 assert_eq!(span.draw.radius, 8.0);
3105 list.clear();
3106 assert!(!list.has_filters(), "clear also drops filters");
3107 }
3108
3109 #[test]
3110 fn filter_begin_rejects_invisible_and_end_drops_empty() {
3111 let mut list = RenderList::new();
3112
3113 assert!(
3114 list.push_filter_begin(
3115 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3116 0.0,
3117 0.0,
3118 SpanMask::None
3119 )
3120 .is_none()
3121 );
3122 assert!(
3123 list.push_filter_begin(
3124 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3125 0.0,
3126 f32::NAN,
3127 SpanMask::None
3128 )
3129 .is_none()
3130 );
3131 assert!(
3132 list.push_filter_begin(
3133 Rect::from_xywh(0.0, 0.0, 0.0, 0.0),
3134 0.0,
3135 8.0,
3136 SpanMask::None
3137 )
3138 .is_none()
3139 );
3140 assert!(!list.has_filters());
3141
3142 let h = list
3143 .push_filter_begin(
3144 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3145 0.0,
3146 8.0,
3147 SpanMask::None,
3148 )
3149 .expect("otwiera");
3150 list.push_filter_end(h);
3151 assert!(!list.has_filters(), "pusty zakres bez contentu");
3152
3153 let mut clipped = RenderList::new();
3154 clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3155 let h = clipped
3156 .push_filter_begin(
3157 Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
3158 4.0,
3159 12.0,
3160 SpanMask::None,
3161 )
3162 .expect("otwiera");
3163 clipped.push_quad(Rect::from_xywh(60.0, 60.0, 10.0, 10.0), Color::RED, 0.0);
3164 clipped.push_filter_end(h);
3165 assert_eq!(
3166 clipped.filter_spans()[0].draw.rect,
3167 Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
3168 );
3169 }
3170
3171 #[test]
3172 fn filter_spans_nest_like_subtrees() {
3173 let mut list = RenderList::new();
3174 let outer = list
3175 .push_filter_begin(
3176 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3177 0.0,
3178 8.0,
3179 SpanMask::None,
3180 )
3181 .expect("outer");
3182 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3183 let inner = list
3184 .push_filter_begin(
3185 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
3186 0.0,
3187 4.0,
3188 SpanMask::None,
3189 )
3190 .expect("inner");
3191 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
3192 list.push_filter_end(inner);
3193 list.push_filter_end(outer);
3194 assert_eq!(list.filter_spans().len(), 2);
3195 assert_eq!(
3196 (list.filter_spans()[0].start, list.filter_spans()[0].end),
3197 (0, 2)
3198 );
3199 assert_eq!(
3200 (list.filter_spans()[1].start, list.filter_spans()[1].end),
3201 (1, 2)
3202 );
3203 }
3204
3205 #[test]
3206 fn backdrop_filters_and_clips() {
3207 let mut list = RenderList::new();
3208
3209 list.push_backdrop(
3210 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
3211 0.0,
3212 0.0,
3213 Color::TRANSPARENT,
3214 0.0,
3215 SpanMask::None,
3216 );
3217 assert!(!list.has_backdrops());
3218
3219 list.push_backdrop(
3220 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
3221 0.0,
3222 0.0,
3223 Color::BLACK.with_alpha(0.25),
3224 0.0,
3225 SpanMask::None,
3226 );
3227 assert_eq!(list.backdrops().len(), 1);
3228
3229 let mut clipped = RenderList::new();
3230 clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3231 clipped.push_backdrop(
3232 Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
3233 4.0,
3234 12.0,
3235 Color::TRANSPARENT,
3236 0.0,
3237 SpanMask::None,
3238 );
3239 assert_eq!(
3240 clipped.backdrops()[0].draw.rect,
3241 Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
3242 );
3243
3244 let mut outside = RenderList::new();
3245 outside.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
3246 outside.push_backdrop(
3247 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3248 0.0,
3249 12.0,
3250 Color::TRANSPARENT,
3251 0.0,
3252 SpanMask::None,
3253 );
3254 assert!(!outside.has_backdrops());
3255
3256 let mut faded = RenderList::new();
3257 faded.push_opacity(0.5);
3258 faded.push_backdrop(
3259 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3260 0.0,
3261 0.0,
3262 Color::BLACK.with_alpha(0.4),
3263 0.5,
3264 SpanMask::None,
3265 );
3266 let d = faded.backdrops()[0].draw;
3267 assert!((d.dim.a - 0.2).abs() < 1e-6, "dim.a 0.4 * 0.5: {}", d.dim.a);
3268 assert!(
3269 (d.noise - 0.25).abs() < 1e-6,
3270 "noise 0.5 * 0.5: {}",
3271 d.noise
3272 );
3273
3274 faded.clear();
3275 assert!(!faded.has_backdrops());
3276 }
3277
3278 #[test]
3279 fn span_mask_encodes_for_gpu() {
3280 assert_eq!(SpanMask::None.mask_vec(), [0.0, 0.0, 0.0, 0.0]);
3281 assert_eq!(
3282 SpanMask::Linear {
3283 from: 1.0,
3284 to: 0.0,
3285 angle_rad: std::f32::consts::FRAC_PI_2,
3286 }
3287 .mask_vec(),
3288 [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
3289 );
3290 assert_eq!(SpanMask::Uniform(0.5).mask_vec(), [2.0, 0.5, 0.0, 0.0]);
3291
3292 assert_eq!(
3293 SpanMask::Linear {
3294 from: 2.0,
3295 to: -1.0,
3296 angle_rad: f32::NAN,
3297 }
3298 .mask_vec()[1..3],
3299 [1.0, 0.0]
3300 );
3301 assert!(SpanMask::None.is_none());
3302 assert!(!SpanMask::Uniform(0.0).is_none());
3303 }
3304
3305 #[test]
3306 fn filter_span_carries_mask() {
3307 let mut list = RenderList::new();
3308 let mask = SpanMask::Linear {
3309 from: 1.0,
3310 to: 0.0,
3311 angle_rad: std::f32::consts::FRAC_PI_2,
3312 };
3313 let h = list
3314 .push_filter_begin(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), 0.0, 8.0, mask)
3315 .expect("otwiera");
3316 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3317 list.push_filter_end(h);
3318 assert_eq!(list.filter_spans()[0].draw.mask, mask);
3319 assert_eq!(
3320 list.filter_spans()[0].draw.mask.mask_vec(),
3321 [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
3322 );
3323 }
3324
3325 #[test]
3326 fn backdrop_marker_carries_mask() {
3327 let mut list = RenderList::new();
3328 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3329 list.push_backdrop(
3330 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3331 0.0,
3332 8.0,
3333 Color::TRANSPARENT,
3334 0.0,
3335 SpanMask::Uniform(0.5),
3336 );
3337 assert_eq!(
3338 list.backdrops()[0].draw.mask,
3339 SpanMask::Uniform(0.5),
3340 "mask in the marker layer"
3341 );
3342 }
3343
3344 #[test]
3345 fn transform_stack_composes_and_snapshots() {
3346 use crate::geometry::Point;
3347 let mut list = RenderList::new();
3348 assert!(list.active_transform().is_identity());
3349
3350 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3351 assert!(list.item_transform(0).is_identity());
3352
3353 list.push_transform(Transform::rotate_around_degrees(90.0, Point::new(5.0, 5.0)));
3354 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3355 assert!(!list.item_transform(1).is_identity());
3356 assert_eq!(list.item_transforms().len(), list.items().len());
3357 list.pop_transform();
3358 assert!(list.active_transform().is_identity());
3359 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3360 assert!(list.item_transform(2).is_identity());
3361 }
3362
3363 #[test]
3364 fn transform_aabb_covers_rotation() {
3365 use crate::geometry::Point;
3366 let r = Rect::from_xywh(0.0, 0.0, 10.0, 20.0);
3367 let t = Transform::rotate_around_degrees(90.0, Point::new(5.0, 10.0));
3368 let aabb = t.apply_rect_aabb(r);
3369 assert!(
3370 (aabb.size.width - 20.0).abs() < 0.01,
3371 "w={}",
3372 aabb.size.width
3373 );
3374 assert!(
3375 (aabb.size.height - 10.0).abs() < 0.01,
3376 "h={}",
3377 aabb.size.height
3378 );
3379 assert!(Transform::IDENTITY.apply_rect_aabb(r) == r);
3380 }
3381
3382 #[test]
3383 fn rotated_text_survives_own_self_clip() {
3384 let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
3385 let mut list = RenderList::new();
3386 list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
3387 list.push_clip(tile);
3388 list.push_text(TextDraw {
3389 color: Color::WHITE,
3390 glyphs: vec![GlyphQuad {
3391 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
3392 uv: [0.0, 0.0, 1.0, 1.0],
3393 color_glyph: false,
3394 }],
3395 });
3396 assert_eq!(list.len(), 1, "rotated glyph in custom clip remains");
3397 }
3398
3399 #[test]
3400 fn shader_draw_truncates_uniforms_and_keeps_last_binding() {
3401 let mut list = RenderList::new();
3402 let uniforms = vec![7u8; MAX_SHADER_UNIFORM_BYTES + 9];
3403 list.push_shader(ShaderDraw {
3404 rect: Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3405 uv: [0.0, 0.0, 1.0, 1.0],
3406 shader: ShaderId::new(4),
3407 uniforms: Arc::from(uniforms),
3408 textures: Arc::from(vec![(2, tex(10)), (1, tex(11)), (2, tex(12))]),
3409 blend: BlendMode::Normal,
3410 opacity: 1.0,
3411 });
3412
3413 let draw = list.shaders().next().expect("shader draw");
3414 assert_eq!(draw.uniforms.len(), MAX_SHADER_UNIFORM_BYTES);
3415 assert_eq!(
3416 draw.textures.as_ref(),
3417 &[(1, tex(11)), (2, tex(12))],
3418 "duplicate bindings keep the last source"
3419 );
3420 assert_eq!(list.items().len(), 1);
3421 assert_eq!(list.item_clips().len(), 1);
3422 assert_eq!(list.item_transforms().len(), 1);
3423 }
3424
3425 #[test]
3426 fn target_markers_and_blit_round_trip_through_the_list() {
3427 let mut list = RenderList::new();
3428 let target = list.begin_target(Size::new(32.0, 16.0), BlendMode::Multiply);
3429 list.end_target(target);
3430 list.push_blit(
3431 Rect::from_xywh(0.0, 0.0, 32.0, 16.0),
3432 [0.0, 0.0, 1.0, 1.0],
3433 target,
3434 BlendMode::Screen,
3435 );
3436
3437 assert_eq!(target.get(), 0);
3438 assert!(matches!(
3439 list.items()[0],
3440 RenderItem::TargetBegin { id, size, blend }
3441 if id == target && size.width == 32.0 && blend == BlendMode::Multiply
3442 ));
3443 assert!(matches!(
3444 list.items()[1],
3445 RenderItem::TargetEnd { id } if id == target
3446 ));
3447 assert!(matches!(
3448 list.items()[2],
3449 RenderItem::Blit { target: id, blend: BlendMode::Screen, .. } if id == target
3450 ));
3451 assert_eq!(list.len(), 3);
3452 }
3453
3454 #[test]
3455 fn fast_flags_track_targets_shaders_and_cached_suffixes() {
3456 let mut list = RenderList::new();
3457 assert!(!list.has_targets());
3458 assert!(!list.has_shaders());
3459
3460 let target = list.begin_target(Size::new(32.0, 16.0), BlendMode::Normal);
3461 list.push_shader(ShaderDraw {
3462 rect: Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3463 uv: [0.0, 0.0, 1.0, 1.0],
3464 shader: ShaderId::new(1),
3465 uniforms: Arc::from(Vec::new()),
3466 textures: Arc::from(Vec::new()),
3467 blend: BlendMode::Normal,
3468 opacity: 1.0,
3469 });
3470 assert!(list.has_targets());
3471 assert!(list.has_shaders());
3472
3473 let mark = list.mark();
3474 list.end_target(target);
3475 let suffix = list.suffix(mark).expect("suffix");
3476 assert!(!suffix.has_targets(), "TargetEnd adds no target");
3477 assert!(!suffix.has_shaders());
3478
3479 let mark = list.mark();
3480 let _ = list.begin_target(Size::new(8.0, 8.0), BlendMode::Normal);
3481 let suffix = list.suffix(mark).expect("suffix");
3482 assert!(suffix.has_targets());
3483 let mut replay = RenderList::new();
3484 replay.append(&suffix);
3485 assert!(replay.has_targets());
3486 replay.clear();
3487 assert!(!replay.has_targets());
3488 assert!(!replay.has_shaders());
3489 }
3490
3491 #[test]
3492 fn ancestor_clip_still_culls_rotated_content() {
3493 let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
3494 let mut list = RenderList::new();
3495 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3496 list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
3497 list.push_text(TextDraw {
3498 color: Color::WHITE,
3499 glyphs: vec![GlyphQuad {
3500 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
3501 uv: [0.0, 0.0, 1.0, 1.0],
3502 color_glyph: false,
3503 }],
3504 });
3505
3506 assert_eq!(list.len(), 1, "clip przodka nie zjada widocznego");
3507
3508 let mut list2 = RenderList::new();
3509 list2.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3510 list2.push_transform(Transform::translate(500.0, 500.0));
3511 list2.push_text(TextDraw {
3512 color: Color::WHITE,
3513 glyphs: vec![GlyphQuad {
3514 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
3515 uv: [0.0, 0.0, 1.0, 1.0],
3516 color_glyph: false,
3517 }],
3518 });
3519 assert_eq!(list2.len(), 0, "clip przodka obcina poza ekranem");
3520 }
3521}