1use crate::pango::visible_width;
12use crate::theme::Theme;
13
14pub enum Line {
16 Center(String),
18 Body(String),
20 Sep,
22}
23
24pub fn render_bordered(lines: &[Line], theme: &Theme) -> String {
27 let blue = &theme.blue;
28 let dim = &theme.dim;
29
30 let mut max_w: usize = 0;
31 for line in lines {
32 let s = match line {
33 Line::Center(s) | Line::Body(s) => s.as_str(),
34 Line::Sep => continue,
35 };
36 let w = visible_width(s);
37 if w > max_w {
38 max_w = w;
39 }
40 }
41 let inner_w = max_w + 1;
42 let border_h: String = "─".repeat(inner_w);
43 let sep_inner: String = "─".repeat(inner_w.saturating_sub(2));
44 let sep_line = format!(" <span foreground='{dim}'>{sep_inner}</span>");
45
46 let mut out = String::with_capacity(256 * lines.len());
47 out.push_str(&format!("<span foreground='{blue}'>╭{border_h}╮</span>\n"));
48 for line in lines {
49 let body = match line {
50 Line::Body(s) => pad_right(s, inner_w),
51 Line::Center(s) => pad_center(s, inner_w),
52 Line::Sep => pad_right(&sep_line, inner_w),
53 };
54 out.push_str(&format!(
55 "<span foreground='{blue}'>│</span>{body}<span foreground='{blue}'>│</span>\n"
56 ));
57 }
58 out.push_str(&format!("<span foreground='{blue}'>╰{border_h}╯</span>"));
59 out
60}
61
62pub fn pad_right(s: &str, inner_w: usize) -> String {
64 let v = visible_width(s);
65 let need = inner_w.saturating_sub(v);
66 format!("{s}{}", " ".repeat(need))
67}
68
69pub fn pad_center(s: &str, inner_w: usize) -> String {
72 let v = visible_width(s);
73 let total = inner_w.saturating_sub(v);
74 let lp = total / 2;
75 let rp = total - lp;
76 format!("{}{s}{}", " ".repeat(lp), " ".repeat(rp))
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn theme() -> Theme {
84 Theme::default()
85 }
86
87 #[test]
88 fn renders_top_and_bottom_borders() {
89 let lines = vec![Line::Center("Hi".into())];
90 let out = render_bordered(&lines, &theme());
91 assert!(out.contains("╭"));
92 assert!(out.contains("╮"));
93 assert!(out.contains("╰"));
94 assert!(out.contains("╯"));
95 assert!(out.contains("Hi"));
96 }
97
98 #[test]
99 fn body_line_is_right_padded_to_inner_width() {
100 let lines = vec![Line::Center("a".into()), Line::Body("longest".into())];
102 let out = render_bordered(&lines, &theme());
103 let opens = out.matches("<span").count();
107 let closes = out.matches("</span>").count();
108 assert_eq!(opens, closes);
109 }
110
111 #[test]
112 fn pad_right_strips_pango_tags_before_measuring() {
113 let s = "<span foreground='#fff'>abc</span>"; let p = pad_right(s, 6);
115 assert!(p.ends_with(" "));
117 }
118
119 #[test]
120 fn pad_center_distributes_extra_space_right_for_odd_diff() {
121 let p = pad_center("X", 4); assert_eq!(p, " X ");
123 }
124
125 #[test]
126 fn separator_line_width_grows_with_content() {
127 let lines = vec![
128 Line::Center("a".into()),
129 Line::Sep,
130 Line::Body("longer body line".into()),
131 ];
132 let out = render_bordered(&lines, &theme());
133 assert!(out.contains("─"));
136 }
137}