1use crate::console::RenderContext;
7use crate::renderable::{Renderable, Segment};
8use crate::style::Style;
9use crate::text::Span;
10
11#[derive(Debug, Clone)]
13pub struct Rule {
14 title: Option<String>,
16 character: char,
18 style: Style,
20 title_style: Style,
22 align: RuleAlign,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum RuleAlign {
29 Left,
31 #[default]
33 Center,
34 Right,
36}
37
38impl Rule {
39 pub fn new(title: &str) -> Self {
41 Rule {
42 title: if title.is_empty() {
43 None
44 } else {
45 Some(title.to_string())
46 },
47 character: '─',
48 style: Style::new(),
49 title_style: Style::new(),
50 align: RuleAlign::Center,
51 }
52 }
53
54 pub fn line() -> Self {
56 Rule {
57 title: None,
58 character: '─',
59 style: Style::new(),
60 title_style: Style::new(),
61 align: RuleAlign::Center,
62 }
63 }
64
65 pub fn character(mut self, c: char) -> Self {
67 self.character = c;
68 self
69 }
70
71 pub fn style(mut self, style: Style) -> Self {
73 self.style = style;
74 self
75 }
76
77 pub fn title_style(mut self, style: Style) -> Self {
79 self.title_style = style;
80 self
81 }
82
83 pub fn align(mut self, align: RuleAlign) -> Self {
85 self.align = align;
86 self
87 }
88}
89
90impl Default for Rule {
91 fn default() -> Self {
92 Rule::line()
93 }
94}
95
96impl Renderable for Rule {
97 fn render(&self, context: &RenderContext) -> Vec<Segment> {
98 let width = context.width;
99
100 match &self.title {
101 None => {
102 let line = self.character.to_string().repeat(width);
104 vec![Segment::new(vec![Span::styled(line, self.style)])]
105 }
106 Some(title) => {
107 let title_text = crate::markup::parse(title);
109 let title_spans = title_text.spans;
110
111 let title_content: String = title_spans.iter().map(|s| s.text.as_ref()).collect();
113 let title_with_spacing = format!(" {} ", title_content);
114 let title_width =
115 unicode_width::UnicodeWidthStr::width(title_with_spacing.as_str());
116
117 if title_width >= width {
118 return vec![Segment::new(title_spans)];
120 }
121
122 let remaining = width - title_width;
123
124 let (left_len, right_len) = match self.align {
125 RuleAlign::Left => (4.min(remaining), remaining.saturating_sub(4)),
126 RuleAlign::Center => {
127 let left = remaining / 2;
128 (left, remaining - left)
129 }
130 RuleAlign::Right => (remaining.saturating_sub(4), 4.min(remaining)),
131 };
132
133 let left_line = self.character.to_string().repeat(left_len);
134 let right_line = self.character.to_string().repeat(right_len);
135
136 let mut spans = Vec::new();
137 spans.push(Span::styled(left_line, self.style));
138 spans.push(Span::raw(" "));
139 spans.extend(title_spans);
140 spans.push(Span::raw(" "));
141 spans.push(Span::styled(right_line, self.style));
142
143 vec![Segment::new(spans)]
144 }
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_rule_simple() {
155 let rule = Rule::line();
156 let context = RenderContext {
157 width: 10,
158 height: None,
159 };
160 let segments = rule.render(&context);
161 assert_eq!(segments.len(), 1);
162 assert_eq!(segments[0].plain_text(), "──────────");
163 }
164
165 #[test]
166 fn test_rule_with_title() {
167 let rule = Rule::new("Title");
168 let context = RenderContext {
169 width: 20,
170 height: None,
171 };
172 let segments = rule.render(&context);
173 let text = segments[0].plain_text();
174 assert!(text.contains("Title"));
175 assert!(text.starts_with("─"));
176 assert!(text.ends_with("─"));
177 }
178
179 #[test]
180 fn test_rule_custom_char() {
181 let rule = Rule::line().character('=');
182 let context = RenderContext {
183 width: 5,
184 height: None,
185 };
186 let segments = rule.render(&context);
187 assert_eq!(segments[0].plain_text(), "=====");
188 }
189}