Skip to main content

photon_ui/components/
divider.rs

1//! A visual divider line — horizontal or vertical.
2
3use crate::{
4    Component,
5    RenderError,
6    Rendered,
7    layout::Direction,
8    theme::{
9        Palette,
10        Style,
11        Theme,
12    },
13};
14
15/// A standalone divider line.
16///
17/// Draws a repeating character (`─` for horizontal, `│` for vertical) across
18/// the full width or height of its assigned area. An optional label can be
19/// centered on the line.
20pub struct Divider {
21    direction: Direction,
22    style: Style,
23    label: Option<String>,
24}
25
26impl Divider {
27    /// Create a horizontal divider.
28    pub fn horizontal() -> Self {
29        Self {
30            direction: Direction::Horizontal,
31            style: Style::new(),
32            label: None,
33        }
34    }
35
36    /// Create a vertical divider.
37    pub fn vertical() -> Self {
38        Self {
39            direction: Direction::Vertical,
40            style: Style::new(),
41            label: None,
42        }
43    }
44
45    /// Apply a style to the divider line.
46    pub fn styled(mut self, style: Style) -> Self {
47        self.style = style;
48        self
49    }
50
51    /// Center a label on the divider.
52    pub fn labeled(mut self, label: impl Into<String>) -> Self {
53        self.label = Some(label.into());
54        self
55    }
56}
57
58impl Component for Divider {
59    fn render(&self, width: u16) -> Result<Rendered, RenderError> {
60        let theme = Theme::current();
61        let style = if self.style == Style::new() {
62            Style::new().fg(theme.border_default())
63        } else {
64            self.style.clone()
65        };
66
67        let line = match self.direction {
68            | Direction::Horizontal => {
69                if let Some(ref label) = self.label {
70                    let label = format!(" {} ", label);
71                    let label_vw = crate::utils::visible_width(&label);
72                    if label_vw + 4 > width as usize {
73                        let line = "─".repeat(width as usize);
74                        crate::theme::stylize(&line, &style)
75                    } else {
76                        let side = (width as usize - label_vw) / 2;
77                        let left = "─".repeat(side);
78                        let right = "─".repeat(width as usize - side - label_vw);
79                        let text = crate::theme::stylize(&label, &style);
80                        let left = crate::theme::stylize(&left, &style);
81                        let right = crate::theme::stylize(&right, &style);
82                        format!("{}{}{}", left, text, right)
83                    }
84                } else {
85                    let line = "─".repeat(width as usize);
86                    crate::theme::stylize(&line, &style)
87                }
88            },
89            | Direction::Vertical => {
90                let line = "│".repeat(width as usize);
91                crate::theme::stylize(&line, &style)
92            },
93        };
94
95        Ok(Rendered {
96            lines: vec![line],
97            cursor: None,
98            images: Vec::new(),
99        })
100    }
101
102    fn render_rect(&self, rect: crate::layout::Rect) -> Result<Rendered, RenderError> {
103        if self.direction == Direction::Vertical && rect.height > 1 {
104            let theme = Theme::current();
105            let style = if self.style == Style::new() {
106                Style::new().fg(theme.border_default())
107            } else {
108                self.style.clone()
109            };
110            let ch = crate::theme::stylize("│", &style);
111            let mut lines = Vec::new();
112            for _ in 0..rect.height {
113                lines.push(ch.clone());
114            }
115            Ok(Rendered {
116                lines,
117                cursor: None,
118                images: Vec::new(),
119            })
120        } else {
121            self.render(rect.width)
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::theme::Theme;
130
131    #[test]
132    fn divider_horizontal_renders_line() {
133        Theme::with(Theme::Light, || {
134            let div = Divider::horizontal();
135            let rendered = div.render(10).unwrap();
136            assert_eq!(rendered.lines.len(), 1);
137            assert!(rendered.lines[0].contains("─"));
138        });
139    }
140
141    #[test]
142    fn divider_vertical_renders_pipe() {
143        Theme::with(Theme::Light, || {
144            let div = Divider::vertical();
145            let rendered = div.render(1).unwrap();
146            assert!(rendered.lines[0].contains("│"));
147        });
148    }
149
150    #[test]
151    fn divider_labeled_renders_label() {
152        Theme::with(Theme::Light, || {
153            let div = Divider::horizontal().labeled("Section");
154            let rendered = div.render(30).unwrap();
155            assert!(rendered.lines[0].contains("Section"));
156        });
157    }
158
159    #[test]
160    fn divider_rect_vertical_multi_line() {
161        Theme::with(Theme::Light, || {
162            let div = Divider::vertical();
163            let rendered = div
164                .render_rect(crate::layout::Rect::new(0, 0, 1, 3))
165                .unwrap();
166            assert_eq!(rendered.lines.len(), 3);
167            for line in &rendered.lines {
168                assert!(line.contains("│"));
169            }
170        });
171    }
172}