Skip to main content

gpui_kit_material/components/
divider.rs

1//! Noninteractive separators from the pinned md3.io divider factory.
2use crate::{color::ColorRole, theme::color};
3use gpui::{prelude::*, *};
4use gpui_base::StyledExt;
5
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum DividerOrientation {
8    #[default]
9    Horizontal,
10    Vertical,
11}
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum DividerVariant {
14    #[default]
15    FullWidth,
16    Inset,
17    MiddleInset,
18}
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum DividerDirection {
21    #[default]
22    Ltr,
23    Rtl,
24}
25/// Explicit equivalent of the reference's ancestor list/card margin rule.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub enum DividerContext {
28    #[default]
29    Plain,
30    List,
31    Card,
32}
33
34/// A separator without focus, activation, or intrinsic animation.
35///
36/// Insets use logical start/end. Inset variants leave their main size automatic:
37/// a vertical divider needs a stretching row or an explicit height from its
38/// caller. Insets are retained when switching back from `FullWidth`.
39/// Uses GPUI block/flex layout; browser inline formatting with text siblings
40/// is not implemented.
41/// Default thickness and spacing are fixed CSS pixel geometry, independent of
42/// typography. Callers can supply rem-valued insets when aligning with text.
43#[derive(gpui::IntoElement)]
44pub struct MaterialDivider {
45    id: ElementId,
46    orientation: DividerOrientation,
47    variant: DividerVariant,
48    direction: DividerDirection,
49    context: DividerContext,
50    thickness: Pixels,
51    inset_start: Option<Length>,
52    inset_end: Option<Length>,
53    color_role: ColorRole,
54    style: StyleRefinement,
55}
56impl MaterialDivider {
57    pub fn new(id: impl Into<ElementId>) -> Self {
58        Self {
59            id: id.into(),
60            orientation: DividerOrientation::Horizontal,
61            variant: DividerVariant::FullWidth,
62            direction: DividerDirection::Ltr,
63            context: DividerContext::Plain,
64            thickness: px(1.),
65            inset_start: None,
66            inset_end: None,
67            color_role: ColorRole::OUTLINE_VARIANT,
68            style: StyleRefinement::default(),
69        }
70    }
71    pub fn orientation(mut self, orientation: DividerOrientation) -> Self {
72        self.orientation = orientation;
73        self
74    }
75    pub fn variant(mut self, variant: DividerVariant) -> Self {
76        self.variant = variant;
77        self
78    }
79    /// Resolves logical horizontal insets; the parent owns its own flow direction.
80    pub fn direction(mut self, direction: DividerDirection) -> Self {
81        self.direction = direction;
82        self
83    }
84    pub fn context(mut self, context: DividerContext) -> Self {
85        self.context = context;
86        self
87    }
88    /// Zero requests the reference default. Nonfinite/negative values also use 1px.
89    pub fn thickness(mut self, thickness: Pixels) -> Self {
90        self.thickness = if f32::from(thickness).is_finite() && thickness > px(0.) {
91            thickness
92        } else {
93            px(1.)
94        };
95        self
96    }
97    pub fn inset_start(mut self, inset: impl Into<Length>) -> Self {
98        self.inset_start = Some(inset.into());
99        self
100    }
101    pub fn inset_end(mut self, inset: impl Into<Length>) -> Self {
102        self.inset_end = Some(inset.into());
103        self
104    }
105    pub fn color_role(mut self, role: ColorRole) -> Self {
106        self.color_role = role;
107        self
108    }
109
110    fn styled_line(&self) -> Div {
111        let horizontal = self.orientation == DividerOrientation::Horizontal;
112        let mut line = div()
113            .block()
114            .m_0()
115            .p_0()
116            .border_0()
117            .bg(color(self.color_role));
118        if self.context != DividerContext::Plain {
119            line = line.my(px(8.));
120        }
121        line = if horizontal {
122            line.w_full().h(self.thickness)
123        } else {
124            line.h_full().w(self.thickness)
125        };
126        if self.variant != DividerVariant::FullWidth {
127            let start = self.inset_start.unwrap_or(px(16.).into());
128            let end = self.inset_end.unwrap_or(
129                px(if self.variant == DividerVariant::MiddleInset {
130                    16.
131                } else {
132                    0.
133                })
134                .into(),
135            );
136            line = if horizontal {
137                let (left, right) = if self.direction == DividerDirection::Ltr {
138                    (start, end)
139                } else {
140                    (end, start)
141                };
142                line.w_auto().ml(left).mr(right)
143            } else {
144                line.h_auto().mt(start).mb(end)
145            };
146        }
147        line.refine_style(&self.style)
148    }
149}
150impl MaterialDivider {
151    fn into_line(self) -> Stateful<Div> {
152        self.styled_line()
153            .id(self.id)
154            .role(Role::Splitter)
155            .when(self.orientation == DividerOrientation::Vertical, |line| {
156                line.aria_orientation(accesskit::Orientation::Vertical)
157            })
158    }
159}
160impl Styled for MaterialDivider {
161    fn style(&mut self) -> &mut StyleRefinement {
162        &mut self.style
163    }
164}
165impl RenderOnce for MaterialDivider {
166    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
167        self.into_line()
168    }
169}
170
171#[cfg(test)]
172#[path = "divider_tests.rs"]
173mod tests;