iced_cube/primitives/
separator.rs1use iced::widget::{column, row, rule, text};
4use iced::{Alignment, Element, Theme};
5
6use crate::theme::{Tokens, space, text_size};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
10pub enum Orientation {
11 #[default]
12 Horizontal,
13 Vertical,
14}
15
16impl Orientation {
17 pub const ALL: [Orientation; 2] = [Orientation::Horizontal, Orientation::Vertical];
18}
19
20#[derive(Debug)]
25pub struct Separator<'a> {
26 orientation: Orientation,
27 label: Option<text::Fragment<'a>>,
28}
29
30pub fn separator<'a>() -> Separator<'a> {
32 Separator {
33 orientation: Orientation::Horizontal,
34 label: None,
35 }
36}
37
38pub fn vertical_separator<'a>() -> Separator<'a> {
40 separator().orientation(Orientation::Vertical)
41}
42
43impl<'a> Separator<'a> {
44 pub fn orientation(mut self, orientation: Orientation) -> Self {
45 self.orientation = orientation;
46 self
47 }
48
49 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
51 self.label = Some(label.into_fragment());
52 self
53 }
54}
55
56impl<'a, Message: 'a> From<Separator<'a>> for Element<'a, Message> {
57 fn from(separator: Separator<'a>) -> Self {
58 let line = move || -> rule::Rule<'a, Theme> {
59 let line = match separator.orientation {
60 Orientation::Horizontal => rule::horizontal(1),
61 Orientation::Vertical => rule::vertical(1),
62 };
63 line.style(|theme| style(&Tokens::of(theme)))
64 };
65
66 let Some(label) = separator.label else {
67 return line().into();
68 };
69
70 let label = text(label).size(text_size::XS).style(|theme| text::Style {
71 color: Some(label_colour(&Tokens::of(theme))),
72 });
73
74 match separator.orientation {
75 Orientation::Horizontal => row![line(), label, line()]
76 .spacing(space::SM)
77 .align_y(Alignment::Center)
78 .into(),
79 Orientation::Vertical => column![line(), label, line()]
80 .spacing(space::SM)
81 .align_x(Alignment::Center)
82 .into(),
83 }
84 }
85}
86
87pub fn style(tokens: &Tokens) -> rule::Style {
89 rule::Style {
90 color: tokens.border,
91 radius: 0.0.into(),
92 fill_mode: rule::FillMode::Full,
93 snap: true,
94 }
95}
96
97pub fn label_colour(tokens: &Tokens) -> iced::Color {
99 tokens.muted_foreground
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::theme::{dark, light};
106
107 #[test]
108 fn default_builder_is_horizontal_without_label() {
109 let s = separator();
110 assert_eq!(s.orientation, Orientation::Horizontal);
111 assert!(s.label.is_none());
112 }
113
114 #[test]
115 fn vertical_constructor_sets_orientation() {
116 assert_eq!(vertical_separator().orientation, Orientation::Vertical);
117 for orientation in Orientation::ALL {
118 assert_eq!(
119 separator().orientation(orientation).orientation,
120 orientation
121 );
122 }
123 }
124
125 #[test]
126 fn label_is_kept() {
127 let s = separator().label("or");
128 assert_eq!(s.label.as_deref(), Some("or"));
129 }
130
131 #[test]
132 fn line_uses_border_token_and_fills() {
133 for theme in [light(), dark()] {
134 let tokens = Tokens::of(&theme);
135 let style = style(&tokens);
136 assert_eq!(style.color, tokens.border);
137 assert_eq!(style.fill_mode, rule::FillMode::Full);
138 assert_ne!(style.color, tokens.background);
139 }
140 }
141
142 #[test]
143 fn label_is_muted_and_readable() {
144 for theme in [light(), dark()] {
145 let tokens = Tokens::of(&theme);
146 assert_eq!(label_colour(&tokens), tokens.muted_foreground);
147 assert_ne!(label_colour(&tokens), tokens.background);
148 }
149 }
150}