1use std::sync::Arc;
2
3use crate::geometry::{Rect, Transform, Vec2};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct Color {
7 pub r: f32,
8
9 pub g: f32,
10
11 pub b: f32,
12
13 pub a: f32,
14}
15
16impl Color {
17 pub const WHITE: Self = Self {
18 r: 1.0,
19 g: 1.0,
20 b: 1.0,
21 a: 1.0,
22 };
23
24 pub const BLACK: Self = Self {
25 r: 0.0,
26 g: 0.0,
27 b: 0.0,
28 a: 1.0,
29 };
30
31 pub const RED: Self = Self {
32 r: 1.0,
33 g: 0.0,
34 b: 0.0,
35 a: 1.0,
36 };
37
38 pub const GREEN: Self = Self {
39 r: 0.0,
40 g: 1.0,
41 b: 0.0,
42 a: 1.0,
43 };
44
45 pub const BLUE: Self = Self {
46 r: 0.0,
47 g: 0.0,
48 b: 1.0,
49 a: 1.0,
50 };
51
52 pub const TRANSPARENT: Self = Self {
53 r: 0.0,
54 g: 0.0,
55 b: 0.0,
56 a: 0.0,
57 };
58
59 pub const ALICEBLUE: Self = Self::hex(0xF0F8FF);
60
61 pub const ANTIQUEWHITE: Self = Self::hex(0xFAEBD7);
62
63 pub const AQUA: Self = Self::hex(0x00FFFF);
64
65 pub const AQUAMARINE: Self = Self::hex(0x7FFFD4);
66
67 pub const AZURE: Self = Self::hex(0xF0FFFF);
68
69 pub const BEIGE: Self = Self::hex(0xF5F5DC);
70
71 pub const BISQUE: Self = Self::hex(0xFFE4C4);
72
73 pub const BLANCHEDALMOND: Self = Self::hex(0xFFEBCD);
74
75 pub const BLUEVIOLET: Self = Self::hex(0x8A2BE2);
76
77 pub const BROWN: Self = Self::hex(0xA52A2A);
78
79 pub const BURLYWOOD: Self = Self::hex(0xDEB887);
80
81 pub const CADETBLUE: Self = Self::hex(0x5F9EA0);
82
83 pub const CHARTREUSE: Self = Self::hex(0x7FFF00);
84
85 pub const CHOCOLATE: Self = Self::hex(0xD2691E);
86
87 pub const CORAL: Self = Self::hex(0xFF7F50);
88
89 pub const CORNFLOWERBLUE: Self = Self::hex(0x6495ED);
90
91 pub const CORNSILK: Self = Self::hex(0xFFF8DC);
92
93 pub const CRIMSON: Self = Self::hex(0xDC143C);
94
95 pub const CYAN: Self = Self::hex(0x00FFFF);
96
97 pub const DARKBLUE: Self = Self::hex(0x00008B);
98
99 pub const DARKCYAN: Self = Self::hex(0x008B8B);
100
101 pub const DARKGOLDENROD: Self = Self::hex(0xB8860B);
102
103 pub const DARKGRAY: Self = Self::hex(0xA9A9A9);
104
105 pub const DARKGREEN: Self = Self::hex(0x006400);
106
107 pub const DARKGREY: Self = Self::hex(0xA9A9A9);
108
109 pub const DARKKHAKI: Self = Self::hex(0xBDB76B);
110
111 pub const DARKMAGENTA: Self = Self::hex(0x8B008B);
112
113 pub const DARKOLIVEGREEN: Self = Self::hex(0x556B2F);
114
115 pub const DARKORANGE: Self = Self::hex(0xFF8C00);
116
117 pub const DARKORCHID: Self = Self::hex(0x9932CC);
118
119 pub const DARKRED: Self = Self::hex(0x8B0000);
120
121 pub const DARKSALMON: Self = Self::hex(0xE9967A);
122
123 pub const DARKSEAGREEN: Self = Self::hex(0x8FBC8F);
124
125 pub const DARKSLATEBLUE: Self = Self::hex(0x483D8B);
126
127 pub const DARKSLATEGRAY: Self = Self::hex(0x2F4F4F);
128
129 pub const DARKSLATEGREY: Self = Self::hex(0x2F4F4F);
130
131 pub const DARKTURQUOISE: Self = Self::hex(0x00CED1);
132
133 pub const DARKVIOLET: Self = Self::hex(0x9400D3);
134
135 pub const DEEPPINK: Self = Self::hex(0xFF1493);
136
137 pub const DEEPSKYBLUE: Self = Self::hex(0x00BFFF);
138
139 pub const DIMGRAY: Self = Self::hex(0x696969);
140
141 pub const DIMGREY: Self = Self::hex(0x696969);
142
143 pub const DODGERBLUE: Self = Self::hex(0x1E90FF);
144
145 pub const FIREBRICK: Self = Self::hex(0xB22222);
146
147 pub const FLORALWHITE: Self = Self::hex(0xFFFAF0);
148
149 pub const FORESTGREEN: Self = Self::hex(0x228B22);
150
151 pub const FUCHSIA: Self = Self::hex(0xFF00FF);
152
153 pub const GAINSBORO: Self = Self::hex(0xDCDCDC);
154
155 pub const GHOSTWHITE: Self = Self::hex(0xF8F8FF);
156
157 pub const GOLD: Self = Self::hex(0xFFD700);
158
159 pub const GOLDENROD: Self = Self::hex(0xDAA520);
160
161 pub const GRAY: Self = Self::hex(0x808080);
162
163 pub const GREENYELLOW: Self = Self::hex(0xADFF2F);
164
165 pub const GREY: Self = Self::hex(0x808080);
166
167 pub const HONEYDEW: Self = Self::hex(0xF0FFF0);
168
169 pub const HOTPINK: Self = Self::hex(0xFF69B4);
170
171 pub const INDIANRED: Self = Self::hex(0xCD5C5C);
172
173 pub const INDIGO: Self = Self::hex(0x4B0082);
174
175 pub const IVORY: Self = Self::hex(0xFFFFF0);
176
177 pub const KHAKI: Self = Self::hex(0xF0E68C);
178
179 pub const LAVENDER: Self = Self::hex(0xE6E6FA);
180
181 pub const LAVENDERBLUSH: Self = Self::hex(0xFFF0F5);
182
183 pub const LAWNGREEN: Self = Self::hex(0x7CFC00);
184
185 pub const LEMONCHIFFON: Self = Self::hex(0xFFFACD);
186
187 pub const LIGHTBLUE: Self = Self::hex(0xADD8E6);
188
189 pub const LIGHTCORAL: Self = Self::hex(0xF08080);
190
191 pub const LIGHTCYAN: Self = Self::hex(0xE0FFFF);
192
193 pub const LIGHTGOLDENRODYELLOW: Self = Self::hex(0xFAFAD2);
194
195 pub const LIGHTGRAY: Self = Self::hex(0xD3D3D3);
196
197 pub const LIGHTGREEN: Self = Self::hex(0x90EE90);
198
199 pub const LIGHTGREY: Self = Self::hex(0xD3D3D3);
200
201 pub const LIGHTPINK: Self = Self::hex(0xFFB6C1);
202
203 pub const LIGHTSALMON: Self = Self::hex(0xFFA07A);
204
205 pub const LIGHTSEAGREEN: Self = Self::hex(0x20B2AA);
206
207 pub const LIGHTSKYBLUE: Self = Self::hex(0x87CEFA);
208
209 pub const LIGHTSLATEGRAY: Self = Self::hex(0x778899);
210
211 pub const LIGHTSLATEGREY: Self = Self::hex(0x778899);
212
213 pub const LIGHTSTEELBLUE: Self = Self::hex(0xB0C4DE);
214
215 pub const LIGHTYELLOW: Self = Self::hex(0xFFFFE0);
216
217 pub const LIME: Self = Self::hex(0x00FF00);
218
219 pub const LIMEGREEN: Self = Self::hex(0x32CD32);
220
221 pub const LINEN: Self = Self::hex(0xFAF0E6);
222
223 pub const MAGENTA: Self = Self::hex(0xFF00FF);
224
225 pub const MAROON: Self = Self::hex(0x800000);
226
227 pub const MEDIUMAQUAMARINE: Self = Self::hex(0x66CDAA);
228
229 pub const MEDIUMBLUE: Self = Self::hex(0x0000CD);
230
231 pub const MEDIUMORCHID: Self = Self::hex(0xBA55D3);
232
233 pub const MEDIUMPURPLE: Self = Self::hex(0x9370DB);
234
235 pub const MEDIUMSEAGREEN: Self = Self::hex(0x3CB371);
236
237 pub const MEDIUMSLATEBLUE: Self = Self::hex(0x7B68EE);
238
239 pub const MEDIUMSPRINGGREEN: Self = Self::hex(0x00FA9A);
240
241 pub const MEDIUMTURQUOISE: Self = Self::hex(0x48D1CC);
242
243 pub const MEDIUMVIOLETRED: Self = Self::hex(0xC71585);
244
245 pub const MIDNIGHTBLUE: Self = Self::hex(0x191970);
246
247 pub const MINTCREAM: Self = Self::hex(0xF5FFFA);
248
249 pub const MISTYROSE: Self = Self::hex(0xFFE4E1);
250
251 pub const MOCCASIN: Self = Self::hex(0xFFE4B5);
252
253 pub const NAVAJOWHITE: Self = Self::hex(0xFFDEAD);
254
255 pub const NAVY: Self = Self::hex(0x000080);
256
257 pub const OLDLACE: Self = Self::hex(0xFDF5E6);
258
259 pub const OLIVE: Self = Self::hex(0x808000);
260
261 pub const OLIVEDRAB: Self = Self::hex(0x6B8E23);
262
263 pub const ORANGE: Self = Self::hex(0xFFA500);
264
265 pub const ORANGERED: Self = Self::hex(0xFF4500);
266
267 pub const ORCHID: Self = Self::hex(0xDA70D6);
268
269 pub const PALEGOLDENROD: Self = Self::hex(0xEEE8AA);
270
271 pub const PALEGREEN: Self = Self::hex(0x98FB98);
272
273 pub const PALETURQUOISE: Self = Self::hex(0xAFEEEE);
274
275 pub const PALEVIOLETRED: Self = Self::hex(0xDB7093);
276
277 pub const PAPAYAWHIP: Self = Self::hex(0xFFEFD5);
278
279 pub const PEACHPUFF: Self = Self::hex(0xFFDAB9);
280
281 pub const PERU: Self = Self::hex(0xCD853F);
282
283 pub const PINK: Self = Self::hex(0xFFC0CB);
284
285 pub const PLUM: Self = Self::hex(0xDDA0DD);
286
287 pub const POWDERBLUE: Self = Self::hex(0xB0E0E6);
288
289 pub const PURPLE: Self = Self::hex(0x800080);
290
291 pub const REBECCAPURPLE: Self = Self::hex(0x663399);
292
293 pub const ROSYBROWN: Self = Self::hex(0xBC8F8F);
294
295 pub const ROYALBLUE: Self = Self::hex(0x4169E1);
296
297 pub const SADDLEBROWN: Self = Self::hex(0x8B4513);
298
299 pub const SALMON: Self = Self::hex(0xFA8072);
300
301 pub const SANDYBROWN: Self = Self::hex(0xF4A460);
302
303 pub const SEAGREEN: Self = Self::hex(0x2E8B57);
304
305 pub const SEASHELL: Self = Self::hex(0xFFF5EE);
306
307 pub const SIENNA: Self = Self::hex(0xA0522D);
308
309 pub const SILVER: Self = Self::hex(0xC0C0C0);
310
311 pub const SKYBLUE: Self = Self::hex(0x87CEEB);
312
313 pub const SLATEBLUE: Self = Self::hex(0x6A5ACD);
314
315 pub const SLATEGRAY: Self = Self::hex(0x708090);
316
317 pub const SLATEGREY: Self = Self::hex(0x708090);
318
319 pub const SNOW: Self = Self::hex(0xFFFAFA);
320
321 pub const SPRINGGREEN: Self = Self::hex(0x00FF7F);
322
323 pub const STEELBLUE: Self = Self::hex(0x4682B4);
324
325 pub const TAN: Self = Self::hex(0xD2B48C);
326
327 pub const TEAL: Self = Self::hex(0x008080);
328
329 pub const THISTLE: Self = Self::hex(0xD8BFD8);
330
331 pub const TOMATO: Self = Self::hex(0xFF6347);
332
333 pub const TURQUOISE: Self = Self::hex(0x40E0D0);
334
335 pub const VIOLET: Self = Self::hex(0xEE82EE);
336
337 pub const WHEAT: Self = Self::hex(0xF5DEB3);
338
339 pub const WHITESMOKE: Self = Self::hex(0xF5F5F5);
340
341 pub const YELLOW: Self = Self::hex(0xFFFF00);
342
343 pub const YELLOWGREEN: Self = Self::hex(0x9ACD32);
344
345 pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
346 Self { r, g, b, a: 1.0 }
347 }
348
349 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
350 Self { r, g, b, a }
351 }
352
353 pub const fn rgb8(r: u8, g: u8, b: u8) -> Self {
354 Self {
355 r: r as f32 / 255.0,
356 g: g as f32 / 255.0,
357 b: b as f32 / 255.0,
358 a: 1.0,
359 }
360 }
361
362 pub const fn hex(hex: u32) -> Self {
363 Self::rgb8((hex >> 16) as u8, (hex >> 8) as u8, hex as u8)
364 }
365
366 pub const fn hexa(hex: u32) -> Self {
367 let mut c = Self::hex(hex >> 8);
368 c.a = (hex as u8) as f32 / 255.0;
369 c
370 }
371
372 pub fn to_linear(self) -> Self {
373 fn channel(c: f32) -> f32 {
374 if c <= 0.040_45 {
375 c / 12.92
376 } else {
377 ((c + 0.055) / 1.055).powf(2.4)
378 }
379 }
380 Self {
381 r: channel(self.r),
382 g: channel(self.g),
383 b: channel(self.b),
384 a: self.a,
385 }
386 }
387
388 pub fn from_linear(self) -> Self {
389 fn channel(c: f32) -> f32 {
390 if c <= 0.003_130_8 {
391 c * 12.92
392 } else {
393 1.055 * c.powf(1.0 / 2.4) - 0.055
394 }
395 }
396 Self {
397 r: channel(self.r),
398 g: channel(self.g),
399 b: channel(self.b),
400 a: self.a,
401 }
402 }
403
404 pub fn parse_hex(s: &str) -> Option<Self> {
405 let h = s.strip_prefix('#').unwrap_or(s);
406 let h = h.strip_prefix("0x").unwrap_or(h);
407 let d = |c: u8| (c as char).to_digit(16);
408 let bytes: [u8; 4] = match h.len() {
409 3 | 4 => {
410 let mut out = [0u8; 4];
411 let mut it = h.bytes();
412 for slot in out.iter_mut().take(h.len()) {
413 let v = d(it.next()?)? as u8;
414 *slot = v * 16 + v;
415 }
416 if h.len() == 3 {
417 out[3] = 0xFF;
418 }
419 out
420 }
421 6 | 8 => {
422 let b = h.as_bytes();
423 let pair =
424 |i: usize| d(b[i]).and_then(|hi| d(b[i + 1]).map(|lo| (hi * 16 + lo) as u8));
425 let mut out = [pair(0)?, pair(2)?, pair(4)?, 0xFF];
426 if h.len() == 8 {
427 out[3] = pair(6)?;
428 }
429 out
430 }
431 _ => return None,
432 };
433 Some(Self {
434 r: bytes[0] as f32 / 255.0,
435 g: bytes[1] as f32 / 255.0,
436 b: bytes[2] as f32 / 255.0,
437 a: bytes[3] as f32 / 255.0,
438 })
439 }
440
441 pub fn to_rgb8(self) -> (u8, u8, u8) {
442 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
443 (q(self.r), q(self.g), q(self.b))
444 }
445
446 pub fn to_rgba8(self) -> (u8, u8, u8, u8) {
447 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
448 (q(self.r), q(self.g), q(self.b), q(self.a))
449 }
450
451 pub fn to_hex_string(self) -> String {
452 let (r, g, b) = self.to_rgb8();
453 format!("#{r:02X}{g:02X}{b:02X}")
454 }
455
456 pub fn to_hex8_string(self) -> String {
457 let (r, g, b, a) = self.to_rgba8();
458 format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
459 }
460
461 pub fn with_alpha(self, alpha: f32) -> Self {
462 Self {
463 a: alpha.clamp(0.0, 1.0),
464 ..self
465 }
466 }
467
468 pub fn to_hsva(self) -> (f32, f32, f32, f32) {
469 let (r, g, b) = (
470 self.r.clamp(0.0, 1.0),
471 self.g.clamp(0.0, 1.0),
472 self.b.clamp(0.0, 1.0),
473 );
474 let max = r.max(g).max(b);
475 let min = r.min(g).min(b);
476 let d = max - min;
477 let v = max;
478 let s = if max <= f32::EPSILON { 0.0 } else { d / max };
479 let h = if d <= f32::EPSILON {
480 0.0
481 } else if max == r {
482 (((g - b) / d) % 6.0 + 6.0) % 6.0 / 6.0
483 } else if max == g {
484 ((b - r) / d + 2.0) / 6.0
485 } else {
486 ((r - g) / d + 4.0) / 6.0
487 };
488 (h, s, v, self.a.clamp(0.0, 1.0))
489 }
490
491 pub fn from_hsva(h: f32, s: f32, v: f32, a: f32) -> Self {
492 let h = if h.is_finite() {
493 h.rem_euclid(1.0)
494 } else {
495 0.0
496 };
497 let (s, v, a) = (s.clamp(0.0, 1.0), v.clamp(0.0, 1.0), a.clamp(0.0, 1.0));
498 if s <= f32::EPSILON {
499 return Self {
500 r: v,
501 g: v,
502 b: v,
503 a,
504 };
505 }
506 let sector = h * 6.0;
507 let i = sector.floor() as i32;
508 let f = sector - sector.floor();
509 let p = v * (1.0 - s);
510 let q = v * (1.0 - s * f);
511 let t = v * (1.0 - s * (1.0 - f));
512 let (r, g, b) = match i.rem_euclid(6) {
513 0 => (v, t, p),
514 1 => (q, v, p),
515 2 => (p, v, t),
516 3 => (p, q, v),
517 4 => (t, p, v),
518 _ => (v, p, q),
519 };
520 Self { r, g, b, a }
521 }
522
523 pub fn from_hsl(h_deg: f32, s: f32, l: f32) -> Self {
524 let h = if h_deg.is_finite() {
525 h_deg.rem_euclid(360.0)
526 } else {
527 0.0
528 };
529 let (s, l) = (s.clamp(0.0, 1.0), l.clamp(0.0, 1.0));
530 let a = s * l.min(1.0 - l);
531 let f = |n: f32| {
532 let k = (n + h / 30.0) % 12.0;
533 l - a * (-1.0f32).max((k - 3.0).min(9.0 - k).min(1.0))
534 };
535 Self {
536 r: f(0.0).clamp(0.0, 1.0),
537 g: f(8.0).clamp(0.0, 1.0),
538 b: f(4.0).clamp(0.0, 1.0),
539 a: 1.0,
540 }
541 }
542
543 pub fn to_hsl(self) -> (f32, f32, f32) {
544 let (r, g, b) = (
545 self.r.clamp(0.0, 1.0),
546 self.g.clamp(0.0, 1.0),
547 self.b.clamp(0.0, 1.0),
548 );
549 let mx = r.max(g).max(b);
550 let mn = r.min(g).min(b);
551 let d = mx - mn;
552 let l = ((mx + mn) * 0.5).clamp(0.0, 1.0);
553 if d <= f32::EPSILON {
554 return (0.0, 0.0, l);
555 }
556 let s = (d / (1.0 - (2.0 * l - 1.0).abs())).clamp(0.0, 1.0);
557 let h = if mx == r {
558 ((g - b) / d) % 6.0
559 } else if mx == g {
560 (b - r) / d + 2.0
561 } else {
562 (r - g) / d + 4.0
563 } * 60.0;
564 (h.rem_euclid(360.0), s, l)
565 }
566
567 pub fn to_hex6(self) -> String {
568 let (r, g, b) = self.to_rgb8();
569 format!("{r:02X}{g:02X}{b:02X}")
570 }
571}
572
573impl Default for Color {
574 fn default() -> Self {
575 Self::TRANSPARENT
576 }
577}
578
579#[derive(Clone, Copy, Debug, PartialEq)]
580pub struct Quad {
581 pub rect: Rect,
582
583 pub color: Color,
584
585 pub radius: f32,
586
587 pub border_color: Color,
588
589 pub border_width: f32,
590}
591
592impl Quad {
593 pub fn has_border(self) -> bool {
594 self.border_width > 0.0 && self.border_color.a > 0.0
595 }
596}
597
598#[derive(Clone, Copy, Debug, PartialEq)]
599pub struct Shadow {
600 pub rect: Rect,
601
602 pub color: Color,
603
604 pub radius: f32,
605
606 pub blur: f32,
607
608 pub spread: f32,
609
610 pub offset: Vec2,
611
612 pub inset: bool,
613}
614
615impl Shadow {
616 pub fn is_visible(&self) -> bool {
617 !self.rect.is_empty() && self.color.a > 0.0 && self.blur > 0.0 && self.blur.is_finite()
618 }
619
620 pub fn bounds(&self) -> Rect {
621 if self.inset {
622 return self.rect;
623 }
624 let margin = self.blur * 0.5 + self.spread.max(0.0);
625 Rect::from_xywh(
626 self.rect.left() + self.offset.x - margin,
627 self.rect.top() + self.offset.y - margin,
628 self.rect.size.width + margin * 2.0,
629 self.rect.size.height + margin * 2.0,
630 )
631 }
632}
633
634#[derive(Clone, Copy, Debug, PartialEq)]
635pub struct GlyphQuad {
636 pub rect: Rect,
637
638 pub uv: [f32; 4],
639
640 pub color_glyph: bool,
641}
642
643#[derive(Clone, Debug, Default, PartialEq)]
644pub struct TextDraw {
645 pub color: Color,
646
647 pub glyphs: Vec<GlyphQuad>,
648}
649
650#[derive(Clone, Debug, PartialEq)]
651#[non_exhaustive]
652pub enum ImageSource {
653 File(String),
654
655 Rgba {
656 id: u64,
657
658 width: u32,
659
660 height: u32,
661
662 data: Arc<Vec<u8>>,
663 },
664
665 Dynamic {
666 id: u64,
667
668 width: u32,
669
670 height: u32,
671
672 data: Arc<Vec<u8>>,
673
674 version: u64,
675 },
676}
677
678impl ImageSource {
679 pub fn cache_key(&self) -> u64 {
680 match self {
681 Self::File(path) => {
682 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
683 for byte in path.as_bytes() {
684 hash ^= u64::from(*byte);
685 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
686 }
687 hash ^ 0x9e37_79b9_7f4a_7c15
688 }
689 Self::Rgba { id, .. } => *id,
690 Self::Dynamic { id, .. } => *id,
691 }
692 }
693
694 pub fn dynamic_version(&self) -> Option<u64> {
695 match self {
696 Self::Dynamic { version, .. } => Some(*version),
697 Self::File(_) | Self::Rgba { .. } => None,
698 }
699 }
700
701 pub fn extent(&self) -> Option<(u32, u32)> {
702 match self {
703 Self::File(_) => None,
704 Self::Rgba { width, height, .. } | Self::Dynamic { width, height, .. } => {
705 Some((*width, *height))
706 }
707 }
708 }
709}
710
711#[derive(Clone, Debug, PartialEq)]
712pub struct ImageDraw {
713 pub rect: Rect,
714
715 pub uv: [f32; 4],
716
717 pub source: ImageSource,
718}
719
720#[derive(Clone, Debug, PartialEq)]
721pub struct WaveformDraw {
722 pub bars: Vec<Rect>,
723
724 pub color: Color,
725}
726
727#[derive(Clone, Copy, Debug, PartialEq)]
728pub struct BackdropDraw {
729 pub rect: Rect,
730
731 pub radius: f32,
732
733 pub blur: f32,
734
735 pub dim: Color,
736
737 pub noise: f32,
738
739 pub mask: SpanMask,
740}
741
742impl BackdropDraw {
743 pub fn is_visible(self) -> bool {
744 !self.rect.is_empty()
745 && (self.blur > 0.0 && self.blur.is_finite() || self.dim.a > 0.0 || self.noise > 0.0)
746 }
747}
748
749#[derive(Clone, Copy, Debug, PartialEq)]
750#[non_exhaustive]
751#[derive(Default)]
752pub enum SpanMask {
753 #[default]
754 None,
755
756 Linear {
757 from: f32,
758 to: f32,
759 angle_rad: f32,
760 },
761
762 Uniform(f32),
763}
764
765impl SpanMask {
766 pub fn mask_vec(self) -> [f32; 4] {
767 match self {
768 SpanMask::None => [0.0, 0.0, 0.0, 0.0],
769 SpanMask::Linear {
770 from,
771 to,
772 angle_rad,
773 } => {
774 let angle = if angle_rad.is_finite() {
775 angle_rad
776 } else {
777 0.0
778 };
779 [1.0, from.clamp(0.0, 1.0), to.clamp(0.0, 1.0), angle]
780 }
781 SpanMask::Uniform(v) => [2.0, v.clamp(0.0, 1.0), 0.0, 0.0],
782 }
783 }
784
785 pub fn is_none(self) -> bool {
786 matches!(self, SpanMask::None)
787 }
788}
789
790#[derive(Clone, Copy, Debug, PartialEq)]
791pub struct BackdropSpan {
792 pub at: usize,
793
794 pub draw: BackdropDraw,
795
796 pub clip: Option<RoundedClip>,
797}
798
799#[derive(Clone, Copy, Debug, PartialEq)]
800pub struct FilterDraw {
801 pub rect: Rect,
802
803 pub radius: f32,
804
805 pub blur: f32,
806
807 pub mask: SpanMask,
808}
809
810impl FilterDraw {
811 pub fn is_visible(self) -> bool {
812 !self.rect.is_empty() && self.blur > 0.0 && self.blur.is_finite()
813 }
814}
815
816#[derive(Clone, Copy, Debug, PartialEq)]
817pub struct FilterSpan {
818 pub start: usize,
819
820 pub end: usize,
821
822 pub draw: FilterDraw,
823
824 pub clip: Option<RoundedClip>,
825}
826
827#[derive(Clone, Debug, PartialEq)]
828pub struct GradientDraw {
829 pub rect: Rect,
830
831 pub radius: f32,
832
833 pub kind: crate::brush::GradientKind,
834
835 pub stops: crate::brush::GradientStops,
836}
837
838impl GradientDraw {
839 pub fn is_visible(&self) -> bool {
840 !self.rect.is_empty() && !self.stops.is_empty()
841 }
842}
843
844#[derive(Clone, Debug, PartialEq)]
845#[non_exhaustive]
846pub enum RenderItem {
847 Quad(Quad),
848
849 Text(TextDraw),
850
851 Image(ImageDraw),
852
853 Shadow(Shadow),
854
855 Waveform(WaveformDraw),
856
857 Gradient(GradientDraw),
858}
859
860pub const MAX_WAVEFORM_BARS: usize = 8192;
861
862fn clip_uv(rect: Rect, uv: [f32; 4], clipped: Rect) -> [f32; 4] {
863 let (w, h) = (rect.size.width, rect.size.height);
864 if w <= 0.0 || h <= 0.0 {
865 return uv;
866 }
867 let [u0, v0, u1, v1] = uv;
868 let fx0 = (clipped.left() - rect.left()) / w;
869 let fx1 = (clipped.right() - rect.left()) / w;
870 let fy0 = (clipped.top() - rect.top()) / h;
871 let fy1 = (clipped.bottom() - rect.top()) / h;
872 [
873 u0 + fx0 * (u1 - u0),
874 v0 + fy0 * (v1 - v0),
875 u0 + fx1 * (u1 - u0),
876 v0 + fy1 * (v1 - v0),
877 ]
878}
879
880#[derive(Clone, Copy, Debug, PartialEq)]
881pub struct RoundedClip {
882 pub rect: Rect,
883
884 pub radius: f32,
885}
886
887#[derive(Clone, Copy, Debug, PartialEq)]
888struct ClipEntry {
889 clip: RoundedClip,
890
891 under: Transform,
892}
893
894impl RoundedClip {
895 pub fn new(rect: Rect, radius: f32) -> Self {
896 let radius = if radius.is_finite() && radius > 0.0 {
897 radius
898 } else {
899 0.0
900 };
901 Self { rect, radius }
902 }
903
904 pub fn is_rounded(self) -> bool {
905 self.radius > 0.0 && !self.rect.is_empty()
906 }
907}
908
909#[derive(Clone, Debug, Default)]
910pub struct RenderList {
911 items: Vec<RenderItem>,
912 clip_stack: Vec<ClipEntry>,
913 opacity_stack: Vec<f32>,
914 transform_stack: Vec<Transform>,
915
916 item_clips: Vec<Option<RoundedClip>>,
917 item_transforms: Vec<Transform>,
918
919 backdrops: Vec<BackdropSpan>,
920
921 filters: Vec<FilterSpan>,
922}
923
924impl RenderList {
925 pub fn new() -> Self {
926 Self::default()
927 }
928
929 pub fn clear(&mut self) {
930 self.items.clear();
931 self.clip_stack.clear();
932 self.opacity_stack.clear();
933 self.transform_stack.clear();
934 self.item_clips.clear();
935 self.item_transforms.clear();
936 self.backdrops.clear();
937 self.filters.clear();
938 }
939
940 pub fn push_opacity(&mut self, opacity: f32) {
941 let o = if opacity.is_finite() {
942 opacity.clamp(0.0, 1.0)
943 } else {
944 1.0
945 };
946 self.opacity_stack.push(o);
947 }
948
949 pub fn pop_opacity(&mut self) {
950 self.opacity_stack.pop();
951 }
952
953 pub fn opacity_depth(&self) -> usize {
954 self.opacity_stack.len()
955 }
956
957 pub fn truncate_opacity(&mut self, depth: usize) {
958 while self.opacity_stack.len() > depth {
959 self.opacity_stack.pop();
960 }
961 }
962
963 pub fn active_opacity(&self) -> f32 {
964 self.opacity_stack.iter().copied().fold(1.0, |a, b| a * b)
965 }
966
967 pub fn push_transform(&mut self, t: Transform) {
968 let active = self.active_transform().then(t);
969 self.transform_stack.push(active);
970 }
971
972 pub fn pop_transform(&mut self) {
973 self.transform_stack.pop();
974 }
975
976 pub fn transform_depth(&self) -> usize {
977 self.transform_stack.len()
978 }
979
980 pub fn truncate_transforms(&mut self, depth: usize) {
981 while self.transform_stack.len() > depth {
982 self.transform_stack.pop();
983 }
984 }
985
986 pub fn active_transform(&self) -> Transform {
987 self.transform_stack.last().copied().unwrap_or_default()
988 }
989
990 pub fn item_transform(&self, index: usize) -> Transform {
991 self.item_transforms.get(index).copied().unwrap_or_default()
992 }
993
994 pub fn item_transforms(&self) -> &[Transform] {
995 &self.item_transforms
996 }
997
998 fn modulated(&self, color: Color) -> Color {
999 let o = self.active_opacity();
1000 if o >= 1.0 {
1001 color
1002 } else {
1003 Color {
1004 a: color.a * o,
1005 ..color
1006 }
1007 }
1008 }
1009
1010 pub fn push_clip(&mut self, rect: Rect) {
1011 self.clip_stack.push(ClipEntry {
1012 clip: RoundedClip::new(rect, 0.0),
1013 under: self.active_transform(),
1014 });
1015 }
1016
1017 pub fn push_rounded_clip(&mut self, rect: Rect, radius: f32) {
1018 self.clip_stack.push(ClipEntry {
1019 clip: RoundedClip::new(rect, radius),
1020 under: self.active_transform(),
1021 });
1022 }
1023
1024 pub fn clip_depth(&self) -> usize {
1025 self.clip_stack.len()
1026 }
1027
1028 pub fn active_clip(&self) -> Option<Rect> {
1029 let mut acc: Option<Rect> = None;
1030 for c in &self.clip_stack {
1031 acc = Some(match acc {
1032 Some(a) => a.intersect(c.clip.rect),
1033 None => c.clip.rect,
1034 });
1035 }
1036 acc
1037 }
1038
1039 pub fn active_clip_for(&self, t_item: Transform) -> Option<Rect> {
1040 if self.clip_stack.is_empty() {
1041 return None;
1042 }
1043 let inv = t_item.inverse()?;
1044 let mut acc: Option<Rect> = None;
1045 for c in &self.clip_stack {
1046 let mapped = c.under.then(inv).apply_rect_aabb(c.clip.rect);
1047 acc = Some(match acc {
1048 Some(a) => a.intersect(mapped),
1049 None => mapped,
1050 });
1051 }
1052 acc
1053 }
1054
1055 pub fn active_rounded_clip(&self) -> Option<RoundedClip> {
1056 self.clip_stack
1057 .iter()
1058 .rev()
1059 .find(|c| c.clip.is_rounded())
1060 .map(|c| c.clip)
1061 }
1062
1063 pub fn item_rounded_clip(&self, index: usize) -> Option<RoundedClip> {
1064 self.item_clips.get(index).copied().flatten()
1065 }
1066
1067 pub fn truncate_clips(&mut self, depth: usize) {
1068 while self.clip_stack.len() > depth {
1069 self.clip_stack.pop();
1070 }
1071 }
1072
1073 pub fn pop_clip(&mut self) {
1074 self.clip_stack.pop();
1075 }
1076
1077 fn clip_rect(&self, mut r: Rect) -> Option<Rect> {
1078 for c in &self.clip_stack {
1079 r = r.intersect(c.clip.rect);
1080 if r.is_empty() {
1081 return None;
1082 }
1083 }
1084 Some(r)
1085 }
1086
1087 pub fn push_quad(&mut self, rect: Rect, color: Color, radius: f32) {
1088 self.push_bordered_quad(rect, color, radius, 0.0, Color::TRANSPARENT);
1089 }
1090
1091 pub fn push_bordered_quad(
1092 &mut self,
1093 rect: Rect,
1094 color: Color,
1095 radius: f32,
1096 border_width: f32,
1097 border_color: Color,
1098 ) {
1099 let t = self.active_transform();
1100
1101 if !t.is_identity() {
1102 if let Some(local) = self.active_clip_for(t)
1103 && !rect.intersects(local)
1104 {
1105 return;
1106 }
1107 let color = self.modulated(color);
1108 let border_color = self.modulated(border_color);
1109 let has_border = border_width > 0.0 && border_color.a > 0.0;
1110 if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1111 return;
1112 }
1113 let rounded = self.active_rounded_clip();
1114 self.items.push(RenderItem::Quad(Quad {
1115 rect,
1116 color,
1117 radius,
1118 border_color,
1119 border_width: if has_border { border_width } else { 0.0 },
1120 }));
1121 self.item_clips.push(rounded);
1122 self.item_transforms.push(t);
1123 return;
1124 }
1125 let Some(rect) = self.clip_rect(rect) else {
1126 return;
1127 };
1128 let color = self.modulated(color);
1129 let border_color = self.modulated(border_color);
1130 let has_border = border_width > 0.0 && border_color.a > 0.0;
1131 if rect.is_empty() || (color.a <= 0.0 && !has_border) {
1132 return;
1133 }
1134 let rounded = self.active_rounded_clip();
1135 self.items.push(RenderItem::Quad(Quad {
1136 rect,
1137 color,
1138 radius,
1139 border_color,
1140 border_width: if has_border { border_width } else { 0.0 },
1141 }));
1142 self.item_clips.push(rounded);
1143 self.item_transforms.push(t);
1144 }
1145
1146 pub fn push_shadow(&mut self, shadow: Shadow) {
1147 let t = self.active_transform();
1148 let shadow = Shadow {
1149 color: self.modulated(shadow.color),
1150 ..shadow
1151 };
1152 if !shadow.is_visible() {
1153 return;
1154 }
1155 if !t.is_identity() {
1156 if let Some(local) = self.active_clip_for(t)
1157 && !shadow.bounds().intersects(local)
1158 {
1159 return;
1160 }
1161 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1162 let clip = self
1163 .active_clip()
1164 .map(|clip_rect| RoundedClip::new(clip_rect, radius));
1165 self.items.push(RenderItem::Shadow(shadow));
1166 self.item_clips.push(clip);
1167 self.item_transforms.push(t);
1168 return;
1169 }
1170 let Some(clip_rect) = self.active_clip() else {
1171 self.items.push(RenderItem::Shadow(shadow));
1172 self.item_clips.push(None);
1173 self.item_transforms.push(t);
1174 return;
1175 };
1176 if clip_rect.is_empty() {
1177 return;
1178 }
1179
1180 if !shadow.bounds().intersects(clip_rect) {
1181 return;
1182 }
1183
1184 let radius = self.active_rounded_clip().map(|c| c.radius).unwrap_or(0.0);
1185 self.items.push(RenderItem::Shadow(shadow));
1186 self.item_clips
1187 .push(Some(RoundedClip::new(clip_rect, radius)));
1188 self.item_transforms.push(self.active_transform());
1189 }
1190
1191 pub fn push_text(&mut self, mut draw: TextDraw) {
1192 let t = self.active_transform();
1193 draw.color = self.modulated(draw.color);
1194 if draw.color.a <= 0.0 {
1195 return;
1196 }
1197 if !t.is_identity() {
1198 if let Some(local) = self.active_clip_for(t) {
1199 let keep = draw.glyphs.iter().any(|g| g.rect.intersects(local));
1200 if !keep {
1201 return;
1202 }
1203 }
1204 draw.glyphs.retain(|g| !g.rect.is_empty());
1205 if !draw.glyphs.is_empty() {
1206 let rounded = self.active_rounded_clip();
1207 self.items.push(RenderItem::Text(draw));
1208 self.item_clips.push(rounded);
1209 self.item_transforms.push(t);
1210 }
1211 return;
1212 }
1213 draw.glyphs.retain_mut(|g| {
1214 let Some(cr) = self.clip_rect(g.rect) else {
1215 return false;
1216 };
1217 g.uv = clip_uv(g.rect, g.uv, cr);
1218 g.rect = cr;
1219 true
1220 });
1221 if !draw.glyphs.is_empty() {
1222 let rounded = self.active_rounded_clip();
1223 self.items.push(RenderItem::Text(draw));
1224 self.item_clips.push(rounded);
1225 self.item_transforms.push(t);
1226 }
1227 }
1228
1229 pub fn push_image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) {
1230 let t = self.active_transform();
1231 if !t.is_identity() {
1232 if let Some(local) = self.active_clip_for(t)
1233 && !rect.intersects(local)
1234 {
1235 return;
1236 }
1237 if rect.is_empty() {
1238 return;
1239 }
1240 let rounded = self.active_rounded_clip();
1241 self.items
1242 .push(RenderItem::Image(ImageDraw { rect, uv, source }));
1243 self.item_clips.push(rounded);
1244 self.item_transforms.push(t);
1245 return;
1246 }
1247 let Some(cr) = self.clip_rect(rect) else {
1248 return;
1249 };
1250 if cr.is_empty() {
1251 return;
1252 }
1253 let uv = clip_uv(rect, uv, cr);
1254 let rounded = self.active_rounded_clip();
1255 self.items.push(RenderItem::Image(ImageDraw {
1256 rect: cr,
1257 uv,
1258 source,
1259 }));
1260 self.item_clips.push(rounded);
1261 self.item_transforms.push(self.active_transform());
1262 }
1263
1264 pub fn push_waveform(&mut self, bars: &[Rect], color: Color) {
1265 let t = self.active_transform();
1266 let color = self.modulated(color);
1267 if bars.is_empty() || color.a <= 0.0 {
1268 return;
1269 }
1270 if !t.is_identity() {
1271 if let Some(local) = self.active_clip_for(t) {
1272 let keep = bars.iter().any(|r| !r.is_empty() && r.intersects(local));
1273 if !keep {
1274 return;
1275 }
1276 }
1277 let clipped: Vec<Rect> = bars
1278 .iter()
1279 .copied()
1280 .filter(|r| !r.is_empty())
1281 .take(MAX_WAVEFORM_BARS)
1282 .collect();
1283 if clipped.is_empty() {
1284 return;
1285 }
1286 let rounded = self.active_rounded_clip();
1287 self.items.push(RenderItem::Waveform(WaveformDraw {
1288 bars: clipped,
1289 color,
1290 }));
1291 self.item_clips.push(rounded);
1292 self.item_transforms.push(t);
1293 return;
1294 }
1295 let clipped: Vec<Rect> = match self.active_clip() {
1296 None => bars
1297 .iter()
1298 .copied()
1299 .filter(|r| !r.is_empty())
1300 .take(MAX_WAVEFORM_BARS)
1301 .collect(),
1302 Some(clip) if clip.is_empty() => return,
1303 Some(clip) => bars
1304 .iter()
1305 .filter_map(|r| {
1306 let c = r.intersect(clip);
1307 if c.is_empty() { None } else { Some(c) }
1308 })
1309 .take(MAX_WAVEFORM_BARS)
1310 .collect(),
1311 };
1312 if clipped.is_empty() {
1313 return;
1314 }
1315 let rounded = self.active_rounded_clip();
1316 self.items.push(RenderItem::Waveform(WaveformDraw {
1317 bars: clipped,
1318 color,
1319 }));
1320 self.item_clips.push(rounded);
1321 self.item_transforms.push(self.active_transform());
1322 }
1323
1324 pub fn items(&self) -> &[RenderItem] {
1325 &self.items
1326 }
1327
1328 pub fn push_gradient(
1329 &mut self,
1330 rect: Rect,
1331 radius: f32,
1332 kind: crate::brush::GradientKind,
1333 stops: crate::brush::GradientStops,
1334 ) {
1335 use crate::brush::GradientStop;
1336 if rect.is_empty() || stops.is_empty() {
1337 return;
1338 }
1339 let t = self.active_transform();
1340
1341 let opacity = self.active_opacity();
1342 let stops = if opacity >= 1.0 {
1343 stops
1344 } else {
1345 let modulated: Vec<GradientStop> = stops
1346 .stops()
1347 .iter()
1348 .map(|s| {
1349 GradientStop::new(
1350 s.at,
1351 Color {
1352 a: s.color.a * opacity,
1353 ..s.color
1354 },
1355 )
1356 })
1357 .collect();
1358 crate::brush::GradientStops::new(modulated)
1359 };
1360
1361 if !t.is_identity() {
1362 if let Some(local) = self.active_clip_for(t)
1363 && !rect.intersects(local)
1364 {
1365 return;
1366 }
1367 } else {
1368 let Some(clipped) = self.clip_rect(rect) else {
1369 return;
1370 };
1371 if clipped.is_empty() {
1372 return;
1373 }
1374
1375 return self.push_gradient_inner(clipped, radius, kind, stops, t);
1376 }
1377 self.push_gradient_inner(rect, radius, kind, stops, t)
1378 }
1379
1380 fn push_gradient_inner(
1381 &mut self,
1382 rect: Rect,
1383 radius: f32,
1384 kind: crate::brush::GradientKind,
1385 stops: crate::brush::GradientStops,
1386 t: Transform,
1387 ) {
1388 if rect.is_empty() || stops.is_empty() {
1389 return;
1390 }
1391
1392 if stops.stops().iter().all(|s| s.color.a <= 0.0) {
1393 return;
1394 }
1395 let radius = if radius.is_finite() {
1396 radius
1397 .clamp(0.0, rect.size.width.min(rect.size.height) * 0.5)
1398 .max(0.0)
1399 } else {
1400 0.0
1401 };
1402 let rounded = self.active_rounded_clip();
1403 self.items.push(RenderItem::Gradient(GradientDraw {
1404 rect,
1405 radius,
1406 kind,
1407 stops,
1408 }));
1409 self.item_clips.push(rounded);
1410 self.item_transforms.push(t);
1411 }
1412
1413 pub fn push_backdrop(
1414 &mut self,
1415 rect: Rect,
1416 radius: f32,
1417 blur: f32,
1418 dim: Color,
1419 noise: f32,
1420 mask: SpanMask,
1421 ) {
1422 let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
1423 let noise = if noise.is_finite() {
1424 noise.clamp(0.0, 1.0)
1425 } else {
1426 0.0
1427 };
1428 let opacity = self.active_opacity();
1429 let dim = Color {
1430 a: dim.a * opacity,
1431 ..dim
1432 };
1433 if !(blur > 0.0 || dim.a > 0.0 || noise > 0.0) {
1434 return;
1435 }
1436 let Some(rect) = self.clip_rect(rect) else {
1437 return;
1438 };
1439 if rect.is_empty() {
1440 return;
1441 }
1442 let radius = if radius.is_finite() {
1443 radius.max(0.0)
1444 } else {
1445 0.0
1446 };
1447 let draw = BackdropDraw {
1448 rect,
1449 radius,
1450 blur,
1451 dim,
1452 noise: noise * opacity,
1453 mask,
1454 };
1455 if !draw.is_visible() {
1456 return;
1457 }
1458 self.backdrops.push(BackdropSpan {
1459 at: self.items.len(),
1460 draw,
1461 clip: self.active_rounded_clip(),
1462 });
1463 }
1464
1465 pub fn backdrops(&self) -> &[BackdropSpan] {
1466 &self.backdrops
1467 }
1468
1469 pub fn has_backdrops(&self) -> bool {
1470 !self.backdrops.is_empty()
1471 }
1472
1473 pub fn push_filter_begin(
1474 &mut self,
1475 rect: Rect,
1476 radius: f32,
1477 blur: f32,
1478 mask: SpanMask,
1479 ) -> Option<usize> {
1480 let blur = if blur.is_finite() { blur.max(0.0) } else { 0.0 };
1481 if !(blur > 0.0) {
1482 return None;
1483 }
1484 let rect = self.clip_rect(rect)?;
1485 if rect.is_empty() {
1486 return None;
1487 }
1488 let radius = if radius.is_finite() {
1489 radius.max(0.0)
1490 } else {
1491 0.0
1492 };
1493 let draw = FilterDraw {
1494 rect,
1495 radius,
1496 blur,
1497 mask,
1498 };
1499 if !draw.is_visible() {
1500 return None;
1501 }
1502 let start = self.items.len();
1503 self.filters.push(FilterSpan {
1504 start,
1505 end: start,
1506 draw,
1507 clip: self.active_rounded_clip(),
1508 });
1509 Some(self.filters.len() - 1)
1510 }
1511
1512 pub fn push_filter_end(&mut self, handle: usize) {
1513 let end = self.items.len();
1514 let Some(span) = self.filters.get_mut(handle) else {
1515 return;
1516 };
1517 span.end = end;
1518 if span.end <= span.start {
1519 self.filters.remove(handle);
1520 }
1521 }
1522
1523 pub fn filter_spans(&self) -> &[FilterSpan] {
1524 &self.filters
1525 }
1526
1527 pub fn has_filters(&self) -> bool {
1528 !self.filters.is_empty()
1529 }
1530
1531 pub fn item_clips(&self) -> &[Option<RoundedClip>] {
1532 &self.item_clips
1533 }
1534
1535 pub fn quads(&self) -> impl Iterator<Item = &Quad> {
1536 self.items.iter().filter_map(|i| match i {
1537 RenderItem::Quad(q) => Some(q),
1538 RenderItem::Text(_)
1539 | RenderItem::Image(_)
1540 | RenderItem::Shadow(_)
1541 | RenderItem::Waveform(_)
1542 | RenderItem::Gradient(_) => None,
1543 })
1544 }
1545
1546 pub fn images(&self) -> impl Iterator<Item = &ImageDraw> {
1547 self.items.iter().filter_map(|i| match i {
1548 RenderItem::Image(d) => Some(d),
1549 RenderItem::Quad(_)
1550 | RenderItem::Text(_)
1551 | RenderItem::Shadow(_)
1552 | RenderItem::Waveform(_)
1553 | RenderItem::Gradient(_) => None,
1554 })
1555 }
1556
1557 pub fn shadows(&self) -> impl Iterator<Item = &Shadow> {
1558 self.items.iter().filter_map(|i| match i {
1559 RenderItem::Shadow(s) => Some(s),
1560 RenderItem::Quad(_)
1561 | RenderItem::Text(_)
1562 | RenderItem::Image(_)
1563 | RenderItem::Waveform(_)
1564 | RenderItem::Gradient(_) => None,
1565 })
1566 }
1567
1568 pub fn waveforms(&self) -> impl Iterator<Item = &WaveformDraw> {
1569 self.items.iter().filter_map(|i| match i {
1570 RenderItem::Waveform(w) => Some(w),
1571 RenderItem::Quad(_)
1572 | RenderItem::Text(_)
1573 | RenderItem::Image(_)
1574 | RenderItem::Shadow(_)
1575 | RenderItem::Gradient(_) => None,
1576 })
1577 }
1578
1579 pub fn gradients(&self) -> impl Iterator<Item = &GradientDraw> {
1580 self.items.iter().filter_map(|i| match i {
1581 RenderItem::Gradient(g) => Some(g),
1582 RenderItem::Quad(_)
1583 | RenderItem::Text(_)
1584 | RenderItem::Image(_)
1585 | RenderItem::Shadow(_)
1586 | RenderItem::Waveform(_) => None,
1587 })
1588 }
1589
1590 pub fn gradient_count(&self) -> usize {
1591 self.items
1592 .iter()
1593 .filter(|i| matches!(i, RenderItem::Gradient(_)))
1594 .count()
1595 }
1596
1597 pub fn waveform_bar_count(&self) -> usize {
1598 self.items
1599 .iter()
1600 .map(|i| match i {
1601 RenderItem::Waveform(w) => w.bars.len(),
1602 _ => 0,
1603 })
1604 .sum()
1605 }
1606
1607 pub fn len(&self) -> usize {
1608 self.items.len()
1609 }
1610
1611 pub fn is_empty(&self) -> bool {
1612 self.items.is_empty()
1613 }
1614}
1615
1616#[cfg(test)]
1617mod tests {
1618 use super::*;
1619
1620 #[test]
1621 fn color_construction() {
1622 assert_eq!(Color::rgb8(255, 0, 0), Color::RED);
1623 assert_eq!(Color::rgb(0.0, 1.0, 0.0).a, 1.0);
1624 assert_eq!(Color::TRANSPARENT.a, 0.0);
1625 }
1626
1627 #[test]
1628 fn hsl_roundtrips_through_rgb() {
1629 for (r, g, b) in [
1630 (255, 0, 0),
1631 (0, 255, 0),
1632 (0, 0, 255),
1633 (112, 143, 234),
1634 (185, 185, 185),
1635 ] {
1636 let c = Color::rgb8(r, g, b);
1637 let (h, s, l) = c.to_hsl();
1638 let back = Color::from_hsl(h, s, l).to_rgb8();
1639 assert!(
1640 (r as i16 - back.0 as i16).abs() <= 1
1641 && (g as i16 - back.1 as i16).abs() <= 1
1642 && (b as i16 - back.2 as i16).abs() <= 1,
1643 "#{r:02X}{g:02X}{b:02X} -> ({h:.1},{s:.3},{l:.3}) -> #{:02X}{:02X}{:02X}",
1644 back.0,
1645 back.1,
1646 back.2,
1647 );
1648 }
1649 assert_eq!(Color::from_hsl(0.0, 0.0, 0.5).to_rgb8(), (128, 128, 128));
1650
1651 assert_eq!(
1652 Color::from_hsl(720.0, 1.0, 0.5),
1653 Color::from_hsl(0.0, 1.0, 0.5)
1654 );
1655 assert_eq!(Color::from_hsl(0.0, 0.5, 0.5).to_hex6(), "BF4040");
1656 }
1657
1658 #[test]
1659 fn hex6_string_is_uppercase_without_hash() {
1660 assert_eq!(Color::rgb8(0x70, 0x8F, 0xEA).to_hex6(), "708FEA");
1661 }
1662
1663 #[test]
1664 fn hex_number_forms() {
1665 assert_eq!(Color::hex(0x0FA4FA), Color::rgb8(15, 164, 250));
1666 assert_eq!(Color::hex(0xFF0000), Color::RED);
1667 assert_eq!(Color::hex(0x000000), Color::BLACK);
1668
1669 assert_eq!(Color::hex(0x1_FF0000), Color::RED);
1670 let half = Color::hexa(0x0FA4FA80);
1671 assert_eq!(half.r, Color::hex(0x0FA4FA).r);
1672 assert_eq!(half.a, 128.0 / 255.0);
1673 assert_eq!(Color::hexa(0xFFFFFFFF).a, 1.0);
1674 }
1675
1676 #[test]
1677 fn parse_hex_forms() {
1678 assert_eq!(Color::parse_hex("#0FA4FA"), Some(Color::rgb8(15, 164, 250)));
1679 assert_eq!(Color::parse_hex("0fa4fa"), Some(Color::rgb8(15, 164, 250)));
1680 assert_eq!(
1681 Color::parse_hex("0x0FA4FA"),
1682 Some(Color::rgb8(15, 164, 250))
1683 );
1684 assert_eq!(Color::parse_hex("#fff"), Some(Color::WHITE));
1685 assert_eq!(Color::parse_hex("#0000"), Some(Color::TRANSPARENT));
1686 assert_eq!(Color::parse_hex("#0FA4FA80"), Some(Color::hexa(0x0FA4FA80)));
1687
1688 assert_eq!(Color::parse_hex("#0af"), Color::parse_hex("#00aaff"));
1689 }
1690
1691 #[test]
1692 fn parse_hex_rejects_garbage() {
1693 assert_eq!(Color::parse_hex(""), None);
1694 assert_eq!(Color::parse_hex("#"), None);
1695 assert_eq!(Color::parse_hex("bogus"), None);
1696 assert_eq!(Color::parse_hex("#12"), None);
1697 assert_eq!(Color::parse_hex("#12345"), None);
1698 assert_eq!(Color::parse_hex("#123456789"), None);
1699 assert_eq!(Color::parse_hex("#gggggg"), None);
1700 assert_eq!(Color::parse_hex(" #fff"), None, "no trimming");
1701 }
1702
1703 #[test]
1704 fn clip_balance_helpers() {
1705 let mut list = RenderList::new();
1706 assert_eq!(list.clip_depth(), 0);
1707 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
1708 list.push_clip(Rect::from_xywh(0.0, 0.0, 5.0, 5.0));
1709 assert_eq!(list.clip_depth(), 2);
1710 list.truncate_clips(1);
1711 assert_eq!(list.clip_depth(), 1);
1712 list.truncate_clips(0);
1713 assert_eq!(list.clip_depth(), 0);
1714 }
1715
1716 #[test]
1717 fn opacity_stack_modulates_quad_alpha() {
1718 let mut list = RenderList::new();
1719 assert_eq!(list.active_opacity(), 1.0);
1720 list.push_opacity(0.5);
1721 list.push_opacity(0.5);
1722 assert_eq!(list.active_opacity(), 0.25, "nestings multiply");
1723 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1724 let q = list.quads().next().expect("quad po push");
1725 assert!(
1726 (q.color.a - 0.25).abs() < 1e-6,
1727 "alfa 1 * 0.5 * 0.5: {}",
1728 q.color.a
1729 );
1730 assert_eq!(q.color.r, 1.0, "rgb untouched");
1731 list.pop_opacity();
1732 list.push_quad(Rect::from_xywh(20.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1733 let qs: Vec<_> = list.quads().collect();
1734 assert!((qs[1].color.a - 0.5).abs() < 1e-6);
1735
1736 list.push_opacity(0.0);
1737 list.push_quad(Rect::from_xywh(40.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1738 assert_eq!(list.quads().count(), 2, "quad przy opacity 0 nie wchodzi");
1739 list.truncate_opacity(0);
1740 assert_eq!(list.opacity_depth(), 0);
1741 assert_eq!(list.active_opacity(), 1.0);
1742
1743 list.push_opacity(f32::NAN);
1744 list.push_quad(Rect::from_xywh(60.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1745 let qs: Vec<_> = list.quads().collect();
1746 assert_eq!(qs[2].color.a, 1.0);
1747 list.clear();
1748 assert_eq!(list.opacity_depth(), 0, "clear also resets opacity");
1749 }
1750
1751 #[test]
1752 fn list_push_and_clear() {
1753 let mut list = RenderList::new();
1754 assert!(list.is_empty());
1755 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1756 assert_eq!(list.len(), 1);
1757 list.push_quad(Rect::ZERO, Color::RED, 0.0);
1758 list.push_quad(
1759 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
1760 Color::TRANSPARENT,
1761 0.0,
1762 );
1763 assert_eq!(list.len(), 1, "empty-value filters");
1764
1765 list.push_text(TextDraw::default());
1766 assert_eq!(list.len(), 1, "empty TextDraw skipped");
1767
1768 list.push_text(TextDraw {
1769 color: Color::WHITE,
1770 glyphs: vec![GlyphQuad {
1771 rect: Rect::ZERO,
1772 uv: [0.0; 4],
1773 color_glyph: false,
1774 }],
1775 });
1776 assert_eq!(list.len(), 2);
1777 assert_eq!(list.quads().count(), 1);
1778
1779 list.clear();
1780 assert!(list.is_empty());
1781 }
1782
1783 #[test]
1784 fn bordered_quad_filters_and_flags() {
1785 let r = Rect::from_xywh(0.0, 0.0, 100.0, 60.0);
1786 let mut list = RenderList::new();
1787
1788 list.push_quad(r, Color::BLUE, 8.0);
1789 let q = list.quads().next().unwrap();
1790 assert!(!q.has_border());
1791 assert_eq!(q.border_width, 0.0);
1792
1793 let bg = Color::rgba(154.0 / 255.0, 214.0 / 255.0, 248.0 / 255.0, 24.0 / 255.0);
1794 list.push_bordered_quad(r, bg, 8.0, 1.0, Color::RED);
1795 let qs: Vec<_> = list.quads().collect();
1796 assert_eq!(qs.len(), 2, "background + border is one quad, not two");
1797 assert_eq!(qs[1].color, bg);
1798 assert_eq!(qs[1].border_color, Color::RED);
1799 assert_eq!(qs[1].border_width, 1.0);
1800 assert!(qs[1].has_border());
1801
1802 let mut list = RenderList::new();
1803 list.push_bordered_quad(r, Color::TRANSPARENT, 8.0, 2.0, Color::GREEN);
1804 assert_eq!(list.len(), 1);
1805
1806 let mut list = RenderList::new();
1807 list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 0.0, Color::RED);
1808 list.push_bordered_quad(r, Color::TRANSPARENT, 0.0, 2.0, Color::TRANSPARENT);
1809 list.push_bordered_quad(Rect::ZERO, Color::RED, 0.0, 1.0, Color::RED);
1810 assert!(list.is_empty());
1811 }
1812
1813 #[test]
1814 fn clip_intersects_and_filters() {
1815 let mut list = RenderList::new();
1816 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1817 list.push_quad(Rect::from_xywh(50.0, 50.0, 100.0, 100.0), Color::RED, 0.0);
1818 list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
1819 assert_eq!(list.len(), 1);
1820 let q = list.quads().next().unwrap();
1821 assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
1822
1823 list.pop_clip();
1824 list.push_quad(Rect::from_xywh(200.0, 200.0, 10.0, 10.0), Color::RED, 0.0);
1825 assert_eq!(list.len(), 2, "unclipped after pop_clip");
1826 }
1827
1828 #[test]
1829 fn nested_clip_intersects() {
1830 let mut list = RenderList::new();
1831 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1832 list.push_clip(Rect::from_xywh(50.0, 50.0, 100.0, 100.0));
1833 list.push_quad(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), Color::RED, 0.0);
1834 assert_eq!(list.len(), 1);
1835 let q = list.quads().next().unwrap();
1836 assert_eq!(q.rect, Rect::from_xywh(50.0, 50.0, 50.0, 50.0));
1837 }
1838
1839 #[test]
1840 fn rounded_clip_stored_per_item_for_shader() {
1841 let mut list = RenderList::new();
1842 let card = Rect::from_xywh(0.0, 0.0, 240.0, 280.0);
1843 list.push_rounded_clip(card, 8.0);
1844 list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
1845 assert_eq!(list.len(), 1);
1846 let clip = list.item_rounded_clip(0).expect("rounded clip stored");
1847 assert_eq!(clip.rect, card);
1848 assert_eq!(clip.radius, 8.0);
1849
1850 let mut list = RenderList::new();
1851 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1852 list.push_quad(Rect::from_xywh(10.0, 10.0, 20.0, 20.0), Color::RED, 0.0);
1853 assert_eq!(list.item_rounded_clip(0), None);
1854 }
1855
1856 #[test]
1857 fn rounded_clip_survives_rectangular_child_clip() {
1858 let mut list = RenderList::new();
1859 list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0), 8.0);
1860 list.push_clip(Rect::from_xywh(0.0, 0.0, 240.0, 280.0));
1861 list.push_quad(Rect::from_xywh(0.0, 0.0, 240.0, 20.0), Color::GREEN, 0.0);
1862 let clip = list.item_rounded_clip(0).expect("outer rounding kept");
1863 assert_eq!(clip.radius, 8.0);
1864 list.pop_clip();
1865 list.pop_clip();
1866 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
1867 assert_eq!(list.item_rounded_clip(1), None, "no clip after pops");
1868 }
1869
1870 fn glyph(x: f32, y: f32, w: f32, h: f32) -> GlyphQuad {
1871 GlyphQuad {
1872 rect: Rect::from_xywh(x, y, w, h),
1873 uv: [0.0, 0.0, 1.0, 1.0],
1874 color_glyph: false,
1875 }
1876 }
1877
1878 fn pushed_glyphs(clip: Rect, g: GlyphQuad) -> Vec<GlyphQuad> {
1879 let mut list = RenderList::new();
1880 list.push_clip(clip);
1881 list.push_text(TextDraw {
1882 color: Color::WHITE,
1883 glyphs: vec![g],
1884 });
1885 match list.items() {
1886 [RenderItem::Text(t)] => t.glyphs.clone(),
1887 _ => panic!("expected a single TextDraw"),
1888 }
1889 }
1890
1891 #[test]
1892 fn clipped_glyph_keeps_proportional_uv() {
1893 let out = pushed_glyphs(
1894 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
1895 glyph(90.0, 10.0, 20.0, 10.0),
1896 );
1897 assert_eq!(out.len(), 1);
1898 assert_eq!(out[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
1899 assert_eq!(out[0].uv, [0.0, 0.0, 0.5, 1.0]);
1900 }
1901
1902 #[test]
1903 fn fully_visible_glyph_keeps_uv() {
1904 let out = pushed_glyphs(
1905 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
1906 glyph(10.0, 10.0, 20.0, 10.0),
1907 );
1908 assert_eq!(out.len(), 1);
1909 assert_eq!(out[0].rect, Rect::from_xywh(10.0, 10.0, 20.0, 10.0));
1910 assert_eq!(out[0].uv, [0.0, 0.0, 1.0, 1.0]);
1911 }
1912
1913 #[test]
1914 fn clipped_glyph_uv_scales_with_atlas_subrect() {
1915 let mut g = glyph(0.0, 0.0, 20.0, 20.0);
1916 g.uv = [0.25, 0.5, 0.75, 1.0];
1917 let out = pushed_glyphs(Rect::from_xywh(10.0, 0.0, 100.0, 100.0), g);
1918 assert_eq!(out.len(), 1);
1919 assert_eq!(out[0].rect, Rect::from_xywh(10.0, 0.0, 10.0, 20.0));
1920 assert_eq!(out[0].uv, [0.5, 0.5, 0.75, 1.0]);
1921 }
1922
1923 fn file_image() -> ImageSource {
1924 ImageSource::File("foto.png".into())
1925 }
1926
1927 #[test]
1928 fn image_push_clip_and_uv() {
1929 let mut list = RenderList::new();
1930 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
1931 list.push_image(
1932 Rect::from_xywh(90.0, 10.0, 20.0, 10.0),
1933 [0.0, 0.0, 1.0, 1.0],
1934 file_image(),
1935 );
1936 list.push_image(
1937 Rect::from_xywh(200.0, 200.0, 10.0, 10.0),
1938 [0.0, 0.0, 1.0, 1.0],
1939 file_image(),
1940 );
1941 let images: Vec<_> = list.images().collect();
1942 assert_eq!(images.len(), 1, "outside the clip is dropped");
1943 assert_eq!(images[0].rect, Rect::from_xywh(90.0, 10.0, 10.0, 10.0));
1944 assert_eq!(
1945 images[0].uv,
1946 [0.0, 0.0, 0.5, 1.0],
1947 "proportional UV, as for glyphs"
1948 );
1949 }
1950
1951 #[test]
1952 fn image_cache_keys() {
1953 assert_eq!(file_image().cache_key(), file_image().cache_key());
1954 assert_ne!(
1955 file_image().cache_key(),
1956 ImageSource::File("inne.png".into()).cache_key()
1957 );
1958 let rgba = |id| ImageSource::Rgba {
1959 id,
1960 width: 2,
1961 height: 2,
1962 data: Arc::new(vec![0u8; 16]),
1963 };
1964 assert_eq!(rgba(7).cache_key(), 7);
1965 assert_ne!(rgba(7).cache_key(), rgba(8).cache_key());
1966 }
1967
1968 fn shadow_spec() -> Shadow {
1969 Shadow {
1970 rect: Rect::from_xywh(10.0, 10.0, 100.0, 60.0),
1971 color: Color::BLACK,
1972 radius: 6.0,
1973 blur: 8.0,
1974 spread: 0.0,
1975 offset: Vec2::new(0.0, 2.0),
1976 inset: false,
1977 }
1978 }
1979
1980 #[test]
1981 fn shadow_push_filters_invisible_shadows() {
1982 let mut list = RenderList::new();
1983 list.push_shadow(Shadow {
1984 blur: 0.0,
1985 ..shadow_spec()
1986 });
1987 list.push_shadow(Shadow {
1988 blur: f32::NAN,
1989 ..shadow_spec()
1990 });
1991 list.push_shadow(Shadow {
1992 color: Color::TRANSPARENT,
1993 ..shadow_spec()
1994 });
1995 list.push_shadow(Shadow {
1996 rect: Rect::ZERO,
1997 ..shadow_spec()
1998 });
1999 assert!(list.is_empty(), "hidden shadows create no items");
2000 }
2001
2002 #[test]
2003 fn shadow_keeps_shape_rect_and_rides_the_clip() {
2004 let mut list = RenderList::new();
2005 list.push_clip(Rect::from_xywh(0.0, 0.0, 50.0, 50.0));
2006 list.push_rounded_clip(Rect::from_xywh(0.0, 0.0, 200.0, 200.0), 12.0);
2007 list.push_shadow(shadow_spec());
2008 assert_eq!(list.len(), 1);
2009 assert_eq!(list.shadows().next().unwrap().rect, shadow_spec().rect);
2010 let clip = list.item_rounded_clip(0).expect("ancestor clip");
2011 assert_eq!(
2012 clip.rect,
2013 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2014 "przeciecie stosu clipow"
2015 );
2016 assert_eq!(clip.radius, 12.0, "rounding from inner clip");
2017 }
2018
2019 #[test]
2020 fn shadow_dropped_when_fully_clipped_out() {
2021 let mut list = RenderList::new();
2022 list.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2023 list.push_shadow(shadow_spec());
2024 assert!(list.is_empty(), "fully outside clip");
2025 }
2026
2027 #[test]
2028 fn shadow_bounds_grow_with_blur_spread_and_offset() {
2029 let s = Shadow {
2030 blur: 10.0,
2031 spread: 4.0,
2032 offset: Vec2::new(2.0, -3.0),
2033 ..shadow_spec()
2034 };
2035 let b = s.bounds();
2036 assert!((b.left() - 3.0).abs() < 1e-4, "{b:?}");
2037 assert!((b.top() - (-2.0)).abs() < 1e-4, "{b:?}");
2038 assert!((b.size.width - 118.0).abs() < 1e-4, "{b:?}");
2039 let inset = Shadow { inset: true, ..s };
2040 assert_eq!(inset.bounds(), inset.rect, "inset stays inside the shape");
2041 }
2042
2043 #[test]
2044 fn waveform_push_is_one_item_not_n() {
2045 let mut list = RenderList::new();
2046 let bars: Vec<Rect> = (0..100)
2047 .map(|i| Rect::from_xywh(i as f32 * 2.0, 0.0, 1.5, 20.0))
2048 .collect();
2049 list.push_waveform(&bars, Color::WHITE);
2050 assert_eq!(list.len(), 1, "whole waveform is 1 item");
2051 assert_eq!(list.waveform_bar_count(), 100);
2052 assert_eq!(list.waveforms().next().unwrap().bars.len(), 100);
2053 }
2054
2055 #[test]
2056 fn waveform_push_clips_and_filters() {
2057 let mut list = RenderList::new();
2058 list.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
2059 let bars = vec![
2060 Rect::from_xywh(0.0, 0.0, 5.0, 5.0),
2061 Rect::from_xywh(200.0, 200.0, 5.0, 5.0),
2062 Rect::ZERO,
2063 ];
2064 list.push_waveform(&bars, Color::WHITE);
2065 assert_eq!(list.len(), 1);
2066 assert_eq!(list.waveform_bar_count(), 1, "outside clip dropped");
2067
2068 let mut list = RenderList::new();
2069 list.push_waveform(&[], Color::WHITE);
2070 list.push_waveform(&[Rect::from_xywh(0.0, 0.0, 5.0, 5.0)], Color::TRANSPARENT);
2071 assert!(list.is_empty());
2072 }
2073
2074 #[test]
2075 fn dynamic_image_cache_key_is_id_and_versioned() {
2076 let a = ImageSource::Dynamic {
2077 id: 7,
2078 width: 4,
2079 height: 1,
2080 data: Arc::new(vec![0u8; 16]),
2081 version: 1,
2082 };
2083 let b = ImageSource::Dynamic {
2084 id: 7,
2085 width: 4,
2086 height: 1,
2087 data: Arc::new(vec![255u8; 16]),
2088 version: 2,
2089 };
2090 assert_eq!(a.cache_key(), 7);
2091 assert_eq!(a.cache_key(), b.cache_key(), "same texture slot");
2092 assert_ne!(a, b, "other version means other data");
2093 assert_eq!(a.dynamic_version(), Some(1));
2094 assert_eq!(
2095 ImageSource::Rgba {
2096 id: 7,
2097 width: 1,
2098 height: 1,
2099 data: Arc::new(vec![0u8; 4]),
2100 }
2101 .dynamic_version(),
2102 None
2103 );
2104 }
2105
2106 #[test]
2107 fn backdrop_side_channel_keeps_item_indices() {
2108 let mut list = RenderList::new();
2109 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2110 list.push_quad(Rect::from_xywh(10.0, 0.0, 10.0, 10.0), Color::GREEN, 0.0);
2111 assert!(!list.has_backdrops());
2112 list.push_backdrop(
2113 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2114 8.0,
2115 16.0,
2116 Color::TRANSPARENT,
2117 0.0,
2118 SpanMask::None,
2119 );
2120 assert!(list.has_backdrops());
2121 assert_eq!(list.len(), 2, "marker does not add items");
2122 let span = list.backdrops()[0];
2123 assert_eq!(span.at, 2, "rozmywa dwa quady spod spodu");
2124 assert_eq!(span.draw.rect, Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2125 assert_eq!(span.draw.blur, 16.0);
2126 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2127 assert_eq!(list.backdrops()[0].at, 2, "at frozen in push");
2128 }
2129
2130 #[test]
2131 fn filter_span_covers_subtree_range() {
2132 let mut list = RenderList::new();
2133 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2134 let h = list
2135 .push_filter_begin(
2136 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2137 8.0,
2138 16.0,
2139 SpanMask::None,
2140 )
2141 .expect("widoczny filtr otwiera zakres");
2142 assert_eq!(list.len(), 1, "begin does not add items");
2143 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2144 list.push_quad(Rect::from_xywh(5.0, 5.0, 5.0, 5.0), Color::GREEN, 0.0);
2145 list.push_filter_end(h);
2146 assert!(list.has_filters());
2147 let span = list.filter_spans()[0];
2148 assert_eq!((span.start, span.end), (1, 3), "tylko poddrzewo");
2149 assert_eq!(span.draw.blur, 16.0);
2150 assert_eq!(span.draw.radius, 8.0);
2151 list.clear();
2152 assert!(!list.has_filters(), "clear also drops filters");
2153 }
2154
2155 #[test]
2156 fn filter_begin_rejects_invisible_and_end_drops_empty() {
2157 let mut list = RenderList::new();
2158
2159 assert!(
2160 list.push_filter_begin(
2161 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2162 0.0,
2163 0.0,
2164 SpanMask::None
2165 )
2166 .is_none()
2167 );
2168 assert!(
2169 list.push_filter_begin(
2170 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2171 0.0,
2172 f32::NAN,
2173 SpanMask::None
2174 )
2175 .is_none()
2176 );
2177 assert!(
2178 list.push_filter_begin(
2179 Rect::from_xywh(0.0, 0.0, 0.0, 0.0),
2180 0.0,
2181 8.0,
2182 SpanMask::None
2183 )
2184 .is_none()
2185 );
2186 assert!(!list.has_filters());
2187
2188 let h = list
2189 .push_filter_begin(
2190 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2191 0.0,
2192 8.0,
2193 SpanMask::None,
2194 )
2195 .expect("otwiera");
2196 list.push_filter_end(h);
2197 assert!(!list.has_filters(), "pusty zakres bez contentu");
2198
2199 let mut clipped = RenderList::new();
2200 clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2201 let h = clipped
2202 .push_filter_begin(
2203 Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
2204 4.0,
2205 12.0,
2206 SpanMask::None,
2207 )
2208 .expect("otwiera");
2209 clipped.push_quad(Rect::from_xywh(60.0, 60.0, 10.0, 10.0), Color::RED, 0.0);
2210 clipped.push_filter_end(h);
2211 assert_eq!(
2212 clipped.filter_spans()[0].draw.rect,
2213 Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
2214 );
2215 }
2216
2217 #[test]
2218 fn filter_spans_nest_like_subtrees() {
2219 let mut list = RenderList::new();
2220 let outer = list
2221 .push_filter_begin(
2222 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2223 0.0,
2224 8.0,
2225 SpanMask::None,
2226 )
2227 .expect("outer");
2228 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2229 let inner = list
2230 .push_filter_begin(
2231 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2232 0.0,
2233 4.0,
2234 SpanMask::None,
2235 )
2236 .expect("inner");
2237 list.push_quad(Rect::from_xywh(0.0, 0.0, 5.0, 5.0), Color::BLUE, 0.0);
2238 list.push_filter_end(inner);
2239 list.push_filter_end(outer);
2240 assert_eq!(list.filter_spans().len(), 2);
2241 assert_eq!(
2242 (list.filter_spans()[0].start, list.filter_spans()[0].end),
2243 (0, 2)
2244 );
2245 assert_eq!(
2246 (list.filter_spans()[1].start, list.filter_spans()[1].end),
2247 (1, 2)
2248 );
2249 }
2250
2251 #[test]
2252 fn backdrop_filters_and_clips() {
2253 let mut list = RenderList::new();
2254
2255 list.push_backdrop(
2256 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2257 0.0,
2258 0.0,
2259 Color::TRANSPARENT,
2260 0.0,
2261 SpanMask::None,
2262 );
2263 assert!(!list.has_backdrops());
2264
2265 list.push_backdrop(
2266 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
2267 0.0,
2268 0.0,
2269 Color::BLACK.with_alpha(0.25),
2270 0.0,
2271 SpanMask::None,
2272 );
2273 assert_eq!(list.backdrops().len(), 1);
2274
2275 let mut clipped = RenderList::new();
2276 clipped.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2277 clipped.push_backdrop(
2278 Rect::from_xywh(50.0, 50.0, 100.0, 100.0),
2279 4.0,
2280 12.0,
2281 Color::TRANSPARENT,
2282 0.0,
2283 SpanMask::None,
2284 );
2285 assert_eq!(
2286 clipped.backdrops()[0].draw.rect,
2287 Rect::from_xywh(50.0, 50.0, 50.0, 50.0)
2288 );
2289
2290 let mut outside = RenderList::new();
2291 outside.push_clip(Rect::from_xywh(500.0, 500.0, 10.0, 10.0));
2292 outside.push_backdrop(
2293 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2294 0.0,
2295 12.0,
2296 Color::TRANSPARENT,
2297 0.0,
2298 SpanMask::None,
2299 );
2300 assert!(!outside.has_backdrops());
2301
2302 let mut faded = RenderList::new();
2303 faded.push_opacity(0.5);
2304 faded.push_backdrop(
2305 Rect::from_xywh(0.0, 0.0, 10.0, 10.0),
2306 0.0,
2307 0.0,
2308 Color::BLACK.with_alpha(0.4),
2309 0.5,
2310 SpanMask::None,
2311 );
2312 let d = faded.backdrops()[0].draw;
2313 assert!((d.dim.a - 0.2).abs() < 1e-6, "dim.a 0.4 * 0.5: {}", d.dim.a);
2314 assert!(
2315 (d.noise - 0.25).abs() < 1e-6,
2316 "noise 0.5 * 0.5: {}",
2317 d.noise
2318 );
2319
2320 faded.clear();
2321 assert!(!faded.has_backdrops());
2322 }
2323
2324 #[test]
2325 fn span_mask_encodes_for_gpu() {
2326 assert_eq!(SpanMask::None.mask_vec(), [0.0, 0.0, 0.0, 0.0]);
2327 assert_eq!(
2328 SpanMask::Linear {
2329 from: 1.0,
2330 to: 0.0,
2331 angle_rad: std::f32::consts::FRAC_PI_2,
2332 }
2333 .mask_vec(),
2334 [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
2335 );
2336 assert_eq!(SpanMask::Uniform(0.5).mask_vec(), [2.0, 0.5, 0.0, 0.0]);
2337
2338 assert_eq!(
2339 SpanMask::Linear {
2340 from: 2.0,
2341 to: -1.0,
2342 angle_rad: f32::NAN,
2343 }
2344 .mask_vec()[1..3],
2345 [1.0, 0.0]
2346 );
2347 assert!(SpanMask::None.is_none());
2348 assert!(!SpanMask::Uniform(0.0).is_none());
2349 }
2350
2351 #[test]
2352 fn filter_span_carries_mask() {
2353 let mut list = RenderList::new();
2354 let mask = SpanMask::Linear {
2355 from: 1.0,
2356 to: 0.0,
2357 angle_rad: std::f32::consts::FRAC_PI_2,
2358 };
2359 let h = list
2360 .push_filter_begin(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), 0.0, 8.0, mask)
2361 .expect("otwiera");
2362 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2363 list.push_filter_end(h);
2364 assert_eq!(list.filter_spans()[0].draw.mask, mask);
2365 assert_eq!(
2366 list.filter_spans()[0].draw.mask.mask_vec(),
2367 [1.0, 1.0, 0.0, std::f32::consts::FRAC_PI_2]
2368 );
2369 }
2370
2371 #[test]
2372 fn backdrop_marker_carries_mask() {
2373 let mut list = RenderList::new();
2374 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2375 list.push_backdrop(
2376 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
2377 0.0,
2378 8.0,
2379 Color::TRANSPARENT,
2380 0.0,
2381 SpanMask::Uniform(0.5),
2382 );
2383 assert_eq!(
2384 list.backdrops()[0].draw.mask,
2385 SpanMask::Uniform(0.5),
2386 "maska w markerze t�'a"
2387 );
2388 }
2389
2390 #[test]
2391 fn transform_stack_composes_and_snapshots() {
2392 use crate::geometry::Point;
2393 let mut list = RenderList::new();
2394 assert!(list.active_transform().is_identity());
2395
2396 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2397 assert!(list.item_transform(0).is_identity());
2398
2399 list.push_transform(Transform::rotate_around_degrees(90.0, Point::new(5.0, 5.0)));
2400 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2401 assert!(!list.item_transform(1).is_identity());
2402 assert_eq!(list.item_transforms().len(), list.items().len());
2403 list.pop_transform();
2404 assert!(list.active_transform().is_identity());
2405 list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
2406 assert!(list.item_transform(2).is_identity());
2407 }
2408
2409 #[test]
2410 fn transform_aabb_covers_rotation() {
2411 use crate::geometry::Point;
2412 let r = Rect::from_xywh(0.0, 0.0, 10.0, 20.0);
2413 let t = Transform::rotate_around_degrees(90.0, Point::new(5.0, 10.0));
2414 let aabb = t.apply_rect_aabb(r);
2415 assert!(
2416 (aabb.size.width - 20.0).abs() < 0.01,
2417 "w={}",
2418 aabb.size.width
2419 );
2420 assert!(
2421 (aabb.size.height - 10.0).abs() < 0.01,
2422 "h={}",
2423 aabb.size.height
2424 );
2425 assert!(Transform::IDENTITY.apply_rect_aabb(r) == r);
2426 }
2427
2428 #[test]
2429 fn rotated_text_survives_own_self_clip() {
2430 let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
2431 let mut list = RenderList::new();
2432 list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
2433 list.push_clip(tile);
2434 list.push_text(TextDraw {
2435 color: Color::WHITE,
2436 glyphs: vec![GlyphQuad {
2437 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2438 uv: [0.0, 0.0, 1.0, 1.0],
2439 color_glyph: false,
2440 }],
2441 });
2442 assert_eq!(list.len(), 1, "rotated glyph in custom clip remains");
2443 }
2444
2445 #[test]
2446 fn ancestor_clip_still_culls_rotated_content() {
2447 let tile = Rect::from_xywh(0.0, 0.0, 60.0, 40.0);
2448 let mut list = RenderList::new();
2449 list.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2450 list.push_transform(Transform::rotate_around_degrees(45.0, tile.center()));
2451 list.push_text(TextDraw {
2452 color: Color::WHITE,
2453 glyphs: vec![GlyphQuad {
2454 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2455 uv: [0.0, 0.0, 1.0, 1.0],
2456 color_glyph: false,
2457 }],
2458 });
2459
2460 assert_eq!(list.len(), 1, "clip przodka nie zjada widocznego");
2461
2462 let mut list2 = RenderList::new();
2463 list2.push_clip(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
2464 list2.push_transform(Transform::translate(500.0, 500.0));
2465 list2.push_text(TextDraw {
2466 color: Color::WHITE,
2467 glyphs: vec![GlyphQuad {
2468 rect: Rect::from_xywh(4.0, 4.0, 12.0, 10.0),
2469 uv: [0.0, 0.0, 1.0, 1.0],
2470 color_glyph: false,
2471 }],
2472 });
2473 assert_eq!(list2.len(), 0, "clip przodka obcina poza ekranem");
2474 }
2475}