use crate::errors::Result;
use crate::measure::{Measurable, MeasureOptions, Measurement, cell_len};
use crate::segment::{Segment, Segments};
use crate::style::Style;
use crate::text::{Justify, Text};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
title: Option<Text>,
character: char,
style: Option<Style>,
title_style: Option<Style>,
align: Justify,
end: Option<String>,
}
impl Rule {
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
title: None,
character: '─',
style: None,
title_style: None,
align: Justify::Center,
end: None,
}
}
#[inline]
#[must_use]
pub fn with_title(title: impl Into<Text>) -> Self {
Self {
title: Some(title.into()),
character: '─',
style: None,
title_style: None,
align: Justify::Center,
end: None,
}
}
#[inline]
#[must_use]
pub const fn character(mut self, ch: char) -> Self {
self.character = ch;
self
}
#[inline]
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
#[inline]
#[must_use]
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = Some(style);
self
}
#[inline]
#[must_use]
pub const fn align(mut self, align: Justify) -> Self {
self.align = align;
self
}
#[inline]
#[must_use]
pub fn end(mut self, end: impl Into<String>) -> Self {
self.end = Some(end.into());
self
}
#[must_use]
pub fn render(&self, width: usize) -> Segments {
let mut segments = Segments::new();
match &self.title {
Some(title) => {
let title_text = title.plain();
let title_width = cell_len(title_text);
let padding_width: usize = 2; let min_line_width: usize = 1;
if title_width
.saturating_add(padding_width.saturating_mul(2))
.saturating_add(min_line_width.saturating_mul(2))
> width
{
self.render_line(&mut segments, width);
} else {
let available = width
.saturating_sub(title_width)
.saturating_sub(padding_width.saturating_mul(2));
let (left_width, right_width) = match self.align {
Justify::Left | Justify::Default => {
(min_line_width, available.saturating_sub(min_line_width))
}
Justify::Right => {
(available.saturating_sub(min_line_width), min_line_width)
}
Justify::Center | Justify::Full => {
let half = available.checked_div(2).unwrap_or(0);
(half, available.saturating_sub(half))
}
};
self.render_line(&mut segments, left_width);
segments.push(Segment::new(" "));
let title_segments = title.to_segments();
if let Some(ref style) = self.title_style {
for seg in title_segments.iter() {
let combined = seg
.style
.clone()
.map(|s| s.combine(style))
.unwrap_or_else(|| style.clone());
segments.push(Segment::styled(seg.text.clone(), combined));
}
} else {
segments.extend(title_segments);
}
segments.push(Segment::new(" "));
self.render_line(&mut segments, right_width);
}
}
None => {
self.render_line(&mut segments, width);
}
}
if let Some(ref end_str) = self.end {
segments.push(Segment::new(end_str.clone()));
} else {
segments.push(Segment::newline());
}
segments
}
fn render_line(&self, segments: &mut Segments, width: usize) {
let line = self.character.to_string().repeat(width);
if let Some(ref style) = self.style {
segments.push(Segment::styled(line, style.clone()));
} else {
segments.push(Segment::new(line));
}
}
}
impl Default for Rule {
fn default() -> Self {
Self::new()
}
}
impl Measurable for Rule {
fn measure(&self, options: &MeasureOptions) -> Result<Measurement> {
Ok(Measurement::fixed(options.max_width))
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::color::{Color, StandardColor};
#[test]
fn test_rule_new() {
let rule = Rule::new();
assert!(rule.title.is_none());
assert_eq!(rule.character, '─');
}
#[test]
fn test_rule_default() {
let rule = Rule::default();
assert!(rule.title.is_none());
assert_eq!(rule.character, '─');
assert!(rule.style.is_none());
assert!(rule.title_style.is_none());
}
#[test]
fn test_rule_with_title() {
let rule = Rule::with_title("Test");
assert!(rule.title.is_some());
}
#[test]
fn test_rule_with_title_text() {
let text = Text::from("Title");
let rule = Rule::with_title(text);
assert!(rule.title.is_some());
}
#[test]
fn test_rule_render() {
let rule = Rule::new();
let segments = rule.render(40);
let text = segments.plain_text();
assert_eq!(text.trim(), "─".repeat(40));
}
#[test]
fn test_rule_render_with_title() {
let rule = Rule::with_title("Title");
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
assert!(text.contains('─'));
}
#[test]
fn test_rule_character() {
let rule = Rule::new().character('=');
let segments = rule.render(20);
let text = segments.plain_text();
assert!(text.contains('='));
}
#[test]
fn test_rule_character_asterisk() {
let rule = Rule::new().character('*');
let segments = rule.render(10);
let text = segments.plain_text();
assert!(text.contains('*'));
}
#[test]
fn test_rule_style() {
let style = Style::new()
.bold()
.with_color(Color::Standard(StandardColor::Red));
let rule = Rule::new().style(style);
let segments = rule.render(20);
assert!(!segments.is_empty());
}
#[test]
fn test_rule_title_style() {
let style = Style::new()
.bold()
.with_color(Color::Standard(StandardColor::Blue));
let rule = Rule::with_title("Title").title_style(style);
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
}
#[test]
fn test_rule_title_with_existing_style() {
let title = Text::from("Title");
let title_style = Style::new().bold();
let rule = Rule::with_title(title).title_style(title_style);
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
}
#[test]
fn test_rule_align_left() {
let rule = Rule::with_title("Title").align(Justify::Left);
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
}
#[test]
fn test_rule_align_right() {
let rule = Rule::with_title("Title").align(Justify::Right);
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
}
#[test]
fn test_rule_align_center() {
let rule = Rule::with_title("Title").align(Justify::Center);
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
}
#[test]
fn test_rule_align_full() {
let rule = Rule::with_title("Title").align(Justify::Full);
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
}
#[test]
fn test_rule_align_default() {
let rule = Rule::with_title("Title").align(Justify::Default);
let segments = rule.render(40);
let text = segments.plain_text();
assert!(text.contains("Title"));
}
#[test]
fn test_rule_end() {
let rule = Rule::new().end("");
let segments = rule.render(20);
let text = segments.plain_text();
assert!(!text.ends_with('\n'));
}
#[test]
fn test_rule_end_custom() {
let rule = Rule::new().end("\r\n");
let segments = rule.render(20);
let text = segments.plain_text();
assert!(text.ends_with("\r\n"));
}
#[test]
fn test_rule_narrow_width() {
let rule = Rule::with_title("Very Long Title That Won't Fit");
let segments = rule.render(10);
let text = segments.plain_text();
assert!(!text.contains("Title"));
}
#[test]
fn test_rule_measure() {
let rule = Rule::new();
let options = MeasureOptions::new(80);
let measurement = rule.measure(&options).unwrap();
assert_eq!(measurement.minimum, 80);
assert_eq!(measurement.maximum, 80);
}
#[test]
fn test_rule_measure_narrow() {
let rule = Rule::with_title("Test");
let options = MeasureOptions::new(20);
let measurement = rule.measure(&options).unwrap();
assert_eq!(measurement.minimum, 20);
}
#[test]
fn test_rule_builder_chain() {
let rule = Rule::with_title("Section")
.character('=')
.style(Style::new().bold())
.title_style(Style::new().italic())
.align(Justify::Left)
.end("");
let segments = rule.render(50);
let text = segments.plain_text();
assert!(text.contains("Section"));
assert!(text.contains('='));
}
}