teksilo_widgets/primitives/
divider.rs1use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal, StrokeStyle};
28use teksilo_core::accessibility::AccessNodeBuilder;
29use teksilo_core::color_prop::ColorProp;
30use teksilo_core::widget::{LayoutContext, PaintContext, Widget};
31#[cfg(test)]
32use teksilo_tokens::Color;
33use teksilo_tokens::Orientation;
34
35#[derive(Debug)]
38pub struct Divider {
39 orientation: Orientation,
40 thickness: Option<f32>,
41 color: Option<ColorProp>,
42}
43
44impl Divider {
45 pub fn new() -> Self {
47 Self {
48 orientation: Orientation::Horizontal,
49 thickness: None,
50 color: None,
51 }
52 }
53
54 pub fn horizontal() -> Self {
56 Self::new()
57 }
58
59 pub fn vertical() -> Self {
61 Self {
62 orientation: Orientation::Vertical,
63 ..Self::new()
64 }
65 }
66
67 pub fn thickness(mut self, thickness: f32) -> Self {
70 self.thickness = Some(thickness);
71 self
72 }
73
74 pub fn color(mut self, color: impl Into<ColorProp>) -> Self {
77 self.color = Some(color.into());
78 self
79 }
80
81 fn resolved_thickness(&self, _theme: &teksilo_core::Theme) -> f32 {
82 self.thickness.unwrap_or(DIVIDER_THICKNESS)
83 }
84}
85
86pub const DIVIDER_THICKNESS: f32 = 1.0;
90
91impl Default for Divider {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl Widget for Divider {
98 fn layout_response(
99 &self,
100 proposal: SizeProposal,
101 ctx: &LayoutContext,
102 ) -> teksilo_core::widget::LayoutResponse {
103 let thickness = self.resolved_thickness(ctx.theme);
104 match self.orientation {
105 Orientation::Horizontal => {
106 let width = proposal.width.unwrap_or(0.0);
107 Size::new(width, thickness)
108 }
109 Orientation::Vertical => {
110 let height = proposal.height.unwrap_or(0.0);
111 Size::new(thickness, height)
112 }
113 }
114 .into()
115 }
116
117 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
118 let color = self
119 .color
120 .as_ref()
121 .map(|c| c.resolve(ctx.theme, ctx.effective_enabled))
122 .unwrap_or(ctx.theme.colors.divider);
123 let thickness = self.resolved_thickness(ctx.theme);
124 let (from, to) = match self.orientation {
125 Orientation::Horizontal => {
126 let y = bounds.y + bounds.height / 2.0;
127 (Point::new(bounds.x, y), Point::new(bounds.right(), y))
128 }
129 Orientation::Vertical => {
130 let x = bounds.x + bounds.width / 2.0;
131 (Point::new(x, bounds.y), Point::new(x, bounds.bottom()))
132 }
133 };
134 canvas.draw_line(from, to, color, StrokeStyle::solid(thickness));
135 }
136
137 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
138 builder.set_role(teksilo_core::accesskit::Role::Splitter);
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use teksilo_core::widget_tree::WidgetTree;
146
147 #[test]
148 fn horizontal_divider_size() {
149 let mut tree = WidgetTree::new();
150 let d = tree.add(Divider::new());
151 tree.layout(SizeProposal {
152 width: Some(200.0),
153 height: None,
154 });
155 let b = tree.bounds(d);
156 assert!((b.width - 200.0).abs() < 0.01);
157 assert!((b.height - 1.0).abs() < 0.01);
158 }
159
160 #[test]
161 fn vertical_divider_size() {
162 let mut tree = WidgetTree::new();
163 let d = tree.add(Divider::vertical());
164 tree.layout(SizeProposal {
165 width: None,
166 height: Some(100.0),
167 });
168 let b = tree.bounds(d);
169 assert!((b.width - 1.0).abs() < 0.01);
170 assert!((b.height - 100.0).abs() < 0.01);
171 }
172
173 #[test]
174 fn custom_thickness() {
175 let mut tree = WidgetTree::new();
176 let d = tree.add(Divider::new().thickness(3.0));
177 tree.layout(SizeProposal {
178 width: Some(200.0),
179 height: None,
180 });
181 let b = tree.bounds(d);
182 assert!((b.height - 3.0).abs() < 0.01);
183 }
184
185 #[test]
186 fn divider_paints_line() {
187 let mut tree = WidgetTree::new();
188 tree.add(Divider::new().color(Color::RED));
189 tree.layout(SizeProposal::exact(200.0, 100.0));
190 let frame = tree.render();
191 assert!(
192 !frame.decorations.is_empty(),
193 "divider should paint a decoration"
194 );
195 }
196}