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 { self.title = title.into(); self }
38
39 pub fn characters(mut self, chars: impl Into<String>) -> Self { self.characters = chars.into(); self }
41
42 pub fn style(mut self, style: Style) -> Self { self.style = style; self }
44
45 pub fn align(mut self, align: AlignMethod) -> Self { self.align = align; self }
47}
48
49impl Renderable for Rule {
50 fn render(&self, options: &ConsoleOptions) -> RenderResult {
51 let width = options.max_width;
52 let chars = if options.ascii_only && !self.characters.is_ascii() {
53 "-"
54 } else {
55 self.characters.as_str()
56 };
57 let char_w = UnicodeWidthStr::width(chars);
58
59 if char_w == 0 {
60 return RenderResult::from_text("");
61 }
62
63 let style_ansi = self.style.to_ansi();
64 let style_reset = if style_ansi.is_empty() { "" } else { "\x1b[0m" };
65
66 if self.title.is_empty() {
67 let count = width / char_w;
69 let line = chars.repeat(count);
70 return RenderResult::from_segments(vec![
71 Segment::new(format!("{style_ansi}{line}{style_reset}")),
72 Segment::line(),
73 ]);
74 }
75
76 let title_w = UnicodeWidthStr::width(self.title.as_str());
77 let required_space = if matches!(self.align, AlignMethod::Center) { 4 } else { 2 };
78 let available = width.saturating_sub(required_space);
79
80 if available < 1 {
81 let count = width / char_w;
83 let line = chars.repeat(count);
84 return RenderResult::from_segments(vec![
85 Segment::new(format!("{style_ansi}{line}{style_reset}")),
86 Segment::line(),
87 ]);
88 }
89
90 let mut segments = Vec::new();
91
92 match self.align {
93 AlignMethod::Center => {
94 let side = (width.saturating_sub(title_w)) / 2;
95 let left_w = side.saturating_sub(1);
96 let right_w = width.saturating_sub(left_w).saturating_sub(title_w).saturating_sub(2);
97
98 let left = chars.repeat((left_w / char_w).max(1));
99 let right = chars.repeat((right_w / char_w).max(1));
100
101 segments.push(Segment::new(format!(
102 "{style_ansi}{left} {}{} {right}{style_reset}",
103 self.title, style_ansi
104 )));
105 }
106 AlignMethod::Left => {
107 let rem = width.saturating_sub(title_w + 1);
108 let right = chars.repeat((rem / char_w).max(1));
109 segments.push(Segment::new(format!(
110 "{style_ansi}{} {right}{style_reset}",
111 self.title
112 )));
113 }
114 AlignMethod::Right => {
115 let rem = width.saturating_sub(title_w + 1);
116 let left = chars.repeat((rem / char_w).max(1));
117 segments.push(Segment::new(format!(
118 "{style_ansi}{left} {}{style_reset}",
119 self.title
120 )));
121 }
122 AlignMethod::Full => {
123 let count = width / char_w;
124 let line = chars.repeat(count);
125 segments.push(Segment::new(format!("{style_ansi}{line}{style_reset}")));
126 }
127 }
128
129 segments.push(Segment::line());
130 RenderResult::from_segments(segments)
131 }
132}
133
134impl Default for Rule {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::console::ConsoleOptions;
144
145 #[test]
146 fn test_plain_rule() {
147 let rule = Rule::new();
148 let opts = ConsoleOptions { max_width: 40, ..Default::default() };
149 let result = rule.render(&opts);
150 let ansi = result.to_ansi();
151 assert!(ansi.contains('─'));
152 }
153
154 #[test]
155 fn test_rule_with_title() {
156 let rule = Rule::new().title("Section");
157 let opts = ConsoleOptions { max_width: 40, ..Default::default() };
158 let result = rule.render(&opts);
159 let ansi = result.to_ansi();
160 assert!(ansi.contains("Section"));
161 }
162}