use crate::cells::cell_len;
use crate::console::{Console, ConsoleOptions};
use crate::renderables::Renderable;
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlignMethod {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VerticalAlignMethod {
#[default]
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone)]
pub struct Align<'a> {
content: Vec<Segment<'a>>,
width: usize,
method: AlignMethod,
pad_style: Style,
}
impl<'a> Align<'a> {
#[must_use]
pub fn new(content: impl IntoIterator<Item = Segment<'a>>, width: usize) -> Self {
Self {
content: content.into_iter().collect(),
width,
method: AlignMethod::Left,
pad_style: Style::new(),
}
}
#[must_use]
pub fn from_str(text: &'a str, width: usize) -> Self {
Self::new(vec![Segment::new(text, None)], width)
}
#[must_use]
pub fn method(mut self, method: AlignMethod) -> Self {
self.method = method;
self
}
#[must_use]
pub fn left(self) -> Self {
self.method(AlignMethod::Left)
}
#[must_use]
pub fn center(self) -> Self {
self.method(AlignMethod::Center)
}
#[must_use]
pub fn right(self) -> Self {
self.method(AlignMethod::Right)
}
#[must_use]
pub fn pad_style(mut self, style: Style) -> Self {
self.pad_style = style;
self
}
#[must_use]
pub fn content_width(&self) -> usize {
self.content.iter().map(|s| cell_len(&s.text)).sum()
}
#[must_use]
pub fn render(self) -> Vec<Segment<'a>> {
let content_width = self.content_width();
if content_width >= self.width {
return self.content;
}
let padding_total = self.width - content_width;
let mut result = Vec::with_capacity(self.content.len() + 2);
match self.method {
AlignMethod::Left => {
result.extend(self.content);
if padding_total > 0 {
result.push(Segment::new(
" ".repeat(padding_total),
Some(self.pad_style),
));
}
}
AlignMethod::Center => {
let left_pad = padding_total / 2;
let right_pad = padding_total - left_pad;
if left_pad > 0 {
result.push(Segment::new(
" ".repeat(left_pad),
Some(self.pad_style.clone()),
));
}
result.extend(self.content);
if right_pad > 0 {
result.push(Segment::new(" ".repeat(right_pad), Some(self.pad_style)));
}
}
AlignMethod::Right => {
if padding_total > 0 {
result.push(Segment::new(
" ".repeat(padding_total),
Some(self.pad_style),
));
}
result.extend(self.content);
}
}
result
}
}
impl Renderable for Align<'_> {
fn render<'b>(&'b self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment<'b>> {
self.clone().render().into_iter().collect()
}
}
#[derive(Debug, Clone)]
pub struct AlignLines<'a> {
lines: Vec<Vec<Segment<'a>>>,
width: usize,
method: AlignMethod,
pad_style: Style,
}
impl<'a> AlignLines<'a> {
#[must_use]
pub fn new(lines: Vec<Vec<Segment<'a>>>, width: usize) -> Self {
Self {
lines,
width,
method: AlignMethod::Left,
pad_style: Style::new(),
}
}
#[must_use]
pub fn method(mut self, method: AlignMethod) -> Self {
self.method = method;
self
}
#[must_use]
pub fn left(self) -> Self {
self.method(AlignMethod::Left)
}
#[must_use]
pub fn center(self) -> Self {
self.method(AlignMethod::Center)
}
#[must_use]
pub fn right(self) -> Self {
self.method(AlignMethod::Right)
}
#[must_use]
pub fn pad_style(mut self, style: Style) -> Self {
self.pad_style = style;
self
}
#[must_use]
pub fn line_width(line: &[Segment]) -> usize {
line.iter().map(|s| cell_len(&s.text)).sum()
}
#[must_use]
pub fn render(self) -> Vec<Vec<Segment<'a>>> {
self.lines
.into_iter()
.map(|line| {
Align::new(line, self.width)
.method(self.method)
.pad_style(self.pad_style.clone())
.render()
})
.collect()
}
}
impl Renderable for AlignLines<'_> {
fn render<'b>(&'b self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment<'b>> {
let lines = self.clone().render();
let mut result = Vec::new();
for (i, line) in lines.into_iter().enumerate() {
if i > 0 {
result.push(Segment::line());
}
result.extend(line);
}
result.into_iter().collect()
}
}
#[must_use]
pub fn align_text(text: &str, width: usize, method: AlignMethod) -> String {
let content_width = cell_len(text);
if content_width >= width {
return text.to_string();
}
let padding_total = width - content_width;
match method {
AlignMethod::Left => {
format!("{text}{}", " ".repeat(padding_total))
}
AlignMethod::Center => {
let left_pad = padding_total / 2;
let right_pad = padding_total - left_pad;
format!("{}{text}{}", " ".repeat(left_pad), " ".repeat(right_pad))
}
AlignMethod::Right => {
format!("{}{text}", " ".repeat(padding_total))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_align_method_default() {
assert_eq!(AlignMethod::default(), AlignMethod::Left);
}
#[test]
fn test_align_left() {
let content = vec![Segment::new("Hi", None)];
let aligned = Align::new(content, 10).left().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "Hi ");
assert_eq!(cell_len(&text), 10);
}
#[test]
fn test_align_center() {
let content = vec![Segment::new("Hi", None)];
let aligned = Align::new(content, 10).center().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, " Hi ");
assert_eq!(cell_len(&text), 10);
}
#[test]
fn test_align_right() {
let content = vec![Segment::new("Hi", None)];
let aligned = Align::new(content, 10).right().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, " Hi");
assert_eq!(cell_len(&text), 10);
}
#[test]
fn test_align_content_too_wide() {
let content = vec![Segment::new("Hello World", None)];
let aligned = Align::new(content, 5).center().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "Hello World");
}
#[test]
fn test_align_exact_width() {
let content = vec![Segment::new("Hello", None)];
let aligned = Align::new(content, 5).center().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "Hello");
}
#[test]
fn test_align_multiple_segments() {
let content = vec![
Segment::new("Hello", None),
Segment::new(" ", None),
Segment::new("World", None),
];
let aligned = Align::new(content, 20).center().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(cell_len(&text), 20);
assert!(text.contains("Hello World"));
}
#[test]
fn test_align_center_odd_padding() {
let content = vec![Segment::new("abc", None)];
let aligned = Align::new(content, 10).center().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, " abc ");
assert_eq!(cell_len(&text), 10);
}
#[test]
fn test_align_from_str() {
let aligned = Align::from_str("Test", 10).right().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, " Test");
}
#[test]
fn test_align_lines() {
let lines = vec![
vec![Segment::new("Short", None)],
vec![Segment::new("Longer line", None)],
vec![Segment::new("Hi", None)],
];
let aligned = AlignLines::new(lines, 15).center().render();
assert_eq!(aligned.len(), 3);
for line in &aligned {
let text: String = line.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(cell_len(&text), 15);
}
}
#[test]
fn test_align_text_function() {
assert_eq!(align_text("Hi", 10, AlignMethod::Left), "Hi ");
assert_eq!(align_text("Hi", 10, AlignMethod::Center), " Hi ");
assert_eq!(align_text("Hi", 10, AlignMethod::Right), " Hi");
}
#[test]
fn test_align_with_cjk() {
let content = vec![Segment::new("日本", None)]; let aligned = Align::new(content, 10).center().render();
let text: String = aligned.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(cell_len(&text), 10);
assert!(text.starts_with(" "));
assert!(text.ends_with(" "));
}
#[test]
fn test_content_width() {
let content = vec![
Segment::new("Hello", None),
Segment::new(" ", None),
Segment::new("World", None),
];
let align = Align::new(content, 20);
assert_eq!(align.content_width(), 11);
}
#[test]
fn test_vertical_align_default() {
assert_eq!(VerticalAlignMethod::default(), VerticalAlignMethod::Top);
}
}