Skip to main content

sequoia_core/
paint.rs

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/// A contiguous range of paint items that share one group opacity.
843///
844/// Items keep their own alpha; the compositor applies `opacity` to the
845/// already-composited range so overlapping children do not accumulate alpha.
846#[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/// A flattened opacity-group tree derived from [`RenderList::opacity_plan`].
858#[derive(Clone, Debug, PartialEq)]
859#[non_exhaustive]
860pub enum OpacityNode {
861    /// A range of paint items drawn directly into the current target.
862    Segment { start: usize, end: usize },
863
864    /// A range that is composited into an offscreen layer at `opacity`.
865    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/// Stable handle for a shader registered with the renderer.
889#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
890pub struct ShaderId(pub u32);
891
892impl ShaderId {
893    /// Wraps a raw shader index.
894    ///
895    /// Handles returned by shader registration are the only valid values.
896    #[must_use]
897    pub const fn new(raw: u32) -> Self {
898        Self(raw)
899    }
900
901    /// Returns the raw numeric handle.
902    #[must_use]
903    pub const fn get(self) -> u32 {
904        self.0
905    }
906}
907
908/// Identifier for an offscreen render target created by [`RenderList::begin_target`].
909#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
910pub struct RenderTargetId(pub u64);
911
912impl RenderTargetId {
913    /// Wraps a raw target identifier.
914    #[must_use]
915    pub const fn new(raw: u64) -> Self {
916        Self(raw)
917    }
918
919    /// Returns the raw target identifier.
920    #[must_use]
921    pub const fn get(self) -> u64 {
922        self.0
923    }
924}
925
926/// How a shader draw is composited with the destination.
927#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
928#[non_exhaustive]
929pub enum BlendMode {
930    /// Standard source-over compositing (premultiplied alpha).
931    #[default]
932    Normal,
933
934    /// Adds source and destination.
935    Add,
936
937    /// Multiplies source and destination.
938    Multiply,
939
940    /// Inverts the product of inverted source and destination.
941    Screen,
942
943    /// Subtracts the source from the destination.
944    Subtract,
945
946    /// Combines Multiply and Screen using the destination as the mask.
947    Overlay,
948
949    /// Keeps the darker of source and destination.
950    Darken,
951
952    /// Keeps the lighter of source and destination.
953    Lighten,
954
955    /// Brightens the destination to reflect the source.
956    ColorDodge,
957
958    /// Darkens the destination to reflect the source.
959    ColorBurn,
960
961    /// Combines Multiply and Screen using the source as the mask.
962    HardLight,
963
964    /// Softens the contrast between source and destination.
965    SoftLight,
966
967    /// Absolute difference between source and destination.
968    Difference,
969
970    /// Inverted difference with lower contrast.
971    Exclusion,
972
973    /// Takes hue from the source and saturation/lightness from the destination.
974    Hue,
975
976    /// Takes saturation from the source and hue/lightness from the destination.
977    Saturation,
978
979    /// Takes hue and saturation from the source and lightness from the destination.
980    Color,
981
982    /// Takes lightness from the source and hue/saturation from the destination.
983    Luminosity,
984
985    /// Selects the source where the destination is opaque.
986    AlphaMask,
987}
988
989impl BlendMode {
990    /// Stable GPU representation used by shader pipelines.
991    #[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/// Texture made available to a shader draw.
1018#[derive(Clone, Debug, PartialEq)]
1019#[non_exhaustive]
1020pub enum TexInput {
1021    /// Output of an earlier offscreen target.
1022    Target(RenderTargetId),
1023
1024    /// Bitmap image source. The renderer uploads and caches it by cache key.
1025    Image(ImageSource),
1026}
1027
1028impl From<RenderTargetId> for TexInput {
1029    fn from(target: RenderTargetId) -> Self {
1030        Self::Target(target)
1031    }
1032}
1033
1034/// A custom-shader rectangle.
1035#[derive(Clone, Debug, PartialEq)]
1036pub struct ShaderDraw {
1037    /// Destination rectangle in logical coordinates.
1038    pub rect: Rect,
1039
1040    /// Texture coordinates as `[u0, v0, u1, v1]`.
1041    pub uv: [f32; 4],
1042
1043    /// Registered shader to execute.
1044    pub shader: ShaderId,
1045
1046    /// Raw uniform bytes exposed to the shader as `uniforms.values`.
1047    ///
1048    /// The renderer truncates payloads to [`MAX_SHADER_UNIFORM_BYTES`] and
1049    /// zero-fills the remainder.
1050    pub uniforms: Arc<[u8]>,
1051
1052    /// Binding index and texture source pairs.
1053    pub textures: Arc<[(u32, TexInput)]>,
1054
1055    /// Compositing mode applied to the shader output.
1056    pub blend: BlendMode,
1057
1058    /// Additional opacity in the range `0..=1`.
1059    pub opacity: f32,
1060}
1061
1062/// One pass of a render-target plan built by [`RenderList::target_plan`].
1063#[derive(Clone, Debug, PartialEq)]
1064#[non_exhaustive]
1065pub enum PassNode {
1066    /// Renders a range of items into an offscreen target.
1067    Render {
1068        /// Target that receives the items.
1069        target: RenderTargetId,
1070
1071        /// Target size in logical coordinates.
1072        size: Size,
1073
1074        /// Blend mode used when the target is composited.
1075        blend: BlendMode,
1076
1077        /// Items to render, as indices into the render list.
1078        items: std::ops::Range<usize>,
1079    },
1080
1081    /// Renders a range of items into the destination surface.
1082    Present {
1083        /// Items to render, as indices into the render list.
1084        items: std::ops::Range<usize>,
1085    },
1086}
1087
1088/// Copy of an offscreen target into the current paint target.
1089#[derive(Clone, Copy, Debug, PartialEq)]
1090pub struct BlitDraw {
1091    /// Destination rectangle in logical coordinates.
1092    pub rect: Rect,
1093
1094    /// Texture coordinates as `[u0, v0, u1, v1]`.
1095    pub uv: [f32; 4],
1096
1097    /// Offscreen target to copy.
1098    pub target: RenderTargetId,
1099
1100    /// Additional opacity in the range `0..=1`.
1101    pub opacity: f32,
1102}
1103
1104/// Maximum number of uniform bytes available to one shader draw.
1105pub const MAX_SHADER_UNIFORM_BYTES: usize = 16 * 4 * 4;
1106
1107/// Marker that starts drawing into an offscreen target.
1108#[derive(Clone, Copy, Debug, PartialEq)]
1109pub struct TargetBegin {
1110    /// Target being started.
1111    pub target: RenderTargetId,
1112
1113    /// Target size in logical coordinates.
1114    pub size: Size,
1115}
1116
1117/// Marker that finishes drawing into an offscreen target.
1118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1119pub struct TargetEnd {
1120    /// Target being finished.
1121    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    /// Custom GPU shader draw.
1140    Shader(ShaderDraw),
1141
1142    /// Offscreen target copy.
1143    Blit {
1144        rect: Rect,
1145        uv: [f32; 4],
1146        target: RenderTargetId,
1147        blend: BlendMode,
1148    },
1149
1150    /// Starts an offscreen pass.
1151    TargetBegin {
1152        id: RenderTargetId,
1153        size: Size,
1154        blend: BlendMode,
1155    },
1156
1157    /// Ends an offscreen pass.
1158    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/// Snapshot of a `RenderList` used to capture a suffix for layer caching.
1294#[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    /// Returns the product of all group opacities covering `index`.
1390    ///
1391    /// Includes groups that are still open, so it is usable while painting.
1392    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    /// Builds a nested plan of item segments and opacity groups.
1408    ///
1409    /// Spans are emitted when a group closes, so they arrive inner-first.
1410    /// Sorting by range and stack depth reconstructs the original nesting.
1411    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    /// Returns `true` when the list contains any offscreen target pass.
1424    pub fn has_targets(&self) -> bool {
1425        self.target_count > 0
1426    }
1427
1428    /// Returns `true` when the list contains any custom-shader draw.
1429    pub fn has_shaders(&self) -> bool {
1430        self.shader_count > 0
1431    }
1432
1433    /// Builds the execution plan for offscreen targets.
1434    ///
1435    /// Target passes are emitted when they close, so a target that feeds another
1436    /// target appears earlier in the plan than its consumer. Everything painted
1437    /// outside a target - including [`RenderItem::Blit`] items, which stay in the
1438    /// pass that contains them so painting order is preserved - becomes a
1439    /// [`PassNode::Present`] segment. Segments and passes cover the whole list in
1440    /// wiring order.
1441    ///
1442    /// Nested targets are not supported: an inner target is still emitted as its
1443    /// own pass, but its items also stay inside the enclosing target's range.
1444    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        // Cropping the quad to the clip rect saves fill for plain boxes, but it
1636        // also moves the geometry: the radius and the border are derived from
1637        // `rect`, so a rounded or bordered quad would draw its corners and edge
1638        // along the clip boundary instead of being cut off by it. Only crop
1639        // when that cannot change pixels.
1640        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        // Plain fills were cropped above, so the shader needs no clip for them.
1658        // Rounded or bordered quads keep their real geometry instead and carry
1659        // the clip as item state, so corners and edges are cut off by the clip
1660        // rather than redrawn along it.
1661        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    /// Pushes a custom-shader rectangle.
1884    ///
1885    /// The draw's uniform payload is truncated to [`MAX_SHADER_UNIFORM_BYTES`].
1886    /// Duplicate texture bindings keep the last source for each index.
1887    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    /// Pushes a copy of an offscreen target into the current target.
1931    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    /// Starts an offscreen pass and returns its handle.
1971    ///
1972    /// Rendering into targets is not implemented yet; the renderer records the
1973    /// marker and warns during a frame. The handle can be referenced by
1974    /// [`TexInput::Target`] in later stages.
1975    #[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    /// Ends an offscreen pass started with [`RenderList::begin_target`].
1987    ///
1988    /// The marker is recorded but ignored by the stage 1 renderer.
1989    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    /// Iterates over custom shader draws in recording order.
1996    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    /// Records the current end of the list for [`RenderList::suffix`].
2259    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    /// Captures everything added since `mark`.
2276    ///
2277    /// Backdrop, filter, and opacity spans are rebased to the start of the
2278    /// suffix so [`RenderList::append`] can replay them. Returns `None` if the
2279    /// clip/transform/opacity stacks are left open or the mark is out of
2280    /// bounds.
2281    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    /// Appends a cached suffix produced by [`RenderList::suffix`].
2332    ///
2333    /// Parent clip/transform/opacity state is not replayed: the captured items
2334    /// already contain their resolved clips and transforms. Backdrop, filter,
2335    /// and opacity spans are shifted by the current item count.
2336    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}