Skip to main content

sequoia_core/
paint.rs

1use std::sync::Arc;
2
3use crate::geometry::{Rect, 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#[derive(Clone, Debug, PartialEq)]
885#[non_exhaustive]
886pub enum RenderItem {
887    Quad(Quad),
888
889    Text(TextDraw),
890
891    Image(ImageDraw),
892
893    Shadow(Shadow),
894
895    Waveform(WaveformDraw),
896
897    Gradient(GradientDraw),
898}
899
900pub const MAX_WAVEFORM_BARS: usize = 8192;
901
902fn clip_uv(rect: Rect, uv: [f32; 4], clipped: Rect) -> [f32; 4] {
903    let (w, h) = (rect.size.width, rect.size.height);
904    if w <= 0.0 || h <= 0.0 {
905        return uv;
906    }
907    let [u0, v0, u1, v1] = uv;
908    let fx0 = (clipped.left() - rect.left()) / w;
909    let fx1 = (clipped.right() - rect.left()) / w;
910    let fy0 = (clipped.top() - rect.top()) / h;
911    let fy1 = (clipped.bottom() - rect.top()) / h;
912    [
913        u0 + fx0 * (u1 - u0),
914        v0 + fy0 * (v1 - v0),
915        u0 + fx1 * (u1 - u0),
916        v0 + fy1 * (v1 - v0),
917    ]
918}
919
920#[derive(Clone, Copy, Debug, PartialEq)]
921pub struct RoundedClip {
922    pub rect: Rect,
923
924    pub radius: f32,
925}
926
927#[derive(Clone, Copy, Debug, PartialEq)]
928struct ClipEntry {
929    clip: RoundedClip,
930
931    under: Transform,
932}
933
934impl RoundedClip {
935    pub fn new(rect: Rect, radius: f32) -> Self {
936        let radius = if radius.is_finite() && radius > 0.0 {
937            radius
938        } else {
939            0.0
940        };
941        Self { rect, radius }
942    }
943
944    pub fn is_rounded(self) -> bool {
945        self.radius > 0.0 && !self.rect.is_empty()
946    }
947}
948
949fn push_opacity_segment(nodes: &mut Vec<OpacityNode>, start: usize, end: usize) {
950    if start >= end {
951        return;
952    }
953    if let Some(OpacityNode::Segment { end: prev_end, .. }) = nodes.last_mut()
954        && *prev_end == start
955    {
956        *prev_end = end;
957        return;
958    }
959    nodes.push(OpacityNode::Segment { start, end });
960}
961
962fn build_opacity_nodes(
963    spans: &[&OpacitySpan],
964    mut index: usize,
965    lo: usize,
966    hi: usize,
967) -> (Vec<OpacityNode>, usize) {
968    let mut nodes = Vec::new();
969    let mut cursor = lo;
970    while index < spans.len() {
971        let span = spans[index];
972        if span.start >= hi {
973            break;
974        }
975        if span.start < cursor || span.end > hi || span.end <= span.start {
976            index += 1;
977            continue;
978        }
979        if span.start > cursor {
980            push_opacity_segment(&mut nodes, cursor, span.start);
981        }
982        let (children, next) = build_opacity_nodes(spans, index + 1, span.start, span.end);
983        nodes.push(OpacityNode::Group {
984            opacity: span.opacity,
985            children,
986        });
987        cursor = span.end;
988        index = next;
989        if cursor >= hi {
990            break;
991        }
992    }
993    if cursor < hi {
994        push_opacity_segment(&mut nodes, cursor, hi);
995    }
996    (nodes, index)
997}
998
999#[derive(Clone, Copy, Debug, PartialEq)]
1000struct OpacityEntry {
1001    opacity: f32,
1002
1003    start: usize,
1004
1005    depth: usize,
1006}
1007
1008#[derive(Clone, Debug, Default)]
1009pub struct RenderList {
1010    items: Vec<RenderItem>,
1011    clip_stack: Vec<ClipEntry>,
1012    opacity_stack: Vec<OpacityEntry>,
1013    opacity_spans: Vec<OpacitySpan>,
1014    transform_stack: Vec<Transform>,
1015
1016    item_clips: Vec<Option<RoundedClip>>,
1017    item_transforms: Vec<Transform>,
1018
1019    backdrops: Vec<BackdropSpan>,
1020
1021    filters: Vec<FilterSpan>,
1022}
1023
1024/// Snapshot of a `RenderList` used to capture a suffix for layer caching.
1025#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1026pub struct RenderMark {
1027    items: usize,
1028    item_clips: usize,
1029    item_transforms: usize,
1030    backdrops: usize,
1031    filters: usize,
1032    opacity_spans: usize,
1033    clip_depth: usize,
1034    opacity_depth: usize,
1035    transform_depth: usize,
1036}
1037
1038impl RenderList {
1039    pub fn new() -> Self {
1040        Self::default()
1041    }
1042
1043    pub fn clear(&mut self) {
1044        self.items.clear();
1045        self.clip_stack.clear();
1046        self.opacity_stack.clear();
1047        self.opacity_spans.clear();
1048        self.transform_stack.clear();
1049        self.item_clips.clear();
1050        self.item_transforms.clear();
1051        self.backdrops.clear();
1052        self.filters.clear();
1053    }
1054
1055    pub fn push_opacity(&mut self, opacity: f32) {
1056        let o = if opacity.is_finite() {
1057            opacity.clamp(0.0, 1.0)
1058        } else {
1059            1.0
1060        };
1061        self.opacity_stack.push(OpacityEntry {
1062            opacity: o,
1063            start: self.items.len(),
1064            depth: self.opacity_stack.len(),
1065        });
1066    }
1067
1068    fn close_opacity(&mut self) {
1069        let Some(entry) = self.opacity_stack.pop() else {
1070            return;
1071        };
1072        if entry.opacity < 1.0 - f32::EPSILON && entry.start < self.items.len() {
1073            self.opacity_spans.push(OpacitySpan {
1074                start: entry.start,
1075                end: self.items.len(),
1076                opacity: entry.opacity,
1077                depth: entry.depth,
1078            });
1079        }
1080    }
1081
1082    pub fn pop_opacity(&mut self) {
1083        self.close_opacity();
1084    }
1085
1086    pub fn opacity_depth(&self) -> usize {
1087        self.opacity_stack.len()
1088    }
1089
1090    pub fn truncate_opacity(&mut self, depth: usize) {
1091        while self.opacity_stack.len() > depth {
1092            self.close_opacity();
1093        }
1094    }
1095
1096    pub fn active_opacity(&self) -> f32 {
1097        self.opacity_stack
1098            .iter()
1099            .map(|e| e.opacity)
1100            .fold(1.0, |a, b| a * b)
1101    }
1102
1103    fn opacity_visible(&self) -> bool {
1104        self.active_opacity() > 0.0
1105    }
1106
1107    pub fn opacity_spans(&self) -> &[OpacitySpan] {
1108        &self.opacity_spans
1109    }
1110
1111    pub fn has_opacity_groups(&self) -> bool {
1112        !self.opacity_spans.is_empty()
1113    }
1114
1115    /// Returns the product of all group opacities covering `index`.
1116    ///
1117    /// Includes groups that are still open, so it is usable while painting.
1118    pub fn opacity_at(&self, index: usize) -> f32 {
1119        let mut opacity = self
1120            .opacity_spans
1121            .iter()
1122            .filter(|span| span.start <= index && index < span.end)
1123            .map(|span| span.opacity)
1124            .fold(1.0, |a, b| a * b);
1125        for entry in &self.opacity_stack {
1126            if entry.start <= index {
1127                opacity *= entry.opacity;
1128            }
1129        }
1130        opacity
1131    }
1132
1133    /// Builds a nested plan of item segments and opacity groups.
1134    ///
1135    /// Spans are emitted when a group closes, so they arrive inner-first.
1136    /// Sorting by range and stack depth reconstructs the original nesting.
1137    pub fn opacity_plan(&self) -> Vec<OpacityNode> {
1138        let mut spans: Vec<&OpacitySpan> = self.opacity_spans.iter().collect();
1139        spans.sort_by(|a, b| {
1140            a.start
1141                .cmp(&b.start)
1142                .then_with(|| b.end.cmp(&a.end))
1143                .then_with(|| a.depth.cmp(&b.depth))
1144        });
1145        let (nodes, _) = build_opacity_nodes(&spans, 0, 0, self.items.len());
1146        nodes
1147    }
1148
1149    pub fn push_transform(&mut self, t: Transform) {
1150        let active = self.active_transform().then(t);
1151        self.transform_stack.push(active);
1152    }
1153
1154    pub fn pop_transform(&mut self) {
1155        self.transform_stack.pop();
1156    }
1157
1158    pub fn transform_depth(&self) -> usize {
1159        self.transform_stack.len()
1160    }
1161
1162    pub fn truncate_transforms(&mut self, depth: usize) {
1163        while self.transform_stack.len() > depth {
1164            self.transform_stack.pop();
1165        }
1166    }
1167
1168    pub fn active_transform(&self) -> Transform {
1169        self.transform_stack.last().copied().unwrap_or_default()
1170    }
1171
1172    pub fn item_transform(&self, index: usize) -> Transform {
1173        self.item_transforms.get(index).copied().unwrap_or_default()
1174    }
1175
1176    pub fn item_transforms(&self) -> &[Transform] {
1177        &self.item_transforms
1178    }
1179
1180    pub fn push_clip(&mut self, rect: Rect) {
1181        self.clip_stack.push(ClipEntry {
1182            clip: RoundedClip::new(rect, 0.0),
1183            under: self.active_transform(),
1184        });
1185    }
1186
1187    pub fn push_rounded_clip(&mut self, rect: Rect, radius: f32) {
1188        self.clip_stack.push(ClipEntry {
1189            clip: RoundedClip::new(rect, radius),
1190            under: self.active_transform(),
1191        });
1192    }
1193
1194    pub fn clip_depth(&self) -> usize {
1195        self.clip_stack.len()
1196    }
1197
1198    pub fn active_clip(&self) -> Option<Rect> {
1199        let mut acc: Option<Rect> = None;
1200        for c in &self.clip_stack {
1201            acc = Some(match acc {
1202                Some(a) => a.intersect(c.clip.rect),
1203                None => c.clip.rect,
1204            });
1205        }
1206        acc
1207    }
1208
1209    pub fn active_clip_for(&self, t_item: Transform) -> Option<Rect> {
1210        if self.clip_stack.is_empty() {
1211            return None;
1212        }
1213        let inv = t_item.inverse()?;
1214        let mut acc: Option<Rect> = None;
1215        for c in &self.clip_stack {
1216            let mapped = c.under.then(inv).apply_rect_aabb(c.clip.rect);
1217            acc = Some(match acc {
1218                Some(a) => a.intersect(mapped),
1219                None => mapped,
1220            });
1221        }
1222        acc
1223    }
1224
1225    pub fn active_rounded_clip(&self) -> Option<RoundedClip> {
1226        self.clip_stack
1227            .iter()
1228            .rev()
1229            .find(|c| c.clip.is_rounded())
1230            .map(|c| c.clip)
1231    }
1232
1233    pub fn item_rounded_clip(&self, index: usize) -> Option<RoundedClip> {
1234        self.item_clips.get(index).copied().flatten()
1235    }
1236
1237    pub fn truncate_clips(&mut self, depth: usize) {
1238        while self.clip_stack.len() > depth {
1239            self.clip_stack.pop();
1240        }
1241    }
1242
1243    pub fn pop_clip(&mut self) {
1244        self.clip_stack.pop();
1245    }
1246
1247    fn clip_rect(&self, mut r: Rect) -> Option<Rect> {
1248        for c in &self.clip_stack {
1249            r = r.intersect(c.clip.rect);
1250            if r.is_empty() {
1251                return None;
1252            }
1253        }
1254        Some(r)
1255    }
1256
1257    pub fn push_quad(&mut self, rect: Rect, color: Color, radius: f32) {
1258        self.push_bordered_quad(rect, color, radius, 0.0, Color::TRANSPARENT);
1259    }
1260
1261    pub fn push_bordered_quad(
1262        &mut self,
1263        rect: Rect,
1264        color: Color,
1265        radius: f32,
1266        border_width: f32,
1267        border_color: Color,
1268    ) {
1269        if !self.opacity_visible() {
1270            return;
1271        }
1272        let t = self.active_transform();
1273
1274        if !t.is_identity() {
1275            if let Some(local) = self.active_clip_for(t)
1276                && !rect.intersects(local)
1277            {
1278                return;
1279            }
1280            let has_border = border_width > 0.0 && border_color.a > 0.0;
1281            if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1282                return;
1283            }
1284            let rounded = self.active_rounded_clip();
1285            self.items.push(RenderItem::Quad(Quad {
1286                rect,
1287                color,
1288                radius,
1289                border_color,
1290                border_width: if has_border { border_width } else { 0.0 },
1291            }));
1292            self.item_clips.push(rounded);
1293            self.item_transforms.push(t);
1294            return;
1295        }
1296        let Some(rect) = self.clip_rect(rect) else {
1297            return;
1298        };
1299        let has_border = border_width > 0.0 && border_color.a > 0.0;
1300        if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1301            return;
1302        }
1303        let rounded = self.active_rounded_clip();
1304        self.items.push(RenderItem::Quad(Quad {
1305            rect,
1306            color,
1307            radius,
1308            border_color,
1309            border_width: if has_border { border_width } else { 0.0 },
1310        }));
1311        self.item_clips.push(rounded);
1312        self.item_transforms.push(t);
1313    }
1314
1315    pub fn push_shadow(&mut self, shadow: Shadow) {
1316        if !self.opacity_visible() {
1317            return;
1318        }
1319        let t = self.active_transform();
1320        if !shadow.is_visible() {
1321            return;
1322        }
1323        if !t.is_identity() {
1324            if let Some(local) = self.active_clip_for(t)
1325                && !shadow.bounds().intersects(local)
1326            {
1327                return;
1328            }
1329            let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1330            let clip = self
1331                .active_clip()
1332                .map(|clip_rect| RoundedClip::new(clip_rect, radius));
1333            self.items.push(RenderItem::Shadow(shadow));
1334            self.item_clips.push(clip);
1335            self.item_transforms.push(t);
1336            return;
1337        }
1338        let Some(clip_rect) = self.active_clip() else {
1339            self.items.push(RenderItem::Shadow(shadow));
1340            self.item_clips.push(None);
1341            self.item_transforms.push(t);
1342            return;
1343        };
1344        if clip_rect.is_empty() {
1345            return;
1346        }
1347
1348        if !shadow.bounds().intersects(clip_rect) {
1349            return;
1350        }
1351
1352        let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1353        self.items.push(RenderItem::Shadow(shadow));
1354        self.item_clips
1355            .push(Some(RoundedClip::new(clip_rect, radius)));
1356        self.item_transforms.push(self.active_transform());
1357    }
1358
1359    pub fn push_text(&mut self, mut draw: TextDraw) {
1360        if !self.opacity_visible() {
1361            return;
1362        }
1363        let t = self.active_transform();
1364        if draw.color.a <= 0.0 {
1365            return;
1366        }
1367        if !t.is_identity() {
1368            if let Some(local) = self.active_clip_for(t) {
1369                let keep = draw.glyphs.iter().any(|g| g.rect.intersects(local));
1370                if !keep {
1371                    return;
1372                }
1373            }
1374            draw.glyphs.retain(|g| !g.rect.is_empty());
1375            if !draw.glyphs.is_empty() {
1376                let rounded = self.active_rounded_clip();
1377                self.items.push(RenderItem::Text(draw));
1378                self.item_clips.push(rounded);
1379                self.item_transforms.push(t);
1380            }
1381            return;
1382        }
1383        draw.glyphs.retain_mut(|g| {
1384            let Some(cr) = self.clip_rect(g.rect) else {
1385                return false;
1386            };
1387            g.uv = clip_uv(g.rect, g.uv, cr);
1388            g.rect = cr;
1389            true
1390        });
1391        if !draw.glyphs.is_empty() {
1392            let rounded = self.active_rounded_clip();
1393            self.items.push(RenderItem::Text(draw));
1394            self.item_clips.push(rounded);
1395            self.item_transforms.push(t);
1396        }
1397    }
1398
1399    pub fn push_image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) {
1400        self.push_image_sampled(rect, uv, source, ImageSampling::Linear);
1401    }
1402
1403    pub fn push_image_sampled(
1404        &mut self,
1405        rect: Rect,
1406        uv: [f32; 4],
1407        source: ImageSource,
1408        sampling: ImageSampling,
1409    ) {
1410        if !self.opacity_visible() {
1411            return;
1412        }
1413        let t = self.active_transform();
1414        if !t.is_identity() {
1415            if let Some(local) = self.active_clip_for(t)
1416                && !rect.intersects(local)
1417            {
1418                return;
1419            }
1420            if rect.is_empty() {
1421                return;
1422            }
1423            let rounded = self.active_rounded_clip();
1424            self.items.push(RenderItem::Image(ImageDraw {
1425                rect,
1426                uv,
1427                source,
1428                sampling,
1429            }));
1430            self.item_clips.push(rounded);
1431            self.item_transforms.push(t);
1432            return;
1433        }
1434        let Some(cr) = self.clip_rect(rect) else {
1435            return;
1436        };
1437        if cr.is_empty() {
1438            return;
1439        }
1440        let uv = clip_uv(rect, uv, cr);
1441        let rounded = self.active_rounded_clip();
1442        self.items.push(RenderItem::Image(ImageDraw {
1443            rect: cr,
1444            uv,
1445            source,
1446            sampling,
1447        }));
1448        self.item_clips.push(rounded);
1449        self.item_transforms.push(self.active_transform());
1450    }
1451
1452    pub fn push_waveform(&mut self, bars: &[Rect], color: Color) {
1453        if !self.opacity_visible() {
1454            return;
1455        }
1456        let t = self.active_transform();
1457        if bars.is_empty() || color.a <= 0.0 {
1458            return;
1459        }
1460        if !t.is_identity() {
1461            if let Some(local) = self.active_clip_for(t) {
1462                let keep = bars.iter().any(|r| !r.is_empty() && r.intersects(local));
1463                if !keep {
1464                    return;
1465                }
1466            }
1467            let clipped: Vec<Rect> = bars
1468                .iter()
1469                .copied()
1470                .filter(|r| !r.is_empty())
1471                .take(MAX_WAVEFORM_BARS)
1472                .collect();
1473            if clipped.is_empty() {
1474                return;
1475            }
1476            let rounded = self.active_rounded_clip();
1477            self.items.push(RenderItem::Waveform(WaveformDraw {
1478                bars: clipped,
1479                color,
1480            }));
1481            self.item_clips.push(rounded);
1482            self.item_transforms.push(t);
1483            return;
1484        }
1485        let clipped: Vec<Rect> = match self.active_clip() {
1486            None => bars
1487                .iter()
1488                .copied()
1489                .filter(|r| !r.is_empty())
1490                .take(MAX_WAVEFORM_BARS)
1491                .collect(),
1492            Some(clip) if clip.is_empty() => return,
1493            Some(clip) => bars
1494                .iter()
1495                .filter_map(|r| {
1496                    let c = r.intersect(clip);
1497                    if c.is_empty() { None } else { Some(c) }
1498                })
1499                .take(MAX_WAVEFORM_BARS)
1500                .collect(),
1501        };
1502        if clipped.is_empty() {
1503            return;
1504        }
1505        let rounded = self.active_rounded_clip();
1506        self.items.push(RenderItem::Waveform(WaveformDraw {
1507            bars: clipped,
1508            color,
1509        }));
1510        self.item_clips.push(rounded);
1511        self.item_transforms.push(self.active_transform());
1512    }
1513
1514    pub fn items(&self) -> &[RenderItem] {
1515        &self.items
1516    }
1517
1518    pub fn push_gradient(
1519        &mut self,
1520        rect: Rect,
1521        radius: f32,
1522        kind: crate::brush::GradientKind,
1523        stops: crate::brush::GradientStops,
1524    ) {
1525        if !self.opacity_visible() {
1526            return;
1527        }
1528        if rect.is_empty() || stops.is_empty() {
1529            return;
1530        }
1531        let t = self.active_transform();
1532
1533        if !t.is_identity() {
1534            if let Some(local) = self.active_clip_for(t)
1535                && !rect.intersects(local)
1536            {
1537                return;
1538            }
1539        } else {
1540            let Some(clipped) = self.clip_rect(rect) else {
1541                return;
1542            };
1543            if clipped.is_empty() {
1544                return;
1545            }
1546
1547            return self.push_gradient_inner(clipped, radius, kind, stops, t);
1548        }
1549        self.push_gradient_inner(rect, radius, kind, stops, t)
1550    }
1551
1552    fn push_gradient_inner(
1553        &mut self,
1554        rect: Rect,
1555        radius: f32,
1556        kind: crate::brush::GradientKind,
1557        stops: crate::brush::GradientStops,
1558        t: Transform,
1559    ) {
1560        if rect.is_empty() || stops.is_empty() {
1561            return;
1562        }
1563
1564        if stops.stops().iter().all(|s| s.color.a <= 0.0) {
1565            return;
1566        }
1567        let radius = if radius.is_finite() {
1568            radius
1569                .clamp(0.0, rect.size.width.min(rect.size.height) * 0.5)
1570                .max(0.0)
1571        } else {
1572            0.0
1573        };
1574        let rounded = self.active_rounded_clip();
1575        self.items.push(RenderItem::Gradient(GradientDraw {
1576            rect,
1577            radius,
1578            kind,
1579            stops,
1580        }));
1581        self.item_clips.push(rounded);
1582        self.item_transforms.push(t);
1583    }
1584
1585    pub fn push_backdrop(
1586        &mut self,
1587        rect: Rect,
1588        radius: f32,
1589        blur: f32,
1590        dim: Color,
1591        noise: f32,
1592        mask: SpanMask,
1593    ) {
1594        if !self.opacity_visible() {
1595            return;
1596        }
1597        let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
1598        let noise = if noise.is_finite() {
1599            noise.clamp(0.0, 1.0)
1600        } else {
1601            0.0
1602        };
1603        let opacity = self.active_opacity();
1604        let dim = Color {
1605            a: dim.a * opacity,
1606            ..dim
1607        };
1608        if !(blur > 0.0 || dim.a > 0.0 || noise > 0.0) {
1609            return;
1610        }
1611        let Some(rect) = self.clip_rect(rect) else {
1612            return;
1613        };
1614        if rect.is_empty() {
1615            return;
1616        }
1617        let radius = if radius.is_finite() {
1618            radius.max(0.0)
1619        } else {
1620            0.0
1621        };
1622        let draw = BackdropDraw {
1623            rect,
1624            radius,
1625            blur,
1626            dim,
1627            noise: noise * opacity,
1628            mask,
1629        };
1630        if !draw.is_visible() {
1631            return;
1632        }
1633        self.backdrops.push(BackdropSpan {
1634            at: self.items.len(),
1635            draw,
1636            clip: self.active_rounded_clip(),
1637        });
1638    }
1639
1640    pub fn backdrops(&self) -> &[BackdropSpan] {
1641        &self.backdrops
1642    }
1643
1644    pub fn has_backdrops(&self) -> bool {
1645        !self.backdrops.is_empty()
1646    }
1647
1648    pub fn push_filter_begin(
1649        &mut self,
1650        rect: Rect,
1651        radius: f32,
1652        blur: f32,
1653        mask: SpanMask,
1654    ) -> Option<usize> {
1655        if !self.opacity_visible() {
1656            return None;
1657        }
1658        let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
1659        if !(blur > 0.0) {
1660            return None;
1661        }
1662        let rect = self.clip_rect(rect)?;
1663        if rect.is_empty() {
1664            return None;
1665        }
1666        let radius = if radius.is_finite() {
1667            radius.max(0.0)
1668        } else {
1669            0.0
1670        };
1671        let draw = FilterDraw {
1672            rect,
1673            radius,
1674            blur,
1675            mask,
1676        };
1677        if !draw.is_visible() {
1678            return None;
1679        }
1680        let start = self.items.len();
1681        self.filters.push(FilterSpan {
1682            start,
1683            end: start,
1684            draw,
1685            clip: self.active_rounded_clip(),
1686        });
1687        Some(self.filters.len() - 1)
1688    }
1689
1690    pub fn push_filter_end(&mut self, handle: usize) {
1691        let end = self.items.len();
1692        let Some(span) = self.filters.get_mut(handle) else {
1693            return;
1694        };
1695        span.end = end;
1696        if span.end <= span.start {
1697            self.filters.remove(handle);
1698        }
1699    }
1700
1701    pub fn filter_spans(&self) -> &[FilterSpan] {
1702        &self.filters
1703    }
1704
1705    pub fn has_filters(&self) -> bool {
1706        !self.filters.is_empty()
1707    }
1708
1709    pub fn item_clips(&self) -> &[Option<RoundedClip>] {
1710        &self.item_clips
1711    }
1712
1713    pub fn quads(&self) -> impl Iterator<Item = &Quad> {
1714        self.items.iter().filter_map(|i| match i {
1715            RenderItem::Quad(q) => Some(q),
1716            RenderItem::Text(_)
1717            | RenderItem::Image(_)
1718            | RenderItem::Shadow(_)
1719            | RenderItem::Waveform(_)
1720            | RenderItem::Gradient(_) => None,
1721        })
1722    }
1723
1724    pub fn images(&self) -> impl Iterator<Item = &ImageDraw> {
1725        self.items.iter().filter_map(|i| match i {
1726            RenderItem::Image(d) => Some(d),
1727            RenderItem::Quad(_)
1728            | RenderItem::Text(_)
1729            | RenderItem::Shadow(_)
1730            | RenderItem::Waveform(_)
1731            | RenderItem::Gradient(_) => None,
1732        })
1733    }
1734
1735    pub fn shadows(&self) -> impl Iterator<Item = &Shadow> {
1736        self.items.iter().filter_map(|i| match i {
1737            RenderItem::Shadow(s) => Some(s),
1738            RenderItem::Quad(_)
1739            | RenderItem::Text(_)
1740            | RenderItem::Image(_)
1741            | RenderItem::Waveform(_)
1742            | RenderItem::Gradient(_) => None,
1743        })
1744    }
1745
1746    pub fn waveforms(&self) -> impl Iterator<Item = &WaveformDraw> {
1747        self.items.iter().filter_map(|i| match i {
1748            RenderItem::Waveform(w) => Some(w),
1749            RenderItem::Quad(_)
1750            | RenderItem::Text(_)
1751            | RenderItem::Image(_)
1752            | RenderItem::Shadow(_)
1753            | RenderItem::Gradient(_) => None,
1754        })
1755    }
1756
1757    pub fn gradients(&self) -> impl Iterator<Item = &GradientDraw> {
1758        self.items.iter().filter_map(|i| match i {
1759            RenderItem::Gradient(g) => Some(g),
1760            RenderItem::Quad(_)
1761            | RenderItem::Text(_)
1762            | RenderItem::Image(_)
1763            | RenderItem::Shadow(_)
1764            | RenderItem::Waveform(_) => None,
1765        })
1766    }
1767
1768    pub fn gradient_count(&self) -> usize {
1769        self.items
1770            .iter()
1771            .filter(|i| matches!(i, RenderItem::Gradient(_)))
1772            .count()
1773    }
1774
1775    pub fn waveform_bar_count(&self) -> usize {
1776        self.items
1777            .iter()
1778            .map(|i| match i {
1779                RenderItem::Waveform(w) => w.bars.len(),
1780                _ => 0,
1781            })
1782            .sum()
1783    }
1784
1785    pub fn len(&self) -> usize {
1786        self.items.len()
1787    }
1788
1789    pub fn is_empty(&self) -> bool {
1790        self.items.is_empty()
1791    }
1792
1793    /// Records the current end of the list for [`RenderList::suffix`].
1794    pub fn mark(&self) -> RenderMark {
1795        RenderMark {
1796            items: self.items.len(),
1797            item_clips: self.item_clips.len(),
1798            item_transforms: self.item_transforms.len(),
1799            backdrops: self.backdrops.len(),
1800            filters: self.filters.len(),
1801            opacity_spans: self.opacity_spans.len(),
1802            clip_depth: self.clip_stack.len(),
1803            opacity_depth: self.opacity_stack.len(),
1804            transform_depth: self.transform_stack.len(),
1805        }
1806    }
1807
1808    /// Captures everything added since `mark`.
1809    ///
1810    /// Backdrop, filter, and opacity spans are rebased to the start of the
1811    /// suffix so [`RenderList::append`] can replay them. Returns `None` if the
1812    /// clip/transform/opacity stacks are left open or the mark is out of
1813    /// bounds.
1814    pub fn suffix(&self, mark: RenderMark) -> Option<Self> {
1815        if mark.items > self.items.len()
1816            || mark.item_clips > self.item_clips.len()
1817            || mark.item_transforms > self.item_transforms.len()
1818            || mark.backdrops > self.backdrops.len()
1819            || mark.filters > self.filters.len()
1820            || mark.opacity_spans > self.opacity_spans.len()
1821            || mark.clip_depth != self.clip_stack.len()
1822            || mark.opacity_depth != self.opacity_stack.len()
1823            || mark.transform_depth != self.transform_stack.len()
1824        {
1825            return None;
1826        }
1827        let base = mark.items;
1828        Some(Self {
1829            items: self.items[mark.items..].to_vec(),
1830            clip_stack: Vec::new(),
1831            opacity_stack: Vec::new(),
1832            opacity_spans: self.opacity_spans[mark.opacity_spans..]
1833                .iter()
1834                .map(|span| OpacitySpan {
1835                    start: span.start.saturating_sub(base),
1836                    end: span.end.saturating_sub(base),
1837                    ..*span
1838                })
1839                .collect(),
1840            transform_stack: Vec::new(),
1841            item_clips: self.item_clips[mark.item_clips..].to_vec(),
1842            item_transforms: self.item_transforms[mark.item_transforms..].to_vec(),
1843            backdrops: self.backdrops[mark.backdrops..]
1844                .iter()
1845                .map(|span| BackdropSpan {
1846                    at: span.at.saturating_sub(base),
1847                    ..*span
1848                })
1849                .collect(),
1850            filters: self.filters[mark.filters..]
1851                .iter()
1852                .map(|span| FilterSpan {
1853                    start: span.start.saturating_sub(base),
1854                    end: span.end.saturating_sub(base),
1855                    ..*span
1856                })
1857                .collect(),
1858        })
1859    }
1860
1861    /// Appends a cached suffix produced by [`RenderList::suffix`].
1862    ///
1863    /// Parent clip/transform/opacity state is not replayed: the captured items
1864    /// already contain their resolved clips and transforms. Backdrop, filter,
1865    /// and opacity spans are shifted by the current item count.
1866    pub fn append(&mut self, other: &Self) {
1867        debug_assert!(
1868            other.clip_stack.is_empty()
1869                && other.transform_stack.is_empty()
1870                && other.opacity_stack.is_empty(),
1871            "append expects a replayable layer suffix"
1872        );
1873        let base = self.items.len();
1874        self.items.extend_from_slice(&other.items);
1875        self.item_clips.extend_from_slice(&other.item_clips);
1876        self.item_transforms
1877            .extend_from_slice(&other.item_transforms);
1878        self.backdrops
1879            .extend(other.backdrops.iter().map(|span| BackdropSpan {
1880                at: span.at + base,
1881                ..*span
1882            }));
1883        self.filters
1884            .extend(other.filters.iter().map(|span| FilterSpan {
1885                start: span.start + base,
1886                end: span.end + base,
1887                ..*span
1888            }));
1889        self.opacity_spans
1890            .extend(other.opacity_spans.iter().map(|span| OpacitySpan {
1891                start: span.start + base,
1892                end: span.end + base,
1893                ..*span
1894            }));
1895    }
1896}
1897
1898#[cfg(test)]
1899mod tests {
1900    use super::*;
1901
1902    #[test]
1903    fn color_construction() {
1904        assert_eq!(Color::rgb8(255, 0, 0), Color::RED);
1905        assert_eq!(Color::rgb(0.0, 1.0, 0.0).a, 1.0);
1906        assert_eq!(Color::TRANSPARENT.a, 0.0);
1907    }
1908
1909    #[test]
1910    fn hsl_roundtrips_through_rgb() {
1911        for (r, g, b) in [
1912            (255, 0, 0),
1913            (0, 255, 0),
1914            (0, 0, 255),
1915            (112, 143, 234),
1916            (185, 185, 185),
1917        ] {
1918            let c = Color::rgb8(r, g, b);
1919            let (h, s, l) = c.to_hsl();
1920            let back = Color::from_hsl(h, s, l).to_rgb8();
1921            assert!(
1922                (r as i16 - back.0 as i16).abs() <= 1
1923                    && (g as i16 - back.1 as i16).abs() <= 1
1924                    && (b as i16 - back.2 as i16).abs() <= 1,
1925                "#{r:02X}{g:02X}{b:02X} -> ({h:.1},{s:.3},{l:.3}) -> #{:02X}{:02X}{:02X}",
1926                back.0,
1927                back.1,
1928                back.2,
1929            );
1930        }
1931        assert_eq!(Color::from_hsl(0.0, 0.0, 0.5).to_rgb8(), (128, 128, 128));
1932
1933        assert_eq!(
1934            Color::from_hsl(720.0, 1.0, 0.5),
1935            Color::from_hsl(0.0, 1.0, 0.5)
1936        );
1937        assert_eq!(Color::from_hsl(0.0, 0.5, 0.5).to_hex6(), "BF4040");
1938    }
1939
1940    #[test]
1941    fn hex6_string_is_uppercase_without_hash() {
1942        assert_eq!(Color::rgb8(0x70, 0x8F, 0xEA).to_hex6(), "708FEA");
1943    }
1944
1945    #[test]
1946    fn hex_number_forms() {
1947        assert_eq!(Color::hex(0x0FA4FA), Color::rgb8(15, 164, 250));
1948        assert_eq!(Color::hex(0xFF0000), Color::RED);
1949        assert_eq!(Color::hex(0x000000), Color::BLACK);
1950
1951        assert_eq!(Color::hex(0x1_FF0000), Color::RED);
1952        let half = Color::hexa(0x0FA4FA80);
1953        assert_eq!(half.r, Color::hex(0x0FA4FA).r);
1954        assert_eq!(half.a, 128.0 / 255.0);
1955        assert_eq!(Color::hexa(0xFFFFFFFF).a, 1.0);
1956    }
1957
1958    #[test]
1959    fn parse_hex_forms() {
1960        assert_eq!(Color::parse_hex("#0FA4FA"), Some(Color::rgb8(15, 164, 250)));
1961        assert_eq!(Color::parse_hex("0fa4fa"), Some(Color::rgb8(15, 164, 250)));
1962        assert_eq!(
1963            Color::parse_hex("0x0FA4FA"),
1964            Some(Color::rgb8(15, 164, 250))
1965        );
1966        assert_eq!(Color::parse_hex("#fff"), Some(Color::WHITE));
1967        assert_eq!(Color::parse_hex("#0000"), Some(Color::TRANSPARENT));
1968        assert_eq!(Color::parse_hex("#0FA4FA80"), Some(Color::hexa(0x0FA4FA80)));
1969
1970        assert_eq!(Color::parse_hex("#0af"), Color::parse_hex("#00aaff"));
1971    }
1972
1973    #[test]
1974    fn parse_hex_rejects_garbage() {
1975        assert_eq!(Color::parse_hex(""), None);
1976        assert_eq!(Color::parse_hex("#"), None);
1977        assert_eq!(Color::parse_hex("bogus"), None);
1978        assert_eq!(Color::parse_hex("#12"), None);
1979        assert_eq!(Color::parse_hex("#12345"), None);
1980        assert_eq!(Color::parse_hex("#123456789"), None);
1981        assert_eq!(Color::parse_hex("#gggggg"), None);
1982        assert_eq!(Color::parse_hex("  #fff"), None, "no trimming");
1983    }
1984
1985    #[test]
1986    fn clip_balance_helpers() {
1987        let mut list = RenderList::new();
1988        assert_eq!(list.clip_depth(), 0);
1989        list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
1990        list.push_clip(Rect::from_xywh(0.0, 0.0, 5.0, 5.0));
1991        assert_eq!(list.clip_depth(), 2);
1992        list.truncate_clips(1);
1993        assert_eq!(list.clip_depth(), 1);
1994        list.truncate_clips(0);
1995        assert_eq!(list.clip_depth(), 0);
1996    }
1997
1998    #[test]
1999    fn opacity_stack_tracks_group_alpha() {
2000        let mut list = RenderList::new();
2001        assert_eq!(list.active_opacity(), 1.0);
2002        list.push_opacity(0.5);
2003        list.push_opacity(0.5);
2004        assert_eq!(list.active_opacity(), 0.25, "nestings multiply");
2005        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2006        let q = list.quads().next().expect("quad po push");
2007        assert_eq!(q.color.a, 1.0, "items keep their own alpha");
2008        assert!((list.opacity_at(0) - 0.25).abs() < 1e-6);
2009        list.pop_opacity();
2010        list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2011        assert_eq!(list.opacity_at(1), 0.5);
2012
2013        list.push_opacity(0.0);
2014        list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2015        assert_eq!(list.quads().count(), 2, "quad przy opacity 0 nie wchodzi");
2016        list.truncate_opacity(0);
2017        assert_eq!(list.opacity_depth(), 0);
2018        assert_eq!(list.active_opacity(), 1.0);
2019        assert_eq!(list.opacity_spans().len(), 2, "outer and inner span");
2020        assert_eq!(list.opacity_at(0), 0.25);
2021        assert_eq!(list.opacity_at(1), 0.5);
2022        assert_eq!(list.opacity_at(2), 1.0, "poza grupami");
2023
2024        list.push_opacity(f32::NAN);
2025        list.push_quad(Rect::from_xywh(60.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2026        let qs: Vec<_> = list.quads().collect();
2027        assert_eq!(qs[2].color.a, 1.0);
2028        assert_eq!(list.opacity_at(2), 1.0);
2029        list.clear();
2030        assert_eq!(list.opacity_depth(), 0, "clear also resets opacity");
2031        assert!(list.opacity_spans().is_empty());
2032    }
2033
2034    #[test]
2035    fn render_list_suffix_and_append_replay_items() {
2036        let mut list = RenderList::new();
2037        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2038        let mark = list.mark();
2039        list.push_clip(Rect::from_xywh(0.0, 0.0, 20.0, 20.0));
2040        list.push_quad(Rect::from_xywh(5.0, 5.0, 10.0, 10.0), Color::GREEN, 2.0);
2041        list.pop_clip();
2042
2043        let suffix = list.suffix(mark).expect("plain items are replayable");
2044        assert_eq!(suffix.len(), 1);
2045        assert_eq!(suffix.item_clips().len(), 1);
2046        assert_eq!(suffix.item_transforms().len(), 1);
2047
2048        let mut out = RenderList::new();
2049        out.append(&suffix);
2050        assert_eq!(out.len(), 1);
2051        assert_eq!(out.item_clips().len(), 1);
2052        assert_eq!(out.item_transforms().len(), 1);
2053        assert_eq!(
2054            out.quads().next().unwrap().rect,
2055            suffix.quads().next().unwrap().rect
2056        );
2057    }
2058
2059    #[test]
2060    fn render_list_suffix_rebases_composited_spans() {
2061        let mut list = RenderList::new();
2062        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2063        let mark = list.mark();
2064
2065        list.push_backdrop(
2066            Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2067            0.0,
2068            4.0,
2069            Color::BLACK,
2070            0.0,
2071            SpanMask::None,
2072        );
2073        let filter = list
2074            .push_filter_begin(
2075                Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2076                0.0,
2077                4.0,
2078                SpanMask::None,
2079            )
2080            .expect("visible filter opens a span");
2081        list.push_opacity(0.5);
2082        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
2083        list.pop_opacity();
2084        list.push_filter_end(filter);
2085
2086        let suffix = list.suffix(mark).expect("closed spans are replayable");
2087        assert_eq!(suffix.backdrops().len(), 1);
2088        assert_eq!(suffix.backdrops()[0].at, 0, "backdrop rebased to suffix");
2089        assert_eq!(suffix.filter_spans().len(), 1);
2090        assert_eq!(
2091            (suffix.filter_spans()[0].start, suffix.filter_spans()[0].end),
2092            (0, 1)
2093        );
2094        assert_eq!(suffix.opacity_spans().len(), 1);
2095        assert_eq!(
2096            (
2097                suffix.opacity_spans()[0].start,
2098                suffix.opacity_spans()[0].end
2099            ),
2100            (0, 1)
2101        );
2102
2103        let mut out = RenderList::new();
2104        out.push_quad(Rect::from_xywh(0.0, 0.0, 1.0, 1.0), Color::BLUE, 0.0);
2105        out.push_quad(Rect::from_xywh(0.0, 0.0, 1.0, 1.0), Color::BLUE, 0.0);
2106        out.append(&suffix);
2107        assert_eq!(out.backdrops()[0].at, 2);
2108        assert_eq!(out.filter_spans()[0].start, 2);
2109        assert_eq!(out.filter_spans()[0].end, 3);
2110        assert_eq!(out.opacity_spans()[0].start, 2);
2111        assert_eq!(out.opacity_spans()[0].end, 3);
2112    }
2113
2114    #[test]
2115    fn render_list_suffix_rejects_open_groups() {
2116        let mut list = RenderList::new();
2117        let mark = list.mark();
2118        list.push_opacity(0.5);
2119        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2120        assert!(
2121            list.suffix(mark).is_none(),
2122            "an open opacity group cannot be replayed"
2123        );
2124        list.pop_opacity();
2125
2126        let mark = list.mark();
2127        list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2128        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2129        assert!(
2130            list.suffix(mark).is_none(),
2131            "an open clip cannot be replayed"
2132        );
2133    }
2134
2135    #[test]
2136    fn opacity_plan_nests_groups_and_merges_segments() {
2137        let mut list = RenderList::new();
2138        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2139        list.push_opacity(0.5);
2140        list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2141        list.push_opacity(0.5);
2142        list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2143        list.pop_opacity();
2144        list.push_quad(Rect::from_xywh(30.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2145        list.pop_opacity();
2146        list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2147
2148        assert_eq!(list.opacity_at(0), 1.0);
2149        assert_eq!(list.opacity_at(1), 0.5);
2150        assert_eq!(list.opacity_at(2), 0.25);
2151        assert_eq!(list.opacity_at(3), 0.5);
2152        assert_eq!(list.opacity_at(4), 1.0);
2153
2154        let plan = list.opacity_plan();
2155        assert_eq!(plan.len(), 3);
2156        assert_eq!(plan[0], OpacityNode::Segment { start: 0, end: 1 });
2157        assert_eq!(plan[2], OpacityNode::Segment { start: 4, end: 5 });
2158        let OpacityNode::Group { opacity, children } = &plan[1] else {
2159            panic!("expected outer group, got {:?}", plan[1]);
2160        };
2161        assert_eq!(*opacity, 0.5);
2162        assert_eq!(children.len(), 3);
2163        assert_eq!(children[0], OpacityNode::Segment { start: 1, end: 2 });
2164        assert_eq!(
2165            children[1],
2166            OpacityNode::Group {
2167                opacity: 0.5,
2168                children: vec![OpacityNode::Segment { start: 2, end: 3 }],
2169            }
2170        );
2171        assert_eq!(children[2], OpacityNode::Segment { start: 3, end: 4 });
2172    }
2173
2174    #[test]
2175    fn list_push_and_clear() {
2176        let mut list = RenderList::new();
2177        assert!(list.is_empty());
2178        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2179        assert_eq!(list.len(), 1);
2180        list.push_quad(Rect::ZERO, Color::RED, 0.0);
2181        list.push_quad(
2182            Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2183            Color::TRANSPARENT,
2184            0.0,
2185        );
2186        assert_eq!(list.len(), 1, "empty-value filters");
2187
2188        list.push_text(TextDraw::default());
2189        assert_eq!(list.len(), 1, "empty TextDraw skipped");
2190
2191        list.push_text(TextDraw {
2192            color: Color::WHITE,
2193            glyphs: vec![GlyphQuad {
2194                rect: Rect::ZERO,
2195                uv: [0.0; 4],
2196                color_glyph: false,
2197            }],
2198        });
2199        assert_eq!(list.len(), 2);
2200        assert_eq!(list.quads().count(), 1);
2201
2202        list.clear();
2203        assert!(list.is_empty());
2204    }
2205
2206    #[test]
2207    fn bordered_quad_filters_and_flags() {
2208        let r = Rect::from_xywh(0.0, 0.0, 100.0, 60.0);
2209        let mut list = RenderList::new();
2210
2211        list.push_quad(r, Color::BLUE, 8.0);
2212        let q = list.quads().next().unwrap();
2213        assert!(!q.has_border());
2214        assert_eq!(q.border_width, 0.0);
2215
2216        let bg = Color::rgba(154.0 / 255.0, 214.0 / 255.0, 248.0 / 255.0, 24.0 / 255.0);
2217        list.push_bordered_quad(r, bg, 8.0, 1.0, Color::RED);
2218        let qs: Vec<_> = list.quads().collect();
2219        assert_eq!(qs.len(), 2, "background + border is one quad, not two");
2220        assert_eq!(qs[1].color, bg);
2221        assert_eq!(qs[1].border_color, Color::RED);
2222        assert_eq!(qs[1].border_width, 1.0);
2223        assert!(qs[1].has_border());
2224
2225        let mut list = RenderList::new();
2226        list.push_bordered_quad(r, Color::TRANSPARENT, 8.0, 2.0, Color::GREEN);
2227        assert_eq!(list.len(), 1);
2228
2229        let mut list = RenderList::new();
2230        list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 0.0, Color::RED);
2231        list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 2.0, Color::TRANSPARENT);
2232        list.push_bordered_quad(Rect::ZERO, Color::RED, 0.0, 1.0, Color::RED);
2233        assert!(list.is_empty());
2234    }
2235
2236    #[test]
2237    fn clip_intersects_and_filters() {
2238        let mut list = RenderList::new();
2239        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2240        list.push_quad(Rect::from_xywh(50.0, 50.0, 100.0, 100.0), Color::RED, 0.0);
2241        list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
2242        assert_eq!(list.len(), 1);
2243        let q = list.quads().next().unwrap();
2244        assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
2245
2246        list.pop_clip();
2247        list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
2248        assert_eq!(list.len(), 2, "unclipped after pop_clip");
2249    }
2250
2251    #[test]
2252    fn nested_clip_intersects() {
2253        let mut list = RenderList::new();
2254        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2255        list.push_clip(Rect::from_xywh(50.0, 50.0, 100.0, 100.0));
2256        list.push_quad(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), Color::RED, 0.0);
2257        assert_eq!(list.len(), 1);
2258        let q = list.quads().next().unwrap();
2259        assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
2260    }
2261
2262    #[test]
2263    fn rounded_clip_stored_per_item_for_shader() {
2264        let mut list = RenderList::new();
2265        let card = Rect::from_xywh(0.0, 0.0, 240.0, 280.0);
2266        list.push_rounded_clip(card, 8.0);
2267        list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
2268        assert_eq!(list.len(), 1);
2269        let clip = list.item_rounded_clip(0).expect("rounded clip stored");
2270        assert_eq!(clip.rect, card);
2271        assert_eq!(clip.radius, 8.0);
2272
2273        let mut list = RenderList::new();
2274        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2275        list.push_quad(Rect::from_xywh(10.0, 10.0, 20.0, 20.0), Color::RED, 0.0);
2276        assert_eq!(list.item_rounded_clip(0), None);
2277    }
2278
2279    #[test]
2280    fn rounded_clip_survives_rectangular_child_clip() {
2281        let mut list = RenderList::new();
2282        list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0), 8.0);
2283        list.push_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0));
2284        list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
2285        let clip = list.item_rounded_clip(0).expect("outer rounding kept");
2286        assert_eq!(clip.radius, 8.0);
2287        list.pop_clip();
2288        list.pop_clip();
2289        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2290        assert_eq!(list.item_rounded_clip(1), None, "no clip after pops");
2291    }
2292
2293    fn glyph(x: f32, y: f32, w: f32, h: f32) -> GlyphQuad {
2294        GlyphQuad {
2295            rect: Rect::from_xywh(x, y, w, h),
2296            uv: [0.0, 0.0, 1.0, 1.0],
2297            color_glyph: false,
2298        }
2299    }
2300
2301    fn pushed_glyphs(clip: Rect, g: GlyphQuad) -> Vec<GlyphQuad> {
2302        let mut list = RenderList::new();
2303        list.push_clip(clip);
2304        list.push_text(TextDraw {
2305            color: Color::WHITE,
2306            glyphs: vec![g],
2307        });
2308        match list.items() {
2309            [RenderItem::Text(t)] => t.glyphs.clone(),
2310            _ => panic!("expected a single TextDraw"),
2311        }
2312    }
2313
2314    #[test]
2315    fn clipped_glyph_keeps_proportional_uv() {
2316        let out = pushed_glyphs(
2317            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2318            glyph(90.0, 10.0, 20.0, 10.0),
2319        );
2320        assert_eq!(out.len(), 1);
2321        assert_eq!(out[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
2322        assert_eq!(out[0].uv, [0.0, 0.0, 0.5, 1.0]);
2323    }
2324
2325    #[test]
2326    fn fully_visible_glyph_keeps_uv() {
2327        let out = pushed_glyphs(
2328            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2329            glyph(10.0, 10.0, 20.0, 10.0),
2330        );
2331        assert_eq!(out.len(), 1);
2332        assert_eq!(out[0].rect, Rect::from_xywh(10.0, 10.0, 20.0, 10.0));
2333        assert_eq!(out[0].uv, [0.0, 0.0, 1.0, 1.0]);
2334    }
2335
2336    #[test]
2337    fn clipped_glyph_uv_scales_with_atlas_subrect() {
2338        let mut g = glyph(0.0, 0.0, 20.0, 20.0);
2339        g.uv = [0.25, 0.5, 0.75, 1.0];
2340        let out = pushed_glyphs(Rect::from_xywh(10.0, 0.0, 100.0, 100.0), g);
2341        assert_eq!(out.len(), 1);
2342        assert_eq!(out[0].rect, Rect::from_xywh(10.0, 0.0, 10.0, 20.0));
2343        assert_eq!(out[0].uv, [0.5, 0.5, 0.75, 1.0]);
2344    }
2345
2346    fn file_image() -> ImageSource {
2347        ImageSource::File("foto.png".into())
2348    }
2349
2350    #[test]
2351    fn image_push_clip_and_uv() {
2352        let mut list = RenderList::new();
2353        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2354        list.push_image(
2355            Rect::from_xywh(90.0, 10.0, 20.0, 10.0),
2356            [0.0, 0.0, 1.0, 1.0],
2357            file_image(),
2358        );
2359        list.push_image(
2360            Rect::from_xywh(200.0, 200.0, 10.0, 10.0),
2361            [0.0, 0.0, 1.0, 1.0],
2362            file_image(),
2363        );
2364        let images: Vec<_> = list.images().collect();
2365        assert_eq!(images.len(), 1, "outside the clip is dropped");
2366        assert_eq!(images[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
2367        assert_eq!(
2368            images[0].uv,
2369            [0.0, 0.0, 0.5, 1.0],
2370            "proportional UV, as for glyphs"
2371        );
2372    }
2373
2374    #[test]
2375    fn image_cache_keys() {
2376        assert_eq!(file_image().cache_key(), file_image().cache_key());
2377        assert_ne!(
2378            file_image().cache_key(),
2379            ImageSource::File("inne.png".into()).cache_key()
2380        );
2381        let rgba = |id| ImageSource::Rgba {
2382            id,
2383            width: 2,
2384            height: 2,
2385            data: Arc::new(vec![0u8; 16]),
2386        };
2387        assert_eq!(rgba(7).cache_key(), 7);
2388        assert_ne!(rgba(7).cache_key(), rgba(8).cache_key());
2389    }
2390
2391    fn shadow_spec() -> Shadow {
2392        Shadow {
2393            rect: Rect::from_xywh(10.0, 10.0, 100.0, 60.0),
2394            color: Color::BLACK,
2395            radius: 6.0,
2396            blur: 8.0,
2397            spread: 0.0,
2398            offset: Vec2::new(0.0, 2.0),
2399            inset: false,
2400        }
2401    }
2402
2403    #[test]
2404    fn shadow_push_filters_invisible_shadows() {
2405        let mut list = RenderList::new();
2406        list.push_shadow(Shadow {
2407            blur: 0.0,
2408            ..shadow_spec()
2409        });
2410        list.push_shadow(Shadow {
2411            blur: f32::NAN,
2412            ..shadow_spec()
2413        });
2414        list.push_shadow(Shadow {
2415            color: Color::TRANSPARENT,
2416            ..shadow_spec()
2417        });
2418        list.push_shadow(Shadow {
2419            rect: Rect::ZERO,
2420            ..shadow_spec()
2421        });
2422        assert!(list.is_empty(), "hidden shadows create no items");
2423    }
2424
2425    #[test]
2426    fn shadow_keeps_shape_rect_and_rides_the_clip() {
2427        let mut list = RenderList::new();
2428        list.push_clip(Rect::from_xywh(0.0, 0.0, 50.0, 50.0));
2429        list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), 12.0);
2430        list.push_shadow(shadow_spec());
2431        assert_eq!(list.len(), 1);
2432        assert_eq!(list.shadows().next().unwrap().rect, shadow_spec().rect);
2433        let clip = list.item_rounded_clip(0).expect("ancestor clip");
2434        assert_eq!(
2435            clip.rect,
2436            Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2437            "przeciecie stosu clipow"
2438        );
2439        assert_eq!(clip.radius, 12.0, "rounding from inner clip");
2440    }
2441
2442    #[test]
2443    fn shadow_dropped_when_fully_clipped_out() {
2444        let mut list = RenderList::new();
2445        list.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2446        list.push_shadow(shadow_spec());
2447        assert!(list.is_empty(), "fully outside clip");
2448    }
2449
2450    #[test]
2451    fn shadow_bounds_grow_with_blur_spread_and_offset() {
2452        let s = Shadow {
2453            blur: 10.0,
2454            spread: 4.0,
2455            offset: Vec2::new(2.0, -3.0),
2456            ..shadow_spec()
2457        };
2458        let b = s.bounds();
2459        assert!((b.left() - 3.0).abs() < 1e-4, "{b:?}");
2460        assert!((b.top() - (-2.0)).abs() < 1e-4, "{b:?}");
2461        assert!((b.size.width - 118.0).abs() < 1e-4, "{b:?}");
2462        let inset = Shadow { inset: true, ..s };
2463        assert_eq!(inset.bounds(), inset.rect, "inset stays inside the shape");
2464    }
2465
2466    #[test]
2467    fn waveform_push_is_one_item_not_n() {
2468        let mut list = RenderList::new();
2469        let bars: Vec<Rect> = (0..100)
2470            .map(|i| Rect::from_xywh(i as f32 * 2.0, 0.0, 1.5, 20.0))
2471            .collect();
2472        list.push_waveform(&bars, Color::WHITE);
2473        assert_eq!(list.len(), 1, "whole waveform is 1 item");
2474        assert_eq!(list.waveform_bar_count(), 100);
2475        assert_eq!(list.waveforms().next().unwrap().bars.len(), 100);
2476    }
2477
2478    #[test]
2479    fn waveform_push_clips_and_filters() {
2480        let mut list = RenderList::new();
2481        list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2482        let bars = vec![
2483            Rect::from_xywh(0.0, 0.0, 5.0, 5.0),
2484            Rect::from_xywh(200.0, 200.0, 5.0, 5.0),
2485            Rect::ZERO,
2486        ];
2487        list.push_waveform(&bars, Color::WHITE);
2488        assert_eq!(list.len(), 1);
2489        assert_eq!(list.waveform_bar_count(), 1, "outside clip dropped");
2490
2491        let mut list = RenderList::new();
2492        list.push_waveform(&[], Color::WHITE);
2493        list.push_waveform(&[Rect::from_xywh(0.0, 0.0, 5.0, 5.0)], Color::TRANSPARENT);
2494        assert!(list.is_empty());
2495    }
2496
2497    #[test]
2498    fn dynamic_image_cache_key_is_id_and_versioned() {
2499        let a = ImageSource::Dynamic {
2500            id: 7,
2501            width: 4,
2502            height: 1,
2503            data: Arc::new(vec![0u8; 16]),
2504            version: 1,
2505        };
2506        let b = ImageSource::Dynamic {
2507            id: 7,
2508            width: 4,
2509            height: 1,
2510            data: Arc::new(vec![255u8; 16]),
2511            version: 2,
2512        };
2513        assert_eq!(a.cache_key(), 7);
2514        assert_eq!(a.cache_key(), b.cache_key(), "same texture slot");
2515        assert_ne!(a, b, "other version means other data");
2516        assert_eq!(a.dynamic_version(), Some(1));
2517        assert_eq!(
2518            ImageSource::Rgba {
2519                id: 7,
2520                width: 1,
2521                height: 1,
2522                data: Arc::new(vec![0u8; 4]),
2523            }
2524            .dynamic_version(),
2525            None
2526        );
2527    }
2528
2529    #[test]
2530    fn backdrop_side_channel_keeps_item_indices() {
2531        let mut list = RenderList::new();
2532        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2533        list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
2534        assert!(!list.has_backdrops());
2535        list.push_backdrop(
2536            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2537            8.0,
2538            16.0,
2539            Color::TRANSPARENT,
2540            0.0,
2541            SpanMask::None,
2542        );
2543        assert!(list.has_backdrops());
2544        assert_eq!(list.len(), 2, "marker does not add items");
2545        let span = list.backdrops()[0];
2546        assert_eq!(span.at, 2, "rozmywa dwa quady spod spodu");
2547        assert_eq!(span.draw.rect, Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2548        assert_eq!(span.draw.blur, 16.0);
2549        list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2550        assert_eq!(list.backdrops()[0].at, 2, "at frozen in push");
2551    }
2552
2553    #[test]
2554    fn filter_span_covers_subtree_range() {
2555        let mut list = RenderList::new();
2556        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2557        let h = list
2558            .push_filter_begin(
2559                Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2560                8.0,
2561                16.0,
2562                SpanMask::None,
2563            )
2564            .expect("widoczny filtr otwiera zakres");
2565        assert_eq!(list.len(), 1, "begin does not add items");
2566        list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2567        list.push_quad(Rect::from_xywh(5.0, 5.0, 5.0, 5.0), Color::GREEN, 0.0);
2568        list.push_filter_end(h);
2569        assert!(list.has_filters());
2570        let span = list.filter_spans()[0];
2571        assert_eq!((span.start, span.end), (1, 3), "tylko poddrzewo");
2572        assert_eq!(span.draw.blur, 16.0);
2573        assert_eq!(span.draw.radius, 8.0);
2574        list.clear();
2575        assert!(!list.has_filters(), "clear also drops filters");
2576    }
2577
2578    #[test]
2579    fn filter_begin_rejects_invisible_and_end_drops_empty() {
2580        let mut list = RenderList::new();
2581
2582        assert!(
2583            list.push_filter_begin(
2584                Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2585                0.0,
2586                0.0,
2587                SpanMask::None
2588            )
2589            .is_none()
2590        );
2591        assert!(
2592            list.push_filter_begin(
2593                Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2594                0.0,
2595                f32::NAN,
2596                SpanMask::None
2597            )
2598            .is_none()
2599        );
2600        assert!(
2601            list.push_filter_begin(
2602                Rect::from_xywh(0.0, 0.0, 0.0, 0.0),
2603                0.0,
2604                8.0,
2605                SpanMask::None
2606            )
2607            .is_none()
2608        );
2609        assert!(!list.has_filters());
2610
2611        let h = list
2612            .push_filter_begin(
2613                Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2614                0.0,
2615                8.0,
2616                SpanMask::None,
2617            )
2618            .expect("otwiera");
2619        list.push_filter_end(h);
2620        assert!(!list.has_filters(), "pusty zakres bez contentu");
2621
2622        let mut clipped = RenderList::new();
2623        clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2624        let h = clipped
2625            .push_filter_begin(
2626                Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
2627                4.0,
2628                12.0,
2629                SpanMask::None,
2630            )
2631            .expect("otwiera");
2632        clipped.push_quad(Rect::from_xywh(60.0, 60.0, 10.0, 10.0), Color::RED, 0.0);
2633        clipped.push_filter_end(h);
2634        assert_eq!(
2635            clipped.filter_spans()[0].draw.rect,
2636            Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
2637        );
2638    }
2639
2640    #[test]
2641    fn filter_spans_nest_like_subtrees() {
2642        let mut list = RenderList::new();
2643        let outer = list
2644            .push_filter_begin(
2645                Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2646                0.0,
2647                8.0,
2648                SpanMask::None,
2649            )
2650            .expect("outer");
2651        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2652        let inner = list
2653            .push_filter_begin(
2654                Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2655                0.0,
2656                4.0,
2657                SpanMask::None,
2658            )
2659            .expect("inner");
2660        list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2661        list.push_filter_end(inner);
2662        list.push_filter_end(outer);
2663        assert_eq!(list.filter_spans().len(), 2);
2664        assert_eq!(
2665            (list.filter_spans()[0].start, list.filter_spans()[0].end),
2666            (0, 2)
2667        );
2668        assert_eq!(
2669            (list.filter_spans()[1].start, list.filter_spans()[1].end),
2670            (1, 2)
2671        );
2672    }
2673
2674    #[test]
2675    fn backdrop_filters_and_clips() {
2676        let mut list = RenderList::new();
2677
2678        list.push_backdrop(
2679            Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2680            0.0,
2681            0.0,
2682            Color::TRANSPARENT,
2683            0.0,
2684            SpanMask::None,
2685        );
2686        assert!(!list.has_backdrops());
2687
2688        list.push_backdrop(
2689            Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2690            0.0,
2691            0.0,
2692            Color::BLACK.with_alpha(0.25),
2693            0.0,
2694            SpanMask::None,
2695        );
2696        assert_eq!(list.backdrops().len(), 1);
2697
2698        let mut clipped = RenderList::new();
2699        clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2700        clipped.push_backdrop(
2701            Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
2702            4.0,
2703            12.0,
2704            Color::TRANSPARENT,
2705            0.0,
2706            SpanMask::None,
2707        );
2708        assert_eq!(
2709            clipped.backdrops()[0].draw.rect,
2710            Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
2711        );
2712
2713        let mut outside = RenderList::new();
2714        outside.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2715        outside.push_backdrop(
2716            Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2717            0.0,
2718            12.0,
2719            Color::TRANSPARENT,
2720            0.0,
2721            SpanMask::None,
2722        );
2723        assert!(!outside.has_backdrops());
2724
2725        let mut faded = RenderList::new();
2726        faded.push_opacity(0.5);
2727        faded.push_backdrop(
2728            Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2729            0.0,
2730            0.0,
2731            Color::BLACK.with_alpha(0.4),
2732            0.5,
2733            SpanMask::None,
2734        );
2735        let d = faded.backdrops()[0].draw;
2736        assert!((d.dim.a - 0.2).abs() < 1e-6, "dim.a 0.4 * 0.5: {}", d.dim.a);
2737        assert!(
2738            (d.noise - 0.25).abs() < 1e-6,
2739            "noise 0.5 * 0.5: {}",
2740            d.noise
2741        );
2742
2743        faded.clear();
2744        assert!(!faded.has_backdrops());
2745    }
2746
2747    #[test]
2748    fn span_mask_encodes_for_gpu() {
2749        assert_eq!(SpanMask::None.mask_vec(), [0.0, 0.0, 0.0, 0.0]);
2750        assert_eq!(
2751            SpanMask::Linear {
2752                from: 1.0,
2753                to: 0.0,
2754                angle_rad: std::f32::consts::FRAC_PI_2,
2755            }
2756            .mask_vec(),
2757            [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
2758        );
2759        assert_eq!(SpanMask::Uniform(0.5).mask_vec(), [2.0, 0.5, 0.0, 0.0]);
2760
2761        assert_eq!(
2762            SpanMask::Linear {
2763                from: 2.0,
2764                to: -1.0,
2765                angle_rad: f32::NAN,
2766            }
2767            .mask_vec()[1..3],
2768            [1.0, 0.0]
2769        );
2770        assert!(SpanMask::None.is_none());
2771        assert!(!SpanMask::Uniform(0.0).is_none());
2772    }
2773
2774    #[test]
2775    fn filter_span_carries_mask() {
2776        let mut list = RenderList::new();
2777        let mask = SpanMask::Linear {
2778            from: 1.0,
2779            to: 0.0,
2780            angle_rad: std::f32::consts::FRAC_PI_2,
2781        };
2782        let h = list
2783            .push_filter_begin(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), 0.0, 8.0, mask)
2784            .expect("otwiera");
2785        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2786        list.push_filter_end(h);
2787        assert_eq!(list.filter_spans()[0].draw.mask, mask);
2788        assert_eq!(
2789            list.filter_spans()[0].draw.mask.mask_vec(),
2790            [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
2791        );
2792    }
2793
2794    #[test]
2795    fn backdrop_marker_carries_mask() {
2796        let mut list = RenderList::new();
2797        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2798        list.push_backdrop(
2799            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2800            0.0,
2801            8.0,
2802            Color::TRANSPARENT,
2803            0.0,
2804            SpanMask::Uniform(0.5),
2805        );
2806        assert_eq!(
2807            list.backdrops()[0].draw.mask,
2808            SpanMask::Uniform(0.5),
2809            "maska w markerze t�'a"
2810        );
2811    }
2812
2813    #[test]
2814    fn transform_stack_composes_and_snapshots() {
2815        use crate::geometry::Point;
2816        let mut list = RenderList::new();
2817        assert!(list.active_transform().is_identity());
2818
2819        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2820        assert!(list.item_transform(0).is_identity());
2821
2822        list.push_transform(Transform::rotate_around_degrees(90.0, Point::new(5.0, 5.0)));
2823        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2824        assert!(!list.item_transform(1).is_identity());
2825        assert_eq!(list.item_transforms().len(), list.items().len());
2826        list.pop_transform();
2827        assert!(list.active_transform().is_identity());
2828        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2829        assert!(list.item_transform(2).is_identity());
2830    }
2831
2832    #[test]
2833    fn transform_aabb_covers_rotation() {
2834        use crate::geometry::Point;
2835        let r = Rect::from_xywh(0.0, 0.0, 10.0, 20.0);
2836        let t = Transform::rotate_around_degrees(90.0, Point::new(5.0, 10.0));
2837        let aabb = t.apply_rect_aabb(r);
2838        assert!(
2839            (aabb.size.width - 20.0).abs() < 0.01,
2840            "w={}",
2841            aabb.size.width
2842        );
2843        assert!(
2844            (aabb.size.height - 10.0).abs() < 0.01,
2845            "h={}",
2846            aabb.size.height
2847        );
2848        assert!(Transform::IDENTITY.apply_rect_aabb(r) == r);
2849    }
2850
2851    #[test]
2852    fn rotated_text_survives_own_self_clip() {
2853        let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
2854        let mut list = RenderList::new();
2855        list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
2856        list.push_clip(tile);
2857        list.push_text(TextDraw {
2858            color: Color::WHITE,
2859            glyphs: vec![GlyphQuad {
2860                rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2861                uv: [0.0, 0.0, 1.0, 1.0],
2862                color_glyph: false,
2863            }],
2864        });
2865        assert_eq!(list.len(), 1, "rotated glyph in custom clip remains");
2866    }
2867
2868    #[test]
2869    fn ancestor_clip_still_culls_rotated_content() {
2870        let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
2871        let mut list = RenderList::new();
2872        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2873        list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
2874        list.push_text(TextDraw {
2875            color: Color::WHITE,
2876            glyphs: vec![GlyphQuad {
2877                rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2878                uv: [0.0, 0.0, 1.0, 1.0],
2879                color_glyph: false,
2880            }],
2881        });
2882
2883        assert_eq!(list.len(), 1, "clip przodka nie zjada widocznego");
2884
2885        let mut list2 = RenderList::new();
2886        list2.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2887        list2.push_transform(Transform::translate(500.0, 500.0));
2888        list2.push_text(TextDraw {
2889            color: Color::WHITE,
2890            glyphs: vec![GlyphQuad {
2891                rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2892                uv: [0.0, 0.0, 1.0, 1.0],
2893                color_glyph: false,
2894            }],
2895        });
2896        assert_eq!(list2.len(), 0, "clip przodka obcina poza ekranem");
2897    }
2898}