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