use crate::core::border::BorderType;
use crate::core::geometry::{Align, Edges};
use crate::core::render::{Renderable, Rendered};
use crate::core::style::Style;
use crate::core::text::{Line, Span, Text};
use crate::core::theme::theme;
use crate::output::compose::pad;
pub struct Rule {
title: Option<Text>,
border: BorderType,
style: Style,
align: Align,
width: Option<u16>,
margin: Edges,
pad: u16,
}
impl Default for Rule {
fn default() -> Self {
let theme = theme();
Self {
title: None,
border: theme.border,
style: theme.secondary,
align: Align::Center,
width: None,
margin: Edges::default(),
pad: 1,
}
}
}
impl Rule {
pub fn new() -> Self {
Self::default()
}
pub fn with_title(title: impl Into<Text>) -> Self {
Self {
title: Some(title.into()),
..Self::default()
}
}
#[must_use]
pub fn border(mut self, border: BorderType) -> Self {
self.border = border;
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn align(mut self, align: Align) -> Self {
self.align = align;
self
}
#[must_use]
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub fn margin(mut self, margin: Edges) -> Self {
self.margin = margin;
self
}
}
impl Renderable for Rule {
fn render(&self, max_width: u16) -> Rendered {
let total = self.width.unwrap_or(max_width) as usize;
let inner = total.saturating_sub(self.margin.horizontal() as usize);
let glyph = self.border.chars().horizontal;
let line = match &self.title {
None => fill_line(glyph, inner, self.style),
Some(title) => self.titled_line(title, glyph, inner),
};
pad(&Rendered::new(vec![line]), self.margin)
}
}
impl Rule {
fn titled_line(&self, title: &Text, glyph: char, width: usize) -> Line {
let title_line = title.lines.first().cloned().unwrap_or_default();
let pad = self.pad as usize;
let title_w = title_line.width() + 2 * pad;
if title_w >= width {
return title_line;
}
let remaining = width - title_w;
let (left, right) = match self.align {
Align::Left => (1.min(remaining), remaining.saturating_sub(1)),
Align::Right => (remaining.saturating_sub(1), 1.min(remaining)),
Align::Center => (remaining / 2, remaining - remaining / 2),
};
let mut spans = vec![glyph_span(glyph, left, self.style)];
spans.push(Span::raw(" ".repeat(pad)));
spans.extend(title_line.spans);
spans.push(Span::raw(" ".repeat(pad)));
spans.push(glyph_span(glyph, right, self.style));
Line::new(spans)
}
}
fn fill_line(glyph: char, width: usize, style: Style) -> Line {
Line::new(vec![glyph_span(glyph, width, style)])
}
fn glyph_span(glyph: char, count: usize, style: Style) -> Span {
Span::styled(glyph.to_string().repeat(count), style)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_rule_fills_the_width() {
let rendered = Rule::new().border(BorderType::Single).render(10);
assert_eq!(rendered.lines[0].plain(), "──────────");
}
#[test]
fn titled_rule_contains_title() {
let rendered = Rule::with_title("Section")
.border(BorderType::Single)
.render(20);
assert!(rendered.lines[0].plain().contains("Section"));
assert_eq!(rendered.lines[0].width(), 20);
}
}