use crate::pango::visible_width;
use crate::theme::Theme;
pub enum Line {
Center(String),
Body(String),
Sep,
}
pub fn render_bordered(lines: &[Line], theme: &Theme) -> String {
let blue = &theme.blue;
let dim = &theme.dim;
let mut max_w: usize = 0;
for line in lines {
let s = match line {
Line::Center(s) | Line::Body(s) => s.as_str(),
Line::Sep => continue,
};
let w = visible_width(s);
if w > max_w {
max_w = w;
}
}
let inner_w = max_w + 1;
let border_h: String = "─".repeat(inner_w);
let sep_inner: String = "─".repeat(inner_w.saturating_sub(2));
let sep_line = format!(" <span foreground='{dim}'>{sep_inner}</span>");
let mut out = String::with_capacity(256 * lines.len());
out.push_str(&format!("<span foreground='{blue}'>╭{border_h}╮</span>\n"));
for line in lines {
let body = match line {
Line::Body(s) => pad_right(s, inner_w),
Line::Center(s) => pad_center(s, inner_w),
Line::Sep => pad_right(&sep_line, inner_w),
};
out.push_str(&format!(
"<span foreground='{blue}'>│</span>{body}<span foreground='{blue}'>│</span>\n"
));
}
out.push_str(&format!("<span foreground='{blue}'>╰{border_h}╯</span>"));
out
}
pub fn pad_right(s: &str, inner_w: usize) -> String {
let v = visible_width(s);
let need = inner_w.saturating_sub(v);
format!("{s}{}", " ".repeat(need))
}
pub fn pad_center(s: &str, inner_w: usize) -> String {
let v = visible_width(s);
let total = inner_w.saturating_sub(v);
let lp = total / 2;
let rp = total - lp;
format!("{}{s}{}", " ".repeat(lp), " ".repeat(rp))
}
#[cfg(test)]
mod tests {
use super::*;
fn theme() -> Theme {
Theme::default()
}
#[test]
fn renders_top_and_bottom_borders() {
let lines = vec![Line::Center("Hi".into())];
let out = render_bordered(&lines, &theme());
assert!(out.contains("╭"));
assert!(out.contains("╮"));
assert!(out.contains("╰"));
assert!(out.contains("╯"));
assert!(out.contains("Hi"));
}
#[test]
fn body_line_is_right_padded_to_inner_width() {
let lines = vec![Line::Center("a".into()), Line::Body("longest".into())];
let out = render_bordered(&lines, &theme());
let opens = out.matches("<span").count();
let closes = out.matches("</span>").count();
assert_eq!(opens, closes);
}
#[test]
fn pad_right_strips_pango_tags_before_measuring() {
let s = "<span foreground='#fff'>abc</span>"; let p = pad_right(s, 6);
assert!(p.ends_with(" "));
}
#[test]
fn pad_center_distributes_extra_space_right_for_odd_diff() {
let p = pad_center("X", 4); assert_eq!(p, " X ");
}
#[test]
fn separator_line_width_grows_with_content() {
let lines = vec![
Line::Center("a".into()),
Line::Sep,
Line::Body("longer body line".into()),
];
let out = render_bordered(&lines, &theme());
assert!(out.contains("─"));
}
}