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