Skip to main content

fleuron/style/properties/
edges.rs

1//! The four edges of a box, and the border on each of them.
2
3use serde::{Deserialize, Serialize};
4
5use crate::pages::{Corners, Radius};
6
7use super::exclusion::Coord;
8use super::value::Color;
9
10/// The four edges of a box, in whatever the property resolves to:
11/// points for `margin` and `padding`, a [`Border`] for `border`.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Edges<T = f32> {
14    /// Top edge.
15    pub top: T,
16    /// Right edge.
17    pub right: T,
18    /// Bottom edge.
19    pub bottom: T,
20    /// Left edge.
21    pub left: T,
22}
23
24impl<T: Copy> Edges<T> {
25    /// All four edges the same.
26    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    /// What the edge `which` holds.
36    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    /// The edge `which`.
46    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    /// What the left and right edges take off a measure.
58    pub fn inline(self) -> f32 {
59        self.left + self.right
60    }
61}
62
63/// How a border edge is drawn.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
65#[serde(rename_all = "snake_case")]
66pub enum BorderStyle {
67    /// `none`: the edge is not drawn, whatever width it was given.
68    None,
69    /// `solid`
70    Solid,
71}
72
73/// One border edge: its style, its width and its colour.
74///
75/// The colour is what `border-color` set, and `None` is
76/// `currentColor`: the element's own `color`, whichever order the two
77/// were declared in.
78#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
79pub struct Border {
80    /// How the edge is drawn.
81    pub style: BorderStyle,
82    /// Thickness in points, whether or not the edge is drawn.
83    pub width: f32,
84    /// What it is painted in, or `None` for the element's `color`.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub color: Option<Color>,
87}
88
89impl Border {
90    /// The initial border: `medium` wide, and not drawn.
91    pub const NONE: Border = Border {
92        style: BorderStyle::None,
93        width: MEDIUM,
94        color: None,
95    };
96
97    /// The thickness this edge takes in the flow: nothing unless it
98    /// is drawn.
99    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
107/// `border-width: medium`, the initial value, in points.
108pub(crate) const MEDIUM: f32 = 2.25;
109
110impl Edges<Border> {
111    /// The four used thicknesses.
112    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    /// Whether any edge is drawn.
122    pub fn paints(self) -> bool {
123        let widths = self.widths();
124        widths.top + widths.right + widths.bottom + widths.left > 0.0
125    }
126}
127
128/// `thin`, `medium` and `thick`, in points: the CSS pixel widths a
129/// browser gives them.
130pub(crate) const LINE_WIDTHS: [(&str, f32); 3] =
131    [("thin", 0.75), ("medium", MEDIUM), ("thick", 3.75)];
132
133/// How far one corner of a box is rounded, as the cascade computed it.
134///
135/// `x` runs along the top or bottom edge and `y` along the left or
136/// right edge. A percentage is a percentage of the width of the box
137/// for `x` and of its height for `y`, so it stays one until layout
138/// knows the box.
139#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
140pub struct CornerRadius {
141    /// Along the top or bottom edge.
142    pub x: Coord,
143    /// Along the left or right edge.
144    pub y: Coord,
145}
146
147impl CornerRadius {
148    /// A corner that is not rounded.
149    pub const SQUARE: CornerRadius = CornerRadius {
150        x: Coord::Points(0.0),
151        y: Coord::Points(0.0),
152    };
153}
154
155/// The four corners of a box, each rounded by what `border-radius` set.
156#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
157pub struct BorderRadius {
158    /// The top left corner.
159    pub top_left: CornerRadius,
160    /// The top right corner.
161    pub top_right: CornerRadius,
162    /// The bottom right corner.
163    pub bottom_right: CornerRadius,
164    /// The bottom left corner.
165    pub bottom_left: CornerRadius,
166}
167
168impl BorderRadius {
169    /// Four square corners: the initial value.
170    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    /// What the corner `which` holds.
178    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    /// The corner `which`.
188    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    /// The corners in points, over a box `width` by `height`.
198    ///
199    /// A corner with a radius of zero on either axis is square. Where
200    /// two corners on one edge would add up to more than the edge is
201    /// long, all four scale down together until they fit.
202    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/// Which corner a one-cornered property sets.
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum Corner {
255    /// `-top-left-`
256    TopLeft,
257    /// `-top-right-`
258    TopRight,
259    /// `-bottom-right-`
260    BottomRight,
261    /// `-bottom-left-`
262    BottomLeft,
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    /// Part: a percentage measures the width across and the height
270    /// down, and two corners that would overlap scale down together.
271    #[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        // 30 and 30 along an edge 40 long scale by two thirds.
284        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/// Which edge a one-sided box property sets.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum Edge {
302    /// `-top`
303    Top,
304    /// `-right`
305    Right,
306    /// `-bottom`
307    Bottom,
308    /// `-left`
309    Left,
310}