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