use crate::grid::{display_width, Style};
use std::borrow::Cow;
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Span<'a> {
pub text: Cow<'a, str>,
pub style: Style,
}
impl<'a> Span<'a> {
pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
Self {
text: text.into(),
style: Style::default(),
}
}
pub fn styled(text: impl Into<Cow<'a, str>>, style: Style) -> Self {
Self {
text: text.into(),
style,
}
}
pub fn width(&self) -> u16 {
display_width(self.text.as_ref())
}
}
impl<'a> From<&'a str> for Span<'a> {
fn from(s: &'a str) -> Self {
Self::raw(s)
}
}
impl From<String> for Span<'_> {
fn from(s: String) -> Self {
Self::raw(Cow::Owned(s))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Line<'a> {
pub spans: Vec<Span<'a>>,
}
impl<'a> Line<'a> {
pub fn new() -> Self {
Self { spans: Vec::new() }
}
pub fn from_spans<I: IntoIterator<Item = Span<'a>>>(spans: I) -> Self {
Self {
spans: spans.into_iter().collect(),
}
}
pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
Self::from_spans([Span::raw(text)])
}
pub fn push<S: Into<Span<'a>>>(mut self, span: S) -> Self {
self.spans.push(span.into());
self
}
pub fn width(&self) -> u16 {
self.spans
.iter()
.fold(0u16, |width, span| width.saturating_add(span.width()))
}
}
impl<'a> From<&'a str> for Line<'a> {
fn from(s: &'a str) -> Self {
Self::raw(s)
}
}
impl From<String> for Line<'_> {
fn from(s: String) -> Self {
Self::raw(Cow::Owned(s))
}
}
impl<'a> From<Span<'a>> for Line<'a> {
fn from(span: Span<'a>) -> Self {
Self::from_spans([span])
}
}
#[macro_export]
macro_rules! line {
() => { $crate::line::Line::new() };
($($span:expr),+ $(,)?) => {{
$crate::line::Line::from_spans([$(::core::convert::Into::<$crate::line::Span<'_>>::into($span)),+])
}};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grid::Color;
#[test]
fn span_from_str_is_default_style() {
let s: Span = "hi".into();
assert_eq!(s.text, "hi");
assert_eq!(s.style, Style::default());
assert_eq!(s.width(), 2);
}
#[test]
fn line_width_sums_spans() {
let l = Line::from_spans([
Span::raw("ab"),
Span::styled("cde", Style::new().fg(Color::Red)),
]);
assert_eq!(l.width(), 5);
}
#[test]
fn line_macro_mixes_strs_and_spans() {
let l = line!["foo", " ", Span::styled("bar", Style::new().fg(Color::Red))];
assert_eq!(l.spans.len(), 3);
assert_eq!(l.spans[0].text, "foo");
assert_eq!(l.spans[1].text, " ");
assert_eq!(l.spans[2].text, "bar");
assert_eq!(l.spans[2].style.fg, Some(Color::Red));
}
#[test]
fn span_width_counts_two_columns_per_wide_char() {
assert_eq!(Span::raw("漢字").width(), 4);
assert_eq!(Span::raw("a漢b").width(), 4);
}
#[test]
fn span_width_is_zero_for_empty_text() {
assert_eq!(Span::raw("").width(), 0);
}
#[test]
fn line_width_zero_for_empty_and_for_all_empty_spans() {
assert_eq!(Line::new().width(), 0);
let l = Line::from_spans([Span::raw(""), Span::raw("")]);
assert_eq!(l.width(), 0);
}
#[test]
fn line_push_appends_a_span() {
let l = Line::new()
.push("a")
.push(Span::styled("b", Style::new().fg(Color::Red)));
assert_eq!(l.spans.len(), 2);
assert_eq!(l.spans[0].text, "a");
assert_eq!(l.spans[1].style.fg, Some(Color::Red));
}
#[test]
fn line_raw_wraps_text_in_a_single_default_span() {
let l = Line::raw("hello");
assert_eq!(l.spans.len(), 1);
assert_eq!(l.spans[0].text, "hello");
assert_eq!(l.spans[0].style, Style::default());
}
}