Skip to main content

oxiui_core/
paint.rs

1//! Draw-command buffer and render-backend abstraction for OxiUI.
2//!
3//! This module defines the canonical *paint* layer:
4//!
5//! - [`DrawCommand`] — a single GPU/CPU-agnostic draw operation.
6//! - [`DrawList`] — an ordered buffer of draw commands with clip-stack
7//!   tracking and accumulated bounds.
8//! - [`RenderBackend`] — a trait that backends implement to consume a
9//!   [`DrawList`].
10//! - Supporting types: [`PathData`], [`PathVerb`], [`StrokeStyle`],
11//!   [`GradientStop`], [`ImageData`], [`ImageFilter`], [`FillRule`],
12//!   [`LineJoin`], [`LineCap`].
13
14use crate::geometry::{Point, Rect, Size};
15use crate::UiError;
16use crate::{Color, FontSpec};
17
18// ── Enums: fill rule, join, cap ─────────────────────────────────────────────
19
20/// The rule used to determine which parts of a self-intersecting path are
21/// considered "inside" for filling purposes.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
23pub enum FillRule {
24    /// The even-odd rule alternates inside/outside on each crossing.
25    EvenOdd,
26    /// The non-zero winding-number rule (the default for most renderers).
27    #[default]
28    NonZero,
29}
30
31/// The style used to join two path segments at a corner.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
33pub enum LineJoin {
34    /// A sharp miter join (clipped at [`StrokeStyle::miter_limit`]).
35    #[default]
36    Miter,
37    /// A flat bevel cut across the outside corner.
38    Bevel,
39    /// A circular arc centered at the corner point.
40    Round,
41}
42
43/// The style applied to the start and end caps of an open path segment.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
45pub enum LineCap {
46    /// No cap — the stroke ends exactly at the path endpoint.
47    #[default]
48    Butt,
49    /// A semicircular cap extending half the stroke width beyond the endpoint.
50    Round,
51    /// A rectangular cap extending half the stroke width beyond the endpoint.
52    Square,
53}
54
55// ── StrokeStyle ─────────────────────────────────────────────────────────────
56
57/// Parameters controlling how a path's outline is stroked.
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct StrokeStyle {
60    /// Stroke width in logical pixels.
61    pub width: f32,
62    /// Corner join style.
63    pub join: LineJoin,
64    /// End-cap style for open sub-paths.
65    pub cap: LineCap,
66    /// Maximum ratio of miter length to stroke width before the join is
67    /// clipped to a bevel.
68    pub miter_limit: f32,
69}
70
71impl Default for StrokeStyle {
72    fn default() -> Self {
73        Self {
74            width: 1.0,
75            join: LineJoin::Miter,
76            cap: LineCap::Butt,
77            miter_limit: 4.0,
78        }
79    }
80}
81
82// ── GradientStop ────────────────────────────────────────────────────────────
83
84/// A single colour stop in a gradient ramp.
85#[derive(Clone, Copy, Debug, PartialEq)]
86pub struct GradientStop {
87    /// Position within the gradient, clamped to `[0.0, 1.0]`.
88    pub offset: f32,
89    /// The colour at this stop.
90    pub color: Color,
91}
92
93impl GradientStop {
94    /// Construct a gradient stop, clamping `offset` to `[0.0, 1.0]`.
95    pub fn new(offset: f32, color: Color) -> Self {
96        Self {
97            offset: offset.clamp(0.0, 1.0),
98            color,
99        }
100    }
101}
102
103// ── BlendMode ───────────────────────────────────────────────────────────────
104
105/// The compositing mode used when drawing subsequent primitives.
106///
107/// Applied to all draw commands after a [`DrawCommand::SetBlendMode`] until
108/// the next blend-mode change or end of the [`DrawList`].
109///
110/// The default mode (at the start of every [`DrawList`]) is [`BlendMode::Normal`]
111/// (standard source-over alpha compositing).
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
113#[non_exhaustive]
114pub enum BlendMode {
115    /// Standard source-over alpha compositing (`Cs + Cd·(1 − αs)`).
116    #[default]
117    Normal,
118    /// Multiply blend: `Cs · Cd` — darkens.
119    Multiply,
120    /// Screen blend: `1 − (1 − Cs)·(1 − Cd)` — lightens.
121    Screen,
122    /// Overlay: multiply for darks, screen for lights.
123    Overlay,
124    /// Darken: `min(Cs, Cd)`.
125    Darken,
126    /// Lighten: `max(Cs, Cd)`.
127    Lighten,
128    /// Source replaces destination (no blending).
129    Copy,
130    /// Destination is unmodified (fully transparent source).
131    Destination,
132}
133
134// ── ImageFilter ─────────────────────────────────────────────────────────────
135
136/// The sampling filter applied when scaling an image.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
138pub enum ImageFilter {
139    /// Nearest-neighbour sampling (blocky, no blending).
140    #[default]
141    Nearest,
142    /// Bilinear interpolation (smoother, slightly blurred).
143    Bilinear,
144}
145
146// ── ImageData ───────────────────────────────────────────────────────────────
147
148/// Owned raw RGBA image data.
149#[derive(Clone, Debug, PartialEq)]
150pub struct ImageData {
151    /// Raw pixel data in row-major RGBA order (`width * height * 4` bytes).
152    pub rgba: Vec<u8>,
153    /// Image width in pixels.
154    pub width: u32,
155    /// Image height in pixels.
156    pub height: u32,
157}
158
159impl ImageData {
160    /// Construct an [`ImageData`] from a raw RGBA byte vector and dimensions.
161    pub fn new(rgba: Vec<u8>, width: u32, height: u32) -> Self {
162        Self {
163            rgba,
164            width,
165            height,
166        }
167    }
168}
169
170// ── PathVerb / PathData ─────────────────────────────────────────────────────
171
172/// A single drawing verb in a [`PathData`] sequence.
173#[derive(Clone, Copy, Debug, PartialEq)]
174pub enum PathVerb {
175    /// Begin a new sub-path at the given point.
176    MoveTo(Point),
177    /// Draw a straight line to the given point.
178    LineTo(Point),
179    /// Draw a quadratic Bézier curve with one control point.
180    QuadTo {
181        /// The single control point.
182        ctrl: Point,
183        /// The end point of the curve.
184        end: Point,
185    },
186    /// Draw a cubic Bézier curve with two control points.
187    CubicTo {
188        /// The first control point.
189        c1: Point,
190        /// The second control point.
191        c2: Point,
192        /// The end point of the curve.
193        end: Point,
194    },
195    /// Close the current sub-path by drawing a line back to the last `MoveTo`.
196    Close,
197}
198
199/// A resolution-independent path built from [`PathVerb`] segments.
200///
201/// Paths are the primitive used for arbitrary filled and stroked shapes.
202/// Build a path with the chaining builder methods, then pass it to
203/// [`DrawList::push_path`] or [`DrawList::push_stroke_path`].
204#[derive(Clone, Debug, Default, PartialEq)]
205pub struct PathData {
206    /// The ordered sequence of drawing verbs that define this path.
207    pub verbs: Vec<PathVerb>,
208    /// The fill rule used when rasterising filled versions of this path.
209    pub fill_rule: FillRule,
210}
211
212impl PathData {
213    /// Construct an empty path with `FillRule::NonZero`.
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    /// Set the [`FillRule`] on this path (builder-style).
219    pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
220        self.fill_rule = rule;
221        self
222    }
223
224    /// Append a `MoveTo` verb.
225    pub fn move_to(&mut self, p: Point) -> &mut Self {
226        self.verbs.push(PathVerb::MoveTo(p));
227        self
228    }
229
230    /// Append a `LineTo` verb.
231    pub fn line_to(&mut self, p: Point) -> &mut Self {
232        self.verbs.push(PathVerb::LineTo(p));
233        self
234    }
235
236    /// Append a quadratic Bézier `QuadTo` verb.
237    pub fn quad_to(&mut self, ctrl: Point, end: Point) -> &mut Self {
238        self.verbs.push(PathVerb::QuadTo { ctrl, end });
239        self
240    }
241
242    /// Append a cubic Bézier `CubicTo` verb.
243    pub fn cubic_to(&mut self, c1: Point, c2: Point, end: Point) -> &mut Self {
244        self.verbs.push(PathVerb::CubicTo { c1, c2, end });
245        self
246    }
247
248    /// Append a `Close` verb, closing the current sub-path.
249    pub fn close(&mut self) -> &mut Self {
250        self.verbs.push(PathVerb::Close);
251        self
252    }
253
254    /// Returns `true` if this path contains no verbs.
255    pub fn is_empty(&self) -> bool {
256        self.verbs.is_empty()
257    }
258
259    /// Conservative axis-aligned bounding box over all control and anchor
260    /// points in the path.
261    ///
262    /// Returns `None` if the path is empty.  Note this is a *control-point*
263    /// AABB, not a tight geometric bounds: Bézier curves can dip outside the
264    /// control-point hull.
265    pub fn bounds(&self) -> Option<Rect> {
266        let mut min_x = f32::MAX;
267        let mut min_y = f32::MAX;
268        let mut max_x = f32::MIN;
269        let mut max_y = f32::MIN;
270        let mut found = false;
271
272        let mut update = |p: Point| {
273            found = true;
274            if p.x < min_x {
275                min_x = p.x;
276            }
277            if p.y < min_y {
278                min_y = p.y;
279            }
280            if p.x > max_x {
281                max_x = p.x;
282            }
283            if p.y > max_y {
284                max_y = p.y;
285            }
286        };
287
288        for verb in &self.verbs {
289            match verb {
290                PathVerb::MoveTo(p) | PathVerb::LineTo(p) => update(*p),
291                PathVerb::QuadTo { ctrl, end } => {
292                    update(*ctrl);
293                    update(*end);
294                }
295                PathVerb::CubicTo { c1, c2, end } => {
296                    update(*c1);
297                    update(*c2);
298                    update(*end);
299                }
300                PathVerb::Close => {}
301            }
302        }
303
304        if found {
305            Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
306        } else {
307            None
308        }
309    }
310}
311
312// ── DrawCommand ─────────────────────────────────────────────────────────────
313
314/// A single, backend-agnostic draw operation.
315///
316/// Commands are stored in a [`DrawList`] and later replayed by a
317/// [`RenderBackend`]. The enum is `#[non_exhaustive]` so that new variants
318/// can be added without breaking downstream code.
319#[derive(Clone, Debug, PartialEq)]
320#[non_exhaustive]
321pub enum DrawCommand {
322    // ── Clipping ──────────────────────────────────────────────────────────
323    /// Push a rectangular clip region onto the clip stack.
324    ///
325    /// All subsequent commands are clipped to the intersection of active clip
326    /// rectangles until the matching [`DrawCommand::PopClip`].
327    PushClip {
328        /// The clip rectangle in logical pixels.
329        rect: Rect,
330    },
331
332    /// Pop the most recently pushed clip rectangle from the clip stack.
333    PopClip,
334
335    // ── Rectangles ────────────────────────────────────────────────────────
336    /// Fill an axis-aligned rectangle with a solid colour.
337    FillRect {
338        /// The rectangle to fill.
339        rect: Rect,
340        /// Fill colour.
341        color: Color,
342    },
343
344    /// Stroke the outline of an axis-aligned rectangle.
345    StrokeRect {
346        /// The rectangle to stroke.
347        rect: Rect,
348        /// Stroke width in logical pixels.
349        thickness: f32,
350        /// Stroke colour.
351        color: Color,
352    },
353
354    /// Fill a rectangle with uniformly rounded corners.
355    FillRoundedRect {
356        /// The rectangle to fill.
357        rect: Rect,
358        /// Corner radius in logical pixels (applied to all four corners).
359        radius: f32,
360        /// Fill colour.
361        color: Color,
362    },
363
364    /// Fill a rectangle with per-corner radii.
365    ///
366    /// `radii` is `[top-left, top-right, bottom-right, bottom-left]`.
367    FillRoundedRectPerCorner {
368        /// The rectangle to fill.
369        rect: Rect,
370        /// Per-corner radii `[tl, tr, br, bl]` in logical pixels.
371        radii: [f32; 4],
372        /// Fill colour.
373        color: Color,
374    },
375
376    // ── Circles / Ellipses ────────────────────────────────────────────────
377    /// Fill a circle with a solid colour.
378    FillCircle {
379        /// Centre point of the circle.
380        center: Point,
381        /// Radius in logical pixels.
382        radius: f32,
383        /// Fill colour.
384        color: Color,
385    },
386
387    /// Fill an ellipse with a solid colour.
388    FillEllipse {
389        /// Centre point of the ellipse.
390        center: Point,
391        /// Horizontal (X-axis) radius in logical pixels.
392        rx: f32,
393        /// Vertical (Y-axis) radius in logical pixels.
394        ry: f32,
395        /// Fill colour.
396        color: Color,
397    },
398
399    // ── Lines ─────────────────────────────────────────────────────────────
400    /// Draw a 1-pixel aliased line segment.
401    Line {
402        /// Start point of the line.
403        from: Point,
404        /// End point of the line.
405        to: Point,
406        /// Line colour.
407        color: Color,
408    },
409
410    /// Draw a 1-pixel anti-aliased line segment.
411    LineAa {
412        /// Start point of the line.
413        from: Point,
414        /// End point of the line.
415        to: Point,
416        /// Line colour.
417        color: Color,
418    },
419
420    /// Draw a thick, filled line segment.
421    LineThick {
422        /// Start point of the line.
423        from: Point,
424        /// End point of the line.
425        to: Point,
426        /// Width of the line in logical pixels.
427        width: f32,
428        /// Line colour.
429        color: Color,
430    },
431
432    /// Draw a dashed line segment.
433    LineDashed {
434        /// Start point of the line.
435        from: Point,
436        /// End point of the line.
437        to: Point,
438        /// Length of each dash in logical pixels.
439        dash_len: f32,
440        /// Length of each gap in logical pixels.
441        gap_len: f32,
442        /// Line colour.
443        color: Color,
444    },
445
446    // ── Paths ─────────────────────────────────────────────────────────────
447    /// Fill a path with a solid colour.
448    FillPath {
449        /// The path to fill.
450        path: PathData,
451        /// Fill colour.
452        color: Color,
453    },
454
455    /// Stroke a path with a solid colour and style.
456    StrokePath {
457        /// The path to stroke.
458        path: PathData,
459        /// Stroke parameters (width, join, cap, miter limit).
460        style: StrokeStyle,
461        /// Stroke colour.
462        color: Color,
463    },
464
465    // ── Gradients ─────────────────────────────────────────────────────────
466    /// Fill a rectangular region with a linear gradient.
467    LinearGradient {
468        /// The destination rectangle (defines the fill area).
469        rect: Rect,
470        /// Start point of the gradient axis.
471        start: Point,
472        /// End point of the gradient axis.
473        end: Point,
474        /// Colour stops defining the ramp.
475        stops: Vec<GradientStop>,
476    },
477
478    /// Fill a rectangular region with a radial gradient.
479    RadialGradient {
480        /// The destination rectangle (defines the fill area).
481        rect: Rect,
482        /// Centre of the radial gradient.
483        center: Point,
484        /// Outer radius of the gradient in logical pixels.
485        radius: f32,
486        /// Colour stops defining the ramp.
487        stops: Vec<GradientStop>,
488    },
489
490    // ── Images ────────────────────────────────────────────────────────────
491    /// Blit a raw RGBA image into a destination rectangle.
492    Image {
493        /// The source image data.
494        image: ImageData,
495        /// Destination rectangle in logical pixels.
496        dest: Rect,
497        /// Resampling filter to use when scaling.
498        filter: ImageFilter,
499    },
500
501    /// Draw an image using 9-slice scaling.
502    ///
503    /// `insets` is `[top, right, bottom, left]` in pixels of the source image.
504    NineSlice {
505        /// The source image data.
506        image: ImageData,
507        /// Destination rectangle in logical pixels.
508        dest: Rect,
509        /// 9-slice insets `[top, right, bottom, left]` in source pixels.
510        insets: [u32; 4],
511    },
512
513    // ── Shadows ───────────────────────────────────────────────────────────
514    /// Draw a box shadow behind a rectangle.
515    BoxShadow {
516        /// The rectangle casting the shadow.
517        rect: Rect,
518        /// Shadow offset relative to `rect`.
519        offset: Point,
520        /// Blur radius in logical pixels (0 = hard edge).
521        blur_radius: f32,
522        /// Shadow colour (typically semi-transparent).
523        color: Color,
524    },
525
526    // ── Text ──────────────────────────────────────────────────────────────
527    /// Draw text into a rectangle.
528    ///
529    /// Full shaping is delegated to the backend; this command is a v1
530    /// placeholder. Backends that do not support text return `Err`.
531    DrawText {
532        /// Bounding rectangle for the text.
533        rect: Rect,
534        /// The string to render.
535        text: String,
536        /// Font specification (family, size, weight, style).
537        font: FontSpec,
538        /// Text colour.
539        color: Color,
540    },
541
542    // ── Blend mode ────────────────────────────────────────────────────────
543    /// Set the blend mode for all subsequent draw commands.
544    ///
545    /// The blend mode remains in effect until the next `SetBlendMode` command
546    /// or the end of the draw list.  The initial blend mode at the start of
547    /// every frame is [`BlendMode::Normal`].
548    ///
549    /// Backends that do not support non-Normal blend modes may ignore this
550    /// command and continue rendering with source-over compositing.
551    SetBlendMode {
552        /// The compositing mode to apply to subsequent commands.
553        mode: BlendMode,
554    },
555
556    // ── Backdrop blur ─────────────────────────────────────────────────────
557    /// Apply a Gaussian blur to the content already rendered behind this rect
558    /// (frosted-glass / backdrop-blur effect).
559    ///
560    /// The blur is applied to the colour data that has been rendered into the
561    /// target *before* this command.  The blurred result replaces the content
562    /// in `rect` on the target.  Commands issued after `BackdropBlur` paint
563    /// on top of the blurred backdrop.
564    ///
565    /// Backends that do not support backdrop blur may ignore this command.
566    BackdropBlur {
567        /// The rectangular region to blur.
568        rect: Rect,
569        /// Blur radius in logical pixels (1 σ ≈ `blur_radius / 3`).
570        blur_radius: f32,
571    },
572}
573
574// ── DrawList ─────────────────────────────────────────────────────────────────
575
576/// An ordered buffer of [`DrawCommand`]s, with integrated clip-stack and bounds
577/// tracking.
578///
579/// Build a list with the typed `push_*` helpers (or the low-level [`push`])
580/// then pass it to a [`RenderBackend::execute`] call.
581///
582/// [`push`]: DrawList::push
583#[derive(Clone, Debug, Default)]
584pub struct DrawList {
585    cmds: Vec<DrawCommand>,
586    clip_depth: usize,
587    bounds: Option<Rect>,
588}
589
590impl DrawList {
591    /// Construct an empty [`DrawList`].
592    pub fn new() -> Self {
593        Self::default()
594    }
595
596    /// Append an arbitrary [`DrawCommand`], automatically updating clip depth
597    /// and accumulated bounds.
598    pub fn push(&mut self, cmd: DrawCommand) {
599        match &cmd {
600            DrawCommand::PushClip { .. } => {
601                self.clip_depth = self.clip_depth.saturating_add(1);
602            }
603            DrawCommand::PopClip => {
604                self.clip_depth = self.clip_depth.saturating_sub(1);
605            }
606            _ => {
607                if let Some(b) = Self::cmd_bounds(&cmd) {
608                    self.bounds = Some(match self.bounds {
609                        None => b,
610                        Some(existing) => existing.union(&b),
611                    });
612                }
613            }
614        }
615        self.cmds.push(cmd);
616    }
617
618    /// Return the number of commands in the list.
619    pub fn len(&self) -> usize {
620        self.cmds.len()
621    }
622
623    /// Return `true` if the list contains no commands.
624    pub fn is_empty(&self) -> bool {
625        self.cmds.is_empty()
626    }
627
628    /// Iterate over all commands in submission order.
629    pub fn iter(&self) -> std::slice::Iter<'_, DrawCommand> {
630        self.cmds.iter()
631    }
632
633    /// Remove all commands and reset clip depth and bounds.
634    pub fn clear(&mut self) {
635        self.cmds.clear();
636        self.clip_depth = 0;
637        self.bounds = None;
638    }
639
640    /// Return the accumulated axis-aligned bounding box of all non-clip draw
641    /// commands, or `None` if no draw commands have been pushed.
642    pub fn bounds(&self) -> Option<Rect> {
643        self.bounds
644    }
645
646    /// Return the current clip-stack depth.  Zero means balanced.
647    pub fn clip_depth(&self) -> usize {
648        self.clip_depth
649    }
650
651    /// Return `true` if the clip stack is balanced (depth == 0).
652    pub fn is_clip_balanced(&self) -> bool {
653        self.clip_depth == 0
654    }
655
656    // ── Typed push helpers ───────────────────────────────────────────────
657
658    /// Push a solid-filled rectangle.
659    pub fn push_rect(&mut self, rect: Rect, color: Color) {
660        self.push(DrawCommand::FillRect { rect, color });
661    }
662
663    /// Push a stroked rectangle outline.
664    pub fn push_stroke_rect(&mut self, rect: Rect, thickness: f32, color: Color) {
665        self.push(DrawCommand::StrokeRect {
666            rect,
667            thickness,
668            color,
669        });
670    }
671
672    /// Push a filled rectangle with uniform corner radius.
673    pub fn push_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
674        self.push(DrawCommand::FillRoundedRect {
675            rect,
676            radius,
677            color,
678        });
679    }
680
681    /// Push a filled rectangle with per-corner radii `[tl, tr, br, bl]`.
682    pub fn push_rounded_rect_per_corner(&mut self, rect: Rect, radii: [f32; 4], color: Color) {
683        self.push(DrawCommand::FillRoundedRectPerCorner { rect, radii, color });
684    }
685
686    /// Push a filled circle.
687    pub fn push_circle(&mut self, center: Point, radius: f32, color: Color) {
688        self.push(DrawCommand::FillCircle {
689            center,
690            radius,
691            color,
692        });
693    }
694
695    /// Push a filled ellipse.
696    pub fn push_ellipse(&mut self, center: Point, rx: f32, ry: f32, color: Color) {
697        self.push(DrawCommand::FillEllipse {
698            center,
699            rx,
700            ry,
701            color,
702        });
703    }
704
705    /// Push a 1-pixel aliased line segment.
706    pub fn push_line(&mut self, from: Point, to: Point, color: Color) {
707        self.push(DrawCommand::Line { from, to, color });
708    }
709
710    /// Push a 1-pixel anti-aliased line segment.
711    pub fn push_line_aa(&mut self, from: Point, to: Point, color: Color) {
712        self.push(DrawCommand::LineAa { from, to, color });
713    }
714
715    /// Push a thick, filled line segment.
716    pub fn push_line_thick(&mut self, from: Point, to: Point, width: f32, color: Color) {
717        self.push(DrawCommand::LineThick {
718            from,
719            to,
720            width,
721            color,
722        });
723    }
724
725    /// Push a dashed line segment.
726    pub fn push_line_dashed(
727        &mut self,
728        from: Point,
729        to: Point,
730        dash_len: f32,
731        gap_len: f32,
732        color: Color,
733    ) {
734        self.push(DrawCommand::LineDashed {
735            from,
736            to,
737            dash_len,
738            gap_len,
739            color,
740        });
741    }
742
743    /// Push a clip rectangle onto the clip stack.
744    pub fn push_clip(&mut self, rect: Rect) {
745        self.push(DrawCommand::PushClip { rect });
746    }
747
748    /// Pop the top clip rectangle from the clip stack.
749    pub fn pop_clip(&mut self) {
750        self.push(DrawCommand::PopClip);
751    }
752
753    /// Push a solid-filled path.
754    pub fn push_path(&mut self, path: PathData, color: Color) {
755        self.push(DrawCommand::FillPath { path, color });
756    }
757
758    /// Push a stroked path.
759    pub fn push_stroke_path(&mut self, path: PathData, style: StrokeStyle, color: Color) {
760        self.push(DrawCommand::StrokePath { path, style, color });
761    }
762
763    /// Push a linear gradient fill over `rect`.
764    pub fn push_gradient_linear(
765        &mut self,
766        rect: Rect,
767        start: Point,
768        end: Point,
769        stops: Vec<GradientStop>,
770    ) {
771        self.push(DrawCommand::LinearGradient {
772            rect,
773            start,
774            end,
775            stops,
776        });
777    }
778
779    /// Push a radial gradient fill over `rect`.
780    pub fn push_gradient_radial(
781        &mut self,
782        rect: Rect,
783        center: Point,
784        radius: f32,
785        stops: Vec<GradientStop>,
786    ) {
787        self.push(DrawCommand::RadialGradient {
788            rect,
789            center,
790            radius,
791            stops,
792        });
793    }
794
795    /// Push a scaled image blit.
796    pub fn push_image(&mut self, image: ImageData, dest: Rect, filter: ImageFilter) {
797        self.push(DrawCommand::Image {
798            image,
799            dest,
800            filter,
801        });
802    }
803
804    /// Push a 9-slice scaled image.
805    pub fn push_nine_slice(&mut self, image: ImageData, dest: Rect, insets: [u32; 4]) {
806        self.push(DrawCommand::NineSlice {
807            image,
808            dest,
809            insets,
810        });
811    }
812
813    /// Push a box shadow.
814    pub fn push_shadow(&mut self, rect: Rect, offset: Point, blur_radius: f32, color: Color) {
815        self.push(DrawCommand::BoxShadow {
816            rect,
817            offset,
818            blur_radius,
819            color,
820        });
821    }
822
823    /// Push a text draw command.
824    pub fn push_text(&mut self, rect: Rect, text: impl Into<String>, font: FontSpec, color: Color) {
825        self.push(DrawCommand::DrawText {
826            rect,
827            text: text.into(),
828            font,
829            color,
830        });
831    }
832
833    /// Set the blend mode for all subsequent draw commands.
834    ///
835    /// The mode remains active until the next `push_blend_mode` call.
836    /// The initial mode at the start of every frame is [`BlendMode::Normal`].
837    pub fn push_blend_mode(&mut self, mode: BlendMode) {
838        self.push(DrawCommand::SetBlendMode { mode });
839    }
840
841    /// Apply a backdrop blur (frosted-glass effect) to the region `rect`.
842    ///
843    /// Blurs the previously-rendered content behind `rect` by `blur_radius` pixels.
844    /// Subsequent commands draw on top of the blurred result.
845    pub fn push_backdrop_blur(&mut self, rect: Rect, blur_radius: f32) {
846        self.push(DrawCommand::BackdropBlur { rect, blur_radius });
847    }
848
849    // ── Private helpers ──────────────────────────────────────────────────
850
851    /// Compute a conservative bounding rect for `cmd`, or `None` for
852    /// clip-stack commands (which don't occupy draw-space geometry).
853    fn cmd_bounds(cmd: &DrawCommand) -> Option<Rect> {
854        match cmd {
855            DrawCommand::FillRect { rect, .. }
856            | DrawCommand::StrokeRect { rect, .. }
857            | DrawCommand::FillRoundedRect { rect, .. }
858            | DrawCommand::FillRoundedRectPerCorner { rect, .. }
859            | DrawCommand::LinearGradient { rect, .. }
860            | DrawCommand::RadialGradient { rect, .. }
861            | DrawCommand::Image { dest: rect, .. }
862            | DrawCommand::NineSlice { dest: rect, .. }
863            | DrawCommand::DrawText { rect, .. } => Some(*rect),
864
865            DrawCommand::BoxShadow {
866                rect,
867                offset,
868                blur_radius,
869                ..
870            } => {
871                let pad = *blur_radius;
872                Some(Rect::new(
873                    rect.left() + offset.x - pad,
874                    rect.top() + offset.y - pad,
875                    rect.width() + 2.0 * pad,
876                    rect.height() + 2.0 * pad,
877                ))
878            }
879
880            DrawCommand::FillCircle { center, radius, .. } => Some(Rect::new(
881                center.x - radius,
882                center.y - radius,
883                radius * 2.0,
884                radius * 2.0,
885            )),
886
887            DrawCommand::FillEllipse { center, rx, ry, .. } => {
888                Some(Rect::new(center.x - rx, center.y - ry, rx * 2.0, ry * 2.0))
889            }
890
891            DrawCommand::Line { from, to, .. } | DrawCommand::LineAa { from, to, .. } => {
892                let x = from.x.min(to.x);
893                let y = from.y.min(to.y);
894                Some(Rect::new(
895                    x,
896                    y,
897                    (from.x - to.x).abs(),
898                    (from.y - to.y).abs(),
899                ))
900            }
901
902            DrawCommand::LineThick {
903                from, to, width, ..
904            } => {
905                let pad = width / 2.0;
906                let x = from.x.min(to.x) - pad;
907                let y = from.y.min(to.y) - pad;
908                let w = (from.x - to.x).abs() + *width;
909                let h = (from.y - to.y).abs() + *width;
910                Some(Rect::new(x, y, w, h))
911            }
912
913            DrawCommand::LineDashed { from, to, .. } => {
914                let x = from.x.min(to.x);
915                let y = from.y.min(to.y);
916                Some(Rect::new(
917                    x,
918                    y,
919                    (from.x - to.x).abs(),
920                    (from.y - to.y).abs(),
921                ))
922            }
923
924            DrawCommand::FillPath { path, .. } => path.bounds(),
925
926            DrawCommand::StrokePath { path, style, .. } => path.bounds().map(|b| {
927                let pad = style.width / 2.0;
928                Rect::new(
929                    b.left() - pad,
930                    b.top() - pad,
931                    b.width() + style.width,
932                    b.height() + style.width,
933                )
934            }),
935
936            DrawCommand::PushClip { .. } | DrawCommand::PopClip => None,
937
938            DrawCommand::SetBlendMode { .. } => None,
939
940            DrawCommand::BackdropBlur { rect, blur_radius } => {
941                let pad = *blur_radius;
942                Some(Rect::new(
943                    rect.left() - pad,
944                    rect.top() - pad,
945                    rect.width() + 2.0 * pad,
946                    rect.height() + 2.0 * pad,
947                ))
948            }
949
950            // Non-exhaustive fallback: catches any future variants added to the
951            // enum before they get explicit bounds entries.
952            #[allow(unreachable_patterns)]
953            _ => None,
954        }
955    }
956}
957
958// ── RenderBackend ─────────────────────────────────────────────────────────────
959
960/// A surface that can consume and render a [`DrawList`].
961///
962/// Implementors are the concrete rendering backends — software rasteriser,
963/// GPU pipeline, SVG emitter, etc.  The trait is intentionally minimal: a
964/// backend need only implement [`execute`] and [`RenderBackend::surface_size`].  The
965/// `supports_*` probes default to `false`; override them to advertise real
966/// capabilities so callers can avoid emitting unsupported commands.
967///
968/// [`execute`]: RenderBackend::execute
969pub trait RenderBackend {
970    /// Replay an entire [`DrawList`] onto the backend's surface.
971    ///
972    /// The whole list is submitted in one call (rather than command-by-command)
973    /// so the backend can guarantee clip-stack continuity across the sequence.
974    fn execute(&mut self, list: &DrawList) -> Result<(), UiError>;
975
976    /// Return the target surface dimensions in *physical* pixels.
977    fn surface_size(&self) -> Size;
978
979    /// Return `true` if this backend can render blur effects (e.g. box shadows).
980    fn supports_blur(&self) -> bool {
981        false
982    }
983
984    /// Return `true` if this backend can render gradient fills.
985    fn supports_gradients(&self) -> bool {
986        false
987    }
988
989    /// Return `true` if this backend can render arbitrary vector paths.
990    fn supports_paths(&self) -> bool {
991        false
992    }
993
994    /// Return `true` if this backend can blit [`ImageData`].
995    fn supports_images(&self) -> bool {
996        false
997    }
998
999    /// Return `true` if this backend can render text via [`DrawCommand::DrawText`].
1000    fn supports_text(&self) -> bool {
1001        false
1002    }
1003
1004    /// Return `true` if this backend honours [`DrawCommand::SetBlendMode`]
1005    /// commands (non-`Normal` blend modes).
1006    ///
1007    /// When `false` the backend renders all primitives with source-over
1008    /// compositing regardless of the active blend mode.
1009    fn supports_blend_modes(&self) -> bool {
1010        false
1011    }
1012
1013    /// Return `true` if this backend can apply a backdrop blur via
1014    /// [`DrawCommand::BackdropBlur`].
1015    fn supports_backdrop_blur(&self) -> bool {
1016        false
1017    }
1018}
1019
1020// ── Tests ─────────────────────────────────────────────────────────────────────
1021
1022#[cfg(test)]
1023mod tests {
1024    use super::*;
1025    use crate::geometry::{Point, Rect};
1026    use crate::Color;
1027
1028    fn red() -> Color {
1029        Color(255, 0, 0, 255)
1030    }
1031    fn blue() -> Color {
1032        Color(0, 0, 255, 255)
1033    }
1034
1035    #[test]
1036    fn draw_list_builder_records_command_sequence() {
1037        let mut dl = DrawList::new();
1038        dl.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
1039        dl.push_clip(Rect::new(0.0, 0.0, 5.0, 5.0));
1040        dl.push_rect(Rect::new(1.0, 1.0, 3.0, 3.0), blue());
1041        dl.pop_clip();
1042        assert_eq!(dl.len(), 4);
1043        // verify order: FillRect, PushClip, FillRect, PopClip
1044        let cmds: Vec<_> = dl.iter().collect();
1045        assert!(matches!(cmds[0], DrawCommand::FillRect { .. }));
1046        assert!(matches!(cmds[1], DrawCommand::PushClip { .. }));
1047        assert!(matches!(cmds[2], DrawCommand::FillRect { .. }));
1048        assert!(matches!(cmds[3], DrawCommand::PopClip));
1049    }
1050
1051    #[test]
1052    fn draw_list_len_and_is_empty() {
1053        let mut dl = DrawList::new();
1054        assert!(dl.is_empty());
1055        assert_eq!(dl.len(), 0);
1056        dl.push_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red());
1057        assert!(!dl.is_empty());
1058        assert_eq!(dl.len(), 1);
1059    }
1060
1061    #[test]
1062    fn clip_push_pop_balance() {
1063        let mut dl = DrawList::new();
1064        assert!(dl.is_clip_balanced());
1065        dl.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
1066        assert_eq!(dl.clip_depth(), 1);
1067        assert!(!dl.is_clip_balanced());
1068        dl.pop_clip();
1069        assert_eq!(dl.clip_depth(), 0);
1070        assert!(dl.is_clip_balanced());
1071        // Extra pop saturates to 0 — no panic, no underflow
1072        dl.pop_clip();
1073        assert_eq!(dl.clip_depth(), 0);
1074    }
1075
1076    #[test]
1077    fn bounds_union_of_draw_commands() {
1078        let mut dl = DrawList::new();
1079        dl.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
1080        dl.push_rect(Rect::new(20.0, 20.0, 5.0, 5.0), blue());
1081        let b = dl.bounds().expect("bounds should be Some");
1082        // union of [0,0,10,10] and [20,20,5,5] = [0,0,25,25]
1083        assert!((b.left() - 0.0).abs() < 0.001);
1084        assert!((b.top() - 0.0).abs() < 0.001);
1085        assert!((b.width() - 25.0).abs() < 0.001);
1086        assert!((b.height() - 25.0).abs() < 0.001);
1087    }
1088
1089    #[test]
1090    fn bounds_excludes_clip_commands() {
1091        let mut dl = DrawList::new();
1092        dl.push_clip(Rect::new(0.0, 0.0, 100.0, 100.0));
1093        dl.pop_clip();
1094        assert!(
1095            dl.bounds().is_none(),
1096            "clip commands must not contribute to bounds"
1097        );
1098    }
1099
1100    #[test]
1101    fn clear_resets_bounds_and_depth() {
1102        let mut dl = DrawList::new();
1103        dl.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
1104        dl.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
1105        dl.clear();
1106        assert!(dl.is_empty());
1107        assert!(dl.bounds().is_none());
1108        assert_eq!(dl.clip_depth(), 0);
1109    }
1110
1111    #[test]
1112    fn path_data_builder_and_bounds() {
1113        let mut p = PathData::new();
1114        p.move_to(Point::new(0.0, 0.0));
1115        p.line_to(Point::new(10.0, 0.0));
1116        p.line_to(Point::new(5.0, 8.0));
1117        p.close();
1118        let b = p.bounds().expect("triangle has bounds");
1119        assert!((b.left() - 0.0).abs() < 0.001);
1120        assert!((b.top() - 0.0).abs() < 0.001);
1121        assert!((b.width() - 10.0).abs() < 0.001);
1122        assert!((b.height() - 8.0).abs() < 0.001);
1123        assert_eq!(p.fill_rule, FillRule::NonZero);
1124        let p2 = PathData::new().with_fill_rule(FillRule::EvenOdd);
1125        assert_eq!(p2.fill_rule, FillRule::EvenOdd);
1126    }
1127
1128    #[test]
1129    fn empty_list_iter_is_empty() {
1130        let dl = DrawList::new();
1131        assert!(dl.iter().next().is_none());
1132    }
1133
1134    #[test]
1135    fn gradient_stop_clamps_offset() {
1136        let s = GradientStop::new(-0.5, red());
1137        assert!((s.offset - 0.0).abs() < 0.001);
1138        let s2 = GradientStop::new(1.5, blue());
1139        assert!((s2.offset - 1.0).abs() < 0.001);
1140    }
1141
1142    #[test]
1143    fn stroke_style_defaults() {
1144        let s = StrokeStyle::default();
1145        assert!((s.width - 1.0).abs() < 0.001);
1146        assert!(matches!(s.join, LineJoin::Miter));
1147        assert!(matches!(s.cap, LineCap::Butt));
1148    }
1149}