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, Debug, PartialEq)]
712pub struct ImageDraw {
713    pub rect: Rect,
714
715    pub uv: [f32; 4],
716
717    pub source: ImageSource,
718}
719
720#[derive(Clone, Debug, PartialEq)]
721pub struct WaveformDraw {
722    pub bars: Vec<Rect>,
723
724    pub color: Color,
725}
726
727#[derive(Clone, Copy, Debug, PartialEq)]
728pub struct BackdropDraw {
729    pub rect: Rect,
730
731    pub radius: f32,
732
733    pub blur: f32,
734
735    pub dim: Color,
736
737    pub noise: f32,
738
739    pub mask: SpanMask,
740}
741
742impl BackdropDraw {
743    pub fn is_visible(self) -> bool {
744        !self.rect.is_empty()
745            && (self.blur > 0.0 && self.blur.is_finite() || self.dim.a > 0.0 || self.noise > 0.0)
746    }
747}
748
749#[derive(Clone, Copy, Debug, PartialEq)]
750#[non_exhaustive]
751#[derive(Default)]
752pub enum SpanMask {
753    #[default]
754    None,
755
756    Linear {
757        from: f32,
758        to: f32,
759        angle_rad: f32,
760    },
761
762    Uniform(f32),
763}
764
765impl SpanMask {
766    pub fn mask_vec(self) -> [f32; 4] {
767        match self {
768            SpanMask::None => [0.0, 0.0, 0.0, 0.0],
769            SpanMask::Linear {
770                from,
771                to,
772                angle_rad,
773            } => {
774                let angle = if angle_rad.is_finite() {
775                    angle_rad
776                } else {
777                    0.0
778                };
779                [1.0, from.clamp(0.0, 1.0), to.clamp(0.0, 1.0), angle]
780            }
781            SpanMask::Uniform(v) => [2.0, v.clamp(0.0, 1.0), 0.0, 0.0],
782        }
783    }
784
785    pub fn is_none(self) -> bool {
786        matches!(self, SpanMask::None)
787    }
788}
789
790#[derive(Clone, Copy, Debug, PartialEq)]
791pub struct BackdropSpan {
792    pub at: usize,
793
794    pub draw: BackdropDraw,
795
796    pub clip: Option<RoundedClip>,
797}
798
799#[derive(Clone, Copy, Debug, PartialEq)]
800pub struct FilterDraw {
801    pub rect: Rect,
802
803    pub radius: f32,
804
805    pub blur: f32,
806
807    pub mask: SpanMask,
808}
809
810impl FilterDraw {
811    pub fn is_visible(self) -> bool {
812        !self.rect.is_empty() && self.blur > 0.0 && self.blur.is_finite()
813    }
814}
815
816#[derive(Clone, Copy, Debug, PartialEq)]
817pub struct FilterSpan {
818    pub start: usize,
819
820    pub end: usize,
821
822    pub draw: FilterDraw,
823
824    pub clip: Option<RoundedClip>,
825}
826
827#[derive(Clone, Debug, PartialEq)]
828pub struct GradientDraw {
829    pub rect: Rect,
830
831    pub radius: f32,
832
833    pub kind: crate::brush::GradientKind,
834
835    pub stops: crate::brush::GradientStops,
836}
837
838impl GradientDraw {
839    pub fn is_visible(&self) -> bool {
840        !self.rect.is_empty() && !self.stops.is_empty()
841    }
842}
843
844#[derive(Clone, Debug, PartialEq)]
845#[non_exhaustive]
846pub enum RenderItem {
847    Quad(Quad),
848
849    Text(TextDraw),
850
851    Image(ImageDraw),
852
853    Shadow(Shadow),
854
855    Waveform(WaveformDraw),
856
857    Gradient(GradientDraw),
858}
859
860pub const MAX_WAVEFORM_BARS: usize = 8192;
861
862fn clip_uv(rect: Rect, uv: [f32; 4], clipped: Rect) -> [f32; 4] {
863    let (w, h) = (rect.size.width, rect.size.height);
864    if w <= 0.0 || h <= 0.0 {
865        return uv;
866    }
867    let [u0, v0, u1, v1] = uv;
868    let fx0 = (clipped.left() - rect.left()) / w;
869    let fx1 = (clipped.right() - rect.left()) / w;
870    let fy0 = (clipped.top() - rect.top()) / h;
871    let fy1 = (clipped.bottom() - rect.top()) / h;
872    [
873        u0 + fx0 * (u1 - u0),
874        v0 + fy0 * (v1 - v0),
875        u0 + fx1 * (u1 - u0),
876        v0 + fy1 * (v1 - v0),
877    ]
878}
879
880#[derive(Clone, Copy, Debug, PartialEq)]
881pub struct RoundedClip {
882    pub rect: Rect,
883
884    pub radius: f32,
885}
886
887#[derive(Clone, Copy, Debug, PartialEq)]
888struct ClipEntry {
889    clip: RoundedClip,
890
891    under: Transform,
892}
893
894impl RoundedClip {
895    pub fn new(rect: Rect, radius: f32) -> Self {
896        let radius = if radius.is_finite() && radius > 0.0 {
897            radius
898        } else {
899            0.0
900        };
901        Self { rect, radius }
902    }
903
904    pub fn is_rounded(self) -> bool {
905        self.radius > 0.0 && !self.rect.is_empty()
906    }
907}
908
909#[derive(Clone, Debug, Default)]
910pub struct RenderList {
911    items: Vec<RenderItem>,
912    clip_stack: Vec<ClipEntry>,
913    opacity_stack: Vec<f32>,
914    transform_stack: Vec<Transform>,
915
916    item_clips: Vec<Option<RoundedClip>>,
917    item_transforms: Vec<Transform>,
918
919    backdrops: Vec<BackdropSpan>,
920
921    filters: Vec<FilterSpan>,
922}
923
924impl RenderList {
925    pub fn new() -> Self {
926        Self::default()
927    }
928
929    pub fn clear(&mut self) {
930        self.items.clear();
931        self.clip_stack.clear();
932        self.opacity_stack.clear();
933        self.transform_stack.clear();
934        self.item_clips.clear();
935        self.item_transforms.clear();
936        self.backdrops.clear();
937        self.filters.clear();
938    }
939
940    pub fn push_opacity(&mut self, opacity: f32) {
941        let o = if opacity.is_finite() {
942            opacity.clamp(0.0, 1.0)
943        } else {
944            1.0
945        };
946        self.opacity_stack.push(o);
947    }
948
949    pub fn pop_opacity(&mut self) {
950        self.opacity_stack.pop();
951    }
952
953    pub fn opacity_depth(&self) -> usize {
954        self.opacity_stack.len()
955    }
956
957    pub fn truncate_opacity(&mut self, depth: usize) {
958        while self.opacity_stack.len() > depth {
959            self.opacity_stack.pop();
960        }
961    }
962
963    pub fn active_opacity(&self) -> f32 {
964        self.opacity_stack.iter().copied().fold(1.0, |a, b| a * b)
965    }
966
967    pub fn push_transform(&mut self, t: Transform) {
968        let active = self.active_transform().then(t);
969        self.transform_stack.push(active);
970    }
971
972    pub fn pop_transform(&mut self) {
973        self.transform_stack.pop();
974    }
975
976    pub fn transform_depth(&self) -> usize {
977        self.transform_stack.len()
978    }
979
980    pub fn truncate_transforms(&mut self, depth: usize) {
981        while self.transform_stack.len() > depth {
982            self.transform_stack.pop();
983        }
984    }
985
986    pub fn active_transform(&self) -> Transform {
987        self.transform_stack.last().copied().unwrap_or_default()
988    }
989
990    pub fn item_transform(&self, index: usize) -> Transform {
991        self.item_transforms.get(index).copied().unwrap_or_default()
992    }
993
994    pub fn item_transforms(&self) -> &[Transform] {
995        &self.item_transforms
996    }
997
998    fn modulated(&self, color: Color) -> Color {
999        let o = self.active_opacity();
1000        if o >= 1.0 {
1001            color
1002        } else {
1003            Color {
1004                a: color.a * o,
1005                ..color
1006            }
1007        }
1008    }
1009
1010    pub fn push_clip(&mut self, rect: Rect) {
1011        self.clip_stack.push(ClipEntry {
1012            clip: RoundedClip::new(rect, 0.0),
1013            under: self.active_transform(),
1014        });
1015    }
1016
1017    pub fn push_rounded_clip(&mut self, rect: Rect, radius: f32) {
1018        self.clip_stack.push(ClipEntry {
1019            clip: RoundedClip::new(rect, radius),
1020            under: self.active_transform(),
1021        });
1022    }
1023
1024    pub fn clip_depth(&self) -> usize {
1025        self.clip_stack.len()
1026    }
1027
1028    pub fn active_clip(&self) -> Option<Rect> {
1029        let mut acc: Option<Rect> = None;
1030        for c in &self.clip_stack {
1031            acc = Some(match acc {
1032                Some(a) => a.intersect(c.clip.rect),
1033                None => c.clip.rect,
1034            });
1035        }
1036        acc
1037    }
1038
1039    pub fn active_clip_for(&self, t_item: Transform) -> Option<Rect> {
1040        if self.clip_stack.is_empty() {
1041            return None;
1042        }
1043        let inv = t_item.inverse()?;
1044        let mut acc: Option<Rect> = None;
1045        for c in &self.clip_stack {
1046            let mapped = c.under.then(inv).apply_rect_aabb(c.clip.rect);
1047            acc = Some(match acc {
1048                Some(a) => a.intersect(mapped),
1049                None => mapped,
1050            });
1051        }
1052        acc
1053    }
1054
1055    pub fn active_rounded_clip(&self) -> Option<RoundedClip> {
1056        self.clip_stack
1057            .iter()
1058            .rev()
1059            .find(|c| c.clip.is_rounded())
1060            .map(|c| c.clip)
1061    }
1062
1063    pub fn item_rounded_clip(&self, index: usize) -> Option<RoundedClip> {
1064        self.item_clips.get(index).copied().flatten()
1065    }
1066
1067    pub fn truncate_clips(&mut self, depth: usize) {
1068        while self.clip_stack.len() > depth {
1069            self.clip_stack.pop();
1070        }
1071    }
1072
1073    pub fn pop_clip(&mut self) {
1074        self.clip_stack.pop();
1075    }
1076
1077    fn clip_rect(&self, mut r: Rect) -> Option<Rect> {
1078        for c in &self.clip_stack {
1079            r = r.intersect(c.clip.rect);
1080            if r.is_empty() {
1081                return None;
1082            }
1083        }
1084        Some(r)
1085    }
1086
1087    pub fn push_quad(&mut self, rect: Rect, color: Color, radius: f32) {
1088        self.push_bordered_quad(rect, color, radius, 0.0, Color::TRANSPARENT);
1089    }
1090
1091    pub fn push_bordered_quad(
1092        &mut self,
1093        rect: Rect,
1094        color: Color,
1095        radius: f32,
1096        border_width: f32,
1097        border_color: Color,
1098    ) {
1099        let t = self.active_transform();
1100
1101        if !t.is_identity() {
1102            if let Some(local) = self.active_clip_for(t)
1103                && !rect.intersects(local)
1104            {
1105                return;
1106            }
1107            let color = self.modulated(color);
1108            let border_color = self.modulated(border_color);
1109            let has_border = border_width > 0.0 && border_color.a > 0.0;
1110            if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1111                return;
1112            }
1113            let rounded = self.active_rounded_clip();
1114            self.items.push(RenderItem::Quad(Quad {
1115                rect,
1116                color,
1117                radius,
1118                border_color,
1119                border_width: if has_border { border_width } else { 0.0 },
1120            }));
1121            self.item_clips.push(rounded);
1122            self.item_transforms.push(t);
1123            return;
1124        }
1125        let Some(rect) = self.clip_rect(rect) else {
1126            return;
1127        };
1128        let color = self.modulated(color);
1129        let border_color = self.modulated(border_color);
1130        let has_border = border_width > 0.0 && border_color.a > 0.0;
1131        if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1132            return;
1133        }
1134        let rounded = self.active_rounded_clip();
1135        self.items.push(RenderItem::Quad(Quad {
1136            rect,
1137            color,
1138            radius,
1139            border_color,
1140            border_width: if has_border { border_width } else { 0.0 },
1141        }));
1142        self.item_clips.push(rounded);
1143        self.item_transforms.push(t);
1144    }
1145
1146    pub fn push_shadow(&mut self, shadow: Shadow) {
1147        let t = self.active_transform();
1148        let shadow = Shadow {
1149            color: self.modulated(shadow.color),
1150            ..shadow
1151        };
1152        if !shadow.is_visible() {
1153            return;
1154        }
1155        if !t.is_identity() {
1156            if let Some(local) = self.active_clip_for(t)
1157                && !shadow.bounds().intersects(local)
1158            {
1159                return;
1160            }
1161            let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1162            let clip = self
1163                .active_clip()
1164                .map(|clip_rect| RoundedClip::new(clip_rect, radius));
1165            self.items.push(RenderItem::Shadow(shadow));
1166            self.item_clips.push(clip);
1167            self.item_transforms.push(t);
1168            return;
1169        }
1170        let Some(clip_rect) = self.active_clip() else {
1171            self.items.push(RenderItem::Shadow(shadow));
1172            self.item_clips.push(None);
1173            self.item_transforms.push(t);
1174            return;
1175        };
1176        if clip_rect.is_empty() {
1177            return;
1178        }
1179
1180        if !shadow.bounds().intersects(clip_rect) {
1181            return;
1182        }
1183
1184        let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1185        self.items.push(RenderItem::Shadow(shadow));
1186        self.item_clips
1187            .push(Some(RoundedClip::new(clip_rect, radius)));
1188        self.item_transforms.push(self.active_transform());
1189    }
1190
1191    pub fn push_text(&mut self, mut draw: TextDraw) {
1192        let t = self.active_transform();
1193        draw.color = self.modulated(draw.color);
1194        if draw.color.a <= 0.0 {
1195            return;
1196        }
1197        if !t.is_identity() {
1198            if let Some(local) = self.active_clip_for(t) {
1199                let keep = draw.glyphs.iter().any(|g| g.rect.intersects(local));
1200                if !keep {
1201                    return;
1202                }
1203            }
1204            draw.glyphs.retain(|g| !g.rect.is_empty());
1205            if !draw.glyphs.is_empty() {
1206                let rounded = self.active_rounded_clip();
1207                self.items.push(RenderItem::Text(draw));
1208                self.item_clips.push(rounded);
1209                self.item_transforms.push(t);
1210            }
1211            return;
1212        }
1213        draw.glyphs.retain_mut(|g| {
1214            let Some(cr) = self.clip_rect(g.rect) else {
1215                return false;
1216            };
1217            g.uv = clip_uv(g.rect, g.uv, cr);
1218            g.rect = cr;
1219            true
1220        });
1221        if !draw.glyphs.is_empty() {
1222            let rounded = self.active_rounded_clip();
1223            self.items.push(RenderItem::Text(draw));
1224            self.item_clips.push(rounded);
1225            self.item_transforms.push(t);
1226        }
1227    }
1228
1229    pub fn push_image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) {
1230        let t = self.active_transform();
1231        if !t.is_identity() {
1232            if let Some(local) = self.active_clip_for(t)
1233                && !rect.intersects(local)
1234            {
1235                return;
1236            }
1237            if rect.is_empty() {
1238                return;
1239            }
1240            let rounded = self.active_rounded_clip();
1241            self.items
1242                .push(RenderItem::Image(ImageDraw { rect, uv, source }));
1243            self.item_clips.push(rounded);
1244            self.item_transforms.push(t);
1245            return;
1246        }
1247        let Some(cr) = self.clip_rect(rect) else {
1248            return;
1249        };
1250        if cr.is_empty() {
1251            return;
1252        }
1253        let uv = clip_uv(rect, uv, cr);
1254        let rounded = self.active_rounded_clip();
1255        self.items.push(RenderItem::Image(ImageDraw {
1256            rect: cr,
1257            uv,
1258            source,
1259        }));
1260        self.item_clips.push(rounded);
1261        self.item_transforms.push(self.active_transform());
1262    }
1263
1264    pub fn push_waveform(&mut self, bars: &[Rect], color: Color) {
1265        let t = self.active_transform();
1266        let color = self.modulated(color);
1267        if bars.is_empty() || color.a <= 0.0 {
1268            return;
1269        }
1270        if !t.is_identity() {
1271            if let Some(local) = self.active_clip_for(t) {
1272                let keep = bars.iter().any(|r| !r.is_empty() && r.intersects(local));
1273                if !keep {
1274                    return;
1275                }
1276            }
1277            let clipped: Vec<Rect> = bars
1278                .iter()
1279                .copied()
1280                .filter(|r| !r.is_empty())
1281                .take(MAX_WAVEFORM_BARS)
1282                .collect();
1283            if clipped.is_empty() {
1284                return;
1285            }
1286            let rounded = self.active_rounded_clip();
1287            self.items.push(RenderItem::Waveform(WaveformDraw {
1288                bars: clipped,
1289                color,
1290            }));
1291            self.item_clips.push(rounded);
1292            self.item_transforms.push(t);
1293            return;
1294        }
1295        let clipped: Vec<Rect> = match self.active_clip() {
1296            None => bars
1297                .iter()
1298                .copied()
1299                .filter(|r| !r.is_empty())
1300                .take(MAX_WAVEFORM_BARS)
1301                .collect(),
1302            Some(clip) if clip.is_empty() => return,
1303            Some(clip) => bars
1304                .iter()
1305                .filter_map(|r| {
1306                    let c = r.intersect(clip);
1307                    if c.is_empty() { None } else { Some(c) }
1308                })
1309                .take(MAX_WAVEFORM_BARS)
1310                .collect(),
1311        };
1312        if clipped.is_empty() {
1313            return;
1314        }
1315        let rounded = self.active_rounded_clip();
1316        self.items.push(RenderItem::Waveform(WaveformDraw {
1317            bars: clipped,
1318            color,
1319        }));
1320        self.item_clips.push(rounded);
1321        self.item_transforms.push(self.active_transform());
1322    }
1323
1324    pub fn items(&self) -> &[RenderItem] {
1325        &self.items
1326    }
1327
1328    pub fn push_gradient(
1329        &mut self,
1330        rect: Rect,
1331        radius: f32,
1332        kind: crate::brush::GradientKind,
1333        stops: crate::brush::GradientStops,
1334    ) {
1335        use crate::brush::GradientStop;
1336        if rect.is_empty() || stops.is_empty() {
1337            return;
1338        }
1339        let t = self.active_transform();
1340
1341        let opacity = self.active_opacity();
1342        let stops = if opacity >= 1.0 {
1343            stops
1344        } else {
1345            let modulated: Vec<GradientStop> = stops
1346                .stops()
1347                .iter()
1348                .map(|s| {
1349                    GradientStop::new(
1350                        s.at,
1351                        Color {
1352                            a: s.color.a * opacity,
1353                            ..s.color
1354                        },
1355                    )
1356                })
1357                .collect();
1358            crate::brush::GradientStops::new(modulated)
1359        };
1360
1361        if !t.is_identity() {
1362            if let Some(local) = self.active_clip_for(t)
1363                && !rect.intersects(local)
1364            {
1365                return;
1366            }
1367        } else {
1368            let Some(clipped) = self.clip_rect(rect) else {
1369                return;
1370            };
1371            if clipped.is_empty() {
1372                return;
1373            }
1374
1375            return self.push_gradient_inner(clipped, radius, kind, stops, t);
1376        }
1377        self.push_gradient_inner(rect, radius, kind, stops, t)
1378    }
1379
1380    fn push_gradient_inner(
1381        &mut self,
1382        rect: Rect,
1383        radius: f32,
1384        kind: crate::brush::GradientKind,
1385        stops: crate::brush::GradientStops,
1386        t: Transform,
1387    ) {
1388        if rect.is_empty() || stops.is_empty() {
1389            return;
1390        }
1391
1392        if stops.stops().iter().all(|s| s.color.a <= 0.0) {
1393            return;
1394        }
1395        let radius = if radius.is_finite() {
1396            radius
1397                .clamp(0.0, rect.size.width.min(rect.size.height) * 0.5)
1398                .max(0.0)
1399        } else {
1400            0.0
1401        };
1402        let rounded = self.active_rounded_clip();
1403        self.items.push(RenderItem::Gradient(GradientDraw {
1404            rect,
1405            radius,
1406            kind,
1407            stops,
1408        }));
1409        self.item_clips.push(rounded);
1410        self.item_transforms.push(t);
1411    }
1412
1413    pub fn push_backdrop(
1414        &mut self,
1415        rect: Rect,
1416        radius: f32,
1417        blur: f32,
1418        dim: Color,
1419        noise: f32,
1420        mask: SpanMask,
1421    ) {
1422        let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
1423        let noise = if noise.is_finite() {
1424            noise.clamp(0.0, 1.0)
1425        } else {
1426            0.0
1427        };
1428        let opacity = self.active_opacity();
1429        let dim = Color {
1430            a: dim.a * opacity,
1431            ..dim
1432        };
1433        if !(blur > 0.0 || dim.a > 0.0 || noise > 0.0) {
1434            return;
1435        }
1436        let Some(rect) = self.clip_rect(rect) else {
1437            return;
1438        };
1439        if rect.is_empty() {
1440            return;
1441        }
1442        let radius = if radius.is_finite() {
1443            radius.max(0.0)
1444        } else {
1445            0.0
1446        };
1447        let draw = BackdropDraw {
1448            rect,
1449            radius,
1450            blur,
1451            dim,
1452            noise: noise * opacity,
1453            mask,
1454        };
1455        if !draw.is_visible() {
1456            return;
1457        }
1458        self.backdrops.push(BackdropSpan {
1459            at: self.items.len(),
1460            draw,
1461            clip: self.active_rounded_clip(),
1462        });
1463    }
1464
1465    pub fn backdrops(&self) -> &[BackdropSpan] {
1466        &self.backdrops
1467    }
1468
1469    pub fn has_backdrops(&self) -> bool {
1470        !self.backdrops.is_empty()
1471    }
1472
1473    pub fn push_filter_begin(
1474        &mut self,
1475        rect: Rect,
1476        radius: f32,
1477        blur: f32,
1478        mask: SpanMask,
1479    ) -> Option<usize> {
1480        let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
1481        if !(blur > 0.0) {
1482            return None;
1483        }
1484        let rect = self.clip_rect(rect)?;
1485        if rect.is_empty() {
1486            return None;
1487        }
1488        let radius = if radius.is_finite() {
1489            radius.max(0.0)
1490        } else {
1491            0.0
1492        };
1493        let draw = FilterDraw {
1494            rect,
1495            radius,
1496            blur,
1497            mask,
1498        };
1499        if !draw.is_visible() {
1500            return None;
1501        }
1502        let start = self.items.len();
1503        self.filters.push(FilterSpan {
1504            start,
1505            end: start,
1506            draw,
1507            clip: self.active_rounded_clip(),
1508        });
1509        Some(self.filters.len() - 1)
1510    }
1511
1512    pub fn push_filter_end(&mut self, handle: usize) {
1513        let end = self.items.len();
1514        let Some(span) = self.filters.get_mut(handle) else {
1515            return;
1516        };
1517        span.end = end;
1518        if span.end <= span.start {
1519            self.filters.remove(handle);
1520        }
1521    }
1522
1523    pub fn filter_spans(&self) -> &[FilterSpan] {
1524        &self.filters
1525    }
1526
1527    pub fn has_filters(&self) -> bool {
1528        !self.filters.is_empty()
1529    }
1530
1531    pub fn item_clips(&self) -> &[Option<RoundedClip>] {
1532        &self.item_clips
1533    }
1534
1535    pub fn quads(&self) -> impl Iterator<Item = &Quad> {
1536        self.items.iter().filter_map(|i| match i {
1537            RenderItem::Quad(q) => Some(q),
1538            RenderItem::Text(_)
1539            | RenderItem::Image(_)
1540            | RenderItem::Shadow(_)
1541            | RenderItem::Waveform(_)
1542            | RenderItem::Gradient(_) => None,
1543        })
1544    }
1545
1546    pub fn images(&self) -> impl Iterator<Item = &ImageDraw> {
1547        self.items.iter().filter_map(|i| match i {
1548            RenderItem::Image(d) => Some(d),
1549            RenderItem::Quad(_)
1550            | RenderItem::Text(_)
1551            | RenderItem::Shadow(_)
1552            | RenderItem::Waveform(_)
1553            | RenderItem::Gradient(_) => None,
1554        })
1555    }
1556
1557    pub fn shadows(&self) -> impl Iterator<Item = &Shadow> {
1558        self.items.iter().filter_map(|i| match i {
1559            RenderItem::Shadow(s) => Some(s),
1560            RenderItem::Quad(_)
1561            | RenderItem::Text(_)
1562            | RenderItem::Image(_)
1563            | RenderItem::Waveform(_)
1564            | RenderItem::Gradient(_) => None,
1565        })
1566    }
1567
1568    pub fn waveforms(&self) -> impl Iterator<Item = &WaveformDraw> {
1569        self.items.iter().filter_map(|i| match i {
1570            RenderItem::Waveform(w) => Some(w),
1571            RenderItem::Quad(_)
1572            | RenderItem::Text(_)
1573            | RenderItem::Image(_)
1574            | RenderItem::Shadow(_)
1575            | RenderItem::Gradient(_) => None,
1576        })
1577    }
1578
1579    pub fn gradients(&self) -> impl Iterator<Item = &GradientDraw> {
1580        self.items.iter().filter_map(|i| match i {
1581            RenderItem::Gradient(g) => Some(g),
1582            RenderItem::Quad(_)
1583            | RenderItem::Text(_)
1584            | RenderItem::Image(_)
1585            | RenderItem::Shadow(_)
1586            | RenderItem::Waveform(_) => None,
1587        })
1588    }
1589
1590    pub fn gradient_count(&self) -> usize {
1591        self.items
1592            .iter()
1593            .filter(|i| matches!(i, RenderItem::Gradient(_)))
1594            .count()
1595    }
1596
1597    pub fn waveform_bar_count(&self) -> usize {
1598        self.items
1599            .iter()
1600            .map(|i| match i {
1601                RenderItem::Waveform(w) => w.bars.len(),
1602                _ => 0,
1603            })
1604            .sum()
1605    }
1606
1607    pub fn len(&self) -> usize {
1608        self.items.len()
1609    }
1610
1611    pub fn is_empty(&self) -> bool {
1612        self.items.is_empty()
1613    }
1614}
1615
1616#[cfg(test)]
1617mod tests {
1618    use super::*;
1619
1620    #[test]
1621    fn color_construction() {
1622        assert_eq!(Color::rgb8(255, 0, 0), Color::RED);
1623        assert_eq!(Color::rgb(0.0, 1.0, 0.0).a, 1.0);
1624        assert_eq!(Color::TRANSPARENT.a, 0.0);
1625    }
1626
1627    #[test]
1628    fn hsl_roundtrips_through_rgb() {
1629        for (r, g, b) in [
1630            (255, 0, 0),
1631            (0, 255, 0),
1632            (0, 0, 255),
1633            (112, 143, 234),
1634            (185, 185, 185),
1635        ] {
1636            let c = Color::rgb8(r, g, b);
1637            let (h, s, l) = c.to_hsl();
1638            let back = Color::from_hsl(h, s, l).to_rgb8();
1639            assert!(
1640                (r as i16 - back.0 as i16).abs() <= 1
1641                    && (g as i16 - back.1 as i16).abs() <= 1
1642                    && (b as i16 - back.2 as i16).abs() <= 1,
1643                "#{r:02X}{g:02X}{b:02X} -> ({h:.1},{s:.3},{l:.3}) -> #{:02X}{:02X}{:02X}",
1644                back.0,
1645                back.1,
1646                back.2,
1647            );
1648        }
1649        assert_eq!(Color::from_hsl(0.0, 0.0, 0.5).to_rgb8(), (128, 128, 128));
1650
1651        assert_eq!(
1652            Color::from_hsl(720.0, 1.0, 0.5),
1653            Color::from_hsl(0.0, 1.0, 0.5)
1654        );
1655        assert_eq!(Color::from_hsl(0.0, 0.5, 0.5).to_hex6(), "BF4040");
1656    }
1657
1658    #[test]
1659    fn hex6_string_is_uppercase_without_hash() {
1660        assert_eq!(Color::rgb8(0x70, 0x8F, 0xEA).to_hex6(), "708FEA");
1661    }
1662
1663    #[test]
1664    fn hex_number_forms() {
1665        assert_eq!(Color::hex(0x0FA4FA), Color::rgb8(15, 164, 250));
1666        assert_eq!(Color::hex(0xFF0000), Color::RED);
1667        assert_eq!(Color::hex(0x000000), Color::BLACK);
1668
1669        assert_eq!(Color::hex(0x1_FF0000), Color::RED);
1670        let half = Color::hexa(0x0FA4FA80);
1671        assert_eq!(half.r, Color::hex(0x0FA4FA).r);
1672        assert_eq!(half.a, 128.0 / 255.0);
1673        assert_eq!(Color::hexa(0xFFFFFFFF).a, 1.0);
1674    }
1675
1676    #[test]
1677    fn parse_hex_forms() {
1678        assert_eq!(Color::parse_hex("#0FA4FA"), Some(Color::rgb8(15, 164, 250)));
1679        assert_eq!(Color::parse_hex("0fa4fa"), Some(Color::rgb8(15, 164, 250)));
1680        assert_eq!(
1681            Color::parse_hex("0x0FA4FA"),
1682            Some(Color::rgb8(15, 164, 250))
1683        );
1684        assert_eq!(Color::parse_hex("#fff"), Some(Color::WHITE));
1685        assert_eq!(Color::parse_hex("#0000"), Some(Color::TRANSPARENT));
1686        assert_eq!(Color::parse_hex("#0FA4FA80"), Some(Color::hexa(0x0FA4FA80)));
1687
1688        assert_eq!(Color::parse_hex("#0af"), Color::parse_hex("#00aaff"));
1689    }
1690
1691    #[test]
1692    fn parse_hex_rejects_garbage() {
1693        assert_eq!(Color::parse_hex(""), None);
1694        assert_eq!(Color::parse_hex("#"), None);
1695        assert_eq!(Color::parse_hex("bogus"), None);
1696        assert_eq!(Color::parse_hex("#12"), None);
1697        assert_eq!(Color::parse_hex("#12345"), None);
1698        assert_eq!(Color::parse_hex("#123456789"), None);
1699        assert_eq!(Color::parse_hex("#gggggg"), None);
1700        assert_eq!(Color::parse_hex("  #fff"), None, "no trimming");
1701    }
1702
1703    #[test]
1704    fn clip_balance_helpers() {
1705        let mut list = RenderList::new();
1706        assert_eq!(list.clip_depth(), 0);
1707        list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
1708        list.push_clip(Rect::from_xywh(0.0, 0.0, 5.0, 5.0));
1709        assert_eq!(list.clip_depth(), 2);
1710        list.truncate_clips(1);
1711        assert_eq!(list.clip_depth(), 1);
1712        list.truncate_clips(0);
1713        assert_eq!(list.clip_depth(), 0);
1714    }
1715
1716    #[test]
1717    fn opacity_stack_modulates_quad_alpha() {
1718        let mut list = RenderList::new();
1719        assert_eq!(list.active_opacity(), 1.0);
1720        list.push_opacity(0.5);
1721        list.push_opacity(0.5);
1722        assert_eq!(list.active_opacity(), 0.25, "nestings multiply");
1723        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1724        let q = list.quads().next().expect("quad po push");
1725        assert!(
1726            (q.color.a - 0.25).abs() < 1e-6,
1727            "alfa 1 * 0.5 * 0.5: {}",
1728            q.color.a
1729        );
1730        assert_eq!(q.color.r, 1.0, "rgb untouched");
1731        list.pop_opacity();
1732        list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1733        let qs: Vec<_> = list.quads().collect();
1734        assert!((qs[1].color.a - 0.5).abs() < 1e-6);
1735
1736        list.push_opacity(0.0);
1737        list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1738        assert_eq!(list.quads().count(), 2, "quad przy opacity 0 nie wchodzi");
1739        list.truncate_opacity(0);
1740        assert_eq!(list.opacity_depth(), 0);
1741        assert_eq!(list.active_opacity(), 1.0);
1742
1743        list.push_opacity(f32::NAN);
1744        list.push_quad(Rect::from_xywh(60.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1745        let qs: Vec<_> = list.quads().collect();
1746        assert_eq!(qs[2].color.a, 1.0);
1747        list.clear();
1748        assert_eq!(list.opacity_depth(), 0, "clear also resets opacity");
1749    }
1750
1751    #[test]
1752    fn list_push_and_clear() {
1753        let mut list = RenderList::new();
1754        assert!(list.is_empty());
1755        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1756        assert_eq!(list.len(), 1);
1757        list.push_quad(Rect::ZERO, Color::RED, 0.0);
1758        list.push_quad(
1759            Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
1760            Color::TRANSPARENT,
1761            0.0,
1762        );
1763        assert_eq!(list.len(), 1, "empty-value filters");
1764
1765        list.push_text(TextDraw::default());
1766        assert_eq!(list.len(), 1, "empty TextDraw skipped");
1767
1768        list.push_text(TextDraw {
1769            color: Color::WHITE,
1770            glyphs: vec![GlyphQuad {
1771                rect: Rect::ZERO,
1772                uv: [0.0; 4],
1773                color_glyph: false,
1774            }],
1775        });
1776        assert_eq!(list.len(), 2);
1777        assert_eq!(list.quads().count(), 1);
1778
1779        list.clear();
1780        assert!(list.is_empty());
1781    }
1782
1783    #[test]
1784    fn bordered_quad_filters_and_flags() {
1785        let r = Rect::from_xywh(0.0, 0.0, 100.0, 60.0);
1786        let mut list = RenderList::new();
1787
1788        list.push_quad(r, Color::BLUE, 8.0);
1789        let q = list.quads().next().unwrap();
1790        assert!(!q.has_border());
1791        assert_eq!(q.border_width, 0.0);
1792
1793        let bg = Color::rgba(154.0 / 255.0, 214.0 / 255.0, 248.0 / 255.0, 24.0 / 255.0);
1794        list.push_bordered_quad(r, bg, 8.0, 1.0, Color::RED);
1795        let qs: Vec<_> = list.quads().collect();
1796        assert_eq!(qs.len(), 2, "background + border is one quad, not two");
1797        assert_eq!(qs[1].color, bg);
1798        assert_eq!(qs[1].border_color, Color::RED);
1799        assert_eq!(qs[1].border_width, 1.0);
1800        assert!(qs[1].has_border());
1801
1802        let mut list = RenderList::new();
1803        list.push_bordered_quad(r, Color::TRANSPARENT, 8.0, 2.0, Color::GREEN);
1804        assert_eq!(list.len(), 1);
1805
1806        let mut list = RenderList::new();
1807        list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 0.0, Color::RED);
1808        list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 2.0, Color::TRANSPARENT);
1809        list.push_bordered_quad(Rect::ZERO, Color::RED, 0.0, 1.0, Color::RED);
1810        assert!(list.is_empty());
1811    }
1812
1813    #[test]
1814    fn clip_intersects_and_filters() {
1815        let mut list = RenderList::new();
1816        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1817        list.push_quad(Rect::from_xywh(50.0, 50.0, 100.0, 100.0), Color::RED, 0.0);
1818        list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
1819        assert_eq!(list.len(), 1);
1820        let q = list.quads().next().unwrap();
1821        assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
1822
1823        list.pop_clip();
1824        list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
1825        assert_eq!(list.len(), 2, "unclipped after pop_clip");
1826    }
1827
1828    #[test]
1829    fn nested_clip_intersects() {
1830        let mut list = RenderList::new();
1831        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1832        list.push_clip(Rect::from_xywh(50.0, 50.0, 100.0, 100.0));
1833        list.push_quad(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), Color::RED, 0.0);
1834        assert_eq!(list.len(), 1);
1835        let q = list.quads().next().unwrap();
1836        assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
1837    }
1838
1839    #[test]
1840    fn rounded_clip_stored_per_item_for_shader() {
1841        let mut list = RenderList::new();
1842        let card = Rect::from_xywh(0.0, 0.0, 240.0, 280.0);
1843        list.push_rounded_clip(card, 8.0);
1844        list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
1845        assert_eq!(list.len(), 1);
1846        let clip = list.item_rounded_clip(0).expect("rounded clip stored");
1847        assert_eq!(clip.rect, card);
1848        assert_eq!(clip.radius, 8.0);
1849
1850        let mut list = RenderList::new();
1851        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1852        list.push_quad(Rect::from_xywh(10.0, 10.0, 20.0, 20.0), Color::RED, 0.0);
1853        assert_eq!(list.item_rounded_clip(0), None);
1854    }
1855
1856    #[test]
1857    fn rounded_clip_survives_rectangular_child_clip() {
1858        let mut list = RenderList::new();
1859        list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0), 8.0);
1860        list.push_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0));
1861        list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
1862        let clip = list.item_rounded_clip(0).expect("outer rounding kept");
1863        assert_eq!(clip.radius, 8.0);
1864        list.pop_clip();
1865        list.pop_clip();
1866        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1867        assert_eq!(list.item_rounded_clip(1), None, "no clip after pops");
1868    }
1869
1870    fn glyph(x: f32, y: f32, w: f32, h: f32) -> GlyphQuad {
1871        GlyphQuad {
1872            rect: Rect::from_xywh(x, y, w, h),
1873            uv: [0.0, 0.0, 1.0, 1.0],
1874            color_glyph: false,
1875        }
1876    }
1877
1878    fn pushed_glyphs(clip: Rect, g: GlyphQuad) -> Vec<GlyphQuad> {
1879        let mut list = RenderList::new();
1880        list.push_clip(clip);
1881        list.push_text(TextDraw {
1882            color: Color::WHITE,
1883            glyphs: vec![g],
1884        });
1885        match list.items() {
1886            [RenderItem::Text(t)] => t.glyphs.clone(),
1887            _ => panic!("expected a single TextDraw"),
1888        }
1889    }
1890
1891    #[test]
1892    fn clipped_glyph_keeps_proportional_uv() {
1893        let out = pushed_glyphs(
1894            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
1895            glyph(90.0, 10.0, 20.0, 10.0),
1896        );
1897        assert_eq!(out.len(), 1);
1898        assert_eq!(out[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
1899        assert_eq!(out[0].uv, [0.0, 0.0, 0.5, 1.0]);
1900    }
1901
1902    #[test]
1903    fn fully_visible_glyph_keeps_uv() {
1904        let out = pushed_glyphs(
1905            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
1906            glyph(10.0, 10.0, 20.0, 10.0),
1907        );
1908        assert_eq!(out.len(), 1);
1909        assert_eq!(out[0].rect, Rect::from_xywh(10.0, 10.0, 20.0, 10.0));
1910        assert_eq!(out[0].uv, [0.0, 0.0, 1.0, 1.0]);
1911    }
1912
1913    #[test]
1914    fn clipped_glyph_uv_scales_with_atlas_subrect() {
1915        let mut g = glyph(0.0, 0.0, 20.0, 20.0);
1916        g.uv = [0.25, 0.5, 0.75, 1.0];
1917        let out = pushed_glyphs(Rect::from_xywh(10.0, 0.0, 100.0, 100.0), g);
1918        assert_eq!(out.len(), 1);
1919        assert_eq!(out[0].rect, Rect::from_xywh(10.0, 0.0, 10.0, 20.0));
1920        assert_eq!(out[0].uv, [0.5, 0.5, 0.75, 1.0]);
1921    }
1922
1923    fn file_image() -> ImageSource {
1924        ImageSource::File("foto.png".into())
1925    }
1926
1927    #[test]
1928    fn image_push_clip_and_uv() {
1929        let mut list = RenderList::new();
1930        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1931        list.push_image(
1932            Rect::from_xywh(90.0, 10.0, 20.0, 10.0),
1933            [0.0, 0.0, 1.0, 1.0],
1934            file_image(),
1935        );
1936        list.push_image(
1937            Rect::from_xywh(200.0, 200.0, 10.0, 10.0),
1938            [0.0, 0.0, 1.0, 1.0],
1939            file_image(),
1940        );
1941        let images: Vec<_> = list.images().collect();
1942        assert_eq!(images.len(), 1, "outside the clip is dropped");
1943        assert_eq!(images[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
1944        assert_eq!(
1945            images[0].uv,
1946            [0.0, 0.0, 0.5, 1.0],
1947            "proportional UV, as for glyphs"
1948        );
1949    }
1950
1951    #[test]
1952    fn image_cache_keys() {
1953        assert_eq!(file_image().cache_key(), file_image().cache_key());
1954        assert_ne!(
1955            file_image().cache_key(),
1956            ImageSource::File("inne.png".into()).cache_key()
1957        );
1958        let rgba = |id| ImageSource::Rgba {
1959            id,
1960            width: 2,
1961            height: 2,
1962            data: Arc::new(vec![0u8; 16]),
1963        };
1964        assert_eq!(rgba(7).cache_key(), 7);
1965        assert_ne!(rgba(7).cache_key(), rgba(8).cache_key());
1966    }
1967
1968    fn shadow_spec() -> Shadow {
1969        Shadow {
1970            rect: Rect::from_xywh(10.0, 10.0, 100.0, 60.0),
1971            color: Color::BLACK,
1972            radius: 6.0,
1973            blur: 8.0,
1974            spread: 0.0,
1975            offset: Vec2::new(0.0, 2.0),
1976            inset: false,
1977        }
1978    }
1979
1980    #[test]
1981    fn shadow_push_filters_invisible_shadows() {
1982        let mut list = RenderList::new();
1983        list.push_shadow(Shadow {
1984            blur: 0.0,
1985            ..shadow_spec()
1986        });
1987        list.push_shadow(Shadow {
1988            blur: f32::NAN,
1989            ..shadow_spec()
1990        });
1991        list.push_shadow(Shadow {
1992            color: Color::TRANSPARENT,
1993            ..shadow_spec()
1994        });
1995        list.push_shadow(Shadow {
1996            rect: Rect::ZERO,
1997            ..shadow_spec()
1998        });
1999        assert!(list.is_empty(), "hidden shadows create no items");
2000    }
2001
2002    #[test]
2003    fn shadow_keeps_shape_rect_and_rides_the_clip() {
2004        let mut list = RenderList::new();
2005        list.push_clip(Rect::from_xywh(0.0, 0.0, 50.0, 50.0));
2006        list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), 12.0);
2007        list.push_shadow(shadow_spec());
2008        assert_eq!(list.len(), 1);
2009        assert_eq!(list.shadows().next().unwrap().rect, shadow_spec().rect);
2010        let clip = list.item_rounded_clip(0).expect("ancestor clip");
2011        assert_eq!(
2012            clip.rect,
2013            Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2014            "przeciecie stosu clipow"
2015        );
2016        assert_eq!(clip.radius, 12.0, "rounding from inner clip");
2017    }
2018
2019    #[test]
2020    fn shadow_dropped_when_fully_clipped_out() {
2021        let mut list = RenderList::new();
2022        list.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2023        list.push_shadow(shadow_spec());
2024        assert!(list.is_empty(), "fully outside clip");
2025    }
2026
2027    #[test]
2028    fn shadow_bounds_grow_with_blur_spread_and_offset() {
2029        let s = Shadow {
2030            blur: 10.0,
2031            spread: 4.0,
2032            offset: Vec2::new(2.0, -3.0),
2033            ..shadow_spec()
2034        };
2035        let b = s.bounds();
2036        assert!((b.left() - 3.0).abs() < 1e-4, "{b:?}");
2037        assert!((b.top() - (-2.0)).abs() < 1e-4, "{b:?}");
2038        assert!((b.size.width - 118.0).abs() < 1e-4, "{b:?}");
2039        let inset = Shadow { inset: true, ..s };
2040        assert_eq!(inset.bounds(), inset.rect, "inset stays inside the shape");
2041    }
2042
2043    #[test]
2044    fn waveform_push_is_one_item_not_n() {
2045        let mut list = RenderList::new();
2046        let bars: Vec<Rect> = (0..100)
2047            .map(|i| Rect::from_xywh(i as f32 * 2.0, 0.0, 1.5, 20.0))
2048            .collect();
2049        list.push_waveform(&bars, Color::WHITE);
2050        assert_eq!(list.len(), 1, "whole waveform is 1 item");
2051        assert_eq!(list.waveform_bar_count(), 100);
2052        assert_eq!(list.waveforms().next().unwrap().bars.len(), 100);
2053    }
2054
2055    #[test]
2056    fn waveform_push_clips_and_filters() {
2057        let mut list = RenderList::new();
2058        list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2059        let bars = vec![
2060            Rect::from_xywh(0.0, 0.0, 5.0, 5.0),
2061            Rect::from_xywh(200.0, 200.0, 5.0, 5.0),
2062            Rect::ZERO,
2063        ];
2064        list.push_waveform(&bars, Color::WHITE);
2065        assert_eq!(list.len(), 1);
2066        assert_eq!(list.waveform_bar_count(), 1, "outside clip dropped");
2067
2068        let mut list = RenderList::new();
2069        list.push_waveform(&[], Color::WHITE);
2070        list.push_waveform(&[Rect::from_xywh(0.0, 0.0, 5.0, 5.0)], Color::TRANSPARENT);
2071        assert!(list.is_empty());
2072    }
2073
2074    #[test]
2075    fn dynamic_image_cache_key_is_id_and_versioned() {
2076        let a = ImageSource::Dynamic {
2077            id: 7,
2078            width: 4,
2079            height: 1,
2080            data: Arc::new(vec![0u8; 16]),
2081            version: 1,
2082        };
2083        let b = ImageSource::Dynamic {
2084            id: 7,
2085            width: 4,
2086            height: 1,
2087            data: Arc::new(vec![255u8; 16]),
2088            version: 2,
2089        };
2090        assert_eq!(a.cache_key(), 7);
2091        assert_eq!(a.cache_key(), b.cache_key(), "same texture slot");
2092        assert_ne!(a, b, "other version means other data");
2093        assert_eq!(a.dynamic_version(), Some(1));
2094        assert_eq!(
2095            ImageSource::Rgba {
2096                id: 7,
2097                width: 1,
2098                height: 1,
2099                data: Arc::new(vec![0u8; 4]),
2100            }
2101            .dynamic_version(),
2102            None
2103        );
2104    }
2105
2106    #[test]
2107    fn backdrop_side_channel_keeps_item_indices() {
2108        let mut list = RenderList::new();
2109        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2110        list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
2111        assert!(!list.has_backdrops());
2112        list.push_backdrop(
2113            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2114            8.0,
2115            16.0,
2116            Color::TRANSPARENT,
2117            0.0,
2118            SpanMask::None,
2119        );
2120        assert!(list.has_backdrops());
2121        assert_eq!(list.len(), 2, "marker does not add items");
2122        let span = list.backdrops()[0];
2123        assert_eq!(span.at, 2, "rozmywa dwa quady spod spodu");
2124        assert_eq!(span.draw.rect, Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2125        assert_eq!(span.draw.blur, 16.0);
2126        list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2127        assert_eq!(list.backdrops()[0].at, 2, "at frozen in push");
2128    }
2129
2130    #[test]
2131    fn filter_span_covers_subtree_range() {
2132        let mut list = RenderList::new();
2133        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2134        let h = list
2135            .push_filter_begin(
2136                Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2137                8.0,
2138                16.0,
2139                SpanMask::None,
2140            )
2141            .expect("widoczny filtr otwiera zakres");
2142        assert_eq!(list.len(), 1, "begin does not add items");
2143        list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2144        list.push_quad(Rect::from_xywh(5.0, 5.0, 5.0, 5.0), Color::GREEN, 0.0);
2145        list.push_filter_end(h);
2146        assert!(list.has_filters());
2147        let span = list.filter_spans()[0];
2148        assert_eq!((span.start, span.end), (1, 3), "tylko poddrzewo");
2149        assert_eq!(span.draw.blur, 16.0);
2150        assert_eq!(span.draw.radius, 8.0);
2151        list.clear();
2152        assert!(!list.has_filters(), "clear also drops filters");
2153    }
2154
2155    #[test]
2156    fn filter_begin_rejects_invisible_and_end_drops_empty() {
2157        let mut list = RenderList::new();
2158
2159        assert!(
2160            list.push_filter_begin(
2161                Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2162                0.0,
2163                0.0,
2164                SpanMask::None
2165            )
2166            .is_none()
2167        );
2168        assert!(
2169            list.push_filter_begin(
2170                Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2171                0.0,
2172                f32::NAN,
2173                SpanMask::None
2174            )
2175            .is_none()
2176        );
2177        assert!(
2178            list.push_filter_begin(
2179                Rect::from_xywh(0.0, 0.0, 0.0, 0.0),
2180                0.0,
2181                8.0,
2182                SpanMask::None
2183            )
2184            .is_none()
2185        );
2186        assert!(!list.has_filters());
2187
2188        let h = list
2189            .push_filter_begin(
2190                Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2191                0.0,
2192                8.0,
2193                SpanMask::None,
2194            )
2195            .expect("otwiera");
2196        list.push_filter_end(h);
2197        assert!(!list.has_filters(), "pusty zakres bez contentu");
2198
2199        let mut clipped = RenderList::new();
2200        clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2201        let h = clipped
2202            .push_filter_begin(
2203                Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
2204                4.0,
2205                12.0,
2206                SpanMask::None,
2207            )
2208            .expect("otwiera");
2209        clipped.push_quad(Rect::from_xywh(60.0, 60.0, 10.0, 10.0), Color::RED, 0.0);
2210        clipped.push_filter_end(h);
2211        assert_eq!(
2212            clipped.filter_spans()[0].draw.rect,
2213            Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
2214        );
2215    }
2216
2217    #[test]
2218    fn filter_spans_nest_like_subtrees() {
2219        let mut list = RenderList::new();
2220        let outer = list
2221            .push_filter_begin(
2222                Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2223                0.0,
2224                8.0,
2225                SpanMask::None,
2226            )
2227            .expect("outer");
2228        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2229        let inner = list
2230            .push_filter_begin(
2231                Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2232                0.0,
2233                4.0,
2234                SpanMask::None,
2235            )
2236            .expect("inner");
2237        list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2238        list.push_filter_end(inner);
2239        list.push_filter_end(outer);
2240        assert_eq!(list.filter_spans().len(), 2);
2241        assert_eq!(
2242            (list.filter_spans()[0].start, list.filter_spans()[0].end),
2243            (0, 2)
2244        );
2245        assert_eq!(
2246            (list.filter_spans()[1].start, list.filter_spans()[1].end),
2247            (1, 2)
2248        );
2249    }
2250
2251    #[test]
2252    fn backdrop_filters_and_clips() {
2253        let mut list = RenderList::new();
2254
2255        list.push_backdrop(
2256            Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2257            0.0,
2258            0.0,
2259            Color::TRANSPARENT,
2260            0.0,
2261            SpanMask::None,
2262        );
2263        assert!(!list.has_backdrops());
2264
2265        list.push_backdrop(
2266            Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2267            0.0,
2268            0.0,
2269            Color::BLACK.with_alpha(0.25),
2270            0.0,
2271            SpanMask::None,
2272        );
2273        assert_eq!(list.backdrops().len(), 1);
2274
2275        let mut clipped = RenderList::new();
2276        clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2277        clipped.push_backdrop(
2278            Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
2279            4.0,
2280            12.0,
2281            Color::TRANSPARENT,
2282            0.0,
2283            SpanMask::None,
2284        );
2285        assert_eq!(
2286            clipped.backdrops()[0].draw.rect,
2287            Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
2288        );
2289
2290        let mut outside = RenderList::new();
2291        outside.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2292        outside.push_backdrop(
2293            Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2294            0.0,
2295            12.0,
2296            Color::TRANSPARENT,
2297            0.0,
2298            SpanMask::None,
2299        );
2300        assert!(!outside.has_backdrops());
2301
2302        let mut faded = RenderList::new();
2303        faded.push_opacity(0.5);
2304        faded.push_backdrop(
2305            Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2306            0.0,
2307            0.0,
2308            Color::BLACK.with_alpha(0.4),
2309            0.5,
2310            SpanMask::None,
2311        );
2312        let d = faded.backdrops()[0].draw;
2313        assert!((d.dim.a - 0.2).abs() < 1e-6, "dim.a 0.4 * 0.5: {}", d.dim.a);
2314        assert!(
2315            (d.noise - 0.25).abs() < 1e-6,
2316            "noise 0.5 * 0.5: {}",
2317            d.noise
2318        );
2319
2320        faded.clear();
2321        assert!(!faded.has_backdrops());
2322    }
2323
2324    #[test]
2325    fn span_mask_encodes_for_gpu() {
2326        assert_eq!(SpanMask::None.mask_vec(), [0.0, 0.0, 0.0, 0.0]);
2327        assert_eq!(
2328            SpanMask::Linear {
2329                from: 1.0,
2330                to: 0.0,
2331                angle_rad: std::f32::consts::FRAC_PI_2,
2332            }
2333            .mask_vec(),
2334            [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
2335        );
2336        assert_eq!(SpanMask::Uniform(0.5).mask_vec(), [2.0, 0.5, 0.0, 0.0]);
2337
2338        assert_eq!(
2339            SpanMask::Linear {
2340                from: 2.0,
2341                to: -1.0,
2342                angle_rad: f32::NAN,
2343            }
2344            .mask_vec()[1..3],
2345            [1.0, 0.0]
2346        );
2347        assert!(SpanMask::None.is_none());
2348        assert!(!SpanMask::Uniform(0.0).is_none());
2349    }
2350
2351    #[test]
2352    fn filter_span_carries_mask() {
2353        let mut list = RenderList::new();
2354        let mask = SpanMask::Linear {
2355            from: 1.0,
2356            to: 0.0,
2357            angle_rad: std::f32::consts::FRAC_PI_2,
2358        };
2359        let h = list
2360            .push_filter_begin(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), 0.0, 8.0, mask)
2361            .expect("otwiera");
2362        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2363        list.push_filter_end(h);
2364        assert_eq!(list.filter_spans()[0].draw.mask, mask);
2365        assert_eq!(
2366            list.filter_spans()[0].draw.mask.mask_vec(),
2367            [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
2368        );
2369    }
2370
2371    #[test]
2372    fn backdrop_marker_carries_mask() {
2373        let mut list = RenderList::new();
2374        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2375        list.push_backdrop(
2376            Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2377            0.0,
2378            8.0,
2379            Color::TRANSPARENT,
2380            0.0,
2381            SpanMask::Uniform(0.5),
2382        );
2383        assert_eq!(
2384            list.backdrops()[0].draw.mask,
2385            SpanMask::Uniform(0.5),
2386            "maska w markerze t�'a"
2387        );
2388    }
2389
2390    #[test]
2391    fn transform_stack_composes_and_snapshots() {
2392        use crate::geometry::Point;
2393        let mut list = RenderList::new();
2394        assert!(list.active_transform().is_identity());
2395
2396        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2397        assert!(list.item_transform(0).is_identity());
2398
2399        list.push_transform(Transform::rotate_around_degrees(90.0, Point::new(5.0, 5.0)));
2400        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2401        assert!(!list.item_transform(1).is_identity());
2402        assert_eq!(list.item_transforms().len(), list.items().len());
2403        list.pop_transform();
2404        assert!(list.active_transform().is_identity());
2405        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2406        assert!(list.item_transform(2).is_identity());
2407    }
2408
2409    #[test]
2410    fn transform_aabb_covers_rotation() {
2411        use crate::geometry::Point;
2412        let r = Rect::from_xywh(0.0, 0.0, 10.0, 20.0);
2413        let t = Transform::rotate_around_degrees(90.0, Point::new(5.0, 10.0));
2414        let aabb = t.apply_rect_aabb(r);
2415        assert!(
2416            (aabb.size.width - 20.0).abs() < 0.01,
2417            "w={}",
2418            aabb.size.width
2419        );
2420        assert!(
2421            (aabb.size.height - 10.0).abs() < 0.01,
2422            "h={}",
2423            aabb.size.height
2424        );
2425        assert!(Transform::IDENTITY.apply_rect_aabb(r) == r);
2426    }
2427
2428    #[test]
2429    fn rotated_text_survives_own_self_clip() {
2430        let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
2431        let mut list = RenderList::new();
2432        list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
2433        list.push_clip(tile);
2434        list.push_text(TextDraw {
2435            color: Color::WHITE,
2436            glyphs: vec![GlyphQuad {
2437                rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2438                uv: [0.0, 0.0, 1.0, 1.0],
2439                color_glyph: false,
2440            }],
2441        });
2442        assert_eq!(list.len(), 1, "rotated glyph in custom clip remains");
2443    }
2444
2445    #[test]
2446    fn ancestor_clip_still_culls_rotated_content() {
2447        let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
2448        let mut list = RenderList::new();
2449        list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2450        list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
2451        list.push_text(TextDraw {
2452            color: Color::WHITE,
2453            glyphs: vec![GlyphQuad {
2454                rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2455                uv: [0.0, 0.0, 1.0, 1.0],
2456                color_glyph: false,
2457            }],
2458        });
2459
2460        assert_eq!(list.len(), 1, "clip przodka nie zjada widocznego");
2461
2462        let mut list2 = RenderList::new();
2463        list2.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2464        list2.push_transform(Transform::translate(500.0, 500.0));
2465        list2.push_text(TextDraw {
2466            color: Color::WHITE,
2467            glyphs: vec![GlyphQuad {
2468                rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2469                uv: [0.0, 0.0, 1.0, 1.0],
2470                color_glyph: false,
2471            }],
2472        });
2473        assert_eq!(list2.len(), 0, "clip przodka obcina poza ekranem");
2474    }
2475}