telar_renderer_core/style/
shape.rs1use geometry_core::Rect;
2
3use crate::{BorderRadius, Color};
4
5use super::paint::{FillRule, Paint, Shadow, Stroke};
6
7pub trait ShapeStyle: Sized {
8 fn fill_mut(&mut self) -> &mut Option<Paint>;
9 fn stroke_mut(&mut self) -> &mut Option<Stroke>;
10 fn shadow_mut(&mut self) -> &mut Option<Shadow>;
11
12 fn with_fill(mut self, fill: impl Into<Paint>) -> Self {
13 *self.fill_mut() = Some(fill.into());
14 self
15 }
16 fn with_stroke(mut self, stroke: Stroke) -> Self {
17 *self.stroke_mut() = Some(stroke);
18 self
19 }
20 fn with_shadow(mut self, shadow: Shadow) -> Self {
21 *self.shadow_mut() = Some(shadow);
22 self
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Default)]
33pub enum BorderWidths {
34 #[default]
35 Uniform,
36 PerSide {
37 top: f32,
38 right: f32,
39 bottom: f32,
40 left: f32,
41 },
42}
43
44impl BorderWidths {
45 pub fn per_side(top: f32, right: f32, bottom: f32, left: f32) -> Self {
46 Self::PerSide {
47 top,
48 right,
49 bottom,
50 left,
51 }
52 }
53
54 pub fn resolve(self, stroke_width: f32) -> [f32; 4] {
57 match self {
58 Self::Uniform => [stroke_width; 4],
59 Self::PerSide {
60 top,
61 right,
62 bottom,
63 left,
64 } => [top, right, bottom, left],
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Default)]
70pub struct RectStyle {
71 pub fill: Option<Paint>,
72 pub stroke: Option<Stroke>,
73 pub shadow: Option<Shadow>,
74 pub radius: BorderRadius,
75 pub border_widths: BorderWidths,
78}
79
80impl RectStyle {
81 pub fn filled(color: Color, radius: f32) -> Self {
82 Self {
83 fill: Some(Paint::Solid(color)),
84 radius: BorderRadius::all(radius),
85 ..Self::default()
86 }
87 }
88
89 pub fn with_radius(mut self, radius: BorderRadius) -> Self {
90 self.radius = radius;
91 self
92 }
93
94 pub fn with_border_widths(mut self, widths: BorderWidths) -> Self {
96 self.border_widths = widths;
97 self
98 }
99
100 pub fn border(&self) -> Option<(Paint, [f32; 4])> {
104 let stroke = self.stroke?;
105 let widths = self.border_widths.resolve(stroke.width);
106 widths
107 .iter()
108 .any(|w| *w > 0.0)
109 .then_some((stroke.paint, widths))
110 }
111}
112
113pub fn border_inner_shape(
123 rect: Rect,
124 radius: BorderRadius,
125 widths: [f32; 4],
126) -> Option<(Rect, BorderRadius)> {
127 let [top, right, bottom, left] = widths;
128 let width = rect.width - left - right;
129 let height = rect.height - top - bottom;
130 if !(width > 0.0 && height > 0.0) {
131 return None;
132 }
133 let max_r = width.min(height) * 0.5;
134 let tighten = |r: f32, a: f32, b: f32| (r - a.max(b)).clamp(0.0, max_r);
138 let inner_radius = BorderRadius {
139 top_left: tighten(radius.top_left, top, left),
140 top_right: tighten(radius.top_right, top, right),
141 bottom_right: tighten(radius.bottom_right, bottom, right),
142 bottom_left: tighten(radius.bottom_left, bottom, left),
143 };
144 Some((
145 Rect::new(rect.x + left, rect.y + top, width, height),
146 inner_radius,
147 ))
148}
149
150impl ShapeStyle for RectStyle {
151 fn fill_mut(&mut self) -> &mut Option<Paint> {
152 &mut self.fill
153 }
154 fn stroke_mut(&mut self) -> &mut Option<Stroke> {
155 &mut self.stroke
156 }
157 fn shadow_mut(&mut self) -> &mut Option<Shadow> {
158 &mut self.shadow
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Default)]
163pub struct PathStyle {
164 pub fill: Option<Paint>,
165 pub stroke: Option<Stroke>,
166 pub shadow: Option<Shadow>,
167 pub fill_rule: FillRule,
168}
169
170impl PathStyle {
171 pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
172 self.fill_rule = rule;
173 self
174 }
175}
176
177impl ShapeStyle for PathStyle {
178 fn fill_mut(&mut self) -> &mut Option<Paint> {
179 &mut self.fill
180 }
181 fn stroke_mut(&mut self) -> &mut Option<Stroke> {
182 &mut self.stroke
183 }
184 fn shadow_mut(&mut self) -> &mut Option<Shadow> {
185 &mut self.shadow
186 }
187}
188
189#[cfg(test)]
190mod border_tests {
191 use super::*;
192
193 fn box_100() -> Rect {
194 Rect::new(0.0, 0.0, 100.0, 100.0)
195 }
196
197 #[test]
198 fn a_plain_stroke_still_means_all_four_sides() {
199 let style = RectStyle::default().with_stroke(Stroke::new(Color::BLACK, 2.0));
200 assert_eq!(style.border(), Some((Paint::Solid(Color::BLACK), [2.0; 4])));
201 }
202
203 #[test]
206 fn every_side_at_zero_is_no_border_at_all() {
207 let style = RectStyle::default()
208 .with_stroke(Stroke::new(Color::BLACK, 2.0))
209 .with_border_widths(BorderWidths::per_side(0.0, 0.0, 0.0, 0.0));
210 assert_eq!(style.border(), None);
211 }
212
213 #[test]
214 fn a_side_of_its_own_overrides_the_strokes_width() {
215 let style = RectStyle::default()
216 .with_stroke(Stroke::new(Color::BLACK, 2.0))
217 .with_border_widths(BorderWidths::per_side(0.0, 0.0, 1.0, 0.0));
218 assert_eq!(
219 style.border(),
220 Some((Paint::Solid(Color::BLACK), [0.0, 0.0, 1.0, 0.0]))
221 );
222 }
223
224 #[test]
225 fn a_colourless_box_has_no_border_however_thick_its_sides_are() {
226 let style =
227 RectStyle::default().with_border_widths(BorderWidths::per_side(4.0, 4.0, 4.0, 4.0));
228 assert_eq!(style.border(), None);
229 }
230
231 #[test]
234 fn a_side_at_zero_leaves_the_inner_edge_flush_with_the_outer() {
235 let (inner, _) =
236 border_inner_shape(box_100(), BorderRadius::zero(), [0.0, 0.0, 1.0, 0.0]).unwrap();
237 assert_eq!(inner, Rect::new(0.0, 0.0, 100.0, 99.0));
238 }
239
240 #[test]
243 fn a_uniform_border_insets_every_side_and_tightens_every_corner() {
244 let (inner, radius) =
245 border_inner_shape(box_100(), BorderRadius::all(8.0), [2.0; 4]).unwrap();
246 assert_eq!(inner, Rect::new(2.0, 2.0, 96.0, 96.0));
247 assert_eq!(radius, BorderRadius::all(6.0));
248 }
249
250 #[test]
253 fn a_corner_is_tightened_by_the_thicker_of_the_two_sides_that_meet_there() {
254 let (_, radius) =
255 border_inner_shape(box_100(), BorderRadius::all(10.0), [1.0, 0.0, 0.0, 4.0]).unwrap();
256 assert_eq!(radius.top_left, 6.0, "top 1, left 4 — the left side wins");
257 assert_eq!(radius.top_right, 9.0, "top 1, right 0");
258 assert_eq!(radius.bottom_right, 10.0, "neither side is drawn");
259 assert_eq!(radius.bottom_left, 6.0, "bottom 0, left 4");
260 }
261
262 #[test]
263 fn a_corner_never_tightens_past_straight() {
264 let (_, radius) = border_inner_shape(box_100(), BorderRadius::all(2.0), [8.0; 4]).unwrap();
265 assert_eq!(radius, BorderRadius::zero());
266 }
267
268 #[test]
271 fn a_border_thicker_than_its_box_leaves_no_interior() {
272 assert!(
273 border_inner_shape(box_100(), BorderRadius::zero(), [60.0, 0.0, 60.0, 0.0]).is_none()
274 );
275 assert!(
276 border_inner_shape(box_100(), BorderRadius::zero(), [0.0, 50.0, 0.0, 50.0]).is_none()
277 );
278 }
279}