Skip to main content

tessera_components/
divider.rs

1//! Material Design divider primitives.
2//!
3//! ## Usage
4//!
5//! Separate sections in lists, menus, and settings screens.
6use tessera_ui::{
7    Color, ComputedData, Dp, LayoutResult, MeasurementError, Modifier, Px,
8    layout::{MeasureScope, layout},
9    tessera, use_context,
10};
11
12use crate::{
13    pipelines::divider::DividerCommand,
14    theme::{MaterialColorScheme, MaterialTheme},
15};
16
17fn resolve_thickness_px(thickness: Dp) -> f32 {
18    let thickness = if thickness.0.is_finite() {
19        thickness
20    } else {
21        DividerDefaults::THICKNESS
22    };
23    if thickness == Dp::ZERO {
24        1.0
25    } else {
26        thickness.to_pixels_f32().max(0.0)
27    }
28}
29
30/// Default values for divider components.
31pub struct DividerDefaults;
32
33impl DividerDefaults {
34    /// Default divider thickness.
35    pub const THICKNESS: Dp = Dp(1.0);
36
37    /// Divider color from the default light palette.
38    ///
39    /// Omit the component color to inherit the active Material theme.
40    pub fn color() -> Color {
41        MaterialColorScheme::default().outline_variant
42    }
43}
44
45#[derive(Clone, Copy, PartialEq)]
46enum DividerOrientation {
47    Horizontal,
48    Vertical,
49}
50
51#[derive(Clone, Copy, PartialEq)]
52struct DividerLayout {
53    thickness: f32,
54    color: Color,
55    orientation: DividerOrientation,
56}
57
58impl tessera_ui::LayoutModifierNode for DividerLayout {
59    fn measure_eq(&self, other: &Self) -> bool {
60        self.thickness == other.thickness && self.orientation == other.orientation
61    }
62
63    fn placement_eq(&self, other: &Self) -> bool {
64        self.measure_eq(other)
65    }
66
67    fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
68        let (width, height) = match self.orientation {
69            DividerOrientation::Horizontal => (
70                input
71                    .parent_constraint()
72                    .width()
73                    .resolve_max()
74                    .unwrap_or(input.parent_constraint().width().resolve_min()),
75                input
76                    .parent_constraint()
77                    .height()
78                    .clamp(Px(self.thickness.round() as i32)),
79            ),
80            DividerOrientation::Vertical => (
81                input
82                    .parent_constraint()
83                    .width()
84                    .clamp(Px(self.thickness.round() as i32)),
85                input
86                    .parent_constraint()
87                    .height()
88                    .resolve_max()
89                    .unwrap_or(input.parent_constraint().height().resolve_min()),
90            ),
91        };
92
93        Ok(LayoutResult::new(ComputedData { width, height }))
94    }
95}
96
97impl tessera_ui::DrawModifierNode for DividerLayout {
98    fn draw(
99        &self,
100        ctx: &mut tessera_ui::DrawModifierContext<'_, '_>,
101        content: &mut dyn tessera_ui::DrawModifierContent,
102    ) {
103        let input = &mut *ctx.render_input;
104        let size = input
105            .metadata_mut()
106            .computed_data()
107            .expect("divider must be measured");
108        let vertical = self.orientation == DividerOrientation::Vertical;
109        let cross = if vertical { size.width } else { size.height };
110        let stroke = if cross < Px(self.thickness.round() as i32) {
111            self.thickness.min(cross.to_f32())
112        } else {
113            self.thickness
114        };
115        if size.width > Px::ZERO && size.height > Px::ZERO && stroke > 0.0 {
116            input
117                .metadata_mut()
118                .fragment_mut()
119                .push_draw_command(DividerCommand {
120                    color: self.color,
121                    stroke,
122                    vertical,
123                });
124        }
125        content.draw(input);
126    }
127}
128
129/// # horizontal_divider
130///
131/// Renders a horizontal divider line that fills the available width.
132///
133/// ## Usage
134///
135/// Separate list groups or content sections within a screen.
136///
137/// ## Parameters
138///
139/// - `modifier` — sizing, insets, placement, and semantics.
140/// - `thickness` — optional line thickness; zero draws one physical pixel, and
141///   negative values resolve to zero. Non-finite values use the default.
142/// - `color` — optional line color override.
143///
144/// ## Examples
145///
146/// ```
147/// use tessera_components::divider::horizontal_divider;
148/// use tessera_ui::{Color, Dp};
149///
150/// # #[tessera_ui::tessera]
151/// # fn example() {
152/// horizontal_divider().thickness(Dp::ZERO).color(Color::BLACK);
153/// # }
154/// # tessera_ui::testing::layout_test(example).run();
155/// ```
156#[tessera]
157pub fn horizontal_divider(modifier: Option<Modifier>, thickness: Option<Dp>, color: Option<Color>) {
158    let thickness_px = resolve_thickness_px(thickness.unwrap_or(DividerDefaults::THICKNESS));
159    let color = if let Some(color) = color {
160        color
161    } else {
162        use_context::<MaterialTheme>()
163            .expect("MaterialTheme must be provided")
164            .get()
165            .color_scheme
166            .outline_variant
167    };
168
169    let policy = DividerLayout {
170        thickness: thickness_px,
171        color,
172        orientation: DividerOrientation::Horizontal,
173    };
174    layout()
175        .modifier(modifier.unwrap_or_default())
176        .policy(policy)
177        .draw(policy);
178}
179
180/// # vertical_divider
181///
182/// Renders a vertical divider line that fills the available height.
183///
184/// ## Usage
185///
186/// Separate side-by-side content regions.
187///
188/// ## Parameters
189///
190/// - `modifier` — sizing, insets, placement, and semantics.
191/// - `thickness` — optional line thickness; zero draws one physical pixel, and
192///   negative values resolve to zero. Non-finite values use the default.
193/// - `color` — optional line color override.
194///
195/// ## Examples
196///
197/// ```
198/// use tessera_components::divider::vertical_divider;
199/// use tessera_ui::{Color, Dp};
200///
201/// # #[tessera_ui::tessera]
202/// # fn example() {
203/// vertical_divider().thickness(Dp(2.0)).color(Color::BLACK);
204/// # }
205/// # tessera_ui::testing::layout_test(example).run();
206/// ```
207#[tessera]
208pub fn vertical_divider(modifier: Option<Modifier>, thickness: Option<Dp>, color: Option<Color>) {
209    let thickness_px = resolve_thickness_px(thickness.unwrap_or(DividerDefaults::THICKNESS));
210    let color = if let Some(color) = color {
211        color
212    } else {
213        use_context::<MaterialTheme>()
214            .expect("MaterialTheme must be provided")
215            .get()
216            .color_scheme
217            .outline_variant
218    };
219
220    let policy = DividerLayout {
221        thickness: thickness_px,
222        color,
223        orientation: DividerOrientation::Vertical,
224    };
225    layout()
226        .modifier(modifier.unwrap_or_default())
227        .policy(policy)
228        .draw(policy);
229}
230
231#[cfg(test)]
232mod tests;
233
234#[cfg(all(test, not(target_family = "wasm")))]
235mod drawing_tests;