Skip to main content

blaeck/components/
divider.rs

1//! Divider component - horizontal separator line.
2//!
3//! The Divider component displays a horizontal line to visually separate content.
4//!
5//! ## When to use Divider
6//!
7//! - Separating sections of content
8//! - Visual break between groups of items
9//! - Emphasizing boundaries in a layout
10//!
11//! ## See also
12//!
13//! - [`Spacer`](super::Spacer) — Empty space (no visible line)
14//! - [`Box`](super::Box) — Containers with borders
15
16use crate::element::{Component, Element};
17use crate::style::{Color, Modifier, Style};
18
19/// Style for the divider line.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum DividerStyle {
22    /// Single line: ─
23    #[default]
24    Single,
25    /// Double line: ═
26    Double,
27    /// Dashed: ┄
28    Dashed,
29    /// Dotted: ···
30    Dotted,
31    /// Bold: ━
32    Bold,
33    /// ASCII: -
34    Ascii,
35}
36
37impl DividerStyle {
38    /// Get the character for this style.
39    pub fn char(&self) -> char {
40        match self {
41            DividerStyle::Single => '─',
42            DividerStyle::Double => '═',
43            DividerStyle::Dashed => '┄',
44            DividerStyle::Dotted => '·',
45            DividerStyle::Bold => '━',
46            DividerStyle::Ascii => '-',
47        }
48    }
49}
50
51/// Properties for the Divider component.
52#[derive(Debug, Clone)]
53pub struct DividerProps {
54    /// Width of the divider (in characters). None = 20 default.
55    pub width: Option<usize>,
56    /// Style of the line.
57    pub style: DividerStyle,
58    /// Color of the line.
59    pub color: Option<Color>,
60    /// Whether to dim the line.
61    pub dim: bool,
62    /// Optional label in the middle of the divider.
63    pub label: Option<String>,
64    /// Label color (defaults to line color).
65    pub label_color: Option<Color>,
66}
67
68impl Default for DividerProps {
69    fn default() -> Self {
70        Self {
71            width: None,
72            style: DividerStyle::Single,
73            color: None,
74            dim: false,
75            label: None,
76            label_color: None,
77        }
78    }
79}
80
81impl DividerProps {
82    /// Create a new divider with default settings.
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Set the width.
88    #[must_use]
89    pub fn width(mut self, width: usize) -> Self {
90        self.width = Some(width);
91        self
92    }
93
94    /// Set the line style.
95    #[must_use]
96    pub fn line_style(mut self, style: DividerStyle) -> Self {
97        self.style = style;
98        self
99    }
100
101    /// Set the color.
102    #[must_use]
103    pub fn color(mut self, color: Color) -> Self {
104        self.color = Some(color);
105        self
106    }
107
108    /// Make the divider dimmed.
109    #[must_use]
110    pub fn dim(mut self) -> Self {
111        self.dim = true;
112        self
113    }
114
115    /// Add a label in the middle.
116    #[must_use]
117    pub fn label(mut self, label: impl Into<String>) -> Self {
118        self.label = Some(label.into());
119        self
120    }
121
122    /// Set the label color.
123    #[must_use]
124    pub fn label_color(mut self, color: Color) -> Self {
125        self.label_color = Some(color);
126        self
127    }
128
129    /// Build the display string.
130    pub fn render_string(&self) -> String {
131        let width = self.width.unwrap_or(20);
132        let ch = self.style.char();
133
134        if let Some(ref label) = self.label {
135            // Label in the middle: ── Label ──
136            let label_len = label.chars().count();
137            let remaining = width.saturating_sub(label_len + 2); // +2 for spaces
138            let left = remaining / 2;
139            let right = remaining - left;
140
141            format!(
142                "{} {} {}",
143                ch.to_string().repeat(left),
144                label,
145                ch.to_string().repeat(right)
146            )
147        } else {
148            ch.to_string().repeat(width)
149        }
150    }
151}
152
153/// A component that displays a horizontal divider line.
154///
155/// # Examples
156///
157/// ```ignore
158/// // Simple divider
159/// Element::node::<Divider>(DividerProps::new().width(40), vec![])
160///
161/// // Divider with label
162/// Element::node::<Divider>(
163///     DividerProps::new()
164///         .width(40)
165///         .label("Section")
166///         .color(Color::DarkGray),
167///     vec![]
168/// )
169/// ```
170pub struct Divider;
171
172impl Component for Divider {
173    type Props = DividerProps;
174
175    fn render(props: &Self::Props) -> Element {
176        let content = props.render_string();
177
178        let mut style = Style::new();
179        if let Some(color) = props.color {
180            style = style.fg(color);
181        }
182        if props.dim {
183            style = style.add_modifier(Modifier::DIM);
184        }
185
186        Element::styled_text(&content, style)
187    }
188}
189
190/// Helper to create a simple divider string.
191pub fn divider(width: usize) -> String {
192    DividerProps::new().width(width).render_string()
193}
194
195/// Helper to create a divider string with a label.
196pub fn divider_with_label(width: usize, label: &str) -> String {
197    DividerProps::new()
198        .width(width)
199        .label(label)
200        .render_string()
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_divider_default() {
209        let props = DividerProps::default();
210        let s = props.render_string();
211        assert_eq!(s.chars().count(), 20);
212        assert!(s.chars().all(|c| c == '─'));
213    }
214
215    #[test]
216    fn test_divider_width() {
217        let props = DividerProps::new().width(10);
218        let s = props.render_string();
219        assert_eq!(s, "──────────");
220    }
221
222    #[test]
223    fn test_divider_double() {
224        let props = DividerProps::new()
225            .width(5)
226            .line_style(DividerStyle::Double);
227        let s = props.render_string();
228        assert_eq!(s, "═════");
229    }
230
231    #[test]
232    fn test_divider_with_label() {
233        let props = DividerProps::new().width(20).label("Test");
234        let s = props.render_string();
235        assert!(s.contains("Test"));
236        assert!(s.contains("─"));
237    }
238
239    #[test]
240    fn test_divider_helper() {
241        let s = divider(10);
242        assert_eq!(s.chars().count(), 10);
243    }
244
245    #[test]
246    fn test_divider_with_label_helper() {
247        let s = divider_with_label(20, "Hello");
248        assert!(s.contains("Hello"));
249    }
250}