Skip to main content

ui/components/
divider.rs

1use gpui::{Hsla, IntoElement, PathBuilder, canvas, point};
2
3use crate::prelude::*;
4
5pub fn divider() -> Divider {
6    Divider {
7        style: DividerStyle::Solid,
8        direction: DividerDirection::Horizontal,
9        color: DividerColor::default(),
10        inset: false,
11        label: None,
12    }
13}
14
15pub fn vertical_divider() -> Divider {
16    Divider {
17        style: DividerStyle::Solid,
18        direction: DividerDirection::Vertical,
19        color: DividerColor::default(),
20        inset: false,
21        label: None,
22    }
23}
24
25#[derive(Clone, Copy, PartialEq)]
26enum DividerStyle {
27    Solid,
28    Dashed,
29}
30
31#[derive(Clone, Copy, PartialEq)]
32enum DividerDirection {
33    Horizontal,
34    Vertical,
35}
36
37/// The color of a [`Divider`].
38#[derive(Default)]
39pub enum DividerColor {
40    Border,
41    BorderFaded,
42    #[default]
43    BorderVariant,
44}
45
46impl DividerColor {
47    pub fn hsla(self, cx: &mut App) -> Hsla {
48        match self {
49            DividerColor::Border => semantic::border(cx),
50            DividerColor::BorderFaded => semantic::border(cx).opacity(0.6),
51            DividerColor::BorderVariant => semantic::border_muted(cx),
52        }
53    }
54}
55
56#[derive(IntoElement, RegisterComponent)]
57pub struct Divider {
58    style: DividerStyle,
59    direction: DividerDirection,
60    color: DividerColor,
61    inset: bool,
62    label: Option<SharedString>,
63}
64
65impl Divider {
66    pub fn horizontal() -> Self {
67        Self {
68            style: DividerStyle::Solid,
69            direction: DividerDirection::Horizontal,
70            color: DividerColor::default(),
71            inset: false,
72            label: None,
73        }
74    }
75
76    pub fn vertical() -> Self {
77        Self {
78            style: DividerStyle::Solid,
79            direction: DividerDirection::Vertical,
80            color: DividerColor::default(),
81            inset: false,
82            label: None,
83        }
84    }
85
86    pub fn horizontal_dashed() -> Self {
87        Self {
88            style: DividerStyle::Dashed,
89            direction: DividerDirection::Horizontal,
90            color: DividerColor::default(),
91            inset: false,
92            label: None,
93        }
94    }
95
96    pub fn vertical_dashed() -> Self {
97        Self {
98            style: DividerStyle::Dashed,
99            direction: DividerDirection::Vertical,
100            color: DividerColor::default(),
101            inset: false,
102            label: None,
103        }
104    }
105
106    pub fn inset(mut self) -> Self {
107        self.inset = true;
108        self
109    }
110
111    pub fn color(mut self, color: DividerColor) -> Self {
112        self.color = color;
113        self
114    }
115
116    /// Renders a label centered on the divider line, with the line
117    /// continuing on both sides. Only supported for horizontal dividers.
118    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
119        self.label = Some(label.into());
120        self
121    }
122
123    pub fn render_solid(self, base: Div, cx: &mut App) -> impl IntoElement {
124        base.bg(self.color.hsla(cx))
125    }
126
127    pub fn render_dashed(self, base: Div) -> impl IntoElement {
128        base.relative().child(
129            canvas(
130                |_, _, _| {},
131                move |bounds, _, window, cx| {
132                    let mut builder = PathBuilder::stroke(px(1.)).dash_array(&[px(4.), px(2.)]);
133                    let (start, end) = match self.direction {
134                        DividerDirection::Horizontal => {
135                            let x = bounds.origin.x;
136                            let y = bounds.origin.y + px(0.5);
137                            (point(x, y), point(x + bounds.size.width, y))
138                        }
139                        DividerDirection::Vertical => {
140                            let x = bounds.origin.x + px(0.5);
141                            let y = bounds.origin.y;
142                            (point(x, y), point(x, y + bounds.size.height))
143                        }
144                    };
145                    builder.move_to(start);
146                    builder.line_to(end);
147                    if let Ok(line) = builder.build() {
148                        window.paint_path(line, self.color.hsla(cx));
149                    }
150                },
151            )
152            .absolute()
153            .size_full(),
154        )
155    }
156}
157
158impl RenderOnce for Divider {
159    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
160        if self.direction == DividerDirection::Horizontal {
161            if let Some(label) = self.label.clone() {
162                let line_color = self.color.hsla(cx);
163                return h_flex()
164                    .w_full()
165                    .items_center()
166                    .gap_2()
167                    .child(div().h_px().flex_1().bg(line_color))
168                    .child(
169                        Label::new(label)
170                            .size(LabelSize::XSmall)
171                            .color(Color::Muted),
172                    )
173                    .child(div().h_px().flex_1().bg(line_color))
174                    .into_any_element();
175            }
176        }
177
178        let base = match self.direction {
179            DividerDirection::Horizontal => div()
180                .min_w_0()
181                .h_px()
182                .w_full()
183                .when(self.inset, |this| this.mx_1p5()),
184            DividerDirection::Vertical => div()
185                .min_w_0()
186                .w_px()
187                .h_full()
188                .when(self.inset, |this| this.my_1p5()),
189        };
190
191        match self.style {
192            DividerStyle::Solid => self.render_solid(base, cx).into_any_element(),
193            DividerStyle::Dashed => self.render_dashed(base).into_any_element(),
194        }
195    }
196}
197
198impl Component for Divider {
199    fn scope() -> ComponentScope {
200        ComponentScope::Layout
201    }
202
203    fn description() -> Option<&'static str> {
204        Some(
205            "Visual separator used to create divisions between groups of content or sections in a layout.",
206        )
207    }
208
209    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
210        Some(
211            v_flex()
212                .gap_6()
213                .children(vec![
214                    example_group_with_title(
215                        "Horizontal Dividers",
216                        vec![
217                            single_example("Default", Divider::horizontal().into_any_element()),
218                            single_example(
219                                "Border Color",
220                                Divider::horizontal()
221                                    .color(DividerColor::Border)
222                                    .into_any_element(),
223                            ),
224                            single_example(
225                                "Inset",
226                                Divider::horizontal().inset().into_any_element(),
227                            ),
228                            single_example(
229                                "Dashed",
230                                Divider::horizontal_dashed().into_any_element(),
231                            ),
232                            single_example(
233                                "Labeled",
234                                Divider::horizontal().label("OR").into_any_element(),
235                            ),
236                        ],
237                    ),
238                    example_group_with_title(
239                        "Vertical Dividers",
240                        vec![
241                            single_example(
242                                "Default",
243                                div().h_16().child(Divider::vertical()).into_any_element(),
244                            ),
245                            single_example(
246                                "Border Color",
247                                div()
248                                    .h_16()
249                                    .child(Divider::vertical().color(DividerColor::Border))
250                                    .into_any_element(),
251                            ),
252                            single_example(
253                                "Inset",
254                                div()
255                                    .h_16()
256                                    .child(Divider::vertical().inset())
257                                    .into_any_element(),
258                            ),
259                            single_example(
260                                "Dashed",
261                                div()
262                                    .h_16()
263                                    .child(Divider::vertical_dashed())
264                                    .into_any_element(),
265                            ),
266                        ],
267                    ),
268                    example_group_with_title(
269                        "Example Usage",
270                        vec![single_example(
271                            "Between Content",
272                            v_flex()
273                                .w_full()
274                                .gap_4()
275                                .px_4()
276                                .child(Label::new("Section One"))
277                                .child(Divider::horizontal())
278                                .child(Label::new("Section Two"))
279                                .child(Divider::horizontal_dashed())
280                                .child(Label::new("Section Three"))
281                                .child(Divider::horizontal().label("End"))
282                                .into_any_element(),
283                        )],
284                    ),
285                ])
286                .into_any_element(),
287        )
288    }
289}