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