use crate::console::RenderContext;
use crate::renderable::{Renderable, Segment};
use crate::style::Style;
use crate::text::Span;
#[derive(Debug, Clone)]
pub struct Rule {
title: Option<String>,
character: char,
style: Style,
title_style: Style,
align: RuleAlign,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RuleAlign {
Left,
#[default]
Center,
Right,
}
impl Rule {
pub fn new(title: &str) -> Self {
Rule {
title: if title.is_empty() {
None
} else {
Some(title.to_string())
},
character: '─',
style: Style::new(),
title_style: Style::new(),
align: RuleAlign::Center,
}
}
pub fn line() -> Self {
Rule {
title: None,
character: '─',
style: Style::new(),
title_style: Style::new(),
align: RuleAlign::Center,
}
}
pub fn character(mut self, c: char) -> Self {
self.character = c;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
pub fn align(mut self, align: RuleAlign) -> Self {
self.align = align;
self
}
}
impl Default for Rule {
fn default() -> Self {
Rule::line()
}
}
impl Renderable for Rule {
fn render(&self, context: &RenderContext) -> Vec<Segment> {
let width = context.width;
match &self.title {
None => {
let line = self.character.to_string().repeat(width);
vec![Segment::new(vec![Span::styled(line, self.style)])]
}
Some(title) => {
let title_text = crate::markup::parse(title);
let title_spans = title_text.spans;
let title_content: String = title_spans.iter().map(|s| s.text.as_ref()).collect();
let title_with_spacing = format!(" {} ", title_content);
let title_width =
unicode_width::UnicodeWidthStr::width(title_with_spacing.as_str());
if title_width >= width {
return vec![Segment::new(title_spans)];
}
let remaining = width - title_width;
let (left_len, right_len) = match self.align {
RuleAlign::Left => (4.min(remaining), remaining.saturating_sub(4)),
RuleAlign::Center => {
let left = remaining / 2;
(left, remaining - left)
}
RuleAlign::Right => (remaining.saturating_sub(4), 4.min(remaining)),
};
let left_line = self.character.to_string().repeat(left_len);
let right_line = self.character.to_string().repeat(right_len);
let mut spans = Vec::new();
spans.push(Span::styled(left_line, self.style));
spans.push(Span::raw(" "));
spans.extend(title_spans);
spans.push(Span::raw(" "));
spans.push(Span::styled(right_line, self.style));
vec![Segment::new(spans)]
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rule_simple() {
let rule = Rule::line();
let context = RenderContext {
width: 10,
height: None,
};
let segments = rule.render(&context);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].plain_text(), "──────────");
}
#[test]
fn test_rule_with_title() {
let rule = Rule::new("Title");
let context = RenderContext {
width: 20,
height: None,
};
let segments = rule.render(&context);
let text = segments[0].plain_text();
assert!(text.contains("Title"));
assert!(text.starts_with("─"));
assert!(text.ends_with("─"));
}
#[test]
fn test_rule_custom_char() {
let rule = Rule::line().character('=');
let context = RenderContext {
width: 5,
height: None,
};
let segments = rule.render(&context);
assert_eq!(segments[0].plain_text(), "=====");
}
}