use crate::align_widget::HorizontalAlign;
use crate::cells::{cell_len, set_cell_size};
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::segment::Segment;
use crate::style::Style;
use crate::text::{OverflowMethod, Text};
#[derive(Debug, Clone)]
pub struct Rule {
pub title: Option<Text>,
pub characters: String,
pub style: Style,
pub end: String,
pub align: HorizontalAlign,
}
impl Rule {
pub fn new() -> Self {
Rule {
title: None,
characters: "\u{2501}".to_string(), style: Style::null(),
end: "\n".to_string(),
align: HorizontalAlign::Center,
}
}
pub fn with_title(title: &str) -> Self {
let mut rule = Rule::new();
rule.title = Some(Text::new(title, Style::null()));
rule
}
#[must_use]
pub fn characters(mut self, chars: &str) -> Self {
self.characters = chars.to_string();
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn align(mut self, align: HorizontalAlign) -> Self {
self.align = align;
self
}
#[must_use]
pub fn end(mut self, end: &str) -> Self {
self.end = end.to_string();
self
}
fn rule_line(&self, width: usize) -> String {
if width == 0 {
return String::new();
}
let char_len = cell_len(&self.characters);
if char_len == 0 {
return " ".repeat(width);
}
let repeats = width / char_len;
let remainder = width % char_len;
let mut line = self.characters.repeat(repeats);
if remainder > 0 {
let partial = set_cell_size(&self.characters, remainder);
line.push_str(&partial);
}
line
}
}
impl Default for Rule {
fn default() -> Self {
Rule::new()
}
}
impl Renderable for Rule {
fn rich_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let width = options.max_width;
let rule_style = if self.style.is_null() {
console
.get_style("rule.line")
.unwrap_or_else(|_| self.style.clone())
} else {
self.style.clone()
};
let chars = if options.ascii_only() && !self.characters.is_ascii() {
"-".to_string()
} else {
self.characters.clone()
};
let rule_with_chars = Rule {
title: self.title.clone(),
characters: chars,
style: rule_style.clone(),
end: self.end.clone(),
align: self.align,
};
let mut segments = Vec::new();
match &self.title {
None => {
let line_text = rule_with_chars.rule_line(width);
let mut text = Text::new(&line_text, rule_style.clone());
text.overflow = Some(OverflowMethod::Crop);
let exact = set_cell_size(text.plain(), width);
segments.push(Segment::styled(&exact, rule_style));
segments.push(Segment::new(&self.end, None, None));
}
Some(title) => {
let mut title_text = title.clone();
let title_style = console
.get_style("rule.text")
.unwrap_or_else(|_| Style::null());
if !title_style.is_null() {
let len = title_text.len();
if len > 0 {
title_text.stylize(title_style, 0, Some(len));
}
}
let char_len = cell_len(&rule_with_chars.characters);
if char_len == 0 {
segments.push(Segment::new(title_text.plain(), None, None));
segments.push(Segment::new(&self.end, None, None));
return segments;
}
let min_side = char_len.max(1);
match self.align {
HorizontalAlign::Center => {
let title_max_width = width.saturating_sub(min_side * 2 + 2);
if title_max_width == 0 || title_text.cell_len() == 0 {
let line_text = rule_with_chars.rule_line(width);
let exact = set_cell_size(&line_text, width);
segments.push(Segment::styled(&exact, rule_style));
segments.push(Segment::new(&self.end, None, None));
return segments;
}
title_text.truncate(title_max_width, Some(OverflowMethod::Ellipsis), false);
let title_width = title_text.cell_len();
let side_width = (width.saturating_sub(title_width + 2)) / 2;
let left_width = side_width;
let right_width = width.saturating_sub(left_width + title_width + 2);
let left_line = rule_with_chars.rule_line(left_width);
let left_exact = set_cell_size(&left_line, left_width);
segments.push(Segment::styled(&left_exact, rule_style.clone()));
segments.push(Segment::new(" ", None, None));
segments.extend(title_text.render().into_iter().filter(|s| s.text != "\n"));
segments.push(Segment::new(" ", None, None));
let right_line = rule_with_chars.rule_line(right_width);
let right_exact = set_cell_size(&right_line, right_width);
segments.push(Segment::styled(&right_exact, rule_style));
segments.push(Segment::new(&self.end, None, None));
}
HorizontalAlign::Left => {
let title_max_width = width.saturating_sub(min_side + 2);
if title_max_width == 0 || title_text.cell_len() == 0 {
let line_text = rule_with_chars.rule_line(width);
let exact = set_cell_size(&line_text, width);
segments.push(Segment::styled(&exact, rule_style));
segments.push(Segment::new(&self.end, None, None));
return segments;
}
title_text.truncate(title_max_width, Some(OverflowMethod::Ellipsis), false);
let title_width = title_text.cell_len();
let rule_width = width.saturating_sub(title_width + 2);
segments.extend(title_text.render().into_iter().filter(|s| s.text != "\n"));
segments.push(Segment::new(" ", None, None));
let line = rule_with_chars.rule_line(rule_width + 1);
let exact = set_cell_size(&line, rule_width + 1);
segments.push(Segment::styled(&exact, rule_style));
segments.push(Segment::new(&self.end, None, None));
}
HorizontalAlign::Right => {
let title_max_width = width.saturating_sub(min_side + 2);
if title_max_width == 0 || title_text.cell_len() == 0 {
let line_text = rule_with_chars.rule_line(width);
let exact = set_cell_size(&line_text, width);
segments.push(Segment::styled(&exact, rule_style));
segments.push(Segment::new(&self.end, None, None));
return segments;
}
title_text.truncate(title_max_width, Some(OverflowMethod::Ellipsis), false);
let title_width = title_text.cell_len();
let rule_width = width.saturating_sub(title_width + 2);
let line = rule_with_chars.rule_line(rule_width + 1);
let exact = set_cell_size(&line, rule_width + 1);
segments.push(Segment::styled(&exact, rule_style));
segments.push(Segment::new(" ", None, None));
segments.extend(title_text.render().into_iter().filter(|s| s.text != "\n"));
segments.push(Segment::new(&self.end, None, None));
}
}
}
}
segments
}
}
impl std::fmt::Display for Rule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut console = Console::builder()
.width(f.width().unwrap_or(80))
.force_terminal(true)
.no_color(true)
.build();
console.begin_capture();
console.print(self);
let output = console.end_capture();
write!(f, "{}", output.trim_end_matches('\n'))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_console(width: usize) -> Console {
Console::builder()
.width(width)
.force_terminal(true)
.no_color(true)
.markup(false)
.build()
}
fn segments_to_text(segments: &[Segment]) -> String {
segments.iter().map(|s| s.text.as_str()).collect()
}
fn render_rule(console: &Console, rule: &Rule) -> String {
let opts = console.options();
let segments = rule.rich_console(console, &opts);
segments_to_text(&segments)
}
#[test]
fn test_no_title() {
let console = make_console(20);
let rule = Rule::new();
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert_eq!(cell_len(line), 20);
}
#[test]
fn test_no_title_custom_char() {
let console = make_console(10);
let rule = Rule::new().characters("-");
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert_eq!(line, "----------");
}
#[test]
fn test_no_title_double_char() {
let console = make_console(10);
let rule = Rule::new().characters("=-");
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert_eq!(cell_len(line), 10);
assert_eq!(line, &"=-=-=-=-=-=-="[..10]);
}
#[test]
fn test_centered_title() {
let console = make_console(40);
let rule = Rule::with_title("Title").characters("-");
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert_eq!(cell_len(line), 40);
assert!(line.contains("Title"));
assert!(line.contains(" Title "));
}
#[test]
fn test_centered_title_has_rule_chars_both_sides() {
let console = make_console(20);
let rule = Rule::with_title("X").characters("-");
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert!(line.contains('-'));
let parts: Vec<&str> = line.split(" X ").collect();
assert!(parts.len() >= 2);
assert!(parts[0].contains('-'));
assert!(parts[1].contains('-'));
}
#[test]
fn test_left_title() {
let console = make_console(30);
let rule = Rule::with_title("Left")
.characters("-")
.align(HorizontalAlign::Left);
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert_eq!(cell_len(line), 30);
assert!(line.starts_with("Left"));
}
#[test]
fn test_right_title() {
let console = make_console(30);
let rule = Rule::with_title("Right")
.characters("-")
.align(HorizontalAlign::Right);
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert_eq!(cell_len(line), 30);
assert!(line.ends_with("Right"));
}
#[test]
fn test_ascii_fallback() {
let console = Console::builder()
.width(20)
.force_terminal(true)
.no_color(true)
.markup(false)
.build();
let rule = Rule::new();
let mut opts = console.options();
opts.encoding = "ascii".to_string();
let segments = rule.rich_console(&console, &opts);
let output = segments_to_text(&segments);
let line = output.trim_end_matches('\n');
assert!(line.is_ascii());
assert_eq!(cell_len(line), 20);
}
#[test]
fn test_custom_style() {
let console = make_console(20);
let custom_style = Style::parse("bold").unwrap();
let rule = Rule::new().style(custom_style);
let opts = console.options();
let segments = rule.rich_console(&console, &opts);
let rule_segs: Vec<&Segment> = segments
.iter()
.filter(|s| s.text.trim().len() > 0 && s.text != "\n")
.collect();
assert!(!rule_segs.is_empty());
for seg in &rule_segs {
assert!(seg.style.is_some());
}
}
#[test]
fn test_default() {
let rule = Rule::default();
assert!(rule.title.is_none());
assert_eq!(rule.end, "\n");
assert_eq!(rule.align, HorizontalAlign::Center);
}
#[test]
fn test_builder_chain() {
let rule = Rule::new()
.characters("=")
.align(HorizontalAlign::Left)
.end("")
.style(Style::parse("bold").unwrap());
assert_eq!(rule.characters, "=");
assert_eq!(rule.align, HorizontalAlign::Left);
assert_eq!(rule.end, "");
assert!(rule.style.bold() == Some(true));
}
#[test]
fn test_rule_line_exact() {
let rule = Rule::new().characters("-");
let line = rule.rule_line(5);
assert_eq!(line, "-----");
}
#[test]
fn test_rule_line_multi_char() {
let rule = Rule::new().characters("=-");
let line = rule.rule_line(6);
assert_eq!(line, "=-=-=-");
}
#[test]
fn test_rule_line_remainder() {
let rule = Rule::new().characters("=-");
let line = rule.rule_line(5);
assert_eq!(cell_len(&line), 5);
}
#[test]
fn test_rule_line_zero_width() {
let rule = Rule::new().characters("-");
let line = rule.rule_line(0);
assert_eq!(line, "");
}
#[test]
fn test_title_truncation() {
let console = make_console(10);
let rule = Rule::with_title("This is a very long title").characters("-");
let output = render_rule(&console, &rule);
let line = output.trim_end_matches('\n');
assert_eq!(cell_len(line), 10);
}
#[test]
fn test_ends_with_newline() {
let console = make_console(20);
let rule = Rule::new();
let output = render_rule(&console, &rule);
assert!(output.ends_with('\n'));
}
#[test]
fn test_no_end() {
let console = make_console(20);
let rule = Rule::new().end("");
let output = render_rule(&console, &rule);
assert!(!output.ends_with('\n'));
}
#[test]
fn test_with_title() {
let rule = Rule::with_title("Hello");
assert!(rule.title.is_some());
assert_eq!(rule.title.as_ref().unwrap().plain(), "Hello");
}
#[test]
fn test_display_trait() {
let rule = Rule::new();
let s = format!("{}", rule);
assert!(!s.is_empty());
}
#[test]
fn test_display_with_title() {
let rule = Rule::with_title("Section");
let s = format!("{}", rule);
assert!(s.contains("Section"));
}
}