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#[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#[derive(Clone, Debug, PartialEq)]
855#[non_exhaustive]
856pub enum OpacityNode {
857 Segment { start: usize, end: usize },
859
860 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 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 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}