Skip to main content

incular_rendering/
paint.rs

1use crate::effects::{BlendMode, ColorFilter};
2use crate::geometry::CornerRadii;
3use crate::gradients::{Brush, Shader};
4use incular_core::finite_non_negative;
5use incular_core::{Color, Lerp, Offset};
6use smart_default::SmartDefault;
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct Border {
10    pub width: f32,
11    pub color: Color,
12}
13impl Border {
14    #[must_use]
15    pub fn new(width: f32, color: Color) -> Self {
16        Self {
17            width: width.max(0.),
18            color,
19        }
20    }
21}
22#[derive(Clone, Debug, Default, PartialEq)]
23pub struct Decoration {
24    pub background: Option<Brush>,
25    pub border: Option<Border>,
26    pub border_radius: CornerRadii,
27}
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub enum FillRule {
30    NonZero,
31    EvenOdd,
32}
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34pub enum LineCap {
35    Butt,
36    Round,
37    Square,
38}
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum LineJoin {
41    Miter,
42    Round,
43    Bevel,
44}
45#[derive(Clone, Copy, Debug, PartialEq, SmartDefault)]
46pub struct Stroke {
47    #[default(1.)]
48    pub width: f32,
49    #[default(LineCap::Butt)]
50    pub cap: LineCap,
51    #[default(LineJoin::Miter)]
52    pub join: LineJoin,
53    #[default(4.)]
54    pub miter_limit: f32,
55}
56
57/// Whether a [`Paint`] covers the interior or the outline of a path.
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
59pub enum PaintStyle {
60    #[default]
61    Fill,
62    Stroke,
63}
64
65/// A renderer-neutral paint description.
66///
67/// `Paint` is deliberately a small immutable builder around the authoritative
68/// [`Brush`], [`Stroke`], [`BlendMode`], and [`ColorFilter`] types. It does not
69/// introduce a second display command model; the canvas path API lowers it
70/// directly to the existing fill/stroke commands.
71#[derive(Clone, Debug, PartialEq, SmartDefault)]
72pub struct Paint {
73    #[default(Brush::Solid(Color::BLACK))]
74    brush: Brush,
75    style: PaintStyle,
76    stroke: Stroke,
77    blend_mode: BlendMode,
78    color_filter: Option<ColorFilter>,
79}
80
81impl Paint {
82    #[must_use]
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    #[must_use]
88    pub fn brush(mut self, brush: impl Into<Brush>) -> Self {
89        self.brush = brush.into();
90        self
91    }
92
93    #[must_use]
94    pub fn shader(self, shader: impl Into<Shader>) -> Self {
95        self.brush(shader)
96    }
97
98    #[must_use]
99    pub fn color(self, color: Color) -> Self {
100        self.brush(color)
101    }
102
103    #[must_use]
104    pub fn style(mut self, style: PaintStyle) -> Self {
105        self.style = style;
106        self
107    }
108
109    #[must_use]
110    pub fn fill(self) -> Self {
111        self.style(PaintStyle::Fill)
112    }
113
114    #[must_use]
115    pub fn stroke(self) -> Self {
116        self.style(PaintStyle::Stroke)
117    }
118
119    #[must_use]
120    pub fn stroke_settings(mut self, stroke: Stroke) -> Self {
121        self.stroke = Stroke {
122            width: finite_non_negative(stroke.width),
123            cap: stroke.cap,
124            join: stroke.join,
125            miter_limit: finite_non_negative(stroke.miter_limit),
126        };
127        self
128    }
129
130    #[must_use]
131    pub fn stroke_width(mut self, width: f32) -> Self {
132        self.stroke.width = finite_non_negative(width);
133        self
134    }
135
136    #[must_use]
137    pub fn blend_mode(mut self, blend_mode: BlendMode) -> Self {
138        self.blend_mode = blend_mode;
139        self
140    }
141
142    #[must_use]
143    pub fn color_filter(mut self, color_filter: Option<ColorFilter>) -> Self {
144        self.color_filter = color_filter;
145        self
146    }
147
148    #[must_use]
149    pub fn brush_value(&self) -> &Brush {
150        &self.brush
151    }
152
153    #[must_use]
154    pub const fn style_value(&self) -> PaintStyle {
155        self.style
156    }
157
158    #[must_use]
159    pub const fn stroke_value(&self) -> Stroke {
160        self.stroke
161    }
162
163    #[must_use]
164    pub const fn blend_mode_value(&self) -> BlendMode {
165        self.blend_mode
166    }
167
168    #[must_use]
169    pub fn color_filter_value(&self) -> Option<ColorFilter> {
170        self.color_filter
171    }
172}
173
174/// Canonical single-shadow value used by text and other paint-producing
175/// domains. Subsystems should use this type instead of defining a parallel
176/// color/offset/blur tuple.
177/// Matches Flutter's `Shadow()` default: opaque black, zero offset, zero blur.
178#[derive(Clone, Copy, Debug, PartialEq, SmartDefault)]
179pub struct Shadow {
180    #[default(Color::BLACK)]
181    pub color: Color,
182    pub offset: Offset,
183    pub blur_radius: f32,
184}
185
186impl Shadow {
187    #[must_use]
188    pub const fn new(color: Color, offset: Offset, blur_radius: f32) -> Self {
189        Self {
190            color,
191            offset,
192            blur_radius: if blur_radius.is_finite() && blur_radius > 0. {
193                blur_radius
194            } else {
195                0.
196            },
197        }
198    }
199}
200
201impl Lerp for Shadow {
202    fn lerp(&self, other: &Self, t: f32) -> Self {
203        Self::new(
204            self.color.lerp(&other.color, t),
205            self.offset.lerp(&other.offset, t),
206            self.blur_radius.lerp(&other.blur_radius, t),
207        )
208    }
209}