use ratatui::layout::Alignment;
use ratatui::text::Line;
use ratatui::widgets::Block;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Title {
text: String,
alignment: TitleAlignment,
position: TitlePosition,
}
impl Title {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
alignment: TitleAlignment::default(),
position: TitlePosition::default(),
}
}
#[must_use]
pub fn left(mut self) -> Self {
self.alignment = TitleAlignment::Start;
self
}
#[must_use]
pub fn center(mut self) -> Self {
self.alignment = TitleAlignment::Center;
self
}
#[must_use]
pub fn right(mut self) -> Self {
self.alignment = TitleAlignment::End;
self
}
#[must_use]
pub fn top(mut self) -> Self {
self.position = TitlePosition::Top;
self
}
#[must_use]
pub fn bottom(mut self) -> Self {
self.position = TitlePosition::Bottom;
self
}
#[must_use]
pub fn render(&self) -> Line<'static> {
Line::from(self.text.clone())
}
#[must_use]
pub fn alignment(&self) -> TitleAlignment {
self.alignment
}
#[must_use]
pub fn position(&self) -> TitlePosition {
self.position
}
}
impl From<Title> for Line<'static> {
fn from(title: Title) -> Self {
title.render()
}
}
impl<T: Into<String>> From<T> for Title {
fn from(text: T) -> Self {
Title::new(text)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TitleAlignment {
Start,
#[default]
Center,
End,
}
impl From<TitleAlignment> for Alignment {
fn from(alignment: TitleAlignment) -> Self {
match alignment {
TitleAlignment::Start => Alignment::Left,
TitleAlignment::Center => Alignment::Center,
TitleAlignment::End => Alignment::Right,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TitlePosition {
#[default]
Top,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TitleStyle {
#[default]
Normal,
Bold,
Italic,
BoldItalic,
Script,
BoldScript,
SansSerif,
BoldSansSerif,
ItalicSansSerif,
Monospace,
}
impl TitleStyle {
#[must_use]
pub fn apply(&self, text: &str) -> String {
match self {
Self::Normal => text.to_string(),
Self::Bold => convert_to_bold(text),
Self::Italic => convert_to_italic(text),
Self::BoldItalic => convert_to_bold_italic(text),
Self::Script => convert_to_script(text),
Self::BoldScript => convert_to_bold_script(text),
Self::SansSerif => convert_to_sans_serif(text),
Self::BoldSansSerif => convert_to_bold_sans_serif(text),
Self::ItalicSansSerif => convert_to_italic_sans_serif(text),
Self::Monospace => convert_to_monospace(text),
}
}
}
macro_rules! unicode_converter {
($name:ident, $upper:expr, $lower:expr, $digit:expr) => {
fn $name(text: &str) -> String {
text.chars()
.map(|c| match c {
'A'..='Z' => char::from_u32($upper + (c as u32 - 'A' as u32)).unwrap(),
'a'..='z' => char::from_u32($lower + (c as u32 - 'a' as u32)).unwrap(),
'0'..='9' => char::from_u32($digit + (c as u32 - '0' as u32)).unwrap(),
_ => c,
})
.collect()
}
};
($name:ident, $upper:expr, $lower:expr) => {
fn $name(text: &str) -> String {
text.chars()
.map(|c| match c {
'A'..='Z' => char::from_u32($upper + (c as u32 - 'A' as u32)).unwrap(),
'a'..='z' => char::from_u32($lower + (c as u32 - 'a' as u32)).unwrap(),
_ => c,
})
.collect()
}
};
}
unicode_converter!(convert_to_bold, 0x1D400, 0x1D41A, 0x1D7CE);
unicode_converter!(convert_to_italic, 0x1D434, 0x1D44E);
unicode_converter!(convert_to_bold_italic, 0x1D468, 0x1D482);
unicode_converter!(convert_to_script, 0x1D49C, 0x1D4B6);
unicode_converter!(convert_to_bold_script, 0x1D4D0, 0x1D4EA);
unicode_converter!(convert_to_sans_serif, 0x1D5A0, 0x1D5BA, 0x1D7E2);
unicode_converter!(convert_to_bold_sans_serif, 0x1D5D4, 0x1D5EE, 0x1D7EC);
unicode_converter!(convert_to_italic_sans_serif, 0x1D608, 0x1D622);
unicode_converter!(convert_to_monospace, 0x1D670, 0x1D68A, 0x1D7F6);
pub trait BlockExt<'a>: Sized {
#[must_use]
fn apply_title(self, title: Title) -> Self;
#[must_use]
fn title_alignment_horizontal(self, alignment: TitleAlignment) -> Self;
#[must_use]
fn title_vertical_position(self, position: TitlePosition) -> Self;
}
impl<'a> BlockExt<'a> for Block<'a> {
fn apply_title(self, title: Title) -> Self {
let styled_text = title.render();
let alignment = title.alignment();
let position = title.position();
let block = match position {
TitlePosition::Top => self.title(styled_text),
TitlePosition::Bottom => self.title_bottom(styled_text),
};
block.title_alignment(alignment.into())
}
fn title_alignment_horizontal(self, alignment: TitleAlignment) -> Self {
self.title_alignment(alignment.into())
}
fn title_vertical_position(self, position: TitlePosition) -> Self {
let _ = position;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn title_new() {
let title = Title::new("Test");
assert_eq!(title.text, "Test");
assert_eq!(title.alignment, TitleAlignment::Center);
assert_eq!(title.position, TitlePosition::Top);
}
#[test]
fn title_builder_alignment() {
let title = Title::new("Test").left();
assert_eq!(title.alignment, TitleAlignment::Start);
}
#[test]
fn title_builder_position() {
let title = Title::new("Test").bottom();
assert_eq!(title.position, TitlePosition::Bottom);
}
#[test]
fn title_builder_chaining() {
let title = Title::new("Test").center().bottom();
assert_eq!(title.alignment, TitleAlignment::Center);
assert_eq!(title.position, TitlePosition::Bottom);
}
#[test]
fn title_from_string() {
let title: Title = "Test".into();
assert_eq!(title.text, "Test");
}
#[test]
fn title_alignment_default() {
assert_eq!(TitleAlignment::default(), TitleAlignment::Center);
}
#[test]
fn title_position_default() {
assert_eq!(TitlePosition::default(), TitlePosition::Top);
}
#[test]
fn title_style_default() {
assert_eq!(TitleStyle::default(), TitleStyle::Normal);
}
#[test]
fn title_alignment_to_ratatui_alignment() {
assert_eq!(Alignment::from(TitleAlignment::Start), Alignment::Left);
assert_eq!(Alignment::from(TitleAlignment::Center), Alignment::Center);
assert_eq!(Alignment::from(TitleAlignment::End), Alignment::Right);
}
#[test]
fn title_alignment_clone() {
let align = TitleAlignment::End;
let cloned = align;
assert_eq!(align, cloned);
}
#[test]
fn title_position_clone() {
let pos = TitlePosition::Bottom;
let cloned = pos;
assert_eq!(pos, cloned);
}
#[test]
fn title_style_clone() {
let style = TitleStyle::Bold;
let cloned = style;
assert_eq!(style, cloned);
}
#[test]
fn title_alignment_debug() {
let align = TitleAlignment::Start;
let debug = format!("{align:?}");
assert_eq!(debug, "Start");
}
#[test]
fn title_position_debug() {
let pos = TitlePosition::Bottom;
let debug = format!("{pos:?}");
assert_eq!(debug, "Bottom");
}
#[test]
fn title_style_debug() {
let style = TitleStyle::Bold;
let debug = format!("{style:?}");
assert_eq!(debug, "Bold");
}
#[test]
fn block_ext_title_alignment() {
let block = Block::bordered()
.title("Test")
.title_alignment_horizontal(TitleAlignment::Center);
assert!(format!("{block:?}").contains("Test"));
}
#[test]
fn block_ext_title_position() {
let block = Block::bordered()
.title("Test")
.title_vertical_position(TitlePosition::Bottom);
assert!(format!("{block:?}").contains("Test"));
}
#[test]
fn block_ext_method_chaining() {
let block = Block::bordered()
.title("Test")
.title_alignment_horizontal(TitleAlignment::End)
.title_vertical_position(TitlePosition::Bottom);
assert!(format!("{block:?}").contains("Test"));
}
#[test]
fn title_style_normal() {
let text = "Hello World";
assert_eq!(TitleStyle::Normal.apply(text), "Hello World");
}
#[test]
fn title_style_bold_letters() {
let result = TitleStyle::Bold.apply("Hello");
assert_ne!(result, "Hello");
assert_eq!(result.chars().count(), 5); }
#[test]
fn title_style_bold_with_numbers() {
let result = TitleStyle::Bold.apply("Chart 2024");
assert!(result.chars().count() >= 10); }
#[test]
fn title_style_italic_letters() {
let result = TitleStyle::Italic.apply("Statistics");
assert_ne!(result, "Statistics");
}
#[test]
fn title_style_preserves_spaces() {
let result = TitleStyle::Bold.apply("Hello World");
assert!(result.contains(' '));
}
#[test]
fn title_style_preserves_punctuation() {
let result = TitleStyle::Bold.apply("Hello!");
assert!(result.ends_with('!'));
}
#[test]
fn title_style_script() {
let result = TitleStyle::Script.apply("Test");
assert_ne!(result, "Test");
}
#[test]
fn title_style_monospace() {
let result = TitleStyle::Monospace.apply("Code");
assert_ne!(result, "Code");
}
#[test]
fn title_style_sans_serif() {
let result = TitleStyle::SansSerif.apply("Modern");
assert_ne!(result, "Modern");
}
#[test]
fn title_style_empty_string() {
assert_eq!(TitleStyle::Bold.apply(""), "");
assert_eq!(TitleStyle::Italic.apply(""), "");
}
#[test]
fn title_style_mixed_case() {
let result = TitleStyle::Bold.apply("TeSt");
assert_ne!(result, "TeSt");
assert_eq!(result.chars().count(), 4);
}
#[test]
fn title_builder_left() {
let title = Title::new("Test").left();
assert_eq!(title.alignment, TitleAlignment::Start);
}
#[test]
fn title_builder_right() {
let title = Title::new("Test").right();
assert_eq!(title.alignment, TitleAlignment::End);
}
#[test]
fn title_builder_top() {
let title = Title::new("Test").bottom().top();
assert_eq!(title.position, TitlePosition::Top);
}
#[test]
fn title_alignment_getter() {
let title = Title::new("Test").right();
assert_eq!(title.alignment(), TitleAlignment::End);
}
#[test]
fn title_position_getter() {
let title = Title::new("Test").bottom();
assert_eq!(title.position(), TitlePosition::Bottom);
}
#[test]
fn title_render_returns_line() {
let title = Title::new("Hello");
let line = title.render();
assert_eq!(line.to_string(), "Hello");
}
#[test]
fn title_into_line() {
let title = Title::new("World");
let line: ratatui::text::Line = title.into();
assert_eq!(line.to_string(), "World");
}
#[test]
fn block_ext_apply_title_top_center() {
let title = Title::new("My Chart").center().top();
let block = Block::bordered().apply_title(title);
assert!(format!("{block:?}").contains("My Chart"));
}
#[test]
fn block_ext_apply_title_bottom_right() {
let title = Title::new("Footer").right().bottom();
let block = Block::bordered().apply_title(title);
assert!(format!("{block:?}").contains("Footer"));
}
#[test]
fn block_ext_apply_title_bottom_left() {
let title = Title::new("Left Footer").left().bottom();
let block = Block::bordered().apply_title(title);
assert!(format!("{block:?}").contains("Left Footer"));
}
#[test]
fn title_style_bold_italic() {
let result = TitleStyle::BoldItalic.apply("Test");
assert_ne!(result, "Test");
assert_eq!(result.chars().count(), 4);
}
#[test]
fn title_style_bold_script() {
let result = TitleStyle::BoldScript.apply("Test");
assert_ne!(result, "Test");
assert_eq!(result.chars().count(), 4);
}
#[test]
fn title_style_bold_sans_serif() {
let result = TitleStyle::BoldSansSerif.apply("Test");
assert_ne!(result, "Test");
assert_eq!(result.chars().count(), 4);
}
#[test]
fn title_style_italic_sans_serif() {
let result = TitleStyle::ItalicSansSerif.apply("Test");
assert_ne!(result, "Test");
assert_eq!(result.chars().count(), 4);
}
#[test]
fn title_style_bold_script_with_numbers() {
let result = TitleStyle::BoldScript.apply("Test 42");
assert!(result.contains('4'));
assert!(result.contains('2'));
}
#[test]
fn title_style_bold_italic_preserves_spaces() {
let result = TitleStyle::BoldItalic.apply("A B");
assert!(result.contains(' '));
}
#[test]
fn block_ext_title_vertical_position_top() {
let block = Block::bordered()
.title("Test")
.title_vertical_position(TitlePosition::Top);
assert!(format!("{block:?}").contains("Test"));
}
#[test]
fn title_style_all_variants_non_empty() {
let text = "ABC";
let variants = [
TitleStyle::Normal,
TitleStyle::Bold,
TitleStyle::Italic,
TitleStyle::BoldItalic,
TitleStyle::Script,
TitleStyle::BoldScript,
TitleStyle::SansSerif,
TitleStyle::BoldSansSerif,
TitleStyle::ItalicSansSerif,
TitleStyle::Monospace,
];
for variant in &variants {
let result = variant.apply(text);
assert_eq!(result.chars().count(), 3, "{variant:?} changed char count");
}
}
}