fret_ui_kit/primitives/
separator.rs1use fret_core::{Px, SemanticsOrientation, SemanticsRole};
2use fret_ui::element::{AnyElement, ContainerProps, Length, SemanticsDecoration, SizeStyle};
3use fret_ui::{ElementContext, Theme, UiHost};
4
5use crate::LayoutRefinement;
6use crate::declarative::style as decl_style;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum SeparatorOrientation {
10 #[default]
11 Horizontal,
12 Vertical,
13}
14
15#[derive(Debug, Clone)]
16pub struct Separator {
17 orientation: SeparatorOrientation,
18 thickness: Option<Px>,
19 flex_stretch_cross_axis: bool,
20 decorative: bool,
21 layout: LayoutRefinement,
22}
23
24impl Separator {
25 pub fn new() -> Self {
26 Self {
27 orientation: SeparatorOrientation::default(),
28 thickness: None,
29 flex_stretch_cross_axis: false,
30 decorative: false,
31 layout: LayoutRefinement::default(),
32 }
33 }
34
35 pub fn orientation(mut self, orientation: SeparatorOrientation) -> Self {
36 self.orientation = orientation;
37 self
38 }
39
40 pub fn thickness(mut self, thickness: Px) -> Self {
41 self.thickness = Some(thickness);
42 self
43 }
44
45 pub fn flex_stretch_cross_axis(mut self, stretch: bool) -> Self {
46 self.flex_stretch_cross_axis = stretch;
47 self
48 }
49
50 pub fn decorative(mut self, decorative: bool) -> Self {
51 self.decorative = decorative;
52 self
53 }
54
55 pub fn refine_layout(mut self, layout: LayoutRefinement) -> Self {
56 self.layout = self.layout.merge(layout);
57 self
58 }
59
60 #[track_caller]
61 pub fn into_element<H: UiHost>(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
62 let (border, thickness, mut layout) = {
63 let theme = Theme::global(&*cx.app);
64
65 let border = theme
66 .color_by_key("border")
67 .unwrap_or_else(|| theme.color_token("border"));
68 let thickness = self.thickness.unwrap_or_else(|| {
69 theme
70 .metric_by_key("component.separator.px")
71 .unwrap_or(Px(1.0))
72 });
73 let layout = decl_style::layout_style(theme, self.layout);
74
75 (border, thickness, layout)
76 };
77 match self.orientation {
78 SeparatorOrientation::Horizontal => {
79 layout.size = SizeStyle {
80 width: Length::Fill,
81 height: Length::Px(thickness),
82 min_height: Some(Length::Px(thickness)),
83 max_height: Some(Length::Px(thickness)),
84 ..layout.size
85 };
86 }
87 SeparatorOrientation::Vertical => {
88 layout.size = SizeStyle {
89 width: Length::Px(thickness),
90 height: if self.flex_stretch_cross_axis {
95 Length::Auto
96 } else {
97 Length::Fill
98 },
99 min_width: Some(Length::Px(thickness)),
100 max_width: Some(Length::Px(thickness)),
101 ..layout.size
102 };
103 }
104 }
105
106 let mut element = cx.container(
107 ContainerProps {
108 layout,
109 background: Some(border),
110 ..Default::default()
111 },
112 |_cx| Vec::new(),
113 );
114
115 let decoration = if self.decorative {
116 SemanticsDecoration::default().hidden(true)
117 } else {
118 let mut semantics = SemanticsDecoration::default().role(SemanticsRole::Separator);
119 if self.orientation == SeparatorOrientation::Vertical {
120 semantics = semantics.orientation(SemanticsOrientation::Vertical);
121 }
122 semantics
123 };
124 element = element.attach_semantics(decoration);
125
126 element
127 }
128}
129
130#[track_caller]
131pub fn separator<H: UiHost>(cx: &mut ElementContext<'_, H>) -> AnyElement {
132 Separator::new().into_element(cx)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 use fret_app::App;
140 use fret_core::{AppWindowId, Point, Rect, Size};
141 use fret_ui::element::{ElementKind, Length};
142
143 fn bounds_200x100() -> Rect {
144 Rect::new(
145 Point::new(Px(0.0), Px(0.0)),
146 Size::new(Px(200.0), Px(100.0)),
147 )
148 }
149
150 #[test]
151 fn separator_defaults_to_semantic_horizontal_rule() {
152 let window = AppWindowId::default();
153 let mut app = App::new();
154
155 let element =
156 fret_ui::elements::with_element_cx(&mut app, window, bounds_200x100(), "test", |cx| {
157 Separator::new().into_element(cx)
158 });
159
160 let ElementKind::Container(props) = &element.kind else {
161 panic!("expected Separator to render a container");
162 };
163 assert_eq!(props.layout.size.width, Length::Fill);
164 assert_eq!(props.layout.size.height, Length::Px(Px(1.0)));
165
166 let decoration = element
167 .semantics_decoration
168 .as_ref()
169 .expect("expected separator semantics decoration");
170 assert_eq!(decoration.role, Some(SemanticsRole::Separator));
171 assert_eq!(decoration.hidden, None);
172 assert_eq!(decoration.orientation, None);
173 }
174
175 #[test]
176 fn decorative_separator_hides_from_semantics() {
177 let window = AppWindowId::default();
178 let mut app = App::new();
179
180 let element =
181 fret_ui::elements::with_element_cx(&mut app, window, bounds_200x100(), "test", |cx| {
182 Separator::new().decorative(true).into_element(cx)
183 });
184
185 let decoration = element
186 .semantics_decoration
187 .as_ref()
188 .expect("expected decorative separator semantics decoration");
189 assert_eq!(decoration.hidden, Some(true));
190 assert_eq!(decoration.role, None);
191 assert_eq!(decoration.orientation, None);
192 }
193
194 #[test]
195 fn vertical_separator_can_expose_vertical_semantics() {
196 let window = AppWindowId::default();
197 let mut app = App::new();
198
199 let element =
200 fret_ui::elements::with_element_cx(&mut app, window, bounds_200x100(), "test", |cx| {
201 Separator::new()
202 .orientation(SeparatorOrientation::Vertical)
203 .flex_stretch_cross_axis(true)
204 .into_element(cx)
205 });
206
207 let ElementKind::Container(props) = &element.kind else {
208 panic!("expected vertical Separator to render a container");
209 };
210 assert_eq!(props.layout.size.width, Length::Px(Px(1.0)));
211 assert_eq!(props.layout.size.height, Length::Auto);
212
213 let decoration = element
214 .semantics_decoration
215 .as_ref()
216 .expect("expected vertical separator semantics decoration");
217 assert_eq!(decoration.role, Some(SemanticsRole::Separator));
218 assert_eq!(decoration.orientation, Some(SemanticsOrientation::Vertical));
219 assert_eq!(decoration.hidden, None);
220 }
221}