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() && self.color.a > 0.0 && self.blur > 0.0 && self.blur.is_finite()
618 }
619
620 pub fn bounds(&self) -> Rect {
621 if self.inset {
622 return self.rect;
623 }
624 let margin = self.blur * 0.5 + self.spread.max(0.0);
625 Rect::from_xywh(
626 self.rect.left() + self.offset.x - margin,
627 self.rect.top() + self.offset.y - margin,
628 self.rect.size.width + margin * 2.0,
629 self.rect.size.height + margin * 2.0,
630 )
631 }
632}
633
634#[derive(Clone, Copy, Debug, PartialEq)]
635pub struct GlyphQuad {
636 pub rect: Rect,
637
638 pub uv: [f32; 4],
639
640 pub color_glyph: bool,
641}
642
643#[derive(Clone, Debug, Default, PartialEq)]
644pub struct TextDraw {
645 pub color: Color,
646
647 pub glyphs: Vec<GlyphQuad>,
648}
649
650#[derive(Clone, Debug, PartialEq)]
651#[non_exhaustive]
652pub enum ImageSource {
653 File(String),
654
655 Rgba {
656 id: u64,
657
658 width: u32,
659
660 height: u32,
661
662 data: Arc<Vec<u8>>,
663 },
664
665 Dynamic {
666 id: u64,
667
668 width: u32,
669
670 height: u32,
671
672 data: Arc<Vec<u8>>,
673
674 version: u64,
675 },
676}
677
678impl ImageSource {
679 pub fn cache_key(&self) -> u64 {
680 match self {
681 Self::File(path) => {
682 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
683 for byte in path.as_bytes() {
684 hash ^= u64::from(*byte);
685 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
686 }
687 hash ^ 0x9e37_79b9_7f4a_7c15
688 }
689 Self::Rgba { id, .. } => *id,
690 Self::Dynamic { id, .. } => *id,
691 }
692 }
693
694 pub fn dynamic_version(&self) -> Option<u64> {
695 match self {
696 Self::Dynamic { version, .. } => Some(*version),
697 Self::File(_) | Self::Rgba { .. } => None,
698 }
699 }
700
701 pub fn extent(&self) -> Option<(u32, u32)> {
702 match self {
703 Self::File(_) => None,
704 Self::Rgba { width, height, .. } | Self::Dynamic { width, height, .. } => {
705 Some((*width, *height))
706 }
707 }
708 }
709}
710
711#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
712#[non_exhaustive]
713pub enum ImageSampling {
714 #[default]
715 Linear,
716
717 Nearest,
718}
719
720#[derive(Clone, Debug, PartialEq)]
721pub struct ImageDraw {
722 pub rect: Rect,
723
724 pub uv: [f32; 4],
725
726 pub source: ImageSource,
727
728 pub sampling: ImageSampling,
729}
730
731#[derive(Clone, Debug, PartialEq)]
732pub struct WaveformDraw {
733 pub bars: Vec<Rect>,
734
735 pub color: Color,
736}
737
738#[derive(Clone, Copy, Debug, PartialEq)]
739pub struct BackdropDraw {
740 pub rect: Rect,
741
742 pub radius: f32,
743
744 pub blur: f32,
745
746 pub dim: Color,
747
748 pub noise: f32,
749
750 pub mask: SpanMask,
751}
752
753impl BackdropDraw {
754 pub fn is_visible(self) -> bool {
755 !self.rect.is_empty()
756 && (self.blur > 0.0 && self.blur.is_finite() || self.dim.a > 0.0 || self.noise > 0.0)
757 }
758}
759
760#[derive(Clone, Copy, Debug, PartialEq)]
761#[non_exhaustive]
762#[derive(Default)]
763pub enum SpanMask {
764 #[default]
765 None,
766
767 Linear {
768 from: f32,
769 to: f32,
770 angle_rad: f32,
771 },
772
773 Uniform(f32),
774}
775
776impl SpanMask {
777 pub fn mask_vec(self) -> [f32; 4] {
778 match self {
779 SpanMask::None => [0.0, 0.0, 0.0, 0.0],
780 SpanMask::Linear {
781 from,
782 to,
783 angle_rad,
784 } => {
785 let angle = if angle_rad.is_finite() {
786 angle_rad
787 } else {
788 0.0
789 };
790 [1.0, from.clamp(0.0, 1.0), to.clamp(0.0, 1.0), angle]
791 }
792 SpanMask::Uniform(v) => [2.0, v.clamp(0.0, 1.0), 0.0, 0.0],
793 }
794 }
795
796 pub fn is_none(self) -> bool {
797 matches!(self, SpanMask::None)
798 }
799}
800
801#[derive(Clone, Copy, Debug, PartialEq)]
802pub struct BackdropSpan {
803 pub at: usize,
804
805 pub draw: BackdropDraw,
806
807 pub clip: Option<RoundedClip>,
808}
809
810#[derive(Clone, Copy, Debug, PartialEq)]
811pub struct FilterDraw {
812 pub rect: Rect,
813
814 pub radius: f32,
815
816 pub blur: f32,
817
818 pub mask: SpanMask,
819}
820
821impl FilterDraw {
822 pub fn is_visible(self) -> bool {
823 !self.rect.is_empty() && self.blur > 0.0 && self.blur.is_finite()
824 }
825}
826
827#[derive(Clone, Copy, Debug, PartialEq)]
828pub struct FilterSpan {
829 pub start: usize,
830
831 pub end: usize,
832
833 pub draw: FilterDraw,
834
835 pub clip: Option<RoundedClip>,
836}
837
838#[derive(Clone, Copy, Debug, PartialEq)]
843pub struct OpacitySpan {
844 pub start: usize,
845
846 pub end: usize,
847
848 pub opacity: f32,
849
850 pub depth: usize,
851}
852
853#[derive(Clone, Debug, PartialEq)]
855#[non_exhaustive]
856pub enum OpacityNode {
857 Segment { start: usize, end: usize },
859
860 Group {
862 opacity: f32,
863 children: Vec<OpacityNode>,
864 },
865}
866
867#[derive(Clone, Debug, PartialEq)]
868pub struct GradientDraw {
869 pub rect: Rect,
870
871 pub radius: f32,
872
873 pub kind: crate::brush::GradientKind,
874
875 pub stops: crate::brush::GradientStops,
876}
877
878impl GradientDraw {
879 pub fn is_visible(&self) -> bool {
880 !self.rect.is_empty() && !self.stops.is_empty()
881 }
882}
883
884#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
886pub struct ShaderId(pub u32);
887
888impl ShaderId {
889 #[must_use]
893 pub const fn new(raw: u32) -> Self {
894 Self(raw)
895 }
896
897 #[must_use]
899 pub const fn get(self) -> u32 {
900 self.0
901 }
902}
903
904#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
906pub struct RenderTargetId(pub u64);
907
908impl RenderTargetId {
909 #[must_use]
911 pub const fn new(raw: u64) -> Self {
912 Self(raw)
913 }
914
915 #[must_use]
917 pub const fn get(self) -> u64 {
918 self.0
919 }
920}
921
922#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
924#[non_exhaustive]
925pub enum BlendMode {
926 #[default]
928 Normal,
929
930 Add,
932
933 Multiply,
935
936 Screen,
938
939 Subtract,
941
942 Overlay,
944
945 Darken,
947
948 Lighten,
950
951 ColorDodge,
953
954 ColorBurn,
956
957 HardLight,
959
960 SoftLight,
962
963 Difference,
965
966 Exclusion,
968
969 Hue,
971
972 Saturation,
974
975 Color,
977
978 Luminosity,
980
981 AlphaMask,
983}
984
985impl BlendMode {
986 #[must_use]
988 pub const fn as_u32(self) -> u32 {
989 match self {
990 Self::Normal => 0,
991 Self::Add => 1,
992 Self::Multiply => 2,
993 Self::Screen => 3,
994 Self::Subtract => 4,
995 Self::Overlay => 5,
996 Self::Darken => 6,
997 Self::Lighten => 7,
998 Self::ColorDodge => 8,
999 Self::ColorBurn => 9,
1000 Self::HardLight => 10,
1001 Self::SoftLight => 11,
1002 Self::Difference => 12,
1003 Self::Exclusion => 13,
1004 Self::Hue => 14,
1005 Self::Saturation => 15,
1006 Self::Color => 16,
1007 Self::Luminosity => 17,
1008 Self::AlphaMask => 18,
1009 }
1010 }
1011}
1012
1013#[derive(Clone, Debug, PartialEq)]
1015#[non_exhaustive]
1016pub enum TexInput {
1017 Target(RenderTargetId),
1019
1020 Image(ImageSource),
1022}
1023
1024impl From<RenderTargetId> for TexInput {
1025 fn from(target: RenderTargetId) -> Self {
1026 Self::Target(target)
1027 }
1028}
1029
1030#[derive(Clone, Debug, PartialEq)]
1032pub struct ShaderDraw {
1033 pub rect: Rect,
1035
1036 pub uv: [f32; 4],
1038
1039 pub shader: ShaderId,
1041
1042 pub uniforms: Arc<[u8]>,
1047
1048 pub textures: Arc<[(u32, TexInput)]>,
1050
1051 pub blend: BlendMode,
1053
1054 pub opacity: f32,
1056}
1057
1058#[derive(Clone, Debug, PartialEq)]
1060#[non_exhaustive]
1061pub enum PassNode {
1062 Render {
1064 target: RenderTargetId,
1066
1067 size: Size,
1069
1070 blend: BlendMode,
1072
1073 items: std::ops::Range<usize>,
1075 },
1076
1077 Present {
1079 items: std::ops::Range<usize>,
1081 },
1082}
1083
1084#[derive(Clone, Copy, Debug, PartialEq)]
1086pub struct BlitDraw {
1087 pub rect: Rect,
1089
1090 pub uv: [f32; 4],
1092
1093 pub target: RenderTargetId,
1095
1096 pub opacity: f32,
1098}
1099
1100pub const MAX_SHADER_UNIFORM_BYTES: usize = 16 * 4 * 4;
1102
1103#[derive(Clone, Copy, Debug, PartialEq)]
1105pub struct TargetBegin {
1106 pub target: RenderTargetId,
1108
1109 pub size: Size,
1111}
1112
1113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1115pub struct TargetEnd {
1116 pub target: RenderTargetId,
1118}
1119
1120#[derive(Clone, Debug, PartialEq)]
1121#[non_exhaustive]
1122pub enum RenderItem {
1123 Quad(Quad),
1124
1125 Text(TextDraw),
1126
1127 Image(ImageDraw),
1128
1129 Shadow(Shadow),
1130
1131 Waveform(WaveformDraw),
1132
1133 Gradient(GradientDraw),
1134
1135 Shader(ShaderDraw),
1137
1138 Blit {
1140 rect: Rect,
1141 uv: [f32; 4],
1142 target: RenderTargetId,
1143 blend: BlendMode,
1144 },
1145
1146 TargetBegin {
1148 id: RenderTargetId,
1149 size: Size,
1150 blend: BlendMode,
1151 },
1152
1153 TargetEnd {
1155 id: RenderTargetId,
1156 },
1157}
1158
1159pub const MAX_WAVEFORM_BARS: usize = 8192;
1160
1161fn clip_uv(rect: Rect, uv: [f32; 4], clipped: Rect) -> [f32; 4] {
1162 let (w, h) = (rect.size.width, rect.size.height);
1163 if w <= 0.0 || h <= 0.0 {
1164 return uv;
1165 }
1166 let [u0, v0, u1, v1] = uv;
1167 let fx0 = (clipped.left() - rect.left()) / w;
1168 let fx1 = (clipped.right() - rect.left()) / w;
1169 let fy0 = (clipped.top() - rect.top()) / h;
1170 let fy1 = (clipped.bottom() - rect.top()) / h;
1171 [
1172 u0 + fx0 * (u1 - u0),
1173 v0 + fy0 * (v1 - v0),
1174 u0 + fx1 * (u1 - u0),
1175 v0 + fy1 * (v1 - v0),
1176 ]
1177}
1178
1179#[derive(Clone, Copy, Debug, PartialEq)]
1180pub struct RoundedClip {
1181 pub rect: Rect,
1182
1183 pub radius: f32,
1184}
1185
1186#[derive(Clone, Copy, Debug, PartialEq)]
1187struct ClipEntry {
1188 clip: RoundedClip,
1189
1190 under: Transform,
1191}
1192
1193impl RoundedClip {
1194 pub fn new(rect: Rect, radius: f32) -> Self {
1195 let radius = if radius.is_finite() && radius > 0.0 {
1196 radius
1197 } else {
1198 0.0
1199 };
1200 Self { rect, radius }
1201 }
1202
1203 pub fn is_rounded(self) -> bool {
1204 self.radius > 0.0 && !self.rect.is_empty()
1205 }
1206}
1207
1208fn push_opacity_segment(nodes: &mut Vec<OpacityNode>, start: usize, end: usize) {
1209 if start >= end {
1210 return;
1211 }
1212 if let Some(OpacityNode::Segment { end: prev_end, .. }) = nodes.last_mut()
1213 && *prev_end == start
1214 {
1215 *prev_end = end;
1216 return;
1217 }
1218 nodes.push(OpacityNode::Segment { start, end });
1219}
1220
1221fn build_opacity_nodes(
1222 spans: &[&OpacitySpan],
1223 mut index: usize,
1224 lo: usize,
1225 hi: usize,
1226) -> (Vec<OpacityNode>, usize) {
1227 let mut nodes = Vec::new();
1228 let mut cursor = lo;
1229 while index < spans.len() {
1230 let span = spans[index];
1231 if span.start >= hi {
1232 break;
1233 }
1234 if span.start < cursor || span.end > hi || span.end <= span.start {
1235 index += 1;
1236 continue;
1237 }
1238 if span.start > cursor {
1239 push_opacity_segment(&mut nodes, cursor, span.start);
1240 }
1241 let (children, next) = build_opacity_nodes(spans, index + 1, span.start, span.end);
1242 nodes.push(OpacityNode::Group {
1243 opacity: span.opacity,
1244 children,
1245 });
1246 cursor = span.end;
1247 index = next;
1248 if cursor >= hi {
1249 break;
1250 }
1251 }
1252 if cursor < hi {
1253 push_opacity_segment(&mut nodes, cursor, hi);
1254 }
1255 (nodes, index)
1256}
1257
1258#[derive(Clone, Copy, Debug, PartialEq)]
1259struct OpacityEntry {
1260 opacity: f32,
1261
1262 start: usize,
1263
1264 depth: usize,
1265}
1266
1267#[derive(Clone, Debug, Default)]
1268pub struct RenderList {
1269 items: Vec<RenderItem>,
1270 clip_stack: Vec<ClipEntry>,
1271 opacity_stack: Vec<OpacityEntry>,
1272 opacity_spans: Vec<OpacitySpan>,
1273 transform_stack: Vec<Transform>,
1274
1275 item_clips: Vec<Option<RoundedClip>>,
1276 item_transforms: Vec<Transform>,
1277
1278 backdrops: Vec<BackdropSpan>,
1279
1280 filters: Vec<FilterSpan>,
1281
1282 next_target: u64,
1283}
1284
1285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1287pub struct RenderMark {
1288 items: usize,
1289 item_clips: usize,
1290 item_transforms: usize,
1291 backdrops: usize,
1292 filters: usize,
1293 opacity_spans: usize,
1294 clip_depth: usize,
1295 opacity_depth: usize,
1296 transform_depth: usize,
1297}
1298
1299impl RenderList {
1300 pub fn new() -> Self {
1301 Self::default()
1302 }
1303
1304 pub fn clear(&mut self) {
1305 self.items.clear();
1306 self.clip_stack.clear();
1307 self.opacity_stack.clear();
1308 self.opacity_spans.clear();
1309 self.transform_stack.clear();
1310 self.item_clips.clear();
1311 self.item_transforms.clear();
1312 self.backdrops.clear();
1313 self.filters.clear();
1314 self.next_target = 0;
1315 }
1316
1317 pub fn push_opacity(&mut self, opacity: f32) {
1318 let o = if opacity.is_finite() {
1319 opacity.clamp(0.0, 1.0)
1320 } else {
1321 1.0
1322 };
1323 self.opacity_stack.push(OpacityEntry {
1324 opacity: o,
1325 start: self.items.len(),
1326 depth: self.opacity_stack.len(),
1327 });
1328 }
1329
1330 fn close_opacity(&mut self) {
1331 let Some(entry) = self.opacity_stack.pop() else {
1332 return;
1333 };
1334 if entry.opacity < 1.0 - f32::EPSILON && entry.start < self.items.len() {
1335 self.opacity_spans.push(OpacitySpan {
1336 start: entry.start,
1337 end: self.items.len(),
1338 opacity: entry.opacity,
1339 depth: entry.depth,
1340 });
1341 }
1342 }
1343
1344 pub fn pop_opacity(&mut self) {
1345 self.close_opacity();
1346 }
1347
1348 pub fn opacity_depth(&self) -> usize {
1349 self.opacity_stack.len()
1350 }
1351
1352 pub fn truncate_opacity(&mut self, depth: usize) {
1353 while self.opacity_stack.len() > depth {
1354 self.close_opacity();
1355 }
1356 }
1357
1358 pub fn active_opacity(&self) -> f32 {
1359 self.opacity_stack
1360 .iter()
1361 .map(|e| e.opacity)
1362 .fold(1.0, |a, b| a * b)
1363 }
1364
1365 fn opacity_visible(&self) -> bool {
1366 self.active_opacity() > 0.0
1367 }
1368
1369 pub fn opacity_spans(&self) -> &[OpacitySpan] {
1370 &self.opacity_spans
1371 }
1372
1373 pub fn has_opacity_groups(&self) -> bool {
1374 !self.opacity_spans.is_empty()
1375 }
1376
1377 pub fn opacity_at(&self, index: usize) -> f32 {
1381 let mut opacity = self
1382 .opacity_spans
1383 .iter()
1384 .filter(|span| span.start <= index && index < span.end)
1385 .map(|span| span.opacity)
1386 .fold(1.0, |a, b| a * b);
1387 for entry in &self.opacity_stack {
1388 if entry.start <= index {
1389 opacity *= entry.opacity;
1390 }
1391 }
1392 opacity
1393 }
1394
1395 pub fn opacity_plan(&self) -> Vec<OpacityNode> {
1400 let mut spans: Vec<&OpacitySpan> = self.opacity_spans.iter().collect();
1401 spans.sort_by(|a, b| {
1402 a.start
1403 .cmp(&b.start)
1404 .then_with(|| b.end.cmp(&a.end))
1405 .then_with(|| a.depth.cmp(&b.depth))
1406 });
1407 let (nodes, _) = build_opacity_nodes(&spans, 0, 0, self.items.len());
1408 nodes
1409 }
1410
1411 pub fn has_targets(&self) -> bool {
1413 self.items
1414 .iter()
1415 .any(|item| matches!(item, RenderItem::TargetBegin { .. }))
1416 }
1417
1418 pub fn target_plan(&self) -> Vec<PassNode> {
1430 let mut nodes = Vec::new();
1431 let mut open: Vec<(RenderTargetId, Size, BlendMode, usize)> = Vec::new();
1432 let mut segment = 0usize;
1433 for (index, item) in self.items.iter().enumerate() {
1434 match item {
1435 RenderItem::TargetBegin { id, size, blend } => {
1436 if open.is_empty() && segment < index {
1437 nodes.push(PassNode::Present {
1438 items: segment..index,
1439 });
1440 }
1441 open.push((*id, *size, *blend, index + 1));
1442 }
1443 RenderItem::TargetEnd { id } => {
1444 let Some(position) = open.iter().rposition(|(open_id, ..)| open_id == id) else {
1445 continue;
1446 };
1447 let (target, size, blend, start) = open.remove(position);
1448 if start <= index {
1449 nodes.push(PassNode::Render {
1450 target,
1451 size,
1452 blend,
1453 items: start..index,
1454 });
1455 }
1456 if open.is_empty() {
1457 segment = index + 1;
1458 }
1459 }
1460 _ => {}
1461 }
1462 }
1463 if segment < self.items.len() {
1464 nodes.push(PassNode::Present {
1465 items: segment..self.items.len(),
1466 });
1467 }
1468 nodes
1469 }
1470
1471 pub fn push_transform(&mut self, t: Transform) {
1472 let active = self.active_transform().then(t);
1473 self.transform_stack.push(active);
1474 }
1475
1476 pub fn pop_transform(&mut self) {
1477 self.transform_stack.pop();
1478 }
1479
1480 pub fn transform_depth(&self) -> usize {
1481 self.transform_stack.len()
1482 }
1483
1484 pub fn truncate_transforms(&mut self, depth: usize) {
1485 while self.transform_stack.len() > depth {
1486 self.transform_stack.pop();
1487 }
1488 }
1489
1490 pub fn active_transform(&self) -> Transform {
1491 self.transform_stack.last().copied().unwrap_or_default()
1492 }
1493
1494 pub fn item_transform(&self, index: usize) -> Transform {
1495 self.item_transforms.get(index).copied().unwrap_or_default()
1496 }
1497
1498 pub fn item_transforms(&self) -> &[Transform] {
1499 &self.item_transforms
1500 }
1501
1502 pub fn push_clip(&mut self, rect: Rect) {
1503 self.clip_stack.push(ClipEntry {
1504 clip: RoundedClip::new(rect, 0.0),
1505 under: self.active_transform(),
1506 });
1507 }
1508
1509 pub fn push_rounded_clip(&mut self, rect: Rect, radius: f32) {
1510 self.clip_stack.push(ClipEntry {
1511 clip: RoundedClip::new(rect, radius),
1512 under: self.active_transform(),
1513 });
1514 }
1515
1516 pub fn clip_depth(&self) -> usize {
1517 self.clip_stack.len()
1518 }
1519
1520 pub fn active_clip(&self) -> Option<Rect> {
1521 let mut acc: Option<Rect> = None;
1522 for c in &self.clip_stack {
1523 acc = Some(match acc {
1524 Some(a) => a.intersect(c.clip.rect),
1525 None => c.clip.rect,
1526 });
1527 }
1528 acc
1529 }
1530
1531 pub fn active_clip_for(&self, t_item: Transform) -> Option<Rect> {
1532 if self.clip_stack.is_empty() {
1533 return None;
1534 }
1535 let inv = t_item.inverse()?;
1536 let mut acc: Option<Rect> = None;
1537 for c in &self.clip_stack {
1538 let mapped = c.under.then(inv).apply_rect_aabb(c.clip.rect);
1539 acc = Some(match acc {
1540 Some(a) => a.intersect(mapped),
1541 None => mapped,
1542 });
1543 }
1544 acc
1545 }
1546
1547 pub fn active_rounded_clip(&self) -> Option<RoundedClip> {
1548 self.clip_stack
1549 .iter()
1550 .rev()
1551 .find(|c| c.clip.is_rounded())
1552 .map(|c| c.clip)
1553 }
1554
1555 pub fn item_rounded_clip(&self, index: usize) -> Option<RoundedClip> {
1556 self.item_clips.get(index).copied().flatten()
1557 }
1558
1559 pub fn truncate_clips(&mut self, depth: usize) {
1560 while self.clip_stack.len() > depth {
1561 self.clip_stack.pop();
1562 }
1563 }
1564
1565 pub fn pop_clip(&mut self) {
1566 self.clip_stack.pop();
1567 }
1568
1569 fn clip_rect(&self, mut r: Rect) -> Option<Rect> {
1570 for c in &self.clip_stack {
1571 r = r.intersect(c.clip.rect);
1572 if r.is_empty() {
1573 return None;
1574 }
1575 }
1576 Some(r)
1577 }
1578
1579 pub fn push_quad(&mut self, rect: Rect, color: Color, radius: f32) {
1580 self.push_bordered_quad(rect, color, radius, 0.0, Color::TRANSPARENT);
1581 }
1582
1583 pub fn push_bordered_quad(
1584 &mut self,
1585 rect: Rect,
1586 color: Color,
1587 radius: f32,
1588 border_width: f32,
1589 border_color: Color,
1590 ) {
1591 if !self.opacity_visible() {
1592 return;
1593 }
1594 let t = self.active_transform();
1595
1596 if !t.is_identity() {
1597 if let Some(local) = self.active_clip_for(t)
1598 && !rect.intersects(local)
1599 {
1600 return;
1601 }
1602 let has_border = border_width > 0.0 && border_color.a > 0.0;
1603 if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1604 return;
1605 }
1606 let rounded = self.active_rounded_clip();
1607 self.items.push(RenderItem::Quad(Quad {
1608 rect,
1609 color,
1610 radius,
1611 border_color,
1612 border_width: if has_border { border_width } else { 0.0 },
1613 }));
1614 self.item_clips.push(rounded);
1615 self.item_transforms.push(t);
1616 return;
1617 }
1618 let has_border = border_width > 0.0 && border_color.a > 0.0;
1619 let plain = radius <= 0.0 && !has_border;
1625 let rect = if plain {
1626 match self.clip_rect(rect) {
1627 Some(cropped) => cropped,
1628 None => return,
1629 }
1630 } else {
1631 if let Some(clip) = self.active_clip()
1632 && !rect.intersects(clip)
1633 {
1634 return;
1635 }
1636 rect
1637 };
1638 if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1639 return;
1640 }
1641 let rounded = if plain {
1646 self.active_rounded_clip()
1647 } else {
1648 self.active_clip().map(|clip| {
1649 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1650 RoundedClip::new(clip, radius)
1651 })
1652 };
1653 self.items.push(RenderItem::Quad(Quad {
1654 rect,
1655 color,
1656 radius,
1657 border_color,
1658 border_width: if has_border { border_width } else { 0.0 },
1659 }));
1660 self.item_clips.push(rounded);
1661 self.item_transforms.push(t);
1662 }
1663
1664 pub fn push_shadow(&mut self, shadow: Shadow) {
1665 if !self.opacity_visible() {
1666 return;
1667 }
1668 let t = self.active_transform();
1669 if !shadow.is_visible() {
1670 return;
1671 }
1672 if !t.is_identity() {
1673 if let Some(local) = self.active_clip_for(t)
1674 && !shadow.bounds().intersects(local)
1675 {
1676 return;
1677 }
1678 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1679 let clip = self
1680 .active_clip()
1681 .map(|clip_rect| RoundedClip::new(clip_rect, radius));
1682 self.items.push(RenderItem::Shadow(shadow));
1683 self.item_clips.push(clip);
1684 self.item_transforms.push(t);
1685 return;
1686 }
1687 let Some(clip_rect) = self.active_clip() else {
1688 self.items.push(RenderItem::Shadow(shadow));
1689 self.item_clips.push(None);
1690 self.item_transforms.push(t);
1691 return;
1692 };
1693 if clip_rect.is_empty() {
1694 return;
1695 }
1696
1697 if !shadow.bounds().intersects(clip_rect) {
1698 return;
1699 }
1700
1701 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1702 self.items.push(RenderItem::Shadow(shadow));
1703 self.item_clips
1704 .push(Some(RoundedClip::new(clip_rect, radius)));
1705 self.item_transforms.push(self.active_transform());
1706 }
1707
1708 pub fn push_text(&mut self, mut draw: TextDraw) {
1709 if !self.opacity_visible() {
1710 return;
1711 }
1712 let t = self.active_transform();
1713 if draw.color.a <= 0.0 {
1714 return;
1715 }
1716 if !t.is_identity() {
1717 if let Some(local) = self.active_clip_for(t) {
1718 let keep = draw.glyphs.iter().any(|g| g.rect.intersects(local));
1719 if !keep {
1720 return;
1721 }
1722 }
1723 draw.glyphs.retain(|g| !g.rect.is_empty());
1724 if !draw.glyphs.is_empty() {
1725 let rounded = self.active_rounded_clip();
1726 self.items.push(RenderItem::Text(draw));
1727 self.item_clips.push(rounded);
1728 self.item_transforms.push(t);
1729 }
1730 return;
1731 }
1732 draw.glyphs.retain_mut(|g| {
1733 let Some(cr) = self.clip_rect(g.rect) else {
1734 return false;
1735 };
1736 g.uv = clip_uv(g.rect, g.uv, cr);
1737 g.rect = cr;
1738 true
1739 });
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 }
1747
1748 pub fn push_image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) {
1749 self.push_image_sampled(rect, uv, source, ImageSampling::Linear);
1750 }
1751
1752 pub fn push_image_sampled(
1753 &mut self,
1754 rect: Rect,
1755 uv: [f32; 4],
1756 source: ImageSource,
1757 sampling: ImageSampling,
1758 ) {
1759 if !self.opacity_visible() {
1760 return;
1761 }
1762 let t = self.active_transform();
1763 if !t.is_identity() {
1764 if let Some(local) = self.active_clip_for(t)
1765 && !rect.intersects(local)
1766 {
1767 return;
1768 }
1769 if rect.is_empty() {
1770 return;
1771 }
1772 let rounded = self.active_rounded_clip();
1773 self.items.push(RenderItem::Image(ImageDraw {
1774 rect,
1775 uv,
1776 source,
1777 sampling,
1778 }));
1779 self.item_clips.push(rounded);
1780 self.item_transforms.push(t);
1781 return;
1782 }
1783 let Some(cr) = self.clip_rect(rect) else {
1784 return;
1785 };
1786 if cr.is_empty() {
1787 return;
1788 }
1789 let uv = clip_uv(rect, uv, cr);
1790 let rounded = self.active_rounded_clip();
1791 self.items.push(RenderItem::Image(ImageDraw {
1792 rect: cr,
1793 uv,
1794 source,
1795 sampling,
1796 }));
1797 self.item_clips.push(rounded);
1798 self.item_transforms.push(self.active_transform());
1799 }
1800
1801 pub fn push_waveform(&mut self, bars: &[Rect], color: Color) {
1802 if !self.opacity_visible() {
1803 return;
1804 }
1805 let t = self.active_transform();
1806 if bars.is_empty() || color.a <= 0.0 {
1807 return;
1808 }
1809 if !t.is_identity() {
1810 if let Some(local) = self.active_clip_for(t) {
1811 let keep = bars.iter().any(|r| !r.is_empty() && r.intersects(local));
1812 if !keep {
1813 return;
1814 }
1815 }
1816 let clipped: Vec<Rect> = bars
1817 .iter()
1818 .copied()
1819 .filter(|r| !r.is_empty())
1820 .take(MAX_WAVEFORM_BARS)
1821 .collect();
1822 if clipped.is_empty() {
1823 return;
1824 }
1825 let rounded = self.active_rounded_clip();
1826 self.items.push(RenderItem::Waveform(WaveformDraw {
1827 bars: clipped,
1828 color,
1829 }));
1830 self.item_clips.push(rounded);
1831 self.item_transforms.push(t);
1832 return;
1833 }
1834 let clipped: Vec<Rect> = match self.active_clip() {
1835 None => bars
1836 .iter()
1837 .copied()
1838 .filter(|r| !r.is_empty())
1839 .take(MAX_WAVEFORM_BARS)
1840 .collect(),
1841 Some(clip) if clip.is_empty() => return,
1842 Some(clip) => bars
1843 .iter()
1844 .filter_map(|r| {
1845 let c = r.intersect(clip);
1846 if c.is_empty() { None } else { Some(c) }
1847 })
1848 .take(MAX_WAVEFORM_BARS)
1849 .collect(),
1850 };
1851 if clipped.is_empty() {
1852 return;
1853 }
1854 let rounded = self.active_rounded_clip();
1855 self.items.push(RenderItem::Waveform(WaveformDraw {
1856 bars: clipped,
1857 color,
1858 }));
1859 self.item_clips.push(rounded);
1860 self.item_transforms.push(self.active_transform());
1861 }
1862
1863 pub fn items(&self) -> &[RenderItem] {
1864 &self.items
1865 }
1866
1867 pub fn push_shader(&mut self, mut draw: ShaderDraw) {
1872 if !self.opacity_visible() || draw.rect.is_empty() {
1873 return;
1874 }
1875 draw.opacity = if draw.opacity.is_finite() {
1876 draw.opacity.clamp(0.0, 1.0)
1877 } else {
1878 0.0
1879 };
1880 if draw.opacity <= 0.0 {
1881 return;
1882 }
1883 if draw.uniforms.len() > MAX_SHADER_UNIFORM_BYTES {
1884 draw.uniforms = Arc::from(&draw.uniforms[..MAX_SHADER_UNIFORM_BYTES]);
1885 }
1886 if !draw.textures.is_empty() {
1887 let mut textures = draw.textures.to_vec();
1888 textures.sort_by_key(|(binding, _)| *binding);
1889 let mut deduped: Vec<(u32, TexInput)> = Vec::with_capacity(textures.len());
1890 for pair in textures {
1891 if let Some(last) = deduped.last_mut()
1892 && last.0 == pair.0
1893 {
1894 *last = pair;
1895 } else {
1896 deduped.push(pair);
1897 }
1898 }
1899 draw.textures = Arc::from(deduped);
1900 }
1901 let t = self.active_transform();
1902 if let Some(local) = self.active_clip_for(t)
1903 && !draw.rect.intersects(local)
1904 {
1905 return;
1906 }
1907 let rounded = self.active_rounded_clip();
1908 self.items.push(RenderItem::Shader(draw));
1909 self.item_clips.push(rounded);
1910 self.item_transforms.push(t);
1911 }
1912
1913 pub fn push_blit(
1915 &mut self,
1916 rect: Rect,
1917 uv: [f32; 4],
1918 target: RenderTargetId,
1919 blend: BlendMode,
1920 ) {
1921 if !self.opacity_visible() || rect.is_empty() {
1922 return;
1923 }
1924 let t = self.active_transform();
1925 let (rect, uv) = if t.is_identity() {
1926 let Some(cropped) = self.clip_rect(rect) else {
1927 return;
1928 };
1929 if cropped.is_empty() {
1930 return;
1931 }
1932 let uv = clip_uv(rect, uv, cropped);
1933 (cropped, uv)
1934 } else {
1935 if let Some(local) = self.active_clip_for(t)
1936 && !rect.intersects(local)
1937 {
1938 return;
1939 }
1940 (rect, uv)
1941 };
1942 let rounded = self.active_rounded_clip();
1943 self.items.push(RenderItem::Blit {
1944 rect,
1945 uv,
1946 target,
1947 blend,
1948 });
1949 self.item_clips.push(rounded);
1950 self.item_transforms.push(t);
1951 }
1952
1953 #[must_use]
1959 pub fn begin_target(&mut self, size: Size, blend: BlendMode) -> RenderTargetId {
1960 let id = RenderTargetId::new(self.next_target);
1961 self.next_target += 1;
1962 self.items.push(RenderItem::TargetBegin { id, size, blend });
1963 self.item_clips.push(None);
1964 self.item_transforms.push(Transform::default());
1965 id
1966 }
1967
1968 pub fn end_target(&mut self, id: RenderTargetId) {
1972 self.items.push(RenderItem::TargetEnd { id });
1973 self.item_clips.push(None);
1974 self.item_transforms.push(Transform::default());
1975 }
1976
1977 pub fn shaders(&self) -> impl Iterator<Item = &ShaderDraw> {
1979 self.items.iter().filter_map(|i| match i {
1980 RenderItem::Shader(s) => Some(s),
1981 _ => None,
1982 })
1983 }
1984
1985 pub fn push_gradient(
1986 &mut self,
1987 rect: Rect,
1988 radius: f32,
1989 kind: crate::brush::GradientKind,
1990 stops: crate::brush::GradientStops,
1991 ) {
1992 if !self.opacity_visible() {
1993 return;
1994 }
1995 if rect.is_empty() || stops.is_empty() {
1996 return;
1997 }
1998 let t = self.active_transform();
1999
2000 if !t.is_identity() {
2001 if let Some(local) = self.active_clip_for(t)
2002 && !rect.intersects(local)
2003 {
2004 return;
2005 }
2006 } else {
2007 let Some(clipped) = self.clip_rect(rect) else {
2008 return;
2009 };
2010 if clipped.is_empty() {
2011 return;
2012 }
2013
2014 return self.push_gradient_inner(clipped, radius, kind, stops, t);
2015 }
2016 self.push_gradient_inner(rect, radius, kind, stops, t)
2017 }
2018
2019 fn push_gradient_inner(
2020 &mut self,
2021 rect: Rect,
2022 radius: f32,
2023 kind: crate::brush::GradientKind,
2024 stops: crate::brush::GradientStops,
2025 t: Transform,
2026 ) {
2027 if rect.is_empty() || stops.is_empty() {
2028 return;
2029 }
2030
2031 if stops.stops().iter().all(|s| s.color.a <= 0.0) {
2032 return;
2033 }
2034 let radius = if radius.is_finite() {
2035 radius
2036 .clamp(0.0, rect.size.width.min(rect.size.height) * 0.5)
2037 .max(0.0)
2038 } else {
2039 0.0
2040 };
2041 let rounded = self.active_rounded_clip();
2042 self.items.push(RenderItem::Gradient(GradientDraw {
2043 rect,
2044 radius,
2045 kind,
2046 stops,
2047 }));
2048 self.item_clips.push(rounded);
2049 self.item_transforms.push(t);
2050 }
2051
2052 pub fn push_backdrop(
2053 &mut self,
2054 rect: Rect,
2055 radius: f32,
2056 blur: f32,
2057 dim: Color,
2058 noise: f32,
2059 mask: SpanMask,
2060 ) {
2061 if !self.opacity_visible() {
2062 return;
2063 }
2064 let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
2065 let noise = if noise.is_finite() {
2066 noise.clamp(0.0, 1.0)
2067 } else {
2068 0.0
2069 };
2070 let opacity = self.active_opacity();
2071 let dim = Color {
2072 a: dim.a * opacity,
2073 ..dim
2074 };
2075 if !(blur > 0.0 || dim.a > 0.0 || noise > 0.0) {
2076 return;
2077 }
2078 let Some(rect) = self.clip_rect(rect) else {
2079 return;
2080 };
2081 if rect.is_empty() {
2082 return;
2083 }
2084 let radius = if radius.is_finite() {
2085 radius.max(0.0)
2086 } else {
2087 0.0
2088 };
2089 let draw = BackdropDraw {
2090 rect,
2091 radius,
2092 blur,
2093 dim,
2094 noise: noise * opacity,
2095 mask,
2096 };
2097 if !draw.is_visible() {
2098 return;
2099 }
2100 self.backdrops.push(BackdropSpan {
2101 at: self.items.len(),
2102 draw,
2103 clip: self.active_rounded_clip(),
2104 });
2105 }
2106
2107 pub fn backdrops(&self) -> &[BackdropSpan] {
2108 &self.backdrops
2109 }
2110
2111 pub fn has_backdrops(&self) -> bool {
2112 !self.backdrops.is_empty()
2113 }
2114
2115 pub fn push_filter_begin(
2116 &mut self,
2117 rect: Rect,
2118 radius: f32,
2119 blur: f32,
2120 mask: SpanMask,
2121 ) -> Option<usize> {
2122 if !self.opacity_visible() {
2123 return None;
2124 }
2125 let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
2126 if !(blur > 0.0) {
2127 return None;
2128 }
2129 let rect = self.clip_rect(rect)?;
2130 if rect.is_empty() {
2131 return None;
2132 }
2133 let radius = if radius.is_finite() {
2134 radius.max(0.0)
2135 } else {
2136 0.0
2137 };
2138 let draw = FilterDraw {
2139 rect,
2140 radius,
2141 blur,
2142 mask,
2143 };
2144 if !draw.is_visible() {
2145 return None;
2146 }
2147 let start = self.items.len();
2148 self.filters.push(FilterSpan {
2149 start,
2150 end: start,
2151 draw,
2152 clip: self.active_rounded_clip(),
2153 });
2154 Some(self.filters.len() - 1)
2155 }
2156
2157 pub fn push_filter_end(&mut self, handle: usize) {
2158 let end = self.items.len();
2159 let Some(span) = self.filters.get_mut(handle) else {
2160 return;
2161 };
2162 span.end = end;
2163 if span.end <= span.start {
2164 self.filters.remove(handle);
2165 }
2166 }
2167
2168 pub fn filter_spans(&self) -> &[FilterSpan] {
2169 &self.filters
2170 }
2171
2172 pub fn has_filters(&self) -> bool {
2173 !self.filters.is_empty()
2174 }
2175
2176 pub fn item_clips(&self) -> &[Option<RoundedClip>] {
2177 &self.item_clips
2178 }
2179
2180 pub fn quads(&self) -> impl Iterator<Item = &Quad> {
2181 self.items.iter().filter_map(|i| match i {
2182 RenderItem::Quad(q) => Some(q),
2183 _ => None,
2184 })
2185 }
2186
2187 pub fn images(&self) -> impl Iterator<Item = &ImageDraw> {
2188 self.items.iter().filter_map(|i| match i {
2189 RenderItem::Image(d) => Some(d),
2190 _ => None,
2191 })
2192 }
2193
2194 pub fn shadows(&self) -> impl Iterator<Item = &Shadow> {
2195 self.items.iter().filter_map(|i| match i {
2196 RenderItem::Shadow(s) => Some(s),
2197 _ => None,
2198 })
2199 }
2200
2201 pub fn waveforms(&self) -> impl Iterator<Item = &WaveformDraw> {
2202 self.items.iter().filter_map(|i| match i {
2203 RenderItem::Waveform(w) => Some(w),
2204 _ => None,
2205 })
2206 }
2207
2208 pub fn gradients(&self) -> impl Iterator<Item = &GradientDraw> {
2209 self.items.iter().filter_map(|i| match i {
2210 RenderItem::Gradient(g) => Some(g),
2211 _ => None,
2212 })
2213 }
2214
2215 pub fn gradient_count(&self) -> usize {
2216 self.items
2217 .iter()
2218 .filter(|i| matches!(i, RenderItem::Gradient(_)))
2219 .count()
2220 }
2221
2222 pub fn waveform_bar_count(&self) -> usize {
2223 self.items
2224 .iter()
2225 .map(|i| match i {
2226 RenderItem::Waveform(w) => w.bars.len(),
2227 _ => 0,
2228 })
2229 .sum()
2230 }
2231
2232 pub fn len(&self) -> usize {
2233 self.items.len()
2234 }
2235
2236 pub fn is_empty(&self) -> bool {
2237 self.items.is_empty()
2238 }
2239
2240 pub fn mark(&self) -> RenderMark {
2242 RenderMark {
2243 items: self.items.len(),
2244 item_clips: self.item_clips.len(),
2245 item_transforms: self.item_transforms.len(),
2246 backdrops: self.backdrops.len(),
2247 filters: self.filters.len(),
2248 opacity_spans: self.opacity_spans.len(),
2249 clip_depth: self.clip_stack.len(),
2250 opacity_depth: self.opacity_stack.len(),
2251 transform_depth: self.transform_stack.len(),
2252 }
2253 }
2254
2255 pub fn suffix(&self, mark: RenderMark) -> Option<Self> {
2262 if mark.items > self.items.len()
2263 || mark.item_clips > self.item_clips.len()
2264 || mark.item_transforms > self.item_transforms.len()
2265 || mark.backdrops > self.backdrops.len()
2266 || mark.filters > self.filters.len()
2267 || mark.opacity_spans > self.opacity_spans.len()
2268 || mark.clip_depth != self.clip_stack.len()
2269 || mark.opacity_depth != self.opacity_stack.len()
2270 || mark.transform_depth != self.transform_stack.len()
2271 {
2272 return None;
2273 }
2274 let base = mark.items;
2275 Some(Self {
2276 items: self.items[mark.items..].to_vec(),
2277 clip_stack: Vec::new(),
2278 opacity_stack: Vec::new(),
2279 opacity_spans: self.opacity_spans[mark.opacity_spans..]
2280 .iter()
2281 .map(|span| OpacitySpan {
2282 start: span.start.saturating_sub(base),
2283 end: span.end.saturating_sub(base),
2284 ..*span
2285 })
2286 .collect(),
2287 transform_stack: Vec::new(),
2288 item_clips: self.item_clips[mark.item_clips..].to_vec(),
2289 item_transforms: self.item_transforms[mark.item_transforms..].to_vec(),
2290 backdrops: self.backdrops[mark.backdrops..]
2291 .iter()
2292 .map(|span| BackdropSpan {
2293 at: span.at.saturating_sub(base),
2294 ..*span
2295 })
2296 .collect(),
2297 filters: self.filters[mark.filters..]
2298 .iter()
2299 .map(|span| FilterSpan {
2300 start: span.start.saturating_sub(base),
2301 end: span.end.saturating_sub(base),
2302 ..*span
2303 })
2304 .collect(),
2305 next_target: self.next_target,
2306 })
2307 }
2308
2309 pub fn append(&mut self, other: &Self) {
2315 debug_assert!(
2316 other.clip_stack.is_empty()
2317 && other.transform_stack.is_empty()
2318 && other.opacity_stack.is_empty(),
2319 "append expects a replayable layer suffix"
2320 );
2321 let base = self.items.len();
2322 self.items.extend_from_slice(&other.items);
2323 self.item_clips.extend_from_slice(&other.item_clips);
2324 self.item_transforms
2325 .extend_from_slice(&other.item_transforms);
2326 self.backdrops
2327 .extend(other.backdrops.iter().map(|span| BackdropSpan {
2328 at: span.at + base,
2329 ..*span
2330 }));
2331 self.filters
2332 .extend(other.filters.iter().map(|span| FilterSpan {
2333 start: span.start + base,
2334 end: span.end + base,
2335 ..*span
2336 }));
2337 self.opacity_spans
2338 .extend(other.opacity_spans.iter().map(|span| OpacitySpan {
2339 start: span.start + base,
2340 end: span.end + base,
2341 ..*span
2342 }));
2343 }
2344}
2345
2346#[cfg(test)]
2347mod tests {
2348 use super::*;
2349
2350 fn tex(id: u64) -> TexInput {
2351 TexInput::Image(ImageSource::Rgba {
2352 id,
2353 width: 1,
2354 height: 1,
2355 data: Arc::new(vec![255; 4]),
2356 })
2357 }
2358
2359 #[test]
2360 fn color_construction() {
2361 assert_eq!(Color::rgb8(255, 0, 0), Color::RED);
2362 assert_eq!(Color::rgb(0.0, 1.0, 0.0).a, 1.0);
2363 assert_eq!(Color::TRANSPARENT.a, 0.0);
2364 }
2365
2366 #[test]
2367 fn hsl_roundtrips_through_rgb() {
2368 for (r, g, b) in [
2369 (255, 0, 0),
2370 (0, 255, 0),
2371 (0, 0, 255),
2372 (112, 143, 234),
2373 (185, 185, 185),
2374 ] {
2375 let c = Color::rgb8(r, g, b);
2376 let (h, s, l) = c.to_hsl();
2377 let back = Color::from_hsl(h, s, l).to_rgb8();
2378 assert!(
2379 (r as i16 - back.0 as i16).abs() <= 1
2380 && (g as i16 - back.1 as i16).abs() <= 1
2381 && (b as i16 - back.2 as i16).abs() <= 1,
2382 "#{r:02X}{g:02X}{b:02X} -> ({h:.1},{s:.3},{l:.3}) -> #{:02X}{:02X}{:02X}",
2383 back.0,
2384 back.1,
2385 back.2,
2386 );
2387 }
2388 assert_eq!(Color::from_hsl(0.0, 0.0, 0.5).to_rgb8(), (128, 128, 128));
2389
2390 assert_eq!(
2391 Color::from_hsl(720.0, 1.0, 0.5),
2392 Color::from_hsl(0.0, 1.0, 0.5)
2393 );
2394 assert_eq!(Color::from_hsl(0.0, 0.5, 0.5).to_hex6(), "BF4040");
2395 }
2396
2397 #[test]
2398 fn hex6_string_is_uppercase_without_hash() {
2399 assert_eq!(Color::rgb8(0x70, 0x8F, 0xEA).to_hex6(), "708FEA");
2400 }
2401
2402 #[test]
2403 fn hex_number_forms() {
2404 assert_eq!(Color::hex(0x0FA4FA), Color::rgb8(15, 164, 250));
2405 assert_eq!(Color::hex(0xFF0000), Color::RED);
2406 assert_eq!(Color::hex(0x000000), Color::BLACK);
2407
2408 assert_eq!(Color::hex(0x1_FF0000), Color::RED);
2409 let half = Color::hexa(0x0FA4FA80);
2410 assert_eq!(half.r, Color::hex(0x0FA4FA).r);
2411 assert_eq!(half.a, 128.0 / 255.0);
2412 assert_eq!(Color::hexa(0xFFFFFFFF).a, 1.0);
2413 }
2414
2415 #[test]
2416 fn parse_hex_forms() {
2417 assert_eq!(Color::parse_hex("#0FA4FA"), Some(Color::rgb8(15, 164, 250)));
2418 assert_eq!(Color::parse_hex("0fa4fa"), Some(Color::rgb8(15, 164, 250)));
2419 assert_eq!(
2420 Color::parse_hex("0x0FA4FA"),
2421 Some(Color::rgb8(15, 164, 250))
2422 );
2423 assert_eq!(Color::parse_hex("#fff"), Some(Color::WHITE));
2424 assert_eq!(Color::parse_hex("#0000"), Some(Color::TRANSPARENT));
2425 assert_eq!(Color::parse_hex("#0FA4FA80"), Some(Color::hexa(0x0FA4FA80)));
2426
2427 assert_eq!(Color::parse_hex("#0af"), Color::parse_hex("#00aaff"));
2428 }
2429
2430 #[test]
2431 fn parse_hex_rejects_garbage() {
2432 assert_eq!(Color::parse_hex(""), None);
2433 assert_eq!(Color::parse_hex("#"), None);
2434 assert_eq!(Color::parse_hex("bogus"), None);
2435 assert_eq!(Color::parse_hex("#12"), None);
2436 assert_eq!(Color::parse_hex("#12345"), None);
2437 assert_eq!(Color::parse_hex("#123456789"), None);
2438 assert_eq!(Color::parse_hex("#gggggg"), None);
2439 assert_eq!(Color::parse_hex(" #fff"), None, "no trimming");
2440 }
2441
2442 #[test]
2443 fn clip_balance_helpers() {
2444 let mut list = RenderList::new();
2445 assert_eq!(list.clip_depth(), 0);
2446 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2447 list.push_clip(Rect::from_xywh(0.0, 0.0, 5.0, 5.0));
2448 assert_eq!(list.clip_depth(), 2);
2449 list.truncate_clips(1);
2450 assert_eq!(list.clip_depth(), 1);
2451 list.truncate_clips(0);
2452 assert_eq!(list.clip_depth(), 0);
2453 }
2454
2455 #[test]
2456 fn opacity_stack_tracks_group_alpha() {
2457 let mut list = RenderList::new();
2458 assert_eq!(list.active_opacity(), 1.0);
2459 list.push_opacity(0.5);
2460 list.push_opacity(0.5);
2461 assert_eq!(list.active_opacity(), 0.25, "nestings multiply");
2462 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2463 let q = list.quads().next().expect("quad po push");
2464 assert_eq!(q.color.a, 1.0, "items keep their own alpha");
2465 assert!((list.opacity_at(0) - 0.25).abs() < 1e-6);
2466 list.pop_opacity();
2467 list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2468 assert_eq!(list.opacity_at(1), 0.5);
2469
2470 list.push_opacity(0.0);
2471 list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2472 assert_eq!(list.quads().count(), 2, "quad przy opacity 0 nie wchodzi");
2473 list.truncate_opacity(0);
2474 assert_eq!(list.opacity_depth(), 0);
2475 assert_eq!(list.active_opacity(), 1.0);
2476 assert_eq!(list.opacity_spans().len(), 2, "outer and inner span");
2477 assert_eq!(list.opacity_at(0), 0.25);
2478 assert_eq!(list.opacity_at(1), 0.5);
2479 assert_eq!(list.opacity_at(2), 1.0, "poza grupami");
2480
2481 list.push_opacity(f32::NAN);
2482 list.push_quad(Rect::from_xywh(60.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2483 let qs: Vec<_> = list.quads().collect();
2484 assert_eq!(qs[2].color.a, 1.0);
2485 assert_eq!(list.opacity_at(2), 1.0);
2486 list.clear();
2487 assert_eq!(list.opacity_depth(), 0, "clear also resets opacity");
2488 assert!(list.opacity_spans().is_empty());
2489 }
2490
2491 #[test]
2492 fn render_list_suffix_and_append_replay_items() {
2493 let mut list = RenderList::new();
2494 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2495 let mark = list.mark();
2496 list.push_clip(Rect::from_xywh(0.0, 0.0, 20.0, 20.0));
2497 list.push_quad(Rect::from_xywh(5.0, 5.0, 10.0, 10.0), Color::GREEN, 2.0);
2498 list.pop_clip();
2499
2500 let suffix = list.suffix(mark).expect("plain items are replayable");
2501 assert_eq!(suffix.len(), 1);
2502 assert_eq!(suffix.item_clips().len(), 1);
2503 assert_eq!(suffix.item_transforms().len(), 1);
2504
2505 let mut out = RenderList::new();
2506 out.append(&suffix);
2507 assert_eq!(out.len(), 1);
2508 assert_eq!(out.item_clips().len(), 1);
2509 assert_eq!(out.item_transforms().len(), 1);
2510 assert_eq!(
2511 out.quads().next().unwrap().rect,
2512 suffix.quads().next().unwrap().rect
2513 );
2514 }
2515
2516 #[test]
2517 fn render_list_suffix_rebases_composited_spans() {
2518 let mut list = RenderList::new();
2519 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2520 let mark = list.mark();
2521
2522 list.push_backdrop(
2523 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2524 0.0,
2525 4.0,
2526 Color::BLACK,
2527 0.0,
2528 SpanMask::None,
2529 );
2530 let filter = list
2531 .push_filter_begin(
2532 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2533 0.0,
2534 4.0,
2535 SpanMask::None,
2536 )
2537 .expect("visible filter opens a span");
2538 list.push_opacity(0.5);
2539 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
2540 list.pop_opacity();
2541 list.push_filter_end(filter);
2542
2543 let suffix = list.suffix(mark).expect("closed spans are replayable");
2544 assert_eq!(suffix.backdrops().len(), 1);
2545 assert_eq!(suffix.backdrops()[0].at, 0, "backdrop rebased to suffix");
2546 assert_eq!(suffix.filter_spans().len(), 1);
2547 assert_eq!(
2548 (suffix.filter_spans()[0].start, suffix.filter_spans()[0].end),
2549 (0, 1)
2550 );
2551 assert_eq!(suffix.opacity_spans().len(), 1);
2552 assert_eq!(
2553 (
2554 suffix.opacity_spans()[0].start,
2555 suffix.opacity_spans()[0].end
2556 ),
2557 (0, 1)
2558 );
2559
2560 let mut out = RenderList::new();
2561 out.push_quad(Rect::from_xywh(0.0, 0.0, 1.0, 1.0), Color::BLUE, 0.0);
2562 out.push_quad(Rect::from_xywh(0.0, 0.0, 1.0, 1.0), Color::BLUE, 0.0);
2563 out.append(&suffix);
2564 assert_eq!(out.backdrops()[0].at, 2);
2565 assert_eq!(out.filter_spans()[0].start, 2);
2566 assert_eq!(out.filter_spans()[0].end, 3);
2567 assert_eq!(out.opacity_spans()[0].start, 2);
2568 assert_eq!(out.opacity_spans()[0].end, 3);
2569 }
2570
2571 #[test]
2572 fn render_list_suffix_rejects_open_groups() {
2573 let mut list = RenderList::new();
2574 let mark = list.mark();
2575 list.push_opacity(0.5);
2576 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2577 assert!(
2578 list.suffix(mark).is_none(),
2579 "an open opacity group cannot be replayed"
2580 );
2581 list.pop_opacity();
2582
2583 let mark = list.mark();
2584 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2585 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2586 assert!(
2587 list.suffix(mark).is_none(),
2588 "an open clip cannot be replayed"
2589 );
2590 }
2591
2592 #[test]
2593 fn opacity_plan_nests_groups_and_merges_segments() {
2594 let mut list = RenderList::new();
2595 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2596 list.push_opacity(0.5);
2597 list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2598 list.push_opacity(0.5);
2599 list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2600 list.pop_opacity();
2601 list.push_quad(Rect::from_xywh(30.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2602 list.pop_opacity();
2603 list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2604
2605 assert_eq!(list.opacity_at(0), 1.0);
2606 assert_eq!(list.opacity_at(1), 0.5);
2607 assert_eq!(list.opacity_at(2), 0.25);
2608 assert_eq!(list.opacity_at(3), 0.5);
2609 assert_eq!(list.opacity_at(4), 1.0);
2610
2611 let plan = list.opacity_plan();
2612 assert_eq!(plan.len(), 3);
2613 assert_eq!(plan[0], OpacityNode::Segment { start: 0, end: 1 });
2614 assert_eq!(plan[2], OpacityNode::Segment { start: 4, end: 5 });
2615 let OpacityNode::Group { opacity, children } = &plan[1] else {
2616 panic!("expected outer group, got {:?}", plan[1]);
2617 };
2618 assert_eq!(*opacity, 0.5);
2619 assert_eq!(children.len(), 3);
2620 assert_eq!(children[0], OpacityNode::Segment { start: 1, end: 2 });
2621 assert_eq!(
2622 children[1],
2623 OpacityNode::Group {
2624 opacity: 0.5,
2625 children: vec![OpacityNode::Segment { start: 2, end: 3 }],
2626 }
2627 );
2628 assert_eq!(children[2], OpacityNode::Segment { start: 3, end: 4 });
2629 }
2630
2631 #[test]
2632 fn list_push_and_clear() {
2633 let mut list = RenderList::new();
2634 assert!(list.is_empty());
2635 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2636 assert_eq!(list.len(), 1);
2637 list.push_quad(Rect::ZERO, Color::RED, 0.0);
2638 list.push_quad(
2639 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2640 Color::TRANSPARENT,
2641 0.0,
2642 );
2643 assert_eq!(list.len(), 1, "empty-value filters");
2644
2645 list.push_text(TextDraw::default());
2646 assert_eq!(list.len(), 1, "empty TextDraw skipped");
2647
2648 list.push_text(TextDraw {
2649 color: Color::WHITE,
2650 glyphs: vec![GlyphQuad {
2651 rect: Rect::ZERO,
2652 uv: [0.0; 4],
2653 color_glyph: false,
2654 }],
2655 });
2656 assert_eq!(list.len(), 2);
2657 assert_eq!(list.quads().count(), 1);
2658
2659 list.clear();
2660 assert!(list.is_empty());
2661 }
2662
2663 #[test]
2664 fn bordered_quad_filters_and_flags() {
2665 let r = Rect::from_xywh(0.0, 0.0, 100.0, 60.0);
2666 let mut list = RenderList::new();
2667
2668 list.push_quad(r, Color::BLUE, 8.0);
2669 let q = list.quads().next().unwrap();
2670 assert!(!q.has_border());
2671 assert_eq!(q.border_width, 0.0);
2672
2673 let bg = Color::rgba(154.0 / 255.0, 214.0 / 255.0, 248.0 / 255.0, 24.0 / 255.0);
2674 list.push_bordered_quad(r, bg, 8.0, 1.0, Color::RED);
2675 let qs: Vec<_> = list.quads().collect();
2676 assert_eq!(qs.len(), 2, "background + border is one quad, not two");
2677 assert_eq!(qs[1].color, bg);
2678 assert_eq!(qs[1].border_color, Color::RED);
2679 assert_eq!(qs[1].border_width, 1.0);
2680 assert!(qs[1].has_border());
2681
2682 let mut list = RenderList::new();
2683 list.push_bordered_quad(r, Color::TRANSPARENT, 8.0, 2.0, Color::GREEN);
2684 assert_eq!(list.len(), 1);
2685
2686 let mut list = RenderList::new();
2687 list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 0.0, Color::RED);
2688 list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 2.0, Color::TRANSPARENT);
2689 list.push_bordered_quad(Rect::ZERO, Color::RED, 0.0, 1.0, Color::RED);
2690 assert!(list.is_empty());
2691 }
2692
2693 #[test]
2694 fn clip_intersects_and_filters() {
2695 let mut list = RenderList::new();
2696 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2697 list.push_quad(Rect::from_xywh(50.0, 50.0, 100.0, 100.0), Color::RED, 0.0);
2698 list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
2699 assert_eq!(list.len(), 1);
2700 let q = list.quads().next().unwrap();
2701 assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
2702
2703 list.pop_clip();
2704 list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
2705 assert_eq!(list.len(), 2, "unclipped after pop_clip");
2706 }
2707
2708 #[test]
2709 fn clipped_rounded_quad_keeps_its_geometry() {
2710 let clip = Rect::from_xywh(0.0, 0.0, 100.0, 100.0);
2711 let object = Rect::from_xywh(60.0, 20.0, 100.0, 80.0);
2712
2713 let mut list = RenderList::new();
2714 list.push_clip(clip);
2715 list.push_bordered_quad(object, Color::RED, 12.0, 2.0, Color::WHITE);
2716 assert_eq!(list.len(), 1);
2717 let q = list.quads().next().unwrap();
2718 assert_eq!(
2719 q.rect, object,
2720 "rounded geometry is cut by the clip, not resized to it"
2721 );
2722 assert_eq!(q.radius, 12.0, "corner radius survives the clip");
2723 assert_eq!(q.border_width, 2.0, "border stays on the real edge");
2724 let item_clip = list
2725 .item_rounded_clip(0)
2726 .expect("clip stored for the shader");
2727 assert_eq!(item_clip.rect, clip);
2728
2729 let mut outside = RenderList::new();
2730 outside.push_clip(clip);
2731 outside.push_bordered_quad(
2732 Rect::from_xywh(200.0, 200.0, 50.0, 50.0),
2733 Color::RED,
2734 8.0,
2735 1.0,
2736 Color::WHITE,
2737 );
2738 assert!(
2739 outside.is_empty(),
2740 "rounded quad fully outside the clip is still culled"
2741 );
2742
2743 let mut plain = RenderList::new();
2744 plain.push_clip(clip);
2745 plain.push_quad(Rect::from_xywh(50.0, 50.0, 100.0, 100.0), Color::RED, 0.0);
2746 assert_eq!(
2747 plain.quads().next().unwrap().rect,
2748 Rect::from_xywh(50.0, 50.0, 50.0, 50.0),
2749 "plain fills keep the cheap crop"
2750 );
2751 }
2752
2753 #[test]
2754 fn nested_clip_intersects() {
2755 let mut list = RenderList::new();
2756 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2757 list.push_clip(Rect::from_xywh(50.0, 50.0, 100.0, 100.0));
2758 list.push_quad(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), Color::RED, 0.0);
2759 assert_eq!(list.len(), 1);
2760 let q = list.quads().next().unwrap();
2761 assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
2762 }
2763
2764 #[test]
2765 fn rounded_clip_stored_per_item_for_shader() {
2766 let mut list = RenderList::new();
2767 let card = Rect::from_xywh(0.0, 0.0, 240.0, 280.0);
2768 list.push_rounded_clip(card, 8.0);
2769 list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
2770 assert_eq!(list.len(), 1);
2771 let clip = list.item_rounded_clip(0).expect("rounded clip stored");
2772 assert_eq!(clip.rect, card);
2773 assert_eq!(clip.radius, 8.0);
2774
2775 let mut list = RenderList::new();
2776 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2777 list.push_quad(Rect::from_xywh(10.0, 10.0, 20.0, 20.0), Color::RED, 0.0);
2778 assert_eq!(list.item_rounded_clip(0), None);
2779 }
2780
2781 #[test]
2782 fn rounded_clip_survives_rectangular_child_clip() {
2783 let mut list = RenderList::new();
2784 list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0), 8.0);
2785 list.push_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0));
2786 list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
2787 let clip = list.item_rounded_clip(0).expect("outer rounding kept");
2788 assert_eq!(clip.radius, 8.0);
2789 list.pop_clip();
2790 list.pop_clip();
2791 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2792 assert_eq!(list.item_rounded_clip(1), None, "no clip after pops");
2793 }
2794
2795 fn glyph(x: f32, y: f32, w: f32, h: f32) -> GlyphQuad {
2796 GlyphQuad {
2797 rect: Rect::from_xywh(x, y, w, h),
2798 uv: [0.0, 0.0, 1.0, 1.0],
2799 color_glyph: false,
2800 }
2801 }
2802
2803 fn pushed_glyphs(clip: Rect, g: GlyphQuad) -> Vec<GlyphQuad> {
2804 let mut list = RenderList::new();
2805 list.push_clip(clip);
2806 list.push_text(TextDraw {
2807 color: Color::WHITE,
2808 glyphs: vec![g],
2809 });
2810 match list.items() {
2811 [RenderItem::Text(t)] => t.glyphs.clone(),
2812 _ => panic!("expected a single TextDraw"),
2813 }
2814 }
2815
2816 #[test]
2817 fn clipped_glyph_keeps_proportional_uv() {
2818 let out = pushed_glyphs(
2819 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2820 glyph(90.0, 10.0, 20.0, 10.0),
2821 );
2822 assert_eq!(out.len(), 1);
2823 assert_eq!(out[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
2824 assert_eq!(out[0].uv, [0.0, 0.0, 0.5, 1.0]);
2825 }
2826
2827 #[test]
2828 fn fully_visible_glyph_keeps_uv() {
2829 let out = pushed_glyphs(
2830 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2831 glyph(10.0, 10.0, 20.0, 10.0),
2832 );
2833 assert_eq!(out.len(), 1);
2834 assert_eq!(out[0].rect, Rect::from_xywh(10.0, 10.0, 20.0, 10.0));
2835 assert_eq!(out[0].uv, [0.0, 0.0, 1.0, 1.0]);
2836 }
2837
2838 #[test]
2839 fn clipped_glyph_uv_scales_with_atlas_subrect() {
2840 let mut g = glyph(0.0, 0.0, 20.0, 20.0);
2841 g.uv = [0.25, 0.5, 0.75, 1.0];
2842 let out = pushed_glyphs(Rect::from_xywh(10.0, 0.0, 100.0, 100.0), g);
2843 assert_eq!(out.len(), 1);
2844 assert_eq!(out[0].rect, Rect::from_xywh(10.0, 0.0, 10.0, 20.0));
2845 assert_eq!(out[0].uv, [0.5, 0.5, 0.75, 1.0]);
2846 }
2847
2848 fn file_image() -> ImageSource {
2849 ImageSource::File("foto.png".into())
2850 }
2851
2852 #[test]
2853 fn image_push_clip_and_uv() {
2854 let mut list = RenderList::new();
2855 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2856 list.push_image(
2857 Rect::from_xywh(90.0, 10.0, 20.0, 10.0),
2858 [0.0, 0.0, 1.0, 1.0],
2859 file_image(),
2860 );
2861 list.push_image(
2862 Rect::from_xywh(200.0, 200.0, 10.0, 10.0),
2863 [0.0, 0.0, 1.0, 1.0],
2864 file_image(),
2865 );
2866 let images: Vec<_> = list.images().collect();
2867 assert_eq!(images.len(), 1, "outside the clip is dropped");
2868 assert_eq!(images[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
2869 assert_eq!(
2870 images[0].uv,
2871 [0.0, 0.0, 0.5, 1.0],
2872 "proportional UV, as for glyphs"
2873 );
2874 }
2875
2876 #[test]
2877 fn image_cache_keys() {
2878 assert_eq!(file_image().cache_key(), file_image().cache_key());
2879 assert_ne!(
2880 file_image().cache_key(),
2881 ImageSource::File("inne.png".into()).cache_key()
2882 );
2883 let rgba = |id| ImageSource::Rgba {
2884 id,
2885 width: 2,
2886 height: 2,
2887 data: Arc::new(vec![0u8; 16]),
2888 };
2889 assert_eq!(rgba(7).cache_key(), 7);
2890 assert_ne!(rgba(7).cache_key(), rgba(8).cache_key());
2891 }
2892
2893 fn shadow_spec() -> Shadow {
2894 Shadow {
2895 rect: Rect::from_xywh(10.0, 10.0, 100.0, 60.0),
2896 color: Color::BLACK,
2897 radius: 6.0,
2898 blur: 8.0,
2899 spread: 0.0,
2900 offset: Vec2::new(0.0, 2.0),
2901 inset: false,
2902 }
2903 }
2904
2905 #[test]
2906 fn shadow_push_filters_invisible_shadows() {
2907 let mut list = RenderList::new();
2908 list.push_shadow(Shadow {
2909 blur: 0.0,
2910 ..shadow_spec()
2911 });
2912 list.push_shadow(Shadow {
2913 blur: f32::NAN,
2914 ..shadow_spec()
2915 });
2916 list.push_shadow(Shadow {
2917 color: Color::TRANSPARENT,
2918 ..shadow_spec()
2919 });
2920 list.push_shadow(Shadow {
2921 rect: Rect::ZERO,
2922 ..shadow_spec()
2923 });
2924 assert!(list.is_empty(), "hidden shadows create no items");
2925 }
2926
2927 #[test]
2928 fn shadow_keeps_shape_rect_and_rides_the_clip() {
2929 let mut list = RenderList::new();
2930 list.push_clip(Rect::from_xywh(0.0, 0.0, 50.0, 50.0));
2931 list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), 12.0);
2932 list.push_shadow(shadow_spec());
2933 assert_eq!(list.len(), 1);
2934 assert_eq!(list.shadows().next().unwrap().rect, shadow_spec().rect);
2935 let clip = list.item_rounded_clip(0).expect("ancestor clip");
2936 assert_eq!(
2937 clip.rect,
2938 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2939 "przeciecie stosu clipow"
2940 );
2941 assert_eq!(clip.radius, 12.0, "rounding from inner clip");
2942 }
2943
2944 #[test]
2945 fn shadow_dropped_when_fully_clipped_out() {
2946 let mut list = RenderList::new();
2947 list.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2948 list.push_shadow(shadow_spec());
2949 assert!(list.is_empty(), "fully outside clip");
2950 }
2951
2952 #[test]
2953 fn shadow_bounds_grow_with_blur_spread_and_offset() {
2954 let s = Shadow {
2955 blur: 10.0,
2956 spread: 4.0,
2957 offset: Vec2::new(2.0, -3.0),
2958 ..shadow_spec()
2959 };
2960 let b = s.bounds();
2961 assert!((b.left() - 3.0).abs() < 1e-4, "{b:?}");
2962 assert!((b.top() - (-2.0)).abs() < 1e-4, "{b:?}");
2963 assert!((b.size.width - 118.0).abs() < 1e-4, "{b:?}");
2964 let inset = Shadow { inset: true, ..s };
2965 assert_eq!(inset.bounds(), inset.rect, "inset stays inside the shape");
2966 }
2967
2968 #[test]
2969 fn waveform_push_is_one_item_not_n() {
2970 let mut list = RenderList::new();
2971 let bars: Vec<Rect> = (0..100)
2972 .map(|i| Rect::from_xywh(i as f32 * 2.0, 0.0, 1.5, 20.0))
2973 .collect();
2974 list.push_waveform(&bars, Color::WHITE);
2975 assert_eq!(list.len(), 1, "whole waveform is 1 item");
2976 assert_eq!(list.waveform_bar_count(), 100);
2977 assert_eq!(list.waveforms().next().unwrap().bars.len(), 100);
2978 }
2979
2980 #[test]
2981 fn waveform_push_clips_and_filters() {
2982 let mut list = RenderList::new();
2983 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2984 let bars = vec![
2985 Rect::from_xywh(0.0, 0.0, 5.0, 5.0),
2986 Rect::from_xywh(200.0, 200.0, 5.0, 5.0),
2987 Rect::ZERO,
2988 ];
2989 list.push_waveform(&bars, Color::WHITE);
2990 assert_eq!(list.len(), 1);
2991 assert_eq!(list.waveform_bar_count(), 1, "outside clip dropped");
2992
2993 let mut list = RenderList::new();
2994 list.push_waveform(&[], Color::WHITE);
2995 list.push_waveform(&[Rect::from_xywh(0.0, 0.0, 5.0, 5.0)], Color::TRANSPARENT);
2996 assert!(list.is_empty());
2997 }
2998
2999 #[test]
3000 fn dynamic_image_cache_key_is_id_and_versioned() {
3001 let a = ImageSource::Dynamic {
3002 id: 7,
3003 width: 4,
3004 height: 1,
3005 data: Arc::new(vec![0u8; 16]),
3006 version: 1,
3007 };
3008 let b = ImageSource::Dynamic {
3009 id: 7,
3010 width: 4,
3011 height: 1,
3012 data: Arc::new(vec![255u8; 16]),
3013 version: 2,
3014 };
3015 assert_eq!(a.cache_key(), 7);
3016 assert_eq!(a.cache_key(), b.cache_key(), "same texture slot");
3017 assert_ne!(a, b, "other version means other data");
3018 assert_eq!(a.dynamic_version(), Some(1));
3019 assert_eq!(
3020 ImageSource::Rgba {
3021 id: 7,
3022 width: 1,
3023 height: 1,
3024 data: Arc::new(vec![0u8; 4]),
3025 }
3026 .dynamic_version(),
3027 None
3028 );
3029 }
3030
3031 #[test]
3032 fn backdrop_side_channel_keeps_item_indices() {
3033 let mut list = RenderList::new();
3034 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3035 list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
3036 assert!(!list.has_backdrops());
3037 list.push_backdrop(
3038 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3039 8.0,
3040 16.0,
3041 Color::TRANSPARENT,
3042 0.0,
3043 SpanMask::None,
3044 );
3045 assert!(list.has_backdrops());
3046 assert_eq!(list.len(), 2, "marker does not add items");
3047 let span = list.backdrops()[0];
3048 assert_eq!(span.at, 2, "rozmywa dwa quady spod spodu");
3049 assert_eq!(span.draw.rect, Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3050 assert_eq!(span.draw.blur, 16.0);
3051 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
3052 assert_eq!(list.backdrops()[0].at, 2, "at frozen in push");
3053 }
3054
3055 #[test]
3056 fn filter_span_covers_subtree_range() {
3057 let mut list = RenderList::new();
3058 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3059 let h = list
3060 .push_filter_begin(
3061 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3062 8.0,
3063 16.0,
3064 SpanMask::None,
3065 )
3066 .expect("widoczny filtr otwiera zakres");
3067 assert_eq!(list.len(), 1, "begin does not add items");
3068 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
3069 list.push_quad(Rect::from_xywh(5.0, 5.0, 5.0, 5.0), Color::GREEN, 0.0);
3070 list.push_filter_end(h);
3071 assert!(list.has_filters());
3072 let span = list.filter_spans()[0];
3073 assert_eq!((span.start, span.end), (1, 3), "tylko poddrzewo");
3074 assert_eq!(span.draw.blur, 16.0);
3075 assert_eq!(span.draw.radius, 8.0);
3076 list.clear();
3077 assert!(!list.has_filters(), "clear also drops filters");
3078 }
3079
3080 #[test]
3081 fn filter_begin_rejects_invisible_and_end_drops_empty() {
3082 let mut list = RenderList::new();
3083
3084 assert!(
3085 list.push_filter_begin(
3086 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3087 0.0,
3088 0.0,
3089 SpanMask::None
3090 )
3091 .is_none()
3092 );
3093 assert!(
3094 list.push_filter_begin(
3095 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3096 0.0,
3097 f32::NAN,
3098 SpanMask::None
3099 )
3100 .is_none()
3101 );
3102 assert!(
3103 list.push_filter_begin(
3104 Rect::from_xywh(0.0, 0.0, 0.0, 0.0),
3105 0.0,
3106 8.0,
3107 SpanMask::None
3108 )
3109 .is_none()
3110 );
3111 assert!(!list.has_filters());
3112
3113 let h = list
3114 .push_filter_begin(
3115 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3116 0.0,
3117 8.0,
3118 SpanMask::None,
3119 )
3120 .expect("otwiera");
3121 list.push_filter_end(h);
3122 assert!(!list.has_filters(), "pusty zakres bez contentu");
3123
3124 let mut clipped = RenderList::new();
3125 clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3126 let h = clipped
3127 .push_filter_begin(
3128 Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
3129 4.0,
3130 12.0,
3131 SpanMask::None,
3132 )
3133 .expect("otwiera");
3134 clipped.push_quad(Rect::from_xywh(60.0, 60.0, 10.0, 10.0), Color::RED, 0.0);
3135 clipped.push_filter_end(h);
3136 assert_eq!(
3137 clipped.filter_spans()[0].draw.rect,
3138 Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
3139 );
3140 }
3141
3142 #[test]
3143 fn filter_spans_nest_like_subtrees() {
3144 let mut list = RenderList::new();
3145 let outer = list
3146 .push_filter_begin(
3147 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3148 0.0,
3149 8.0,
3150 SpanMask::None,
3151 )
3152 .expect("outer");
3153 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3154 let inner = list
3155 .push_filter_begin(
3156 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
3157 0.0,
3158 4.0,
3159 SpanMask::None,
3160 )
3161 .expect("inner");
3162 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
3163 list.push_filter_end(inner);
3164 list.push_filter_end(outer);
3165 assert_eq!(list.filter_spans().len(), 2);
3166 assert_eq!(
3167 (list.filter_spans()[0].start, list.filter_spans()[0].end),
3168 (0, 2)
3169 );
3170 assert_eq!(
3171 (list.filter_spans()[1].start, list.filter_spans()[1].end),
3172 (1, 2)
3173 );
3174 }
3175
3176 #[test]
3177 fn backdrop_filters_and_clips() {
3178 let mut list = RenderList::new();
3179
3180 list.push_backdrop(
3181 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
3182 0.0,
3183 0.0,
3184 Color::TRANSPARENT,
3185 0.0,
3186 SpanMask::None,
3187 );
3188 assert!(!list.has_backdrops());
3189
3190 list.push_backdrop(
3191 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
3192 0.0,
3193 0.0,
3194 Color::BLACK.with_alpha(0.25),
3195 0.0,
3196 SpanMask::None,
3197 );
3198 assert_eq!(list.backdrops().len(), 1);
3199
3200 let mut clipped = RenderList::new();
3201 clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3202 clipped.push_backdrop(
3203 Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
3204 4.0,
3205 12.0,
3206 Color::TRANSPARENT,
3207 0.0,
3208 SpanMask::None,
3209 );
3210 assert_eq!(
3211 clipped.backdrops()[0].draw.rect,
3212 Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
3213 );
3214
3215 let mut outside = RenderList::new();
3216 outside.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
3217 outside.push_backdrop(
3218 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3219 0.0,
3220 12.0,
3221 Color::TRANSPARENT,
3222 0.0,
3223 SpanMask::None,
3224 );
3225 assert!(!outside.has_backdrops());
3226
3227 let mut faded = RenderList::new();
3228 faded.push_opacity(0.5);
3229 faded.push_backdrop(
3230 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3231 0.0,
3232 0.0,
3233 Color::BLACK.with_alpha(0.4),
3234 0.5,
3235 SpanMask::None,
3236 );
3237 let d = faded.backdrops()[0].draw;
3238 assert!((d.dim.a - 0.2).abs() < 1e-6, "dim.a 0.4 * 0.5: {}", d.dim.a);
3239 assert!(
3240 (d.noise - 0.25).abs() < 1e-6,
3241 "noise 0.5 * 0.5: {}",
3242 d.noise
3243 );
3244
3245 faded.clear();
3246 assert!(!faded.has_backdrops());
3247 }
3248
3249 #[test]
3250 fn span_mask_encodes_for_gpu() {
3251 assert_eq!(SpanMask::None.mask_vec(), [0.0, 0.0, 0.0, 0.0]);
3252 assert_eq!(
3253 SpanMask::Linear {
3254 from: 1.0,
3255 to: 0.0,
3256 angle_rad: std::f32::consts::FRAC_PI_2,
3257 }
3258 .mask_vec(),
3259 [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
3260 );
3261 assert_eq!(SpanMask::Uniform(0.5).mask_vec(), [2.0, 0.5, 0.0, 0.0]);
3262
3263 assert_eq!(
3264 SpanMask::Linear {
3265 from: 2.0,
3266 to: -1.0,
3267 angle_rad: f32::NAN,
3268 }
3269 .mask_vec()[1..3],
3270 [1.0, 0.0]
3271 );
3272 assert!(SpanMask::None.is_none());
3273 assert!(!SpanMask::Uniform(0.0).is_none());
3274 }
3275
3276 #[test]
3277 fn filter_span_carries_mask() {
3278 let mut list = RenderList::new();
3279 let mask = SpanMask::Linear {
3280 from: 1.0,
3281 to: 0.0,
3282 angle_rad: std::f32::consts::FRAC_PI_2,
3283 };
3284 let h = list
3285 .push_filter_begin(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), 0.0, 8.0, mask)
3286 .expect("otwiera");
3287 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3288 list.push_filter_end(h);
3289 assert_eq!(list.filter_spans()[0].draw.mask, mask);
3290 assert_eq!(
3291 list.filter_spans()[0].draw.mask.mask_vec(),
3292 [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
3293 );
3294 }
3295
3296 #[test]
3297 fn backdrop_marker_carries_mask() {
3298 let mut list = RenderList::new();
3299 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3300 list.push_backdrop(
3301 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
3302 0.0,
3303 8.0,
3304 Color::TRANSPARENT,
3305 0.0,
3306 SpanMask::Uniform(0.5),
3307 );
3308 assert_eq!(
3309 list.backdrops()[0].draw.mask,
3310 SpanMask::Uniform(0.5),
3311 "mask in the marker layer"
3312 );
3313 }
3314
3315 #[test]
3316 fn transform_stack_composes_and_snapshots() {
3317 use crate::geometry::Point;
3318 let mut list = RenderList::new();
3319 assert!(list.active_transform().is_identity());
3320
3321 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3322 assert!(list.item_transform(0).is_identity());
3323
3324 list.push_transform(Transform::rotate_around_degrees(90.0, Point::new(5.0, 5.0)));
3325 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3326 assert!(!list.item_transform(1).is_identity());
3327 assert_eq!(list.item_transforms().len(), list.items().len());
3328 list.pop_transform();
3329 assert!(list.active_transform().is_identity());
3330 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
3331 assert!(list.item_transform(2).is_identity());
3332 }
3333
3334 #[test]
3335 fn transform_aabb_covers_rotation() {
3336 use crate::geometry::Point;
3337 let r = Rect::from_xywh(0.0, 0.0, 10.0, 20.0);
3338 let t = Transform::rotate_around_degrees(90.0, Point::new(5.0, 10.0));
3339 let aabb = t.apply_rect_aabb(r);
3340 assert!(
3341 (aabb.size.width - 20.0).abs() < 0.01,
3342 "w={}",
3343 aabb.size.width
3344 );
3345 assert!(
3346 (aabb.size.height - 10.0).abs() < 0.01,
3347 "h={}",
3348 aabb.size.height
3349 );
3350 assert!(Transform::IDENTITY.apply_rect_aabb(r) == r);
3351 }
3352
3353 #[test]
3354 fn rotated_text_survives_own_self_clip() {
3355 let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
3356 let mut list = RenderList::new();
3357 list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
3358 list.push_clip(tile);
3359 list.push_text(TextDraw {
3360 color: Color::WHITE,
3361 glyphs: vec![GlyphQuad {
3362 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
3363 uv: [0.0, 0.0, 1.0, 1.0],
3364 color_glyph: false,
3365 }],
3366 });
3367 assert_eq!(list.len(), 1, "rotated glyph in custom clip remains");
3368 }
3369
3370 #[test]
3371 fn shader_draw_truncates_uniforms_and_keeps_last_binding() {
3372 let mut list = RenderList::new();
3373 let uniforms = vec![7u8; MAX_SHADER_UNIFORM_BYTES + 9];
3374 list.push_shader(ShaderDraw {
3375 rect: Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
3376 uv: [0.0, 0.0, 1.0, 1.0],
3377 shader: ShaderId::new(4),
3378 uniforms: Arc::from(uniforms),
3379 textures: Arc::from(vec![(2, tex(10)), (1, tex(11)), (2, tex(12))]),
3380 blend: BlendMode::Normal,
3381 opacity: 1.0,
3382 });
3383
3384 let draw = list.shaders().next().expect("shader draw");
3385 assert_eq!(draw.uniforms.len(), MAX_SHADER_UNIFORM_BYTES);
3386 assert_eq!(
3387 draw.textures.as_ref(),
3388 &[(1, tex(11)), (2, tex(12))],
3389 "duplicate bindings keep the last source"
3390 );
3391 assert_eq!(list.items().len(), 1);
3392 assert_eq!(list.item_clips().len(), 1);
3393 assert_eq!(list.item_transforms().len(), 1);
3394 }
3395
3396 #[test]
3397 fn target_markers_and_blit_round_trip_through_the_list() {
3398 let mut list = RenderList::new();
3399 let target = list.begin_target(Size::new(32.0, 16.0), BlendMode::Multiply);
3400 list.end_target(target);
3401 list.push_blit(
3402 Rect::from_xywh(0.0, 0.0, 32.0, 16.0),
3403 [0.0, 0.0, 1.0, 1.0],
3404 target,
3405 BlendMode::Screen,
3406 );
3407
3408 assert_eq!(target.get(), 0);
3409 assert!(matches!(
3410 list.items()[0],
3411 RenderItem::TargetBegin { id, size, blend }
3412 if id == target && size.width == 32.0 && blend == BlendMode::Multiply
3413 ));
3414 assert!(matches!(
3415 list.items()[1],
3416 RenderItem::TargetEnd { id } if id == target
3417 ));
3418 assert!(matches!(
3419 list.items()[2],
3420 RenderItem::Blit { target: id, blend: BlendMode::Screen, .. } if id == target
3421 ));
3422 assert_eq!(list.len(), 3);
3423 }
3424
3425 #[test]
3426 fn ancestor_clip_still_culls_rotated_content() {
3427 let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
3428 let mut list = RenderList::new();
3429 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3430 list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
3431 list.push_text(TextDraw {
3432 color: Color::WHITE,
3433 glyphs: vec![GlyphQuad {
3434 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
3435 uv: [0.0, 0.0, 1.0, 1.0],
3436 color_glyph: false,
3437 }],
3438 });
3439
3440 assert_eq!(list.len(), 1, "clip przodka nie zjada widocznego");
3441
3442 let mut list2 = RenderList::new();
3443 list2.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
3444 list2.push_transform(Transform::translate(500.0, 500.0));
3445 list2.push_text(TextDraw {
3446 color: Color::WHITE,
3447 glyphs: vec![GlyphQuad {
3448 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
3449 uv: [0.0, 0.0, 1.0, 1.0],
3450 color_glyph: false,
3451 }],
3452 });
3453 assert_eq!(list2.len(), 0, "clip przodka obcina poza ekranem");
3454 }
3455}