use std::borrow::Cow;
use crate::components::alignment::HorizontalAlignmentKind;
use crate::components::alignment::VerticalAlignmentKind;
use crate::components::Aligned;
use crate::Component;
use crate::Dimensions;
use crate::DrawMode;
use crate::Line;
use crate::Lines;
use crate::Span;
#[derive(Debug)]
pub struct Bordered<C: Component = Box<dyn Component>> {
child: Aligned<C>,
pub border: BorderedSpec,
}
#[derive(Debug)]
pub struct BorderedSpec {
pub left: Option<Span>,
pub right: Option<Span>,
pub top: Option<Span>,
pub bottom: Option<Span>,
}
impl Default for BorderedSpec {
fn default() -> Self {
let vertical = Some(Span::new_unstyled("|").unwrap());
let horizontal = Some(Span::new_unstyled("-").unwrap());
Self {
left: vertical.clone(),
right: vertical,
top: horizontal.clone(),
bottom: horizontal,
}
}
}
impl<C: Component> Bordered<C> {
pub fn new(child: C, border: BorderedSpec) -> Self {
Self {
child: Aligned {
child,
horizontal: HorizontalAlignmentKind::Left(true),
vertical: VerticalAlignmentKind::Top,
},
border,
}
}
}
fn construct_vertical_padding(padding: Span, width: usize) -> Vec<Line> {
padding
.iter()
.map(|mut span| {
span.content = Cow::Owned(span.content.repeat(width / span.len()));
Line::from_iter([span])
})
.collect()
}
impl<C: Component> Component for Bordered<C> {
fn draw_unchecked(
&self,
Dimensions { width, height }: Dimensions,
mode: DrawMode,
) -> anyhow::Result<Lines> {
let opt_len = |opt_word: &Option<Span>| match opt_word {
Some(word) => word.len(),
None => 0,
};
let new_dims = Dimensions {
width: width.saturating_sub(opt_len(&self.border.left) + opt_len(&self.border.right)),
height: height.saturating_sub(opt_len(&self.border.top) + opt_len(&self.border.bottom)),
};
let mut output = self.child.draw(new_dims, mode)?;
for line in output.iter_mut() {
if let Some(left) = &self.border.left {
line.push_front(left.clone());
}
if let Some(right) = &self.border.right {
line.push(right.clone());
}
}
if let Some(top) = &self.border.top {
let lines = construct_vertical_padding(top.clone(), output.max_line_length());
output.0.splice(0..0, lines.into_iter());
}
if let Some(bottom) = &self.border.bottom {
let lines = construct_vertical_padding(bottom.clone(), output.max_line_length());
output.0.extend(lines.into_iter());
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
use derive_more::AsRef;
use super::*;
use crate::components::echo::Echo;
#[derive(AsRef, Debug)]
struct Msg(Lines);
#[test]
fn test_basic() -> anyhow::Result<()> {
let msg = Lines(vec![
vec!["Test"].try_into()?, vec!["Longer"].try_into()?, vec!["Even Longer", "ok"].try_into()?, Line::default(),
]);
let component = Bordered::new(Echo(msg), BorderedSpec::default());
let output = component.draw(Dimensions::new(14, 5), DrawMode::Normal)?;
let expected = Lines(vec![
vec!["-".repeat(14)].try_into()?,
vec!["|", "Test", &" ".repeat(12 - 4), "|"].try_into()?,
vec!["|", "Longer", &" ".repeat(12 - 6), "|"].try_into()?,
vec!["|", "Even Longer", "o", "|"].try_into()?,
vec!["-".repeat(14)].try_into()?,
]);
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_complex() -> anyhow::Result<()> {
let msg = Lines(vec![
vec!["Test"].try_into()?, vec!["Longer"].try_into()?, vec!["Even Longer", "ok"].try_into()?, Line::default(),
]);
let component = Bordered::new(
Echo(msg),
BorderedSpec {
top: Some("@@@".try_into()?),
left: None,
bottom: Some("@".try_into()?),
..Default::default()
},
);
let output = component.draw(Dimensions::new(13, 7), DrawMode::Normal)?;
let expected = Lines(vec![
vec!["@".repeat(13)].try_into()?,
vec!["@".repeat(13)].try_into()?,
vec!["@".repeat(13)].try_into()?,
vec!["Test", &" ".repeat(12 - 4), "|"].try_into()?,
vec!["Longer", &" ".repeat(12 - 6), "|"].try_into()?,
vec!["Even Longer", "o", "|"].try_into()?,
vec!["@".repeat(13)].try_into()?,
]);
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_multi_width_unicode() -> anyhow::Result<()> {
let multi_width = "🦶";
let msg = Lines(vec![vec!["Tested"].try_into()?]);
let component = Bordered::new(
Echo(msg),
BorderedSpec {
top: Some(multi_width.try_into()?),
left: None,
right: None,
bottom: None,
},
);
let output = component.draw(Dimensions::new(13, 7), DrawMode::Normal)?;
let expected = Lines(vec![vec!["🦶🦶🦶"].try_into()?, vec!["Tested"].try_into()?]);
assert_eq!(output, expected);
Ok(())
}
}