Skip to main content

jag_draw/
upload.rs

1use anyhow::Result;
2use bytemuck::{Pod, Zeroable};
3
4use crate::allocator::{BufKey, OwnedBuffer, RenderAllocator};
5use crate::display_list::{Command, DisplayList, ExternalTextureId};
6use crate::scene::{
7    Brush, FillRule, Path, PathCmd, Rect, RoundedRect, Stroke, TextRun, Transform2D,
8};
9
10#[repr(C)]
11#[derive(Clone, Copy, Debug, Default, Pod, Zeroable)]
12pub struct Vertex {
13    pub pos: [f32; 2],
14    pub color: [f32; 4],
15    pub z_index: f32,
16}
17
18pub struct GpuScene {
19    pub vertex: OwnedBuffer,
20    pub index: OwnedBuffer,
21    pub vertices: u32,
22    pub indices: u32,
23}
24
25/// Extracted text draw from DisplayList
26#[derive(Clone, Debug)]
27pub struct ExtractedTextDraw {
28    pub run: TextRun,
29    pub z: i32,
30    pub transform: Transform2D,
31}
32
33/// Extracted image draw from DisplayList (placeholder for future)
34#[derive(Clone, Debug)]
35pub struct ExtractedImageDraw {
36    pub path: std::path::PathBuf,
37    pub origin: [f32; 2],
38    pub size: [f32; 2],
39    pub z: i32,
40    pub transform: Transform2D,
41    pub opacity: f32,
42}
43
44/// Extracted SVG draw from DisplayList (placeholder for future)
45#[derive(Clone, Debug)]
46pub struct ExtractedSvgDraw {
47    pub path: std::path::PathBuf,
48    pub origin: [f32; 2],
49    pub size: [f32; 2],
50    pub z: i32,
51    pub transform: Transform2D,
52    pub opacity: f32,
53}
54
55/// Extracted external texture draw from DisplayList.
56#[derive(Clone, Debug)]
57pub struct ExtractedExternalTextureDraw {
58    pub texture_id: ExternalTextureId,
59    pub origin: [f32; 2],
60    pub size: [f32; 2],
61    pub z: i32,
62    pub opacity: f32,
63    pub premultiplied: bool,
64}
65
66/// A contiguous range inside the transparent index buffer for a given z-index.
67#[derive(Clone, Copy, Debug)]
68pub struct TransparentBatch {
69    pub z: i32,
70    pub index_start: u32,
71    pub index_count: u32,
72}
73
74/// Complete unified scene data extracted from DisplayList
75pub struct UnifiedSceneData {
76    pub gpu_scene: GpuScene,
77    pub transparent_gpu_scene: GpuScene,
78    pub transparent_batches: Vec<TransparentBatch>,
79    pub text_draws: Vec<ExtractedTextDraw>,
80    pub image_draws: Vec<ExtractedImageDraw>,
81    pub svg_draws: Vec<ExtractedSvgDraw>,
82    pub external_texture_draws: Vec<ExtractedExternalTextureDraw>,
83}
84
85fn apply_transform(p: [f32; 2], t: Transform2D) -> [f32; 2] {
86    let [a, b, c, d, e, f] = t.m;
87    [a * p[0] + c * p[1] + e, b * p[0] + d * p[1] + f]
88}
89
90fn rect_to_verts(rect: Rect, color: [f32; 4], t: Transform2D, z: f32) -> ([Vertex; 4], [u16; 6]) {
91    let x0 = rect.x;
92    let y0 = rect.y;
93    let x1 = rect.x + rect.w;
94    let y1 = rect.y + rect.h;
95    let p0 = apply_transform([x0, y0], t);
96    let p1 = apply_transform([x1, y0], t);
97    let p2 = apply_transform([x1, y1], t);
98    let p3 = apply_transform([x0, y1], t);
99    (
100        [
101            Vertex {
102                pos: p0,
103                color,
104                z_index: z,
105            },
106            Vertex {
107                pos: p1,
108                color,
109                z_index: z,
110            },
111            Vertex {
112                pos: p2,
113                color,
114                z_index: z,
115            },
116            Vertex {
117                pos: p3,
118                color,
119                z_index: z,
120            },
121        ],
122        [0, 1, 2, 0, 2, 3],
123    )
124}
125
126fn push_rect_linear_gradient(
127    vertices: &mut Vec<Vertex>,
128    indices: &mut Vec<u16>,
129    rect: Rect,
130    stops: &[(f32, [f32; 4])],
131    t: Transform2D,
132    z: f32,
133) {
134    if stops.len() < 2 {
135        return;
136    }
137    // ensure sorted
138    let mut s = stops.to_vec();
139    s.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
140    let y0 = rect.y;
141    let y1 = rect.y + rect.h;
142    for pair in s.windows(2) {
143        let (t0, c0) = (pair[0].0.clamp(0.0, 1.0), pair[0].1);
144        let (t1, c1) = (pair[1].0.clamp(0.0, 1.0), pair[1].1);
145        if (t1 - t0).abs() < 1e-6 {
146            continue;
147        }
148        let x0 = rect.x + rect.w * t0;
149        let x1 = rect.x + rect.w * t1;
150        let p0 = apply_transform([x0, y0], t);
151        let p1 = apply_transform([x1, y0], t);
152        let p2 = apply_transform([x1, y1], t);
153        let p3 = apply_transform([x0, y1], t);
154        let base = vertices.len() as u16;
155        vertices.extend_from_slice(&[
156            Vertex {
157                pos: p0,
158                color: c0,
159                z_index: z,
160            },
161            Vertex {
162                pos: p1,
163                color: c1,
164                z_index: z,
165            },
166            Vertex {
167                pos: p2,
168                color: c1,
169                z_index: z,
170            },
171            Vertex {
172                pos: p3,
173                color: c0,
174                z_index: z,
175            },
176        ]);
177        indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
178    }
179}
180
181fn normalize_gradient_stops(stops: &[(f32, [f32; 4])]) -> Vec<(f32, [f32; 4])> {
182    if stops.is_empty() {
183        return Vec::new();
184    }
185    let mut out = stops.to_vec();
186    out.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
187    if out.first().map(|s| s.0).unwrap_or(0.0) > 0.0
188        && let Some((_, c)) = out.first().copied()
189    {
190        out.insert(0, (0.0, c));
191    }
192    if out.last().map(|s| s.0).unwrap_or(1.0) < 1.0
193        && let Some((_, c)) = out.last().copied()
194    {
195        out.push((1.0, c));
196    }
197    for stop in &mut out {
198        stop.0 = stop.0.clamp(0.0, 1.0);
199    }
200    out
201}
202
203fn linear_to_srgb(c: f32) -> f32 {
204    let x = c.clamp(0.0, 1.0);
205    if x <= 0.0031308 {
206        12.92 * x
207    } else {
208        1.055 * x.powf(1.0 / 2.4) - 0.055
209    }
210}
211
212fn srgb_to_linear(c: f32) -> f32 {
213    let x = c.clamp(0.0, 1.0);
214    if x <= 0.04045 {
215        x / 12.92
216    } else {
217        ((x + 0.055) / 1.055).powf(2.4)
218    }
219}
220
221pub fn lerp_color(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
222    // CSS gradients interpolate in sRGB by default.
223    let ar = linear_to_srgb(a[0]);
224    let ag = linear_to_srgb(a[1]);
225    let ab = linear_to_srgb(a[2]);
226    let br = linear_to_srgb(b[0]);
227    let bg = linear_to_srgb(b[1]);
228    let bb = linear_to_srgb(b[2]);
229
230    let rr = ar + (br - ar) * t;
231    let rg = ag + (bg - ag) * t;
232    let rb = ab + (bb - ab) * t;
233
234    [
235        srgb_to_linear(rr),
236        srgb_to_linear(rg),
237        srgb_to_linear(rb),
238        a[3] + (b[3] - a[3]) * t,
239    ]
240}
241
242pub fn sample_gradient_stops(stops: &[(f32, [f32; 4])], t: f32) -> [f32; 4] {
243    if stops.is_empty() {
244        return [0.0, 0.0, 0.0, 0.0];
245    }
246    let tc = t.clamp(0.0, 1.0);
247    if tc <= stops[0].0 {
248        return stops[0].1;
249    }
250    if tc >= stops[stops.len() - 1].0 {
251        return stops[stops.len() - 1].1;
252    }
253
254    for pair in stops.windows(2) {
255        let (t0, c0) = pair[0];
256        let (t1, c1) = pair[1];
257        if tc >= t0 && tc <= t1 {
258            let span = (t1 - t0).max(1e-6);
259            let local_t = ((tc - t0) / span).clamp(0.0, 1.0);
260            return lerp_color(c0, c1, local_t);
261        }
262    }
263    stops[stops.len() - 1].1
264}
265
266fn push_ellipse(
267    vertices: &mut Vec<Vertex>,
268    indices: &mut Vec<u16>,
269    center: [f32; 2],
270    radii: [f32; 2],
271    color: [f32; 4],
272    z: f32,
273    t: Transform2D,
274) {
275    let segs = 64u32;
276    let base = vertices.len() as u16;
277    let c = apply_transform(center, t);
278    vertices.push(Vertex {
279        pos: c,
280        color,
281        z_index: z,
282    });
283
284    for i in 0..segs {
285        let theta = (i as f32) / (segs as f32) * std::f32::consts::TAU;
286        let p = [
287            center[0] + radii[0] * theta.cos(),
288            center[1] + radii[1] * theta.sin(),
289        ];
290        let p = apply_transform(p, t);
291        vertices.push(Vertex {
292            pos: p,
293            color,
294            z_index: z,
295        });
296    }
297    for i in 0..segs {
298        let i0 = base;
299        let i1 = base + 1 + i as u16;
300        let i2 = base + 1 + ((i + 1) % segs) as u16;
301        indices.extend_from_slice(&[i0, i1, i2]);
302    }
303}
304
305fn push_ellipse_radial_gradient(
306    vertices: &mut Vec<Vertex>,
307    indices: &mut Vec<u16>,
308    center: [f32; 2],
309    radii: [f32; 2],
310    stops: &[(f32, [f32; 4])],
311    z: f32,
312    t: Transform2D,
313) {
314    if stops.len() < 2 {
315        return;
316    }
317    let mut s = stops.to_vec();
318    s.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
319    let segs = 64u32;
320    let base_center = vertices.len() as u16;
321    // Center vertex with first stop color
322    let cpos = apply_transform(center, t);
323    vertices.push(Vertex {
324        pos: cpos,
325        color: s[0].1,
326        z_index: z,
327    });
328
329    // First ring
330    let mut prev_ring_start = vertices.len() as u16;
331    let prev_color = s[0].1;
332    let prev_t0 = s[0].0.clamp(0.0, 1.0);
333    let prev_t = if prev_t0 <= 0.0 { 0.0 } else { prev_t0 };
334    for i in 0..segs {
335        let theta = (i as f32) / (segs as f32) * std::f32::consts::TAU;
336        let p = [
337            center[0] + radii[0] * prev_t * theta.cos(),
338            center[1] + radii[1] * prev_t * theta.sin(),
339        ];
340        let p = apply_transform(p, t);
341        vertices.push(Vertex {
342            pos: p,
343            color: prev_color,
344            z_index: z,
345        });
346    }
347    // Connect center to first ring if needed
348    if prev_t == 0.0 {
349        for i in 0..segs {
350            let i1 = base_center;
351            let i2 = prev_ring_start + i as u16;
352            let i3 = prev_ring_start + ((i + 1) % segs) as u16;
353            indices.extend_from_slice(&[i1, i2, i3]);
354        }
355    }
356
357    for si in 1..s.len() {
358        let (tcur, ccur) = (s[si].0.clamp(0.0, 1.0), s[si].1);
359        let ring_start = vertices.len() as u16;
360        for i in 0..segs {
361            let theta = (i as f32) / (segs as f32) * std::f32::consts::TAU;
362            let p = [
363                center[0] + radii[0] * tcur * theta.cos(),
364                center[1] + radii[1] * tcur * theta.sin(),
365            ];
366            let p = apply_transform(p, t);
367            vertices.push(Vertex {
368                pos: p,
369                color: ccur,
370                z_index: z,
371            });
372        }
373        // stitch prev ring to current ring
374        for i in 0..segs {
375            let a0 = prev_ring_start + i as u16;
376            let a1 = prev_ring_start + ((i + 1) % segs) as u16;
377            let b0 = ring_start + i as u16;
378            let b1 = ring_start + ((i + 1) % segs) as u16;
379            indices.extend_from_slice(&[a0, b0, b1, a0, b1, a1]);
380        }
381        prev_ring_start = ring_start;
382    }
383}
384
385fn push_rounded_rect_linear_gradient(
386    vertices: &mut Vec<Vertex>,
387    indices: &mut Vec<u16>,
388    rrect: RoundedRect,
389    start: [f32; 2],
390    end: [f32; 2],
391    stops: &[(f32, [f32; 4])],
392    z: f32,
393    t: Transform2D,
394) {
395    let packed = normalize_gradient_stops(stops);
396    if packed.len() < 2 {
397        return;
398    }
399
400    let path = rounded_rect_to_path(rrect);
401    let dx = end[0] - start[0];
402    let dy = end[1] - start[1];
403    let denom = (dx * dx + dy * dy).max(1e-6);
404
405    tessellate_path_fill_with_color_fn(vertices, indices, &path, z, t, |p| {
406        let proj = ((p[0] - start[0]) * dx + (p[1] - start[1]) * dy) / denom;
407        sample_gradient_stops(&packed, proj)
408    });
409}
410
411fn push_rounded_rect_radial_gradient(
412    vertices: &mut Vec<Vertex>,
413    indices: &mut Vec<u16>,
414    rrect: RoundedRect,
415    center: [f32; 2],
416    radius: f32,
417    stops: &[(f32, [f32; 4])],
418    z: f32,
419    t: Transform2D,
420) {
421    let packed = normalize_gradient_stops(stops);
422    if packed.len() < 2 {
423        return;
424    }
425
426    let path = rounded_rect_to_path(rrect);
427    let r = radius.abs().max(1e-6);
428    tessellate_path_fill_subdivided_with_color_fn(vertices, indices, &path, z, t, 6.0, |p| {
429        let dx = p[0] - center[0];
430        let dy = p[1] - center[1];
431        let dist = (dx * dx + dy * dy).sqrt();
432        sample_gradient_stops(&packed, dist / r)
433    });
434}
435
436fn tessellate_path_fill(
437    vertices: &mut Vec<Vertex>,
438    indices: &mut Vec<u16>,
439    path: &Path,
440    color: [f32; 4],
441    z: f32,
442    t: Transform2D,
443) {
444    tessellate_path_fill_with_color_fn(vertices, indices, path, z, t, |_| color);
445}
446
447fn tessellate_path_fill_with_color_fn<F>(
448    vertices: &mut Vec<Vertex>,
449    indices: &mut Vec<u16>,
450    path: &Path,
451    z: f32,
452    t: Transform2D,
453    mut color_at: F,
454) where
455    F: FnMut([f32; 2]) -> [f32; 4],
456{
457    let Some(geom) = tessellate_path_fill_geometry(path) else {
458        return;
459    };
460
461    // Transform and append
462    if vertices.len() > u16::MAX as usize {
463        return;
464    }
465    let base = vertices.len() as u16;
466    for p in &geom.vertices {
467        let tp = apply_transform(*p, t);
468        vertices.push(Vertex {
469            pos: tp,
470            color: color_at(*p),
471            z_index: z,
472        });
473    }
474    indices.extend(geom.indices.iter().map(|i| base + *i));
475}
476
477fn tessellate_path_fill_subdivided_with_color_fn<F>(
478    vertices: &mut Vec<Vertex>,
479    indices: &mut Vec<u16>,
480    path: &Path,
481    z: f32,
482    t: Transform2D,
483    max_edge: f32,
484    mut color_at: F,
485) where
486    F: FnMut([f32; 2]) -> [f32; 4],
487{
488    let Some(geom) = tessellate_path_fill_geometry(path) else {
489        return;
490    };
491
492    let max_edge = max_edge.max(1.0);
493    for tri in geom.indices.chunks_exact(3) {
494        let p0 = geom.vertices[tri[0] as usize];
495        let p1 = geom.vertices[tri[1] as usize];
496        let p2 = geom.vertices[tri[2] as usize];
497        append_subdivided_triangle(vertices, indices, p0, p1, p2, z, t, max_edge, &mut color_at);
498    }
499}
500
501fn tessellate_path_fill_geometry(
502    path: &Path,
503) -> Option<lyon_tessellation::VertexBuffers<[f32; 2], u16>> {
504    use lyon_geom::point;
505    use lyon_path::Path as LyonPath;
506    use lyon_tessellation::{
507        BuffersBuilder, FillOptions, FillTessellator, FillVertex, VertexBuffers,
508    };
509
510    let mut builder = lyon_path::Path::builder();
511    let mut started = false;
512    for cmd in &path.cmds {
513        match *cmd {
514            PathCmd::MoveTo(p) => {
515                if started {
516                    builder.end(false);
517                }
518                builder.begin(point(p[0], p[1]));
519                started = true;
520            }
521            PathCmd::LineTo(p) => {
522                if !started {
523                    builder.begin(point(p[0], p[1]));
524                    started = true;
525                } else {
526                    builder.line_to(point(p[0], p[1]));
527                }
528            }
529            PathCmd::QuadTo(c, p) => {
530                builder.quadratic_bezier_to(point(c[0], c[1]), point(p[0], p[1]));
531            }
532            PathCmd::CubicTo(c1, c2, p) => {
533                builder.cubic_bezier_to(
534                    point(c1[0], c1[1]),
535                    point(c2[0], c2[1]),
536                    point(p[0], p[1]),
537                );
538            }
539            PathCmd::Close => {
540                builder.end(true);
541                started = false;
542            }
543        }
544    }
545    if started {
546        builder.end(false);
547    }
548
549    let lyon_path: LyonPath = builder.build();
550    let mut tess = FillTessellator::new();
551    let tol = std::env::var("LYON_TOLERANCE")
552        .ok()
553        .and_then(|v| v.parse::<f32>().ok())
554        .unwrap_or(0.1);
555    let base_opts = FillOptions::default().with_tolerance(tol);
556    let options = match path.fill_rule {
557        FillRule::NonZero => base_opts.with_fill_rule(lyon_tessellation::FillRule::NonZero),
558        FillRule::EvenOdd => base_opts.with_fill_rule(lyon_tessellation::FillRule::EvenOdd),
559    };
560
561    let mut geom: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
562    let result = tess.tessellate_path(
563        lyon_path.as_slice(),
564        &options,
565        &mut BuffersBuilder::new(&mut geom, |fv: FillVertex| {
566            let p = fv.position();
567            [p.x, p.y]
568        }),
569    );
570    if result.is_err() {
571        return None;
572    }
573    Some(geom)
574}
575
576fn append_subdivided_triangle<F>(
577    vertices: &mut Vec<Vertex>,
578    indices: &mut Vec<u16>,
579    p0: [f32; 2],
580    p1: [f32; 2],
581    p2: [f32; 2],
582    z: f32,
583    t: Transform2D,
584    max_edge: f32,
585    color_at: &mut F,
586) where
587    F: FnMut([f32; 2]) -> [f32; 4],
588{
589    let edge_len = |a: [f32; 2], b: [f32; 2]| -> f32 {
590        let dx = b[0] - a[0];
591        let dy = b[1] - a[1];
592        (dx * dx + dy * dy).sqrt()
593    };
594    let max_len = edge_len(p0, p1).max(edge_len(p1, p2)).max(edge_len(p2, p0));
595    let steps = ((max_len / max_edge).ceil() as usize).clamp(1, 12);
596    let steps_f = steps as f32;
597
598    let base = vertices.len() as u16;
599    let mut row_offsets = Vec::with_capacity(steps + 1);
600    let mut offset = 0usize;
601    for row in 0..=steps {
602        row_offsets.push(offset);
603        offset += steps - row + 1;
604    }
605
606    for i in 0..=steps {
607        for j in 0..=(steps - i) {
608            let a = i as f32 / steps_f;
609            let b = j as f32 / steps_f;
610            let c = 1.0 - a - b;
611            let p = [
612                p0[0] * c + p1[0] * a + p2[0] * b,
613                p0[1] * c + p1[1] * a + p2[1] * b,
614            ];
615            vertices.push(Vertex {
616                pos: apply_transform(p, t),
617                color: color_at(p),
618                z_index: z,
619            });
620        }
621    }
622
623    let tri_area = (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p1[1] - p0[1]) * (p2[0] - p0[0]);
624    let ccw = tri_area >= 0.0;
625    let idx = |row: usize, col: usize| -> u16 { base + (row_offsets[row] + col) as u16 };
626
627    for i in 0..steps {
628        for j in 0..(steps - i) {
629            let a = idx(i, j);
630            let b = idx(i + 1, j);
631            let c = idx(i, j + 1);
632
633            if ccw {
634                indices.extend_from_slice(&[a, b, c]);
635            } else {
636                indices.extend_from_slice(&[a, c, b]);
637            }
638
639            if j < (steps - i - 1) {
640                let d = idx(i + 1, j + 1);
641                if ccw {
642                    indices.extend_from_slice(&[b, d, c]);
643                } else {
644                    indices.extend_from_slice(&[b, c, d]);
645                }
646            }
647        }
648    }
649}
650
651fn tessellate_path_stroke(
652    vertices: &mut Vec<Vertex>,
653    indices: &mut Vec<u16>,
654    path: &Path,
655    stroke: Stroke,
656    color: [f32; 4],
657    z: f32,
658    t: Transform2D,
659) {
660    use lyon_geom::point;
661    use lyon_path::Path as LyonPath;
662    use lyon_tessellation::{
663        BuffersBuilder, LineCap, LineJoin, StrokeOptions, StrokeTessellator, StrokeVertex,
664        VertexBuffers,
665    };
666
667    // Build lyon path
668    let mut builder = lyon_path::Path::builder();
669    let mut started = false;
670    for cmd in &path.cmds {
671        match *cmd {
672            PathCmd::MoveTo(p) => {
673                if started {
674                    builder.end(false);
675                }
676                builder.begin(point(p[0], p[1]));
677                started = true;
678            }
679            PathCmd::LineTo(p) => {
680                if !started {
681                    builder.begin(point(p[0], p[1]));
682                    started = true;
683                } else {
684                    builder.line_to(point(p[0], p[1]));
685                }
686            }
687            PathCmd::QuadTo(c, p) => {
688                builder.quadratic_bezier_to(point(c[0], c[1]), point(p[0], p[1]));
689            }
690            PathCmd::CubicTo(c1, c2, p) => {
691                builder.cubic_bezier_to(
692                    point(c1[0], c1[1]),
693                    point(c2[0], c2[1]),
694                    point(p[0], p[1]),
695                );
696            }
697            PathCmd::Close => {
698                builder.end(true);
699                started = false;
700            }
701        }
702    }
703    // End any open sub-path
704    if started {
705        builder.end(false);
706    }
707    let lyon_path: LyonPath = builder.build();
708
709    let mut tess = StrokeTessellator::new();
710    let tol = std::env::var("LYON_TOLERANCE")
711        .ok()
712        .and_then(|v| v.parse::<f32>().ok())
713        .unwrap_or(0.1);
714    let options = StrokeOptions::default()
715        .with_line_width(stroke.width.max(0.0))
716        .with_tolerance(tol)
717        .with_line_join(LineJoin::Round)
718        .with_start_cap(LineCap::Round)
719        .with_end_cap(LineCap::Round);
720    let mut geom: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
721    let result = tess.tessellate_path(
722        lyon_path.as_slice(),
723        &options,
724        &mut BuffersBuilder::new(&mut geom, |sv: StrokeVertex| {
725            let p = sv.position();
726            [p.x, p.y]
727        }),
728    );
729    if result.is_err() {
730        return;
731    }
732    let base = vertices.len() as u16;
733    for p in &geom.vertices {
734        let tp = apply_transform(*p, t);
735        vertices.push(Vertex {
736            pos: tp,
737            color,
738            z_index: z,
739        });
740    }
741    indices.extend(geom.indices.iter().map(|i| base + *i));
742}
743
744/// Build a Path representing a rounded rectangle using cubic Beziers (kappa approximation).
745/// This path is then tessellated by lyon for precise coverage (avoids fan artifacts on small radii).
746fn rounded_rect_to_path(rrect: RoundedRect) -> Path {
747    let rect = rrect.rect;
748    let mut tl = rrect.radii.tl.min(rect.w * 0.5).min(rect.h * 0.5);
749    let mut tr = rrect.radii.tr.min(rect.w * 0.5).min(rect.h * 0.5);
750    let mut br = rrect.radii.br.min(rect.w * 0.5).min(rect.h * 0.5);
751    let mut bl = rrect.radii.bl.min(rect.w * 0.5).min(rect.h * 0.5);
752
753    // Clamp negative or NaN just in case
754    for r in [&mut tl, &mut tr, &mut br, &mut bl] {
755        if !r.is_finite() || *r < 0.0 {
756            *r = 0.0;
757        }
758    }
759
760    // If radii are effectively zero, fall back to a plain rect path
761    if tl <= 0.0 && tr <= 0.0 && br <= 0.0 && bl <= 0.0 {
762        return Path {
763            cmds: vec![
764                PathCmd::MoveTo([rect.x, rect.y]),
765                PathCmd::LineTo([rect.x + rect.w, rect.y]),
766                PathCmd::LineTo([rect.x + rect.w, rect.y + rect.h]),
767                PathCmd::LineTo([rect.x, rect.y + rect.h]),
768                PathCmd::Close,
769            ],
770            fill_rule: FillRule::NonZero,
771        };
772    }
773
774    // Kappa for quarter circle cubic approximation
775    const K: f32 = 0.552_284_749_831;
776    let x0 = rect.x;
777    let y0 = rect.y;
778    let x1 = rect.x + rect.w;
779    let y1 = rect.y + rect.h;
780
781    // Start at top-left corner top edge tangent
782    let mut cmds: Vec<PathCmd> = Vec::new();
783    cmds.push(PathCmd::MoveTo([x0 + tl, y0]));
784
785    // Top edge to before TR arc
786    cmds.push(PathCmd::LineTo([x1 - tr, y0]));
787    // TR arc (clockwise): from (x1 - tr, y0) to (x1, y0 + tr)
788    if tr > 0.0 {
789        let c1 = [x1 - tr + K * tr, y0];
790        let c2 = [x1, y0 + tr - K * tr];
791        let p = [x1, y0 + tr];
792        cmds.push(PathCmd::CubicTo(c1, c2, p));
793    } else {
794        cmds.push(PathCmd::LineTo([x1, y0]));
795        cmds.push(PathCmd::LineTo([x1, y0 + tr]));
796    }
797
798    // Right edge down to before BR arc
799    cmds.push(PathCmd::LineTo([x1, y1 - br]));
800    // BR arc: from (x1, y1 - br) to (x1 - br, y1)
801    if br > 0.0 {
802        let c1 = [x1, y1 - br + K * br];
803        let c2 = [x1 - br + K * br, y1];
804        let p = [x1 - br, y1];
805        cmds.push(PathCmd::CubicTo(c1, c2, p));
806    } else {
807        cmds.push(PathCmd::LineTo([x1, y1]));
808        cmds.push(PathCmd::LineTo([x1 - br, y1]));
809    }
810
811    // Bottom edge to before BL arc
812    cmds.push(PathCmd::LineTo([x0 + bl, y1]));
813    // BL arc: from (x0 + bl, y1) to (x0, y1 - bl)
814    if bl > 0.0 {
815        let c1 = [x0 + bl - K * bl, y1];
816        let c2 = [x0, y1 - bl + K * bl];
817        let p = [x0, y1 - bl];
818        cmds.push(PathCmd::CubicTo(c1, c2, p));
819    } else {
820        cmds.push(PathCmd::LineTo([x0, y1]));
821        cmds.push(PathCmd::LineTo([x0, y1 - bl]));
822    }
823
824    // Left edge up to before TL arc
825    cmds.push(PathCmd::LineTo([x0, y0 + tl]));
826    // TL arc: from (x0, y0 + tl) to (x0 + tl, y0)
827    if tl > 0.0 {
828        let c1 = [x0, y0 + tl - K * tl];
829        let c2 = [x0 + tl - K * tl, y0];
830        let p = [x0 + tl, y0];
831        cmds.push(PathCmd::CubicTo(c1, c2, p));
832    } else {
833        cmds.push(PathCmd::LineTo([x0, y0]));
834        cmds.push(PathCmd::LineTo([x0 + tl, y0]));
835    }
836
837    cmds.push(PathCmd::Close);
838    Path {
839        cmds,
840        fill_rule: FillRule::NonZero,
841    }
842}
843
844fn push_rounded_rect(
845    vertices: &mut Vec<Vertex>,
846    indices: &mut Vec<u16>,
847    rrect: RoundedRect,
848    color: [f32; 4],
849    z: f32,
850    t: Transform2D,
851) {
852    // Delegate to lyon's robust tessellator via our generic path fill
853    let path = rounded_rect_to_path(rrect);
854    tessellate_path_fill(vertices, indices, &path, color, z, t);
855}
856
857fn push_rect_stroke(
858    vertices: &mut Vec<Vertex>,
859    indices: &mut Vec<u16>,
860    rect: Rect,
861    stroke: Stroke,
862    color: [f32; 4],
863    z: f32,
864    t: Transform2D,
865) {
866    let w = stroke.width.max(0.0);
867    if w <= 0.0001 {
868        return;
869    }
870
871    // Analytic-style AA for thin 1px strokes:
872    // fade the outer edge of the stroke to transparent so the border
873    // blends smoothly against the background instead of hard-stepping.
874    let use_aa = w <= 1.5;
875    let (outer_color, inner_color) = if use_aa {
876        let mut oc = [0.0; 4];
877        // Scale premultiplied RGBA by a small factor for the outer edge.
878        // Using 0.0 gives the sharpest falloff; tweakable if needed.
879        let scale = 0.0f32;
880        oc[0] = color[0] * scale;
881        oc[1] = color[1] * scale;
882        oc[2] = color[2] * scale;
883        oc[3] = color[3] * scale;
884        (oc, color)
885    } else {
886        (color, color)
887    };
888
889    // Outer corners
890    let o0 = apply_transform([rect.x, rect.y], t);
891    let o1 = apply_transform([rect.x + rect.w, rect.y], t);
892    let o2 = apply_transform([rect.x + rect.w, rect.y + rect.h], t);
893    let o3 = apply_transform([rect.x, rect.y + rect.h], t);
894    // Inner corners (shrink by width)
895    let ix0 = rect.x + w;
896    let iy0 = rect.y + w;
897    let ix1 = (rect.x + rect.w - w).max(ix0);
898    let iy1 = (rect.y + rect.h - w).max(iy0);
899    let i0 = apply_transform([ix0, iy0], t);
900    let i1 = apply_transform([ix1, iy0], t);
901    let i2 = apply_transform([ix1, iy1], t);
902    let i3 = apply_transform([ix0, iy1], t);
903
904    let base = vertices.len() as u16;
905    vertices.extend_from_slice(&[
906        Vertex {
907            pos: o0,
908            color: outer_color,
909            z_index: z,
910        }, // 0
911        Vertex {
912            pos: o1,
913            color: outer_color,
914            z_index: z,
915        }, // 1
916        Vertex {
917            pos: o2,
918            color: outer_color,
919            z_index: z,
920        }, // 2
921        Vertex {
922            pos: o3,
923            color: outer_color,
924            z_index: z,
925        }, // 3
926        Vertex {
927            pos: i0,
928            color: inner_color,
929            z_index: z,
930        }, // 4
931        Vertex {
932            pos: i1,
933            color: inner_color,
934            z_index: z,
935        }, // 5
936        Vertex {
937            pos: i2,
938            color: inner_color,
939            z_index: z,
940        }, // 6
941        Vertex {
942            pos: i3,
943            color: inner_color,
944            z_index: z,
945        }, // 7
946    ]);
947    // Build ring from quads on each edge
948    let idx: [u16; 24] = [
949        // top edge: o0-o1-i1-i0
950        0, 1, 5, 0, 5, 4, // right edge: o1-o2-i2-i1
951        1, 2, 6, 1, 6, 5, // bottom edge: o2-o3-i3-i2
952        2, 3, 7, 2, 7, 6, // left edge: o3-o0-i0-i3
953        3, 0, 4, 3, 4, 7,
954    ];
955    indices.extend(idx.iter().map(|i| base + i));
956}
957
958fn push_rounded_rect_stroke(
959    vertices: &mut Vec<Vertex>,
960    indices: &mut Vec<u16>,
961    rrect: RoundedRect,
962    stroke: Stroke,
963    color: [f32; 4],
964    z: f32,
965    t: Transform2D,
966) {
967    let w = stroke.width.max(0.0);
968    if w <= 0.0001 {
969        return;
970    }
971    let path = rounded_rect_to_path(rrect);
972    tessellate_path_stroke(vertices, indices, &path, Stroke { width: w }, color, z, t);
973}
974
975pub fn upload_display_list(
976    allocator: &mut RenderAllocator,
977    queue: &wgpu::Queue,
978    list: &DisplayList,
979) -> Result<GpuScene> {
980    let mut vertices: Vec<Vertex> = Vec::new();
981    let mut indices: Vec<u16> = Vec::new();
982
983    // NOTE: Z-index sorting disabled because it breaks clip/transform stacks.
984    // For proper z-ordering, we need to either:
985    // 1. Use a depth buffer, or
986    // 2. Ensure commands are emitted in the correct z-order from the start
987    // let mut sorted_list = list.clone();
988    // sorted_list.sort_by_z();
989
990    for cmd in &list.commands {
991        match cmd {
992            Command::DrawRect {
993                rect,
994                brush,
995                transform,
996                z,
997                ..
998            } => {
999                match brush {
1000                    Brush::Solid(col) => {
1001                        let color = [col.r, col.g, col.b, col.a];
1002                        let (v, i) = rect_to_verts(*rect, color, *transform, *z as f32);
1003                        let base = vertices.len() as u16;
1004                        vertices.extend_from_slice(&v);
1005                        indices.extend(i.iter().map(|idx| base + idx));
1006                    }
1007                    Brush::LinearGradient { stops, .. } => {
1008                        // Only handle horizontal gradients for now: map t along x within rect
1009                        let mut packed: Vec<(f32, [f32; 4])> = stops
1010                            .iter()
1011                            .map(|(tpos, c)| (*tpos, [c.r, c.g, c.b, c.a]))
1012                            .collect();
1013                        if packed.is_empty() {
1014                            continue;
1015                        }
1016                        // Clamp and ensure 0 and 1 exist
1017                        if packed.first().unwrap().0 > 0.0 {
1018                            let c = packed.first().unwrap().1;
1019                            packed.insert(0, (0.0, c));
1020                        }
1021                        if packed.last().unwrap().0 < 1.0 {
1022                            let c = packed.last().unwrap().1;
1023                            packed.push((1.0, c));
1024                        }
1025                        push_rect_linear_gradient(
1026                            &mut vertices,
1027                            &mut indices,
1028                            *rect,
1029                            &packed,
1030                            *transform,
1031                            *z as f32,
1032                        );
1033                    }
1034                    _ => {}
1035                }
1036            }
1037            Command::DrawRoundedRect {
1038                rrect,
1039                brush,
1040                transform,
1041                z,
1042                ..
1043            } => match brush {
1044                Brush::Solid(col) => {
1045                    let color = [col.r, col.g, col.b, col.a];
1046                    push_rounded_rect(
1047                        &mut vertices,
1048                        &mut indices,
1049                        *rrect,
1050                        color,
1051                        *z as f32,
1052                        *transform,
1053                    );
1054                }
1055                Brush::LinearGradient { start, end, stops } => {
1056                    let packed: Vec<(f32, [f32; 4])> = stops
1057                        .iter()
1058                        .map(|(tpos, c)| (*tpos, [c.r, c.g, c.b, c.a]))
1059                        .collect();
1060                    if packed.is_empty() {
1061                        continue;
1062                    }
1063                    push_rounded_rect_linear_gradient(
1064                        &mut vertices,
1065                        &mut indices,
1066                        *rrect,
1067                        *start,
1068                        *end,
1069                        &packed,
1070                        *z as f32,
1071                        *transform,
1072                    );
1073                }
1074                Brush::RadialGradient {
1075                    center,
1076                    radius,
1077                    stops,
1078                } => {
1079                    let packed: Vec<(f32, [f32; 4])> = stops
1080                        .iter()
1081                        .map(|(tpos, c)| (*tpos, [c.r, c.g, c.b, c.a]))
1082                        .collect();
1083                    if packed.is_empty() {
1084                        continue;
1085                    }
1086                    push_rounded_rect_radial_gradient(
1087                        &mut vertices,
1088                        &mut indices,
1089                        *rrect,
1090                        *center,
1091                        *radius,
1092                        &packed,
1093                        *z as f32,
1094                        *transform,
1095                    );
1096                }
1097            },
1098            Command::StrokeRect {
1099                rect,
1100                stroke,
1101                brush,
1102                transform,
1103                z,
1104                ..
1105            } => {
1106                if let Brush::Solid(col) = brush {
1107                    let color = [col.r, col.g, col.b, col.a];
1108                    push_rect_stroke(
1109                        &mut vertices,
1110                        &mut indices,
1111                        *rect,
1112                        *stroke,
1113                        color,
1114                        *z as f32,
1115                        *transform,
1116                    );
1117                }
1118            }
1119            Command::StrokeRoundedRect {
1120                rrect,
1121                stroke,
1122                brush,
1123                transform,
1124                z,
1125                ..
1126            } => {
1127                if let Brush::Solid(col) = brush {
1128                    let color = [col.r, col.g, col.b, col.a];
1129                    push_rounded_rect_stroke(
1130                        &mut vertices,
1131                        &mut indices,
1132                        *rrect,
1133                        *stroke,
1134                        color,
1135                        *z as f32,
1136                        *transform,
1137                    );
1138                }
1139            }
1140            Command::DrawEllipse {
1141                center,
1142                radii,
1143                brush,
1144                transform,
1145                z,
1146                ..
1147            } => match brush {
1148                Brush::Solid(col) => {
1149                    let color = [col.r, col.g, col.b, col.a];
1150                    push_ellipse(
1151                        &mut vertices,
1152                        &mut indices,
1153                        *center,
1154                        *radii,
1155                        color,
1156                        *z as f32,
1157                        *transform,
1158                    );
1159                }
1160                Brush::RadialGradient {
1161                    center: _gcenter,
1162                    radius: _r,
1163                    stops,
1164                } => {
1165                    let mut packed: Vec<(f32, [f32; 4])> = stops
1166                        .iter()
1167                        .map(|(t, c)| (*t, [c.r, c.g, c.b, c.a]))
1168                        .collect();
1169                    if packed.is_empty() {
1170                        continue;
1171                    }
1172                    if packed.first().unwrap().0 > 0.0 {
1173                        let c = packed.first().unwrap().1;
1174                        packed.insert(0, (0.0, c));
1175                    }
1176                    if packed.last().unwrap().0 < 1.0 {
1177                        let c = packed.last().unwrap().1;
1178                        packed.push((1.0, c));
1179                    }
1180                    push_ellipse_radial_gradient(
1181                        &mut vertices,
1182                        &mut indices,
1183                        *center,
1184                        *radii,
1185                        &packed,
1186                        *z as f32,
1187                        *transform,
1188                    );
1189                }
1190                _ => {}
1191            },
1192            Command::FillPath {
1193                path,
1194                color,
1195                transform,
1196                z,
1197                ..
1198            } => {
1199                let col = [color.r, color.g, color.b, color.a];
1200                tessellate_path_fill(
1201                    &mut vertices,
1202                    &mut indices,
1203                    path,
1204                    col,
1205                    *z as f32,
1206                    *transform,
1207                );
1208            }
1209            Command::StrokePath {
1210                path,
1211                stroke,
1212                color,
1213                transform,
1214                z,
1215                ..
1216            } => {
1217                let col = [color.r, color.g, color.b, color.a];
1218                tessellate_path_stroke(
1219                    &mut vertices,
1220                    &mut indices,
1221                    path,
1222                    *stroke,
1223                    col,
1224                    *z as f32,
1225                    *transform,
1226                );
1227            }
1228            // BoxShadow commands are handled by PassManager as a separate pipeline.
1229            Command::BoxShadow { .. } => {}
1230            // Hit-only regions: intentionally not rendered.
1231            Command::HitRegionRect { .. } => {}
1232            Command::HitRegionRoundedRect { .. } => {}
1233            Command::HitRegionEllipse { .. } => {}
1234            _ => {}
1235        }
1236    }
1237
1238    // Ensure index buffer size meets COPY_BUFFER_ALIGNMENT (4 bytes)
1239    if (indices.len() % 2) != 0 {
1240        if indices.len() >= 3 {
1241            let a = indices[indices.len() - 3];
1242            let b = indices[indices.len() - 2];
1243            let c = indices[indices.len() - 1];
1244            indices.extend_from_slice(&[a, b, c]);
1245        } else {
1246            indices.push(0);
1247        }
1248    }
1249
1250    // Allocate GPU buffers and upload
1251    let vsize = (vertices.len() * std::mem::size_of::<Vertex>()) as u64;
1252    let isize = (indices.len() * std::mem::size_of::<u16>()) as u64;
1253    let vbuf = allocator.allocate_buffer(BufKey {
1254        size: vsize.max(4),
1255        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1256    });
1257    let ibuf = allocator.allocate_buffer(BufKey {
1258        size: isize.max(4),
1259        usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1260    });
1261    if vsize > 0 {
1262        queue.write_buffer(&vbuf.buffer, 0, bytemuck::cast_slice(&vertices));
1263    }
1264    if isize > 0 {
1265        queue.write_buffer(&ibuf.buffer, 0, bytemuck::cast_slice(&indices));
1266    }
1267
1268    Ok(GpuScene {
1269        vertex: vbuf,
1270        index: ibuf,
1271        vertices: vertices.len() as u32,
1272        indices: indices.len() as u32,
1273    })
1274}
1275
1276/// Upload a DisplayList extracting all element types for unified rendering.
1277/// This is the main entry point for the unified rendering system.
1278///
1279/// Returns:
1280/// - GpuScene: Uploaded solid geometry (rectangles, paths, etc.)
1281/// - text_draws: Text runs with their transforms and z-indices
1282/// - image_draws: Image draws (currently placeholder, will be implemented)
1283/// - svg_draws: SVG draws (currently placeholder, will be implemented)
1284pub fn upload_display_list_unified(
1285    allocator: &mut RenderAllocator,
1286    queue: &wgpu::Queue,
1287    list: &DisplayList,
1288) -> Result<UnifiedSceneData> {
1289    let mut vertices: Vec<Vertex> = Vec::new();
1290    let mut indices: Vec<u16> = Vec::new();
1291    let mut transparent_vertices: Vec<Vertex> = Vec::new();
1292    let mut transparent_indices: Vec<u16> = Vec::new();
1293    let mut transparent_batches: Vec<TransparentBatch> = Vec::new();
1294    let mut text_draws: Vec<ExtractedTextDraw> = Vec::new();
1295    let mut image_draws: Vec<ExtractedImageDraw> = Vec::new();
1296    let mut svg_draws: Vec<ExtractedSvgDraw> = Vec::new();
1297    let mut external_texture_draws: Vec<ExtractedExternalTextureDraw> = Vec::new();
1298    let is_transparent = |alpha: f32| alpha < 0.999;
1299    let record_transparent_batch =
1300        |batches: &mut Vec<TransparentBatch>, z: i32, index_start: usize, index_end: usize| {
1301            if index_end <= index_start {
1302                return;
1303            }
1304            let start = index_start as u32;
1305            let count = (index_end - index_start) as u32;
1306            if let Some(last) = batches.last_mut()
1307                && last.z == z
1308                && last.index_start + last.index_count == start
1309            {
1310                last.index_count += count;
1311            } else {
1312                batches.push(TransparentBatch {
1313                    z,
1314                    index_start: start,
1315                    index_count: count,
1316                });
1317            }
1318        };
1319
1320    // Track transform stack for completeness, but note that draw commands
1321    // already carry fully-composed world transforms. For unified upload we
1322    // treat the per-command transform as authoritative and use the stack
1323    // only to mirror the current state (kept for potential future use).
1324    let mut transform_stack: Vec<Transform2D> = vec![Transform2D::identity()];
1325    let mut _current_transform = Transform2D::identity();
1326
1327    // Track CSS-style group opacity. Each PushOpacity pushes the effective
1328    // (accumulated) opacity onto the stack; PopOpacity restores the previous
1329    // level.  All vertex colours are pre-multiplied by the current effective
1330    // opacity so that nested opacities compose correctly.
1331    let mut opacity_stack: Vec<f32> = vec![1.0];
1332    let current_opacity = |stack: &[f32]| *stack.last().unwrap_or(&1.0);
1333
1334    // Helper: multiply a premultiplied-alpha colour by group opacity.
1335    // All four channels are scaled so the premultiplied invariant holds.
1336    fn premul_opa(c: [f32; 4], o: f32) -> [f32; 4] {
1337        if o >= 0.999 {
1338            return c;
1339        }
1340        [c[0] * o, c[1] * o, c[2] * o, c[3] * o]
1341    }
1342
1343    for cmd in &list.commands {
1344        match cmd {
1345            // Handle transform stack
1346            Command::PushTransform(t) => {
1347                // `t` is already the composed world transform at this stack depth.
1348                _current_transform = *t;
1349                transform_stack.push(_current_transform);
1350            }
1351            Command::PopTransform => {
1352                transform_stack.pop();
1353                _current_transform = transform_stack
1354                    .last()
1355                    .copied()
1356                    .unwrap_or(Transform2D::identity());
1357            }
1358
1359            // Extract text commands
1360            Command::DrawText {
1361                run, z, transform, ..
1362            } => {
1363                // Text draws already carry the full world transform.
1364                let final_transform = *transform;
1365                let opa = current_opacity(&opacity_stack);
1366                let mut text_run = run.clone();
1367                if opa < 0.999 {
1368                    text_run.color.r *= opa;
1369                    text_run.color.g *= opa;
1370                    text_run.color.b *= opa;
1371                    text_run.color.a *= opa;
1372                }
1373                text_draws.push(ExtractedTextDraw {
1374                    run: text_run,
1375                    z: *z,
1376                    transform: final_transform,
1377                });
1378            }
1379
1380            // Extract hyperlink as text + optional underline
1381            Command::DrawHyperlink {
1382                hyperlink,
1383                z,
1384                transform,
1385                ..
1386            } => {
1387                // Hyperlink commands also carry their full world transform.
1388                let final_transform = *transform;
1389                let opa = current_opacity(&opacity_stack);
1390
1391                // Extract hyperlink text as a text draw (with group opacity)
1392                let mut link_color = hyperlink.color;
1393                if opa < 0.999 {
1394                    link_color.r *= opa;
1395                    link_color.g *= opa;
1396                    link_color.b *= opa;
1397                    link_color.a *= opa;
1398                }
1399                let text_run = TextRun {
1400                    text: hyperlink.text.clone(),
1401                    pos: hyperlink.pos,
1402                    size: hyperlink.size,
1403                    color: link_color,
1404                    weight: hyperlink.weight,
1405                    style: hyperlink.style,
1406                    family: hyperlink.family.clone(),
1407                };
1408                text_draws.push(ExtractedTextDraw {
1409                    run: text_run,
1410                    z: *z,
1411                    transform: final_transform,
1412                });
1413
1414                // Draw underline if enabled
1415                if hyperlink.underline {
1416                    let underline_color = hyperlink.underline_color.unwrap_or(hyperlink.color);
1417                    let color = premul_opa(
1418                        [
1419                            underline_color.r,
1420                            underline_color.g,
1421                            underline_color.b,
1422                            underline_color.a,
1423                        ],
1424                        opa,
1425                    );
1426
1427                    // Prefer explicit measured width from layout. Fall back to heuristic.
1428                    let (underline_x, text_width) =
1429                        if let Some(w) = hyperlink.measured_width.map(|v| v.max(0.0)) {
1430                            (hyperlink.pos[0], w)
1431                        } else {
1432                            let trimmed = hyperlink.text.trim_end();
1433                            let char_count = trimmed.chars().count() as f32;
1434                            let weight_boost = ((hyperlink.weight - 400.0).max(0.0) / 500.0) * 0.08;
1435                            let char_width = hyperlink.size * (0.50 + weight_boost);
1436                            let mut width = char_count * char_width;
1437                            let inset = hyperlink.size * 0.10;
1438                            if width > inset * 2.0 {
1439                                width -= inset * 2.0;
1440                            }
1441                            (hyperlink.pos[0] + inset, width)
1442                        };
1443
1444                    // Underline is a thin rect slightly below the baseline.
1445                    // `hyperlink.pos[1]` is the baseline Y coordinate; place the
1446                    // underline about ~10% of the font size below it.
1447                    let underline_thickness = (hyperlink.size * 0.08).max(1.0);
1448                    let underline_offset = hyperlink.size * 0.10; // Slightly closer to glyphs
1449
1450                    let underline_rect = Rect {
1451                        x: underline_x,
1452                        y: hyperlink.pos[1] + underline_offset,
1453                        w: text_width,
1454                        h: underline_thickness,
1455                    };
1456
1457                    let (v, i) = rect_to_verts(underline_rect, color, final_transform, *z as f32);
1458                    if is_transparent(color[3]) {
1459                        let index_start = transparent_indices.len();
1460                        let base = transparent_vertices.len() as u16;
1461                        transparent_vertices.extend_from_slice(&v);
1462                        transparent_indices.extend(i.iter().map(|idx| base + idx));
1463                        record_transparent_batch(
1464                            &mut transparent_batches,
1465                            *z,
1466                            index_start,
1467                            transparent_indices.len(),
1468                        );
1469                    } else {
1470                        let base = vertices.len() as u16;
1471                        vertices.extend_from_slice(&v);
1472                        indices.extend(i.iter().map(|idx| base + idx));
1473                    }
1474                }
1475            }
1476
1477            // Process solid geometry commands
1478            Command::DrawRect {
1479                rect,
1480                brush,
1481                transform,
1482                z,
1483                ..
1484            } => {
1485                // Rect draws already carry the full world transform.
1486                let final_transform = *transform;
1487                let opa = current_opacity(&opacity_stack);
1488                match brush {
1489                    Brush::Solid(col) => {
1490                        let color = premul_opa([col.r, col.g, col.b, col.a], opa);
1491                        let (v, i) = rect_to_verts(*rect, color, final_transform, *z as f32);
1492                        if is_transparent(color[3]) {
1493                            let index_start = transparent_indices.len();
1494                            let base = transparent_vertices.len() as u16;
1495                            transparent_vertices.extend_from_slice(&v);
1496                            transparent_indices.extend(i.iter().map(|idx| base + idx));
1497                            record_transparent_batch(
1498                                &mut transparent_batches,
1499                                *z,
1500                                index_start,
1501                                transparent_indices.len(),
1502                            );
1503                        } else {
1504                            let base = vertices.len() as u16;
1505                            vertices.extend_from_slice(&v);
1506                            indices.extend(i.iter().map(|idx| base + idx));
1507                        }
1508                    }
1509                    Brush::LinearGradient { stops, .. } => {
1510                        // Only handle horizontal gradients for now: map t along x within rect
1511                        let mut packed: Vec<(f32, [f32; 4])> = stops
1512                            .iter()
1513                            .map(|(tpos, c)| (*tpos, premul_opa([c.r, c.g, c.b, c.a], opa)))
1514                            .collect();
1515                        if packed.is_empty() {
1516                            continue;
1517                        }
1518                        // Clamp and ensure 0 and 1 exist
1519                        if packed.first().unwrap().0 > 0.0 {
1520                            let c = packed.first().unwrap().1;
1521                            packed.insert(0, (0.0, c));
1522                        }
1523                        if packed.last().unwrap().0 < 1.0 {
1524                            let c = packed.last().unwrap().1;
1525                            packed.push((1.0, c));
1526                        }
1527                        let gradient_transparent = packed.iter().any(|(_, c)| is_transparent(c[3]));
1528                        if gradient_transparent {
1529                            let index_start = transparent_indices.len();
1530                            push_rect_linear_gradient(
1531                                &mut transparent_vertices,
1532                                &mut transparent_indices,
1533                                *rect,
1534                                &packed,
1535                                final_transform,
1536                                *z as f32,
1537                            );
1538                            record_transparent_batch(
1539                                &mut transparent_batches,
1540                                *z,
1541                                index_start,
1542                                transparent_indices.len(),
1543                            );
1544                        } else {
1545                            push_rect_linear_gradient(
1546                                &mut vertices,
1547                                &mut indices,
1548                                *rect,
1549                                &packed,
1550                                final_transform,
1551                                *z as f32,
1552                            );
1553                        }
1554                    }
1555                    _ => {}
1556                }
1557            }
1558            Command::DrawRoundedRect {
1559                rrect,
1560                brush,
1561                transform,
1562                z,
1563                ..
1564            } => {
1565                let final_transform = *transform;
1566                let opa = current_opacity(&opacity_stack);
1567                match brush {
1568                    Brush::Solid(col) => {
1569                        let color = premul_opa([col.r, col.g, col.b, col.a], opa);
1570                        if is_transparent(color[3]) {
1571                            let index_start = transparent_indices.len();
1572                            push_rounded_rect(
1573                                &mut transparent_vertices,
1574                                &mut transparent_indices,
1575                                *rrect,
1576                                color,
1577                                *z as f32,
1578                                final_transform,
1579                            );
1580                            record_transparent_batch(
1581                                &mut transparent_batches,
1582                                *z,
1583                                index_start,
1584                                transparent_indices.len(),
1585                            );
1586                        } else {
1587                            push_rounded_rect(
1588                                &mut vertices,
1589                                &mut indices,
1590                                *rrect,
1591                                color,
1592                                *z as f32,
1593                                final_transform,
1594                            );
1595                        }
1596                    }
1597                    Brush::LinearGradient { start, end, stops } => {
1598                        let packed: Vec<(f32, [f32; 4])> = stops
1599                            .iter()
1600                            .map(|(tpos, c)| (*tpos, premul_opa([c.r, c.g, c.b, c.a], opa)))
1601                            .collect();
1602                        if packed.is_empty() {
1603                            continue;
1604                        }
1605                        let gradient_transparent = packed.iter().any(|(_, c)| is_transparent(c[3]));
1606                        if gradient_transparent {
1607                            let index_start = transparent_indices.len();
1608                            push_rounded_rect_linear_gradient(
1609                                &mut transparent_vertices,
1610                                &mut transparent_indices,
1611                                *rrect,
1612                                *start,
1613                                *end,
1614                                &packed,
1615                                *z as f32,
1616                                final_transform,
1617                            );
1618                            record_transparent_batch(
1619                                &mut transparent_batches,
1620                                *z,
1621                                index_start,
1622                                transparent_indices.len(),
1623                            );
1624                        } else {
1625                            push_rounded_rect_linear_gradient(
1626                                &mut vertices,
1627                                &mut indices,
1628                                *rrect,
1629                                *start,
1630                                *end,
1631                                &packed,
1632                                *z as f32,
1633                                final_transform,
1634                            );
1635                        }
1636                    }
1637                    Brush::RadialGradient {
1638                        center,
1639                        radius,
1640                        stops,
1641                    } => {
1642                        let packed: Vec<(f32, [f32; 4])> = stops
1643                            .iter()
1644                            .map(|(tpos, c)| (*tpos, premul_opa([c.r, c.g, c.b, c.a], opa)))
1645                            .collect();
1646                        if packed.is_empty() {
1647                            continue;
1648                        }
1649                        let gradient_transparent = packed.iter().any(|(_, c)| is_transparent(c[3]));
1650                        if gradient_transparent {
1651                            let index_start = transparent_indices.len();
1652                            push_rounded_rect_radial_gradient(
1653                                &mut transparent_vertices,
1654                                &mut transparent_indices,
1655                                *rrect,
1656                                *center,
1657                                *radius,
1658                                &packed,
1659                                *z as f32,
1660                                final_transform,
1661                            );
1662                            record_transparent_batch(
1663                                &mut transparent_batches,
1664                                *z,
1665                                index_start,
1666                                transparent_indices.len(),
1667                            );
1668                        } else {
1669                            push_rounded_rect_radial_gradient(
1670                                &mut vertices,
1671                                &mut indices,
1672                                *rrect,
1673                                *center,
1674                                *radius,
1675                                &packed,
1676                                *z as f32,
1677                                final_transform,
1678                            );
1679                        }
1680                    }
1681                }
1682            }
1683            Command::StrokeRect {
1684                rect,
1685                stroke,
1686                brush,
1687                transform,
1688                z,
1689                ..
1690            } => {
1691                let final_transform = *transform;
1692                let opa = current_opacity(&opacity_stack);
1693                if let Brush::Solid(col) = brush {
1694                    let color = premul_opa([col.r, col.g, col.b, col.a], opa);
1695                    if is_transparent(color[3]) {
1696                        let index_start = transparent_indices.len();
1697                        push_rect_stroke(
1698                            &mut transparent_vertices,
1699                            &mut transparent_indices,
1700                            *rect,
1701                            *stroke,
1702                            color,
1703                            *z as f32,
1704                            final_transform,
1705                        );
1706                        record_transparent_batch(
1707                            &mut transparent_batches,
1708                            *z,
1709                            index_start,
1710                            transparent_indices.len(),
1711                        );
1712                    } else {
1713                        push_rect_stroke(
1714                            &mut vertices,
1715                            &mut indices,
1716                            *rect,
1717                            *stroke,
1718                            color,
1719                            *z as f32,
1720                            final_transform,
1721                        );
1722                    }
1723                }
1724            }
1725            Command::StrokeRoundedRect {
1726                rrect,
1727                stroke,
1728                brush,
1729                transform,
1730                z,
1731                ..
1732            } => {
1733                let final_transform = *transform;
1734                let opa = current_opacity(&opacity_stack);
1735                if let Brush::Solid(col) = brush {
1736                    let color = premul_opa([col.r, col.g, col.b, col.a], opa);
1737                    if is_transparent(color[3]) {
1738                        let index_start = transparent_indices.len();
1739                        push_rounded_rect_stroke(
1740                            &mut transparent_vertices,
1741                            &mut transparent_indices,
1742                            *rrect,
1743                            *stroke,
1744                            color,
1745                            *z as f32,
1746                            final_transform,
1747                        );
1748                        record_transparent_batch(
1749                            &mut transparent_batches,
1750                            *z,
1751                            index_start,
1752                            transparent_indices.len(),
1753                        );
1754                    } else {
1755                        push_rounded_rect_stroke(
1756                            &mut vertices,
1757                            &mut indices,
1758                            *rrect,
1759                            *stroke,
1760                            color,
1761                            *z as f32,
1762                            final_transform,
1763                        );
1764                    }
1765                }
1766            }
1767            Command::DrawEllipse {
1768                center,
1769                radii,
1770                brush,
1771                transform,
1772                z,
1773                ..
1774            } => {
1775                let final_transform = *transform;
1776                let opa = current_opacity(&opacity_stack);
1777                match brush {
1778                    Brush::Solid(col) => {
1779                        let color = premul_opa([col.r, col.g, col.b, col.a], opa);
1780                        if is_transparent(color[3]) {
1781                            let index_start = transparent_indices.len();
1782                            push_ellipse(
1783                                &mut transparent_vertices,
1784                                &mut transparent_indices,
1785                                *center,
1786                                *radii,
1787                                color,
1788                                *z as f32,
1789                                final_transform,
1790                            );
1791                            record_transparent_batch(
1792                                &mut transparent_batches,
1793                                *z,
1794                                index_start,
1795                                transparent_indices.len(),
1796                            );
1797                        } else {
1798                            push_ellipse(
1799                                &mut vertices,
1800                                &mut indices,
1801                                *center,
1802                                *radii,
1803                                color,
1804                                *z as f32,
1805                                final_transform,
1806                            );
1807                        }
1808                    }
1809                    Brush::RadialGradient {
1810                        center: _gcenter,
1811                        radius: _r,
1812                        stops,
1813                    } => {
1814                        let mut packed: Vec<(f32, [f32; 4])> = stops
1815                            .iter()
1816                            .map(|(t, c)| (*t, premul_opa([c.r, c.g, c.b, c.a], opa)))
1817                            .collect();
1818                        if packed.is_empty() {
1819                            continue;
1820                        }
1821                        if packed.first().unwrap().0 > 0.0 {
1822                            let c = packed.first().unwrap().1;
1823                            packed.insert(0, (0.0, c));
1824                        }
1825                        if packed.last().unwrap().0 < 1.0 {
1826                            let c = packed.last().unwrap().1;
1827                            packed.push((1.0, c));
1828                        }
1829                        let gradient_transparent = packed.iter().any(|(_, c)| is_transparent(c[3]));
1830                        if gradient_transparent {
1831                            let index_start = transparent_indices.len();
1832                            push_ellipse_radial_gradient(
1833                                &mut transparent_vertices,
1834                                &mut transparent_indices,
1835                                *center,
1836                                *radii,
1837                                &packed,
1838                                *z as f32,
1839                                final_transform,
1840                            );
1841                            record_transparent_batch(
1842                                &mut transparent_batches,
1843                                *z,
1844                                index_start,
1845                                transparent_indices.len(),
1846                            );
1847                        } else {
1848                            push_ellipse_radial_gradient(
1849                                &mut vertices,
1850                                &mut indices,
1851                                *center,
1852                                *radii,
1853                                &packed,
1854                                *z as f32,
1855                                final_transform,
1856                            );
1857                        }
1858                    }
1859                    _ => {}
1860                }
1861            }
1862            Command::FillPath {
1863                path,
1864                color,
1865                transform,
1866                z,
1867                ..
1868            } => {
1869                let final_transform = *transform;
1870                let opa = current_opacity(&opacity_stack);
1871                let col = premul_opa([color.r, color.g, color.b, color.a], opa);
1872                if is_transparent(col[3]) {
1873                    let index_start = transparent_indices.len();
1874                    tessellate_path_fill(
1875                        &mut transparent_vertices,
1876                        &mut transparent_indices,
1877                        path,
1878                        col,
1879                        *z as f32,
1880                        final_transform,
1881                    );
1882                    record_transparent_batch(
1883                        &mut transparent_batches,
1884                        *z,
1885                        index_start,
1886                        transparent_indices.len(),
1887                    );
1888                } else {
1889                    tessellate_path_fill(
1890                        &mut vertices,
1891                        &mut indices,
1892                        path,
1893                        col,
1894                        *z as f32,
1895                        final_transform,
1896                    );
1897                }
1898            }
1899            Command::StrokePath {
1900                path,
1901                stroke,
1902                color,
1903                transform,
1904                z,
1905                ..
1906            } => {
1907                let final_transform = *transform;
1908                let opa = current_opacity(&opacity_stack);
1909                let col = premul_opa([color.r, color.g, color.b, color.a], opa);
1910                if is_transparent(col[3]) {
1911                    let index_start = transparent_indices.len();
1912                    tessellate_path_stroke(
1913                        &mut transparent_vertices,
1914                        &mut transparent_indices,
1915                        path,
1916                        *stroke,
1917                        col,
1918                        *z as f32,
1919                        final_transform,
1920                    );
1921                    record_transparent_batch(
1922                        &mut transparent_batches,
1923                        *z,
1924                        index_start,
1925                        transparent_indices.len(),
1926                    );
1927                } else {
1928                    tessellate_path_stroke(
1929                        &mut vertices,
1930                        &mut indices,
1931                        path,
1932                        *stroke,
1933                        col,
1934                        *z as f32,
1935                        final_transform,
1936                    );
1937                }
1938            }
1939            Command::DrawImage {
1940                path,
1941                origin,
1942                size,
1943                z,
1944                transform,
1945            } => {
1946                // Apply the command's world transform to the image origin.
1947                let final_transform = *transform;
1948                let world_origin = apply_transform(*origin, final_transform);
1949                let opa = current_opacity(&opacity_stack);
1950                image_draws.push(ExtractedImageDraw {
1951                    path: path.clone(),
1952                    origin: world_origin,
1953                    size: *size,
1954                    z: *z,
1955                    transform: final_transform,
1956                    opacity: opa,
1957                });
1958            }
1959            Command::DrawSvg {
1960                path,
1961                origin,
1962                max_size,
1963                z,
1964                transform,
1965            } => {
1966                // Apply the command's world transform to the SVG origin.
1967                let final_transform = *transform;
1968                let world_origin = apply_transform(*origin, final_transform);
1969                let opa = current_opacity(&opacity_stack);
1970                svg_draws.push(ExtractedSvgDraw {
1971                    path: path.clone(),
1972                    origin: world_origin,
1973                    size: *max_size,
1974                    z: *z,
1975                    transform: final_transform,
1976                    opacity: opa,
1977                });
1978            }
1979            Command::DrawExternalTexture {
1980                rect,
1981                texture_id,
1982                z,
1983                transform,
1984                opacity,
1985                premultiplied,
1986            } => {
1987                let final_transform = *transform;
1988                let world_origin = apply_transform([rect.x, rect.y], final_transform);
1989                let opa = current_opacity(&opacity_stack);
1990                external_texture_draws.push(ExtractedExternalTextureDraw {
1991                    texture_id: *texture_id,
1992                    origin: world_origin,
1993                    size: [rect.w, rect.h],
1994                    z: *z,
1995                    opacity: *opacity * opa,
1996                    premultiplied: *premultiplied,
1997                });
1998            }
1999            // BoxShadow commands are handled by PassManager as a separate pipeline.
2000            Command::BoxShadow { .. } => {}
2001            // Hit-only regions: intentionally not rendered.
2002            Command::HitRegionRect { .. } => {}
2003            Command::HitRegionRoundedRect { .. } => {}
2004            Command::HitRegionEllipse { .. } => {}
2005            // Clip commands would need special handling in unified rendering
2006            Command::PushClip(_) => {}
2007            Command::PopClip => {}
2008            Command::PushOpacity(alpha) => {
2009                let parent = current_opacity(&opacity_stack);
2010                opacity_stack.push(parent * alpha.clamp(0.0, 1.0));
2011            }
2012            Command::PopOpacity => {
2013                if opacity_stack.len() > 1 {
2014                    opacity_stack.pop();
2015                }
2016            }
2017        }
2018    }
2019
2020    // Ensure index buffer size meets COPY_BUFFER_ALIGNMENT (4 bytes)
2021    let align_indices = |indices: &mut Vec<u16>| {
2022        if (indices.len() % 2) != 0 {
2023            if indices.len() >= 3 {
2024                let a = indices[indices.len() - 3];
2025                let b = indices[indices.len() - 2];
2026                let c = indices[indices.len() - 1];
2027                indices.extend_from_slice(&[a, b, c]);
2028            } else {
2029                indices.push(0);
2030            }
2031        }
2032    };
2033    align_indices(&mut indices);
2034    align_indices(&mut transparent_indices);
2035
2036    // Allocate GPU buffers and upload
2037    let upload_scene = |allocator: &mut RenderAllocator,
2038                        queue: &wgpu::Queue,
2039                        vertices: &[Vertex],
2040                        indices: &[u16]|
2041     -> GpuScene {
2042        let vsize = (vertices.len() * std::mem::size_of::<Vertex>()) as u64;
2043        let isize = (indices.len() * std::mem::size_of::<u16>()) as u64;
2044        let vbuf = allocator.allocate_buffer(BufKey {
2045            size: vsize.max(4),
2046            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2047        });
2048        let ibuf = allocator.allocate_buffer(BufKey {
2049            size: isize.max(4),
2050            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2051        });
2052        if vsize > 0 {
2053            queue.write_buffer(&vbuf.buffer, 0, bytemuck::cast_slice(vertices));
2054        }
2055        if isize > 0 {
2056            queue.write_buffer(&ibuf.buffer, 0, bytemuck::cast_slice(indices));
2057        }
2058        GpuScene {
2059            vertex: vbuf,
2060            index: ibuf,
2061            vertices: vertices.len() as u32,
2062            indices: indices.len() as u32,
2063        }
2064    };
2065    let gpu_scene = upload_scene(allocator, queue, &vertices, &indices);
2066    let transparent_gpu_scene = upload_scene(
2067        allocator,
2068        queue,
2069        &transparent_vertices,
2070        &transparent_indices,
2071    );
2072
2073    Ok(UnifiedSceneData {
2074        gpu_scene,
2075        transparent_gpu_scene,
2076        transparent_batches,
2077        text_draws,
2078        image_draws,
2079        svg_draws,
2080        external_texture_draws,
2081    })
2082}