use std::borrow::Cow;
use crate::style::Style;
use crate::width::str_cols;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,
}
impl<'a> Span<'a> {
pub fn raw<T: Into<Cow<'a, str>>>(content: T) -> Self {
Self {
content: content.into(),
style: Style::default(),
}
}
pub fn styled<T: Into<Cow<'a, str>>, S: Into<Style>>(content: T, style: S) -> Self {
Self {
content: content.into(),
style: style.into(),
}
}
#[must_use = "returns a new Span"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "returns a new Span"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
}
pub fn width(&self) -> u16 {
str_cols(self.content.as_ref())
}
pub fn into_owned(self) -> Span<'static> {
Span {
content: Cow::Owned(self.content.into_owned()),
style: self.style,
}
}
}
impl<'a, T> From<T> for Span<'a>
where
T: Into<Cow<'a, str>>,
{
fn from(content: T) -> Self {
Self::raw(content)
}
}
impl std::fmt::Display for Span<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.content.as_ref())
}
}
fn split_lines(content: Cow<'_, str>) -> Vec<Span<'_>> {
match content {
Cow::Borrowed(text) => text.lines().map(Span::raw).collect(),
Cow::Owned(text) => {
let mut lines = text.lines();
match (lines.next(), lines.next()) {
(None, _) => Vec::new(),
(Some(only), None) if only.len() == text.len() => {
vec![Span::raw(Cow::Owned(text))]
}
_ => text
.lines()
.map(|line| Span::raw(Cow::Owned(line.to_owned())))
.collect(),
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Line<'a> {
pub spans: Vec<Span<'a>>,
pub style: Style,
pub alignment: Option<Alignment>,
}
impl<'a> Line<'a> {
pub fn raw<T: Into<Cow<'a, str>>>(content: T) -> Self {
Self {
spans: split_lines(content.into()),
style: Style::default(),
alignment: None,
}
}
pub fn styled<T: Into<Cow<'a, str>>, S: Into<Style>>(content: T, style: S) -> Self {
Self {
spans: split_lines(content.into()),
style: style.into(),
alignment: None,
}
}
#[must_use = "returns a new Line"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "returns a new Line"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
}
#[must_use = "returns a new Line"]
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = Some(alignment);
self
}
#[must_use = "returns a new Line"]
pub fn left_aligned(self) -> Self {
self.alignment(Alignment::Left)
}
#[must_use = "returns a new Line"]
pub fn centered(self) -> Self {
self.alignment(Alignment::Center)
}
#[must_use = "returns a new Line"]
pub fn right_aligned(self) -> Self {
self.alignment(Alignment::Right)
}
pub fn push_span<S: Into<Span<'a>>>(&mut self, span: S) {
self.spans.push(span.into());
}
pub fn width(&self) -> u16 {
self.spans
.iter()
.map(Span::width)
.fold(0, u16::saturating_add)
}
pub fn iter(&self) -> std::slice::Iter<'_, Span<'a>> {
self.spans.iter()
}
pub fn into_owned(self) -> Line<'static> {
Line {
spans: self.spans.into_iter().map(Span::into_owned).collect(),
style: self.style,
alignment: self.alignment,
}
}
}
impl<'a> From<Span<'a>> for Line<'a> {
fn from(span: Span<'a>) -> Self {
Self {
spans: vec![span],
style: Style::default(),
alignment: None,
}
}
}
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
fn from(spans: Vec<Span<'a>>) -> Self {
Self {
spans,
style: Style::default(),
alignment: None,
}
}
}
impl From<String> for Line<'_> {
fn from(content: String) -> Self {
Self::raw(content)
}
}
impl<'a> From<&'a str> for Line<'a> {
fn from(content: &'a str) -> Self {
Self::raw(content)
}
}
impl<'a> From<Cow<'a, str>> for Line<'a> {
fn from(content: Cow<'a, str>) -> Self {
Self::raw(content)
}
}
impl<'a> FromIterator<Span<'a>> for Line<'a> {
fn from_iter<I: IntoIterator<Item = Span<'a>>>(iter: I) -> Self {
Self::from(iter.into_iter().collect::<Vec<_>>())
}
}
impl<'a> IntoIterator for Line<'a> {
type Item = Span<'a>;
type IntoIter = std::vec::IntoIter<Span<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.spans.into_iter()
}
}
impl<'a, 'b> IntoIterator for &'b Line<'a> {
type Item = &'b Span<'a>;
type IntoIter = std::slice::Iter<'b, Span<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.spans.iter()
}
}
impl std::fmt::Display for Line<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for span in &self.spans {
f.write_str(span.content.as_ref())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Color, Modifier};
#[test]
fn a_str_becomes_a_one_span_line() {
let line = Line::from("hello");
assert_eq!(line.spans.len(), 1);
assert_eq!(line.to_string(), "hello");
assert_eq!(line.alignment, None);
}
#[test]
fn raw_splits_on_line_breaks() {
assert!(Line::raw("").spans.is_empty());
assert!(Line::raw(String::new()).spans.is_empty());
assert_eq!(Line::raw("one").spans.len(), 1);
let split = Line::raw("a\nb");
assert_eq!(split.spans.len(), 2);
assert_eq!(split.to_string(), "ab");
assert_eq!(Line::raw(String::from("a\nb")).spans.len(), 2);
}
#[test]
fn display_concatenates_spans_and_drops_styling() {
let line = Line::from(vec![
Span::styled("bo", Style::new().add_modifier(Modifier::BOLD)),
Span::raw("ld"),
]);
assert_eq!(line.to_string(), "bold");
}
#[test]
fn width_counts_display_columns_not_chars() {
let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
assert_eq!(Span::raw(family).width(), 2);
assert_eq!(Span::raw("abc").width(), 3);
assert_eq!(Line::from(family).width(), 2);
}
#[test]
fn line_width_sums_its_spans() {
let line = Line::from(vec![Span::raw("ab"), Span::raw("cde")]);
assert_eq!(line.width(), 5);
}
#[test]
fn width_saturates_rather_than_overflowing() {
let wide = "x".repeat(u16::MAX as usize);
let line = Line::from(vec![Span::raw(wide.clone()), Span::raw(wide)]);
assert_eq!(line.width(), u16::MAX);
}
#[test]
fn alignment_helpers_set_the_field() {
assert_eq!(Line::raw("x").centered().alignment, Some(Alignment::Center));
assert_eq!(
Line::raw("x").right_aligned().alignment,
Some(Alignment::Right)
);
assert_eq!(
Line::raw("x").left_aligned().alignment,
Some(Alignment::Left)
);
}
#[test]
fn span_style_patches_over_line_style() {
let line =
Line::styled("x", Style::new().fg(Color::Red)).style(Style::new().fg(Color::Red));
let span = Span::styled("x", Style::new().fg(Color::Blue));
assert_eq!(line.style.patch(span.style).fg, Some(Color::Blue));
}
#[test]
fn into_owned_detaches_from_the_borrow() {
let owned = {
let text = String::from("borrowed");
Line::raw(text.as_str()).into_owned()
};
assert_eq!(owned.to_string(), "borrowed");
}
#[test]
fn collecting_spans_builds_a_line() {
let line: Line = vec![Span::raw("a"), Span::raw("b")].into_iter().collect();
assert_eq!(line.to_string(), "ab");
}
}