Skip to main content

revue/widget/display/
divider.rs

1//! Divider widget for visual separation
2
3use crate::style::Color;
4use crate::widget::traits::{RenderContext, View, WidgetProps};
5use crate::{impl_props_builders, impl_styled_view};
6
7/// Orientation for the divider
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum Orientation {
10    /// Horizontal divider (default)
11    #[default]
12    Horizontal,
13    /// Vertical divider
14    Vertical,
15}
16
17/// Divider style
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum DividerStyle {
20    /// Solid line (default)
21    #[default]
22    Solid,
23    /// Dashed line
24    Dashed,
25    /// Dotted line
26    Dotted,
27    /// Double line
28    Double,
29    /// Thick line
30    Thick,
31}
32
33/// A divider widget for visual separation
34///
35/// # Example
36///
37/// ```rust,ignore
38/// use revue::prelude::*;
39///
40/// vstack()
41///     .child(text("Section 1"))
42///     .child(divider())
43///     .child(text("Section 2"))
44/// ```
45pub struct Divider {
46    /// Orientation
47    orientation: Orientation,
48    /// Style
49    style: DividerStyle,
50    /// Color
51    color: Color,
52    /// Label (centered in the divider)
53    label: Option<String>,
54    /// Label color
55    label_color: Option<Color>,
56    /// Margin (space before and after)
57    margin: u16,
58    /// Length (0 = auto/full width)
59    length: u16,
60    /// Widget props for CSS integration
61    props: WidgetProps,
62}
63
64impl Divider {
65    /// Create a new horizontal divider
66    pub fn new() -> Self {
67        Self {
68            orientation: Orientation::Horizontal,
69            style: DividerStyle::Solid,
70            color: Color::rgb(80, 80, 80),
71            label: None,
72            label_color: None,
73            margin: 0,
74            length: 0,
75            props: WidgetProps::new(),
76        }
77    }
78
79    /// Create a vertical divider
80    pub fn vertical() -> Self {
81        Self {
82            orientation: Orientation::Vertical,
83            ..Self::new()
84        }
85    }
86
87    /// Set orientation
88    pub fn orientation(mut self, orientation: Orientation) -> Self {
89        self.orientation = orientation;
90        self
91    }
92
93    /// Set style
94    pub fn style(mut self, style: DividerStyle) -> Self {
95        self.style = style;
96        self
97    }
98
99    /// Set color
100    pub fn color(mut self, color: Color) -> Self {
101        self.color = color;
102        self
103    }
104
105    /// Set label (centered text)
106    pub fn label(mut self, label: impl Into<String>) -> Self {
107        self.label = Some(label.into());
108        self
109    }
110
111    /// Set label color
112    pub fn label_color(mut self, color: Color) -> Self {
113        self.label_color = Some(color);
114        self
115    }
116
117    /// Set margin (space before and after the line)
118    pub fn margin(mut self, margin: u16) -> Self {
119        self.margin = margin;
120        self
121    }
122
123    /// Set length (0 = auto/full)
124    pub fn length(mut self, length: u16) -> Self {
125        self.length = length;
126        self
127    }
128
129    /// Dashed style shorthand
130    pub fn dashed(mut self) -> Self {
131        self.style = DividerStyle::Dashed;
132        self
133    }
134
135    /// Dotted style shorthand
136    pub fn dotted(mut self) -> Self {
137        self.style = DividerStyle::Dotted;
138        self
139    }
140
141    /// Double line shorthand
142    pub fn double(mut self) -> Self {
143        self.style = DividerStyle::Double;
144        self
145    }
146
147    /// Thick line shorthand
148    pub fn thick(mut self) -> Self {
149        self.style = DividerStyle::Thick;
150        self
151    }
152
153    /// Get the line character based on style and orientation
154    fn line_char(&self) -> char {
155        match (self.orientation, self.style) {
156            (Orientation::Horizontal, DividerStyle::Solid) => '─',
157            (Orientation::Horizontal, DividerStyle::Dashed) => '╌',
158            (Orientation::Horizontal, DividerStyle::Dotted) => '┄',
159            (Orientation::Horizontal, DividerStyle::Double) => '═',
160            (Orientation::Horizontal, DividerStyle::Thick) => '━',
161            (Orientation::Vertical, DividerStyle::Solid) => '│',
162            (Orientation::Vertical, DividerStyle::Dashed) => '╎',
163            (Orientation::Vertical, DividerStyle::Dotted) => '┆',
164            (Orientation::Vertical, DividerStyle::Double) => '║',
165            (Orientation::Vertical, DividerStyle::Thick) => '┃',
166        }
167    }
168}
169
170impl Default for Divider {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176impl View for Divider {
177    crate::impl_view_meta!("Divider");
178
179    fn render(&self, ctx: &mut RenderContext) {
180        let area = ctx.area;
181        let line_char = self.line_char();
182
183        match self.orientation {
184            Orientation::Horizontal => {
185                let y = area.y;
186                let start_x = area.x + self.margin;
187                let end_x = if self.length > 0 {
188                    (start_x + self.length).min(area.x + area.width)
189                } else {
190                    area.x + area.width.saturating_sub(self.margin)
191                };
192
193                // Draw the line
194                if let Some(ref label) = self.label {
195                    // Line with label centered
196                    let label_len = crate::utils::unicode::display_width(label) as u16;
197                    let total_width = end_x - start_x;
198
199                    if label_len + 4 <= total_width {
200                        let label_start = start_x + (total_width - label_len) / 2 - 1;
201                        let label_end = label_start + label_len + 2;
202
203                        // Left part
204                        ctx.draw_hline(start_x, y, label_start - start_x, line_char, self.color);
205
206                        // Space before label
207                        ctx.draw_char(label_start, y, ' ', self.color);
208
209                        // Label
210                        let label_color = self.label_color.unwrap_or(self.color);
211                        ctx.draw_text(label_start + 1, y, label, label_color);
212
213                        // Space after label
214                        ctx.draw_char(label_end - 1, y, ' ', self.color);
215
216                        // Right part
217                        ctx.draw_hline(label_end, y, end_x - label_end, line_char, self.color);
218                    } else {
219                        // Not enough space, just draw label (clipped)
220                        let label_color = self.label_color.unwrap_or(self.color);
221                        ctx.draw_text_clipped(start_x, y, label, label_color, end_x - start_x);
222                    }
223                } else {
224                    // Simple line without label
225                    ctx.draw_hline(start_x, y, end_x - start_x, line_char, self.color);
226                }
227            }
228            Orientation::Vertical => {
229                let x = area.x;
230                let start_y = area.y + self.margin;
231                let end_y = if self.length > 0 {
232                    (start_y + self.length).min(area.y + area.height)
233                } else {
234                    area.y + area.height.saturating_sub(self.margin)
235                };
236
237                ctx.draw_vline(x, start_y, end_y - start_y, line_char, self.color);
238            }
239        }
240    }
241}
242
243impl_styled_view!(Divider);
244impl_props_builders!(Divider);
245
246/// Create a new horizontal divider
247pub fn divider() -> Divider {
248    Divider::new()
249}
250
251/// Create a new vertical divider
252pub fn vdivider() -> Divider {
253    Divider::vertical()
254}
255
256// All tests moved to tests/widget/divider.rs