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}