1use sequoia_core::{Color, Edges, Length, Rect, Size};
2use sequoia_layout::{LayoutCtx, LayoutStyle};
3use sequoia_style::DividerStyle;
4use sequoia_view::{PaintCtx, Unit, View};
5
6const THICKNESS: f32 = 1.0;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum DividerAxis {
11 #[default]
12 Horizontal,
13
14 Vertical,
15}
16
17pub fn divider_line_rect(rect: Rect, axis: DividerAxis, thickness: f32) -> Rect {
18 let t = thickness.max(0.0);
19 match axis {
20 DividerAxis::Horizontal => {
21 let h = t.min(rect.size.height.max(0.0));
22 Rect::from_xywh(
23 rect.left(),
24 rect.top() + (rect.size.height - h) * 0.5,
25 rect.size.width.max(0.0),
26 h,
27 )
28 }
29 DividerAxis::Vertical => {
30 let w = t.min(rect.size.width.max(0.0));
31 Rect::from_xywh(
32 rect.left() + (rect.size.width - w) * 0.5,
33 rect.top(),
34 w,
35 rect.size.height.max(0.0),
36 )
37 }
38 }
39}
40
41pub struct Divider {
42 axis: DividerAxis,
43 style: Option<DividerStyle>,
44 color: Option<Color>,
45 thickness: Option<f32>,
46 margin: Edges,
47}
48
49impl Divider {
50 pub fn horizontal() -> Self {
51 Self {
52 axis: DividerAxis::Horizontal,
53 style: None,
54 color: None,
55 thickness: None,
56 margin: Edges::ZERO,
57 }
58 }
59
60 pub fn vertical() -> Self {
61 Self {
62 axis: DividerAxis::Vertical,
63 style: None,
64 color: None,
65 thickness: None,
66 margin: Edges::ZERO,
67 }
68 }
69
70 pub fn axis(mut self, axis: DividerAxis) -> Self {
71 self.axis = axis;
72 self
73 }
74
75 pub fn style(mut self, style: DividerStyle) -> Self {
76 self.style = Some(style);
77 self
78 }
79
80 pub fn color(mut self, color: Color) -> Self {
81 self.color = Some(color);
82 self
83 }
84
85 pub fn thickness(mut self, px: f32) -> Self {
86 self.thickness = Some(px);
87 self
88 }
89
90 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
91 let m = margin.into();
92 self.margin = Edges::only(
93 sanitize_margin(m.left),
94 sanitize_margin(m.top),
95 sanitize_margin(m.right),
96 sanitize_margin(m.bottom),
97 );
98 self
99 }
100
101 pub fn margin_xy(self, x: f32, y: f32) -> Self {
102 self.margin(Edges::xy(x, y))
103 }
104
105 pub fn margin_x(mut self, x: f32) -> Self {
106 let x = sanitize_margin(x);
107 self.margin.left = x;
108 self.margin.right = x;
109 self
110 }
111
112 pub fn margin_y(mut self, y: f32) -> Self {
113 let y = sanitize_margin(y);
114 self.margin.top = y;
115 self.margin.bottom = y;
116 self
117 }
118
119 pub fn margin_left(mut self, v: f32) -> Self {
120 self.margin.left = sanitize_margin(v);
121 self
122 }
123
124 pub fn margin_right(mut self, v: f32) -> Self {
125 self.margin.right = sanitize_margin(v);
126 self
127 }
128
129 pub fn margin_top(mut self, v: f32) -> Self {
130 self.margin.top = sanitize_margin(v);
131 self
132 }
133
134 pub fn margin_bottom(mut self, v: f32) -> Self {
135 self.margin.bottom = sanitize_margin(v);
136 self
137 }
138
139 fn resolved(&self, style: DividerStyle, theme: &sequoia_style::Theme) -> (Color, f32) {
140 let _ = theme;
141 let color = self.color.or(style.color).unwrap_or(Color::hex(0x1F1F1F));
142 let thickness = self
143 .thickness
144 .or(style.thickness)
145 .unwrap_or(THICKNESS)
146 .max(0.0);
147 (color, thickness)
148 }
149}
150
151impl Default for Divider {
152 fn default() -> Self {
153 Self::horizontal()
154 }
155}
156
157fn sanitize_margin(v: f32) -> f32 {
158 if v.is_finite() { v.max(0.0) } else { 0.0 }
159}
160
161impl View for Divider {
162 fn body(&self) -> impl View + 'static {
163 Unit
164 }
165 fn is_leaf(&self) -> bool {
166 true
167 }
168 fn hit_testable(&self) -> bool {
169 false
170 }
171 fn layout_style(&self) -> LayoutStyle {
172 match self.axis {
173 DividerAxis::Horizontal => LayoutStyle {
174 width: Length::Fill(1.0),
175 height: Length::Fit,
176 margin: self.margin,
177 ..LayoutStyle::default().hit_testable(false)
178 },
179 DividerAxis::Vertical => LayoutStyle {
180 width: Length::Fit,
181 height: Length::Fill(1.0),
182 margin: self.margin,
183 ..LayoutStyle::default().hit_testable(false)
184 },
185 }
186 }
187 fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
188 let style = self.style.unwrap_or_default();
189 let t = self
190 .thickness
191 .or(style.thickness)
192 .unwrap_or(THICKNESS)
193 .max(0.0);
194 Some(match self.axis {
195 DividerAxis::Horizontal => Size::new(0.0, t),
196 DividerAxis::Vertical => Size::new(t, 0.0),
197 })
198 }
199 fn paint_self(&self, ctx: &mut PaintCtx) {
200 let style = self.style.unwrap_or_default();
201 let theme = ctx.theme();
202 let (color, thickness) = self.resolved(style, &theme);
203 if thickness <= 0.0 || color.a <= 0.0 {
204 return;
205 }
206 let line = divider_line_rect(ctx.rect, self.axis, thickness);
207 if line.size.width <= 0.0 || line.size.height <= 0.0 {
208 return;
209 }
210 ctx.quad(line, color, 0.0);
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn horizontal_centers_thickness() {
220 let rect = Rect::from_xywh(0.0, 0.0, 200.0, 21.0);
221 assert_eq!(
222 divider_line_rect(rect, DividerAxis::Horizontal, 1.0),
223 Rect::from_xywh(0.0, 10.0, 200.0, 1.0)
224 );
225 }
226
227 #[test]
228 fn vertical_centers_thickness() {
229 let rect = Rect::from_xywh(0.0, 0.0, 21.0, 200.0);
230 assert_eq!(
231 divider_line_rect(rect, DividerAxis::Vertical, 1.0),
232 Rect::from_xywh(10.0, 0.0, 1.0, 200.0)
233 );
234 }
235
236 #[test]
237 fn thickness_never_overflows_rect() {
238 let rect = Rect::from_xywh(5.0, 5.0, 10.0, 2.0);
239 let line = divider_line_rect(rect, DividerAxis::Horizontal, 8.0);
240 assert_eq!(line.size.height, 2.0);
241 assert_eq!(line.top(), 5.0);
242 }
243}