use std::fmt;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)]
pub struct RenderedBlock {
cells: Vec<Vec<String>>,
width: usize,
height: usize,
baseline: usize,
}
impl RenderedBlock {
pub fn new(cells: Vec<Vec<String>>, baseline: usize) -> Self {
let height = cells.len();
let width = cells.first().map_or(0, |row| {
row.iter().map(|c| UnicodeWidthStr::width(c.as_str())).sum()
});
Self {
cells,
width,
height,
baseline,
}
}
pub fn from_char(ch: char) -> Self {
let s = ch.to_string();
let width = UnicodeWidthStr::width(s.as_str()).max(1);
Self {
cells: vec![vec![s]],
width,
height: 1,
baseline: 0,
}
}
pub fn from_text(text: &str) -> Self {
if text.is_empty() {
return Self::empty();
}
let cells: Vec<String> = text.chars().map(|c| c.to_string()).collect();
let width = UnicodeWidthStr::width(text);
Self {
cells: vec![cells],
width,
height: 1,
baseline: 0,
}
}
pub fn empty() -> Self {
Self {
cells: vec![],
width: 0,
height: 0,
baseline: 0,
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn baseline(&self) -> usize {
self.baseline
}
pub fn cells(&self) -> &[Vec<String>] {
&self.cells
}
pub fn is_empty(&self) -> bool {
self.height == 0 || self.width == 0
}
pub fn beside(&self, other: &RenderedBlock) -> RenderedBlock {
if self.is_empty() {
return other.clone();
}
if other.is_empty() {
return self.clone();
}
let baseline = self.baseline.max(other.baseline);
let above_baseline = baseline;
let self_below = self.height.saturating_sub(self.baseline + 1);
let other_below = other.height.saturating_sub(other.baseline + 1);
let below_baseline = self_below.max(other_below);
let total_height = above_baseline + 1 + below_baseline;
let total_width = self.width + other.width;
let self_top_pad = above_baseline - self.baseline;
let other_top_pad = above_baseline - other.baseline;
let mut rows = Vec::with_capacity(total_height);
for row_idx in 0..total_height {
let mut row = Vec::new();
let self_row = row_idx.checked_sub(self_top_pad);
if let Some(sr) = self_row {
if sr < self.height {
row.extend(self.cells[sr].iter().cloned());
} else {
row.extend(std::iter::repeat_n(" ".to_string(), self.width));
}
} else {
row.extend(std::iter::repeat_n(" ".to_string(), self.width));
}
let other_row = row_idx.checked_sub(other_top_pad);
if let Some(or_idx) = other_row {
if or_idx < other.height {
row.extend(other.cells[or_idx].iter().cloned());
} else {
row.extend(std::iter::repeat_n(" ".to_string(), other.width));
}
} else {
row.extend(std::iter::repeat_n(" ".to_string(), other.width));
}
rows.push(row);
}
RenderedBlock {
cells: rows,
width: total_width,
height: total_height,
baseline,
}
}
pub fn above(
top: &RenderedBlock,
bottom: &RenderedBlock,
baseline_row: usize,
) -> RenderedBlock {
let width = top.width.max(bottom.width);
let mut rows = Vec::with_capacity(top.height + bottom.height);
for r in 0..top.height {
rows.push(Self::pad_row_to_width(&top.cells[r], top.width, width));
}
for r in 0..bottom.height {
rows.push(Self::pad_row_to_width(
&bottom.cells[r],
bottom.width,
width,
));
}
RenderedBlock {
cells: rows,
width,
height: top.height + bottom.height,
baseline: baseline_row,
}
}
pub fn pad(&self, left: usize, right: usize, top: usize, bottom: usize) -> RenderedBlock {
let new_width = left + self.width + right;
let new_height = top + self.height + bottom;
let mut rows = Vec::with_capacity(new_height);
for _ in 0..top {
rows.push(vec![" ".to_string(); new_width]);
}
for r in 0..self.height {
let mut row = Vec::with_capacity(new_width);
row.extend(std::iter::repeat_n(" ".to_string(), left));
row.extend(self.cells[r].iter().cloned());
row.extend(std::iter::repeat_n(" ".to_string(), right));
rows.push(row);
}
for _ in 0..bottom {
rows.push(vec![" ".to_string(); new_width]);
}
RenderedBlock {
cells: rows,
width: new_width,
height: new_height,
baseline: self.baseline + top,
}
}
pub fn center_in(&self, target_width: usize) -> RenderedBlock {
if target_width <= self.width {
return self.clone();
}
let total_pad = target_width - self.width;
let left_pad = total_pad / 2;
let right_pad = total_pad - left_pad;
self.pad(left_pad, right_pad, 0, 0)
}
fn pad_row_to_width(row: &[String], current_width: usize, target_width: usize) -> Vec<String> {
let mut result = row.to_vec();
let pad = target_width.saturating_sub(current_width);
result.extend(std::iter::repeat_n(" ".to_string(), pad));
result
}
pub fn hline(ch: char, width: usize) -> RenderedBlock {
let cells = vec![vec![ch.to_string(); width]];
RenderedBlock {
cells,
width,
height: 1,
baseline: 0,
}
}
}
impl fmt::Display for RenderedBlock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, row) in self.cells.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
for cell in row {
write!(f, "{}", cell)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_char() {
let block = RenderedBlock::from_char('x');
assert_eq!(block.width(), 1);
assert_eq!(block.height(), 1);
assert_eq!(block.baseline(), 0);
assert_eq!(format!("{}", block), "x");
}
#[test]
fn test_from_text() {
let block = RenderedBlock::from_text("hello");
assert_eq!(block.width(), 5);
assert_eq!(block.height(), 1);
assert_eq!(format!("{}", block), "hello");
}
#[test]
fn test_beside_baseline_aligned() {
let a = RenderedBlock::from_text("ab");
let b = RenderedBlock::from_text("cd");
let result = a.beside(&b);
assert_eq!(result.width(), 4);
assert_eq!(result.height(), 1);
assert_eq!(format!("{}", result), "abcd");
}
#[test]
fn test_beside_different_heights() {
let a = RenderedBlock::new(
vec![vec!["a".into()], vec!["b".into()], vec!["c".into()]],
1,
);
let d = RenderedBlock::from_char('d');
let result = a.beside(&d);
assert_eq!(result.height(), 3);
assert_eq!(result.baseline(), 1);
let output = format!("{}", result);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines[0], "a ");
assert_eq!(lines[1], "bd");
assert_eq!(lines[2], "c ");
}
#[test]
fn test_center_in() {
let block = RenderedBlock::from_text("ab");
let centered = block.center_in(6);
assert_eq!(centered.width(), 6);
assert_eq!(format!("{}", centered), " ab ");
}
#[test]
fn test_above() {
let top = RenderedBlock::from_text("abc");
let bottom = RenderedBlock::from_text("de");
let result = RenderedBlock::above(&top, &bottom, 0);
assert_eq!(result.height(), 2);
assert_eq!(result.width(), 3);
let output = format!("{}", result);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines[0], "abc");
assert_eq!(lines[1], "de ");
}
#[test]
fn test_pad() {
let block = RenderedBlock::from_char('x');
let padded = block.pad(1, 1, 1, 1);
assert_eq!(padded.width(), 3);
assert_eq!(padded.height(), 3);
assert_eq!(padded.baseline(), 1);
let output = format!("{}", padded);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines[0], " ");
assert_eq!(lines[1], " x ");
assert_eq!(lines[2], " ");
}
}