1use crate::align::AlignMethod;
4use crate::console::{ConsoleOptions, RenderResult, Renderable};
5use crate::segment::Segment;
6use crate::style::Style;
7use unicode_width::UnicodeWidthStr;
8
9#[derive(Debug, Clone)]
11pub struct Rule {
12 pub title: String,
14 pub characters: String,
16 pub style: Style,
18 pub end: String,
20 pub align: AlignMethod,
22}
23
24impl Rule {
25 pub fn new() -> Self {
27 Self {
28 title: String::new(),
29 characters: "─".to_string(),
30 style: Style::new(),
31 end: "\n".to_string(),
32 align: AlignMethod::Center,
33 }
34 }
35
36 pub fn title(mut self, title: impl Into<String>) -> Self {
38 self.title = title.into();
39 self
40 }
41
42 pub fn characters(mut self, chars: impl Into<String>) -> Self {
44 self.characters = chars.into();
45 self
46 }
47
48 pub fn style(mut self, style: Style) -> Self {
50 self.style = style;
51 self
52 }
53
54 pub fn align(mut self, align: AlignMethod) -> Self {
56 self.align = align;
57 self
58 }
59}
60
61impl Renderable for Rule {
62 fn render(&self, options: &ConsoleOptions) -> RenderResult {
63 let width = options.max_width;
64 let chars = if options.ascii_only && !self.characters.is_ascii() {
65 "-"
66 } else {
67 self.characters.as_str()
68 };
69 let char_w = UnicodeWidthStr::width(chars);
70
71 if char_w == 0 {
72 return RenderResult::from_text("");
73 }
74
75 let style_ansi = self.style.to_ansi();
76 let style_reset = if style_ansi.is_empty() { "" } else { "\x1b[0m" };
77
78 if self.title.is_empty() {
79 let count = width / char_w;
81 let line = chars.repeat(count);
82 return RenderResult::from_segments(vec![
83 Segment::new(format!("{style_ansi}{line}{style_reset}")),
84 Segment::line(),
85 ]);
86 }
87
88 let title_w = UnicodeWidthStr::width(self.title.as_str());
89 let required_space = if matches!(self.align, AlignMethod::Center) {
90 4
91 } else {
92 2
93 };
94 let available = width.saturating_sub(required_space);
95
96 if available < 1 {
97 let count = width / char_w;
99 let line = chars.repeat(count);
100 return RenderResult::from_segments(vec![
101 Segment::new(format!("{style_ansi}{line}{style_reset}")),
102 Segment::line(),
103 ]);
104 }
105
106 let mut segments = Vec::new();
107
108 match self.align {
109 AlignMethod::Center => {
110 let side = (width.saturating_sub(title_w)) / 2;
111 let left_w = side.saturating_sub(1);
112 let right_w = width
113 .saturating_sub(left_w)
114 .saturating_sub(title_w)
115 .saturating_sub(2);
116
117 let left = chars.repeat((left_w / char_w).max(1));
118 let right = chars.repeat((right_w / char_w).max(1));
119
120 segments.push(Segment::new(format!(
121 "{style_ansi}{left} {}{} {right}{style_reset}",
122 self.title, style_ansi
123 )));
124 }
125 AlignMethod::Left => {
126 let rem = width.saturating_sub(title_w + 1);
127 let right = chars.repeat((rem / char_w).max(1));
128 segments.push(Segment::new(format!(
129 "{style_ansi}{} {right}{style_reset}",
130 self.title
131 )));
132 }
133 AlignMethod::Right => {
134 let rem = width.saturating_sub(title_w + 1);
135 let left = chars.repeat((rem / char_w).max(1));
136 segments.push(Segment::new(format!(
137 "{style_ansi}{left} {}{style_reset}",
138 self.title
139 )));
140 }
141 AlignMethod::Full => {
142 let count = width / char_w;
143 let line = chars.repeat(count);
144 segments.push(Segment::new(format!("{style_ansi}{line}{style_reset}")));
145 }
146 }
147
148 segments.push(Segment::line());
149 RenderResult::from_segments(segments)
150 }
151}
152
153impl Default for Rule {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::console::ConsoleOptions;
163
164 #[test]
165 fn test_plain_rule() {
166 let rule = Rule::new();
167 let opts = ConsoleOptions {
168 max_width: 40,
169 ..Default::default()
170 };
171 let result = rule.render(&opts);
172 let ansi = result.to_ansi();
173 assert!(ansi.contains('─'));
174 }
175
176 #[test]
177 fn test_rule_with_title() {
178 let rule = Rule::new().title("Section");
179 let opts = ConsoleOptions {
180 max_width: 40,
181 ..Default::default()
182 };
183 let result = rule.render(&opts);
184 let ansi = result.to_ansi();
185 assert!(ansi.contains("Section"));
186 }
187}