1use crate::core::border::BorderType;
4use crate::core::geometry::{Align, Edges};
5use crate::core::render::{Renderable, Rendered};
6use crate::core::style::Style;
7use crate::core::text::{Line, Span, Text};
8use crate::core::theme::theme;
9use crate::output::compose::pad;
10
11pub struct Rule {
22 title: Option<Text>,
23 border: BorderType,
24 style: Style,
25 align: Align,
26 width: Option<u16>,
27 margin: Edges,
28 pad: u16,
29}
30
31impl Default for Rule {
32 fn default() -> Self {
33 let theme = theme();
34 Self {
35 title: None,
36 border: theme.border,
37 style: theme.secondary,
38 align: Align::Center,
39 width: None,
40 margin: Edges::default(),
41 pad: 1,
42 }
43 }
44}
45
46impl Rule {
47 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn with_title(title: impl Into<Text>) -> Self {
54 Self {
55 title: Some(title.into()),
56 ..Self::default()
57 }
58 }
59
60 #[must_use]
62 pub fn border(mut self, border: BorderType) -> Self {
63 self.border = border;
64 self
65 }
66
67 #[must_use]
69 pub fn style(mut self, style: Style) -> Self {
70 self.style = style;
71 self
72 }
73
74 #[must_use]
76 pub fn align(mut self, align: Align) -> Self {
77 self.align = align;
78 self
79 }
80
81 #[must_use]
83 pub fn width(mut self, width: u16) -> Self {
84 self.width = Some(width);
85 self
86 }
87
88 #[must_use]
90 pub fn margin(mut self, margin: Edges) -> Self {
91 self.margin = margin;
92 self
93 }
94}
95
96impl Renderable for Rule {
97 fn render(&self, max_width: u16) -> Rendered {
98 let total = self.width.unwrap_or(max_width) as usize;
99 let inner = total.saturating_sub(self.margin.horizontal() as usize);
100 let glyph = self.border.chars().horizontal;
101 let line = match &self.title {
102 None => fill_line(glyph, inner, self.style),
103 Some(title) => self.titled_line(title, glyph, inner),
104 };
105 pad(&Rendered::new(vec![line]), self.margin)
106 }
107}
108
109impl Rule {
110 fn titled_line(&self, title: &Text, glyph: char, width: usize) -> Line {
112 let title_line = title.lines.first().cloned().unwrap_or_default();
113 let pad = self.pad as usize;
114 let title_w = title_line.width() + 2 * pad;
115 if title_w >= width {
116 return title_line;
117 }
118 let remaining = width - title_w;
119 let (left, right) = match self.align {
120 Align::Left => (1.min(remaining), remaining.saturating_sub(1)),
121 Align::Right => (remaining.saturating_sub(1), 1.min(remaining)),
122 Align::Center => (remaining / 2, remaining - remaining / 2),
123 };
124 let mut spans = vec![glyph_span(glyph, left, self.style)];
125 spans.push(Span::raw(" ".repeat(pad)));
126 spans.extend(title_line.spans);
127 spans.push(Span::raw(" ".repeat(pad)));
128 spans.push(glyph_span(glyph, right, self.style));
129 Line::new(spans)
130 }
131}
132
133fn fill_line(glyph: char, width: usize, style: Style) -> Line {
135 Line::new(vec![glyph_span(glyph, width, style)])
136}
137
138fn glyph_span(glyph: char, count: usize, style: Style) -> Span {
140 Span::styled(glyph.to_string().repeat(count), style)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn plain_rule_fills_the_width() {
149 let rendered = Rule::new().border(BorderType::Single).render(10);
150 assert_eq!(rendered.lines[0].plain(), "──────────");
151 }
152
153 #[test]
154 fn titled_rule_contains_title() {
155 let rendered = Rule::with_title("Section")
156 .border(BorderType::Single)
157 .render(20);
158 assert!(rendered.lines[0].plain().contains("Section"));
159 assert_eq!(rendered.lines[0].width(), 20);
160 }
161}