fleuron/style/properties/
edges.rs1use serde::{Deserialize, Serialize};
4
5use crate::pages::{Corners, Radius};
6
7use super::exclusion::Coord;
8use super::value::Color;
9
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Edges<T = f32> {
14 pub top: T,
16 pub right: T,
18 pub bottom: T,
20 pub left: T,
22}
23
24impl<T: Copy> Edges<T> {
25 pub const fn all(value: T) -> Edges<T> {
27 Edges {
28 top: value,
29 right: value,
30 bottom: value,
31 left: value,
32 }
33 }
34
35 pub fn get(&self, which: Edge) -> T {
37 match which {
38 Edge::Top => self.top,
39 Edge::Right => self.right,
40 Edge::Bottom => self.bottom,
41 Edge::Left => self.left,
42 }
43 }
44
45 pub fn edge(&mut self, which: Edge) -> &mut T {
47 match which {
48 Edge::Top => &mut self.top,
49 Edge::Right => &mut self.right,
50 Edge::Bottom => &mut self.bottom,
51 Edge::Left => &mut self.left,
52 }
53 }
54}
55
56impl Edges<f32> {
57 pub fn inline(self) -> f32 {
59 self.left + self.right
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
65#[serde(rename_all = "snake_case")]
66pub enum BorderStyle {
67 None,
69 Solid,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
79pub struct Border {
80 pub style: BorderStyle,
82 pub width: f32,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub color: Option<Color>,
87}
88
89impl Border {
90 pub const NONE: Border = Border {
92 style: BorderStyle::None,
93 width: MEDIUM,
94 color: None,
95 };
96
97 pub fn used(self) -> f32 {
100 match self.style {
101 BorderStyle::None => 0.0,
102 BorderStyle::Solid => self.width.max(0.0),
103 }
104 }
105}
106
107pub(crate) const MEDIUM: f32 = 2.25;
109
110impl Edges<Border> {
111 pub fn widths(self) -> Edges {
113 Edges {
114 top: self.top.used(),
115 right: self.right.used(),
116 bottom: self.bottom.used(),
117 left: self.left.used(),
118 }
119 }
120
121 pub fn paints(self) -> bool {
123 let widths = self.widths();
124 widths.top + widths.right + widths.bottom + widths.left > 0.0
125 }
126}
127
128pub(crate) const LINE_WIDTHS: [(&str, f32); 3] =
131 [("thin", 0.75), ("medium", MEDIUM), ("thick", 3.75)];
132
133#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
140pub struct CornerRadius {
141 pub x: Coord,
143 pub y: Coord,
145}
146
147impl CornerRadius {
148 pub const SQUARE: CornerRadius = CornerRadius {
150 x: Coord::Points(0.0),
151 y: Coord::Points(0.0),
152 };
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
157pub struct BorderRadius {
158 pub top_left: CornerRadius,
160 pub top_right: CornerRadius,
162 pub bottom_right: CornerRadius,
164 pub bottom_left: CornerRadius,
166}
167
168impl BorderRadius {
169 pub const SQUARE: BorderRadius = BorderRadius {
171 top_left: CornerRadius::SQUARE,
172 top_right: CornerRadius::SQUARE,
173 bottom_right: CornerRadius::SQUARE,
174 bottom_left: CornerRadius::SQUARE,
175 };
176
177 pub fn get(&self, which: Corner) -> CornerRadius {
179 match which {
180 Corner::TopLeft => self.top_left,
181 Corner::TopRight => self.top_right,
182 Corner::BottomRight => self.bottom_right,
183 Corner::BottomLeft => self.bottom_left,
184 }
185 }
186
187 pub fn corner(&mut self, which: Corner) -> &mut CornerRadius {
189 match which {
190 Corner::TopLeft => &mut self.top_left,
191 Corner::TopRight => &mut self.top_right,
192 Corner::BottomRight => &mut self.bottom_right,
193 Corner::BottomLeft => &mut self.bottom_left,
194 }
195 }
196
197 pub fn resolve(&self, width: f32, height: f32) -> Corners {
203 let one = |corner: CornerRadius| {
204 let radius = Radius {
205 x: corner.x.to_points(width).max(0.0),
206 y: corner.y.to_points(height).max(0.0),
207 };
208 if radius.is_square() {
209 Radius::SQUARE
210 } else {
211 radius
212 }
213 };
214 let [top_left, top_right, bottom_right, bottom_left] = [
215 self.top_left,
216 self.top_right,
217 self.bottom_right,
218 self.bottom_left,
219 ]
220 .map(one);
221 let fit = |edge: f32, sum: f32| {
222 if sum > edge {
223 (edge / sum).max(0.0)
224 } else {
225 1.0
226 }
227 };
228 let scale = fit(width, top_left.x + top_right.x)
229 .min(fit(width, bottom_left.x + bottom_right.x))
230 .min(fit(height, top_left.y + bottom_left.y))
231 .min(fit(height, top_right.y + bottom_right.y));
232 let scaled = |radius: Radius| {
233 let radius = Radius {
234 x: radius.x * scale,
235 y: radius.y * scale,
236 };
237 if radius.is_square() {
238 Radius::SQUARE
239 } else {
240 radius
241 }
242 };
243 Corners {
244 top_left: scaled(top_left),
245 top_right: scaled(top_right),
246 bottom_right: scaled(bottom_right),
247 bottom_left: scaled(bottom_left),
248 }
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum Corner {
255 TopLeft,
257 TopRight,
259 BottomRight,
261 BottomLeft,
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
272 fn radii_resolve_against_the_box_and_scale_down_to_fit() {
273 let all = |x: Coord, y: Coord| BorderRadius {
274 top_left: CornerRadius { x, y },
275 top_right: CornerRadius { x, y },
276 bottom_right: CornerRadius { x, y },
277 bottom_left: CornerRadius { x, y },
278 };
279 let halves = all(Coord::Percent(50.0), Coord::Percent(50.0)).resolve(80.0, 20.0);
280 assert_eq!(halves.top_left, Radius { x: 40.0, y: 10.0 });
281 assert_eq!(halves.bottom_right, Radius { x: 40.0, y: 10.0 });
282
283 let crowded = all(Coord::Points(30.0), Coord::Points(30.0)).resolve(40.0, 100.0);
285 let scale = 40.0 / 60.0;
286 assert_eq!(
287 crowded.top_left,
288 Radius {
289 x: 30.0 * scale,
290 y: 30.0 * scale
291 }
292 );
293
294 let flat = all(Coord::Points(6.0), Coord::Points(0.0)).resolve(80.0, 20.0);
295 assert!(flat.is_square(), "{flat:?}");
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum Edge {
302 Top,
304 Right,
306 Bottom,
308 Left,
310}