Skip to main content

oxiui_render_wgpu/gpu/
geometry.rs

1//! CPU-side geometry building for [`WgpuBackend`].
2//!
3//! This module owns:
4//!
5//! - `DrawSegment` — a scissor-bounded range in the solid vertex buffer.
6//! - `GradientDraw` — per-draw gradient vertex + uniform data.
7//! - `build_geometry` — the main CPU geometry builder that walks a
8//!   [`DrawList`] and emits `(Vertex[], DrawSegment[], GradientDraw[],
9//!   TexturedDraw[])`.
10//! - Visibility culling helpers: `cmd_bounds`, `rect_from_points`,
11//!   `rects_intersect`, `scissor_to_rect`.
12//! - Stroke/dashed-line emitters and gradient uniform builders.
13//!
14//! [`WgpuBackend`]: super::renderer::WgpuBackend
15
16use oxiui_core::geometry::Rect;
17use oxiui_core::paint::{DrawCommand, DrawList, GradientStop, ImageFilter};
18use oxiui_core::Color;
19
20use crate::clip::{ClipRect, ClipStack};
21use crate::gpu::buffer::{
22    push_circle_quad, push_ellipse_quad, push_gradient_quad, push_line_quad, push_nine_slice_quads,
23    push_rect_quad, push_rounded_rect_per_corner_quad, push_rounded_rect_quad, push_textured_quad,
24    GradientUniforms, GradientVertex, LineQuadParams, TexQuadParams, Vertex, MAX_GRADIENT_STOPS,
25};
26use crate::gpu::tessellator::{tessellate_fill, tessellate_stroke};
27use crate::gpu::texture::TexturedDraw;
28
29// ── DrawSegment ───────────────────────────────────────────────────────────────
30
31/// A scissor-bounded range of vertices in the solid vertex buffer.
32#[derive(Clone, Copy, Debug)]
33pub(crate) struct DrawSegment {
34    pub(crate) start: u32,
35    pub(crate) end: u32,
36    pub(crate) scissor: Option<[u32; 4]>,
37}
38
39// ── GradientDraw ──────────────────────────────────────────────────────────────
40
41/// Per-draw gradient vertex + uniform data.
42pub(crate) struct GradientDraw {
43    pub(crate) verts: Vec<GradientVertex>,
44    pub(crate) uniforms: GradientUniforms,
45    pub(crate) scissor: Option<[u32; 4]>,
46}
47
48// ── BackdropBlurDraw ──────────────────────────────────────────────────────────
49
50/// Data for a single `BackdropBlur` draw command.
51///
52/// These are collected by [`build_geometry`] and will be executed by the
53/// renderer as a copy-from-colour + Gaussian blur + write-back pass.
54#[allow(dead_code)] // fields consumed in the renderer's backdrop-blur pass
55pub(crate) struct BackdropBlurDraw {
56    /// The region to blur, in pixel coordinates `[x, y, w, h]`.
57    pub(crate) rect: [f32; 4],
58    /// Blur radius in pixels.
59    pub(crate) blur_radius: f32,
60}
61
62// ── Scissor helpers ───────────────────────────────────────────────────────────
63
64/// Compute scissor from clip stack, clamped to the viewport.
65pub(crate) fn scissor_from_stack(
66    stack: &ClipStack,
67    viewport_w: u32,
68    viewport_h: u32,
69) -> Option<[u32; 4]> {
70    let raw = stack.as_scissor()?;
71    Some(clamp_scissor(raw, viewport_w, viewport_h))
72}
73
74/// Clamp a scissor rect to the viewport bounds.
75pub(crate) fn clamp_scissor([x, y, w, h]: [u32; 4], viewport_w: u32, viewport_h: u32) -> [u32; 4] {
76    let x = x.min(viewport_w);
77    let y = y.min(viewport_h);
78    let w = w.min(viewport_w - x);
79    let h = h.min(viewport_h - y);
80    [x, y, w, h]
81}
82
83// ── Main geometry builder ─────────────────────────────────────────────────────
84
85/// Output of [`build_geometry`]: a tuple of the four draw-data collections.
86pub(crate) type GeometryOutput = (
87    Vec<Vertex>,
88    Vec<DrawSegment>,
89    Vec<GradientDraw>,
90    Vec<TexturedDraw>,
91    Vec<BackdropBlurDraw>,
92);
93
94/// Walk `list` and emit all CPU geometry for the solid, gradient and textured passes.
95///
96/// Returns a [`GeometryOutput`] tuple:
97/// `(solid_vertices, draw_segments, gradient_draws, textured_draws, backdrop_blur_draws)`.
98pub(crate) fn build_geometry(list: &DrawList, viewport_w: u32, viewport_h: u32) -> GeometryOutput {
99    let mut verts: Vec<Vertex> = Vec::new();
100    let mut segments: Vec<DrawSegment> = Vec::new();
101    let mut gradient_draws: Vec<GradientDraw> = Vec::new();
102    let mut textured_draws: Vec<TexturedDraw> = Vec::new();
103    let mut backdrop_blur_draws: Vec<BackdropBlurDraw> = Vec::new();
104    let mut stack = ClipStack::new();
105
106    let mut current_scissor = scissor_from_stack(&stack, viewport_w, viewport_h);
107    let mut segment_start: u32 = 0;
108
109    let flush = |segs: &mut Vec<DrawSegment>, start: u32, end: u32, sc: Option<[u32; 4]>| {
110        if end > start {
111            segs.push(DrawSegment {
112                start,
113                end,
114                scissor: sc,
115            });
116        }
117    };
118
119    for cmd in list.iter() {
120        // ── Clip-stack management (always processed, never culled) ─────────
121        match cmd {
122            DrawCommand::PushClip { rect } => {
123                flush(
124                    &mut segments,
125                    segment_start,
126                    verts.len() as u32,
127                    current_scissor,
128                );
129                stack.push(ClipRect::new(
130                    rect.left(),
131                    rect.top(),
132                    rect.width(),
133                    rect.height(),
134                ));
135                current_scissor = scissor_from_stack(&stack, viewport_w, viewport_h);
136                segment_start = verts.len() as u32;
137                continue;
138            }
139            DrawCommand::PopClip => {
140                flush(
141                    &mut segments,
142                    segment_start,
143                    verts.len() as u32,
144                    current_scissor,
145                );
146                stack.pop();
147                current_scissor = scissor_from_stack(&stack, viewport_w, viewport_h);
148                segment_start = verts.len() as u32;
149                continue;
150            }
151            _ => {}
152        }
153
154        // ── Visibility culling ────────────────────────────────────────────
155        // Skip commands whose bounding rect lies entirely outside the
156        // current scissor rect.  When there is no active scissor (full
157        // viewport), nothing is culled.  When the bounding rect is
158        // unknown (None), we render conservatively (no cull).
159        if let Some(scissor) = current_scissor {
160            // Degenerate scissors (zero area) cull everything.
161            if scissor[2] == 0 || scissor[3] == 0 {
162                continue;
163            }
164            if let Some(bounds) = cmd_bounds(cmd) {
165                let scissor_rect = scissor_to_rect(scissor);
166                if !rects_intersect(&bounds, &scissor_rect) {
167                    continue;
168                }
169            }
170        }
171
172        // ── Geometry emission ─────────────────────────────────────────────
173        match cmd {
174            // Clip ops already handled above via `continue`.
175            DrawCommand::PushClip { .. } | DrawCommand::PopClip => {}
176
177            DrawCommand::FillRect { rect, color } => {
178                push_rect_quad(
179                    &mut verts,
180                    rect.left(),
181                    rect.top(),
182                    rect.width(),
183                    rect.height(),
184                    *color,
185                );
186            }
187            DrawCommand::StrokeRect {
188                rect,
189                thickness,
190                color,
191            } => {
192                emit_stroke_rect(
193                    &mut verts,
194                    rect.left(),
195                    rect.top(),
196                    rect.width(),
197                    rect.height(),
198                    *thickness,
199                    *color,
200                );
201            }
202            DrawCommand::FillRoundedRect {
203                rect,
204                radius,
205                color,
206            } => {
207                push_rounded_rect_quad(
208                    &mut verts,
209                    rect.left(),
210                    rect.top(),
211                    rect.width(),
212                    rect.height(),
213                    *radius,
214                    *color,
215                );
216            }
217            DrawCommand::FillRoundedRectPerCorner { rect, radii, color } => {
218                push_rounded_rect_per_corner_quad(
219                    &mut verts,
220                    rect.left(),
221                    rect.top(),
222                    rect.width(),
223                    rect.height(),
224                    *radii,
225                    *color,
226                );
227            }
228            DrawCommand::FillCircle {
229                center,
230                radius,
231                color,
232            } => {
233                push_circle_quad(&mut verts, center.x, center.y, *radius, *color);
234            }
235            DrawCommand::FillEllipse {
236                center,
237                rx,
238                ry,
239                color,
240            } => {
241                push_ellipse_quad(&mut verts, center.x, center.y, *rx, *ry, *color);
242            }
243            DrawCommand::Line { from, to, color } => {
244                push_line_quad(
245                    &mut verts,
246                    LineQuadParams {
247                        from_x: from.x,
248                        from_y: from.y,
249                        to_x: to.x,
250                        to_y: to.y,
251                        half_width: 0.5,
252                        color: *color,
253                        aa_smooth: false,
254                    },
255                );
256            }
257            DrawCommand::LineAa { from, to, color } => {
258                push_line_quad(
259                    &mut verts,
260                    LineQuadParams {
261                        from_x: from.x,
262                        from_y: from.y,
263                        to_x: to.x,
264                        to_y: to.y,
265                        half_width: 0.5,
266                        color: *color,
267                        aa_smooth: true,
268                    },
269                );
270            }
271            DrawCommand::LineThick {
272                from,
273                to,
274                width,
275                color,
276            } => {
277                push_line_quad(
278                    &mut verts,
279                    LineQuadParams {
280                        from_x: from.x,
281                        from_y: from.y,
282                        to_x: to.x,
283                        to_y: to.y,
284                        half_width: width * 0.5,
285                        color: *color,
286                        aa_smooth: true,
287                    },
288                );
289            }
290            DrawCommand::LineDashed {
291                from,
292                to,
293                dash_len,
294                gap_len,
295                color,
296            } => {
297                emit_dashed_line(
298                    &mut verts,
299                    DashedLineParams {
300                        x0: from.x,
301                        y0: from.y,
302                        x1: to.x,
303                        y1: to.y,
304                        dash_len: *dash_len,
305                        gap_len: *gap_len,
306                        color: *color,
307                    },
308                );
309            }
310            DrawCommand::FillPath { path, color } => {
311                tessellate_fill(&mut verts, path, *color);
312            }
313            DrawCommand::StrokePath { path, style, color } => {
314                tessellate_stroke(&mut verts, path, style, *color);
315            }
316            DrawCommand::LinearGradient {
317                rect,
318                start,
319                end,
320                stops,
321            } => {
322                if let Some(gd) = build_gradient_draw_linear(LinearGradientParams {
323                    x: rect.left(),
324                    y: rect.top(),
325                    w: rect.width(),
326                    h: rect.height(),
327                    sx: start.x,
328                    sy: start.y,
329                    ex: end.x,
330                    ey: end.y,
331                    stops,
332                    scissor: current_scissor,
333                }) {
334                    gradient_draws.push(gd);
335                }
336            }
337            DrawCommand::RadialGradient {
338                rect,
339                center,
340                radius,
341                stops,
342            } => {
343                if let Some(gd) = build_gradient_draw_radial(RadialGradientParams {
344                    x: rect.left(),
345                    y: rect.top(),
346                    w: rect.width(),
347                    h: rect.height(),
348                    cx: center.x,
349                    cy: center.y,
350                    radius: *radius,
351                    stops,
352                    scissor: current_scissor,
353                }) {
354                    gradient_draws.push(gd);
355                }
356            }
357            DrawCommand::Image {
358                image,
359                dest,
360                filter,
361            } => {
362                let mut tex_verts = Vec::new();
363                push_textured_quad(
364                    &mut tex_verts,
365                    TexQuadParams {
366                        x: dest.left(),
367                        y: dest.top(),
368                        w: dest.width(),
369                        h: dest.height(),
370                        u0: 0.0,
371                        v0: 0.0,
372                        u1: 1.0,
373                        v1: 1.0,
374                        tint: [1.0, 1.0, 1.0, 1.0],
375                    },
376                );
377                textured_draws.push(TexturedDraw {
378                    verts: tex_verts,
379                    image: image.clone(),
380                    filter: *filter,
381                    scissor: current_scissor,
382                });
383            }
384            DrawCommand::NineSlice {
385                image,
386                dest,
387                insets,
388            } => {
389                let mut tex_verts = Vec::new();
390                push_nine_slice_quads(
391                    &mut tex_verts,
392                    [dest.left(), dest.top(), dest.width(), dest.height()],
393                    image.width,
394                    image.height,
395                    *insets,
396                    [1.0, 1.0, 1.0, 1.0],
397                );
398                textured_draws.push(TexturedDraw {
399                    verts: tex_verts,
400                    image: image.clone(),
401                    filter: ImageFilter::Nearest,
402                    scissor: current_scissor,
403                });
404            }
405            DrawCommand::BackdropBlur { rect, blur_radius } => {
406                // Record a backdrop blur request.  The renderer will execute
407                // this as a copy-from-colour-texture + blur + write-back pass
408                // between the solid and the next pass.
409                backdrop_blur_draws.push(BackdropBlurDraw {
410                    rect: [rect.left(), rect.top(), rect.width(), rect.height()],
411                    blur_radius: *blur_radius,
412                });
413            }
414            // SetBlendMode, BoxShadow: BoxShadow is handled by shadow::collect_shadows;
415            // SetBlendMode is informational for now (blend mode plumbing is
416            // handled by BlendPipelineSet in the renderer).
417            //
418            // DrawText: when the `text` feature is enabled, `DrawText` commands
419            // are pre-expanded into `DrawCommand::Image` blits by
420            // `TextBridge::expand_draw_text_commands` in `renderer.rs` *before*
421            // `build_geometry` is called.  Any residual `DrawText` that reaches
422            // here (e.g. when `text` feature is disabled) is silently skipped.
423            _ => {}
424        }
425    }
426
427    flush(
428        &mut segments,
429        segment_start,
430        verts.len() as u32,
431        current_scissor,
432    );
433    (
434        verts,
435        segments,
436        gradient_draws,
437        textured_draws,
438        backdrop_blur_draws,
439    )
440}
441
442// ── Visibility culling helpers ────────────────────────────────────────────────
443
444/// Compute a conservative bounding [`Rect`] for a draw command, or `None` for
445/// commands that have no geometric extent (clip stack ops) or for which a
446/// conservative bound cannot be computed cheaply (wildcard future variants).
447///
448/// The returned rect is used for scissor-intersection culling only; it need
449/// not be tight, but it must be a *superset* of the actual drawn pixels.
450pub(crate) fn cmd_bounds(cmd: &DrawCommand) -> Option<Rect> {
451    match cmd {
452        DrawCommand::FillRect { rect, .. }
453        | DrawCommand::StrokeRect { rect, .. }
454        | DrawCommand::FillRoundedRect { rect, .. }
455        | DrawCommand::FillRoundedRectPerCorner { rect, .. }
456        | DrawCommand::LinearGradient { rect, .. }
457        | DrawCommand::RadialGradient { rect, .. }
458        | DrawCommand::Image { dest: rect, .. }
459        | DrawCommand::NineSlice { dest: rect, .. }
460        | DrawCommand::DrawText { rect, .. } => Some(*rect),
461
462        DrawCommand::FillCircle { center, radius, .. } => Some(Rect::new(
463            center.x - radius,
464            center.y - radius,
465            radius * 2.0,
466            radius * 2.0,
467        )),
468
469        DrawCommand::FillEllipse { center, rx, ry, .. } => {
470            Some(Rect::new(center.x - rx, center.y - ry, rx * 2.0, ry * 2.0))
471        }
472
473        DrawCommand::Line { from, to, .. } | DrawCommand::LineAa { from, to, .. } => {
474            Some(rect_from_points(*from, *to))
475        }
476
477        DrawCommand::LineThick {
478            from, to, width, ..
479        } => {
480            let r = rect_from_points(*from, *to);
481            Some(Rect::new(
482                r.left() - width,
483                r.top() - width,
484                r.width() + width * 2.0,
485                r.height() + width * 2.0,
486            ))
487        }
488
489        DrawCommand::LineDashed { from, to, .. } => Some(rect_from_points(*from, *to)),
490
491        DrawCommand::FillPath { path, .. } => path.bounds(),
492
493        DrawCommand::StrokePath { path, style, .. } => path.bounds().map(|b| {
494            let pad = style.width / 2.0;
495            Rect::new(
496                b.left() - pad,
497                b.top() - pad,
498                b.width() + style.width,
499                b.height() + style.width,
500            )
501        }),
502
503        // Clip ops have no draw geometry.
504        DrawCommand::PushClip { .. } | DrawCommand::PopClip => None,
505
506        // BoxShadow is handled by collect_shadows/render_shadows before
507        // build_geometry runs; it does not emit solid vertices here, so we
508        // return None so it falls through to the `_ => {}` wildcard below.
509        DrawCommand::BoxShadow { .. } => None,
510
511        // Forward-compatibility: unknown variants are rendered conservatively.
512        _ => None,
513    }
514}
515
516/// Build a bounding rect that covers two points (at least 1×1 in each
517/// dimension so degenerate lines don't collapse to zero area).
518pub(crate) fn rect_from_points(
519    a: oxiui_core::geometry::Point,
520    b: oxiui_core::geometry::Point,
521) -> Rect {
522    let x = a.x.min(b.x);
523    let y = a.y.min(b.y);
524    let w = (a.x - b.x).abs().max(1.0);
525    let h = (a.y - b.y).abs().max(1.0);
526    Rect::new(x, y, w, h)
527}
528
529/// Test whether two axis-aligned rectangles overlap (at least one shared
530/// pixel column and row).  A zero-area overlap (tangent border) is NOT
531/// considered an intersection — it returns `false`.
532pub(crate) fn rects_intersect(a: &Rect, b: &Rect) -> bool {
533    a.left() < b.left() + b.width()
534        && a.left() + a.width() > b.left()
535        && a.top() < b.top() + b.height()
536        && a.top() + a.height() > b.top()
537}
538
539/// Convert a hardware scissor `[x, y, w, h]` back to a floating-point
540/// [`Rect`] for intersection tests.
541pub(crate) fn scissor_to_rect(s: [u32; 4]) -> Rect {
542    Rect::new(s[0] as f32, s[1] as f32, s[2] as f32, s[3] as f32)
543}
544
545// ── Geometry helpers ──────────────────────────────────────────────────────────
546
547pub(crate) fn emit_stroke_rect(
548    out: &mut Vec<Vertex>,
549    x: f32,
550    y: f32,
551    w: f32,
552    h: f32,
553    t: f32,
554    color: Color,
555) {
556    push_rect_quad(out, x, y, w, t, color);
557    push_rect_quad(out, x, y + h - t, w, t, color);
558    push_rect_quad(out, x, y + t, t, h - 2.0 * t, color);
559    push_rect_quad(out, x + w - t, y + t, t, h - 2.0 * t, color);
560}
561
562pub(crate) struct DashedLineParams {
563    pub(crate) x0: f32,
564    pub(crate) y0: f32,
565    pub(crate) x1: f32,
566    pub(crate) y1: f32,
567    pub(crate) dash_len: f32,
568    pub(crate) gap_len: f32,
569    pub(crate) color: Color,
570}
571
572pub(crate) fn emit_dashed_line(out: &mut Vec<Vertex>, p: DashedLineParams) {
573    let DashedLineParams {
574        x0,
575        y0,
576        x1,
577        y1,
578        dash_len,
579        gap_len,
580        color,
581    } = p;
582    let dx = x1 - x0;
583    let dy = y1 - y0;
584    let total = (dx * dx + dy * dy).sqrt();
585    if total < 1e-6 || dash_len <= 0.0 {
586        return;
587    }
588    let ux = dx / total;
589    let uy = dy / total;
590    let period = dash_len + gap_len.max(0.0);
591    if period < 1e-6 {
592        return;
593    }
594    let mut t = 0.0_f32;
595    while t < total {
596        let end = (t + dash_len).min(total);
597        push_line_quad(
598            out,
599            LineQuadParams {
600                from_x: x0 + ux * t,
601                from_y: y0 + uy * t,
602                to_x: x0 + ux * end,
603                to_y: y0 + uy * end,
604                half_width: 0.5,
605                color,
606                aa_smooth: false,
607            },
608        );
609        t += period;
610    }
611}
612
613pub(crate) struct LinearGradientParams<'a> {
614    pub(crate) x: f32,
615    pub(crate) y: f32,
616    pub(crate) w: f32,
617    pub(crate) h: f32,
618    pub(crate) sx: f32,
619    pub(crate) sy: f32,
620    pub(crate) ex: f32,
621    pub(crate) ey: f32,
622    pub(crate) stops: &'a [GradientStop],
623    pub(crate) scissor: Option<[u32; 4]>,
624}
625
626pub(crate) fn build_gradient_draw_linear(p: LinearGradientParams<'_>) -> Option<GradientDraw> {
627    let LinearGradientParams {
628        x,
629        y,
630        w,
631        h,
632        sx,
633        sy,
634        ex,
635        ey,
636        stops,
637        scissor,
638    } = p;
639    let uniforms = build_gradient_uniforms(0, [sx, sy], [ex, ey], 0.0, stops)?;
640    let mut verts = Vec::new();
641    push_gradient_quad(&mut verts, x, y, w, h);
642    Some(GradientDraw {
643        verts,
644        uniforms,
645        scissor,
646    })
647}
648
649pub(crate) struct RadialGradientParams<'a> {
650    pub(crate) x: f32,
651    pub(crate) y: f32,
652    pub(crate) w: f32,
653    pub(crate) h: f32,
654    pub(crate) cx: f32,
655    pub(crate) cy: f32,
656    pub(crate) radius: f32,
657    pub(crate) stops: &'a [GradientStop],
658    pub(crate) scissor: Option<[u32; 4]>,
659}
660
661pub(crate) fn build_gradient_draw_radial(p: RadialGradientParams<'_>) -> Option<GradientDraw> {
662    let RadialGradientParams {
663        x,
664        y,
665        w,
666        h,
667        cx,
668        cy,
669        radius,
670        stops,
671        scissor,
672    } = p;
673    let uniforms = build_gradient_uniforms(1, [cx, cy], [0.0, 0.0], radius, stops)?;
674    let mut verts = Vec::new();
675    push_gradient_quad(&mut verts, x, y, w, h);
676    Some(GradientDraw {
677        verts,
678        uniforms,
679        scissor,
680    })
681}
682
683pub(crate) fn build_gradient_uniforms(
684    gradient_type: u32,
685    p0: [f32; 2],
686    p1: [f32; 2],
687    radius: f32,
688    stops: &[GradientStop],
689) -> Option<GradientUniforms> {
690    if stops.is_empty() {
691        return None;
692    }
693    let count = stops.len().min(MAX_GRADIENT_STOPS);
694    let mut stop_offsets = [[0.0f32; 4]; MAX_GRADIENT_STOPS];
695    let mut stop_colors = [[0.0f32; 4]; MAX_GRADIENT_STOPS];
696    for (i, s) in stops.iter().take(count).enumerate() {
697        stop_offsets[i] = [s.offset, 0.0, 0.0, 0.0];
698        stop_colors[i] = [
699            s.color.0 as f32 / 255.0,
700            s.color.1 as f32 / 255.0,
701            s.color.2 as f32 / 255.0,
702            s.color.3 as f32 / 255.0,
703        ];
704    }
705    Some(GradientUniforms {
706        p0,
707        p1,
708        radius,
709        gradient_type,
710        stop_count: count as u32,
711        _pad: 0,
712        stop_offsets,
713        stop_colors,
714    })
715}
716
717// ── Tests ─────────────────────────────────────────────────────────────────────
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722    use oxiui_core::geometry::Rect;
723    use oxiui_core::paint::{DrawList, ImageData, ImageFilter};
724    use oxiui_core::Color;
725
726    /// A solid rect produces exactly 6 vertices (2 triangles × 3 vertices).
727    #[test]
728    fn solid_rect_produces_6_vertices() {
729        let mut list = DrawList::new();
730        list.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color(255, 0, 0, 255));
731        let (verts, segments, grads, textures, _blurs) = build_geometry(&list, 100, 100);
732        assert_eq!(verts.len(), 6, "one rect = 6 vertices");
733        assert_eq!(segments.len(), 1, "one draw segment");
734        assert!(grads.is_empty());
735        assert!(textures.is_empty());
736    }
737
738    /// N solid rects produce N×6 vertices in a single draw segment.
739    #[test]
740    fn n_solid_rects_produce_n_times_6_vertices() {
741        const N: usize = 5;
742        let mut list = DrawList::new();
743        for i in 0..N {
744            list.push_rect(
745                Rect::new(i as f32 * 12.0, 0.0, 10.0, 10.0),
746                Color(255, 0, 0, 255),
747            );
748        }
749        let (verts, segments, _, _, _) = build_geometry(&list, 200, 100);
750        assert_eq!(verts.len(), N * 6, "{N} rects = {} vertices", N * 6);
751        assert_eq!(segments.len(), 1, "all in one draw segment");
752    }
753
754    /// An image command produces one TexturedDraw with 6 vertices (2 triangles).
755    #[test]
756    fn image_produces_one_textured_draw_with_6_vertices() {
757        let mut list = DrawList::new();
758        list.push_image(
759            ImageData::new(vec![255, 0, 0, 255], 1, 1),
760            Rect::new(0.0, 0.0, 10.0, 10.0),
761            ImageFilter::Nearest,
762        );
763        let (_verts, _segs, _grads, textures, _blurs) = build_geometry(&list, 100, 100);
764        assert_eq!(textures.len(), 1, "one textured draw");
765        // push_textured_quad emits 6 vertices (quad = 2 triangles)
766        assert_eq!(textures[0].verts.len(), 6, "2 triangles = 6 vertices");
767    }
768
769    /// Clip push/pop correctly produces 3 draw segments (before/inside/after clip).
770    #[test]
771    fn clip_pushpop_produces_correct_segments() {
772        let mut list = DrawList::new();
773        // Rect before clip.
774        list.push_rect(Rect::new(0.0, 0.0, 5.0, 5.0), Color(255, 0, 0, 255));
775        // Push a clip, draw inside, pop clip.
776        list.push_clip(Rect::new(0.0, 0.0, 50.0, 50.0));
777        list.push_rect(Rect::new(1.0, 1.0, 4.0, 4.0), Color(0, 255, 0, 255));
778        list.pop_clip();
779        // Rect after clip.
780        list.push_rect(Rect::new(10.0, 0.0, 5.0, 5.0), Color(0, 0, 255, 255));
781
782        let (verts, segments, _, _, _) = build_geometry(&list, 100, 100);
783        // 3 rects × 6 vertices each = 18 total solid vertices.
784        assert_eq!(verts.len(), 18, "3 rects = 18 vertices");
785        // Three distinct segments: pre-clip, clipped, post-clip.
786        assert_eq!(segments.len(), 3, "three draw segments for push/pop clip");
787    }
788
789    /// Scissor culling drops rects outside the scissor rect.
790    #[test]
791    fn scissor_culls_offscreen_rects() {
792        let mut list = DrawList::new();
793        // Small scissor region: 10×10 at origin.
794        list.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
795        // Inside rect: should NOT be culled.
796        list.push_rect(Rect::new(0.0, 0.0, 5.0, 5.0), Color(255, 0, 0, 255));
797        // Completely outside: should be culled.
798        list.push_rect(Rect::new(100.0, 100.0, 5.0, 5.0), Color(0, 255, 0, 255));
799        list.pop_clip();
800
801        let (verts, _, _, _, _) = build_geometry(&list, 200, 200);
802        // Only the inside rect's 6 vertices should be emitted.
803        assert_eq!(
804            verts.len(),
805            6,
806            "outside rect must be culled; only 6 vertices expected"
807        );
808    }
809}