use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::prelude::*;
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,
}
impl<'a> Span<'a> {
pub fn raw<T>(content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
Self {
content: content.into(),
style: Style::default(),
}
}
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Self {
content: content.into(),
style: style.into(),
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn content<T>(mut self, content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
self.content = content.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(self) -> Self {
self.patch_style(Style::reset())
}
pub fn width(&self) -> usize {
self.content.width()
}
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
let style = base_style.into().patch(self.style);
self.content
.as_ref()
.graphemes(true)
.filter(|g| *g != "\n")
.map(move |g| StyledGrapheme { symbol: g, style })
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_left_aligned_line(self) -> Line<'a> {
Line::from(self).left_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_left_aligned_line"]
pub fn to_left_aligned_line(self) -> Line<'a> {
self.into_left_aligned_line()
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_centered_line(self) -> Line<'a> {
Line::from(self).centered()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_centered_line"]
pub fn to_centered_line(self) -> Line<'a> {
self.into_centered_line()
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_right_aligned_line(self) -> Line<'a> {
Line::from(self).right_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_right_aligned_line"]
pub fn to_right_aligned_line(self) -> Line<'a> {
self.into_right_aligned_line()
}
}
impl<'a, T> From<T> for Span<'a>
where
T: Into<Cow<'a, str>>,
{
fn from(s: T) -> Self {
Span::raw(s.into())
}
}
impl<'a> Styled for Span<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl Widget for Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
let Rect {
x: mut current_x,
y,
width,
..
} = area;
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
for g in self.styled_graphemes(Style::default()) {
let symbol_width = g.symbol.width();
let next_x = current_x.saturating_add(symbol_width as u16);
if next_x > max_x {
break;
}
buf.get_mut(current_x, y)
.set_symbol(g.symbol)
.set_style(g.style);
for i in (current_x + 1)..next_x {
buf.get_mut(i, y).reset();
}
current_x = next_x;
}
}
}
impl fmt::Display for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.content, f)
}
}
#[cfg(test)]
mod tests {
use rstest::fixture;
use super::*;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn default() {
let span = Span::default();
assert_eq!(span.content, Cow::Borrowed(""));
assert_eq!(span.style, Style::default());
}
#[test]
fn raw_str() {
let span = Span::raw("test content");
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::default());
}
#[test]
fn raw_string() {
let content = String::from("test content");
let span = Span::raw(content.clone());
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn styled_str() {
let style = Style::new().red();
let span = Span::styled("test content", style);
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::new().red());
}
#[test]
fn styled_string() {
let content = String::from("test content");
let style = Style::new().green();
let span = Span::styled(content.clone(), style);
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, style);
}
#[test]
fn set_content() {
let span = Span::default().content("test content");
assert_eq!(span.content, Cow::Borrowed("test content"));
}
#[test]
fn set_style() {
let span = Span::default().style(Style::new().green());
assert_eq!(span.style, Style::new().green());
}
#[test]
fn from_ref_str_borrowed_cow() {
let content = "test content";
let span = Span::from(content);
assert_eq!(span.content, Cow::Borrowed(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_string_ref_str_borrowed_cow() {
let content = String::from("test content");
let span = Span::from(content.as_str());
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_string_owned_cow() {
let content = String::from("test content");
let span = Span::from(content.clone());
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_ref_string_borrowed_cow() {
let content = String::from("test content");
let span = Span::from(&content);
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
assert_eq!(span.style, Style::default());
}
#[test]
fn reset_style() {
let span = Span::styled("test content", Style::new().green()).reset_style();
assert_eq!(span.style, Style::reset());
}
#[test]
fn patch_style() {
let span = Span::styled("test content", Style::new().green().on_yellow())
.patch_style(Style::new().red().bold());
assert_eq!(span.style, Style::new().red().on_yellow().bold());
}
#[test]
fn width() {
assert_eq!(Span::raw("").width(), 0);
assert_eq!(Span::raw("test").width(), 4);
assert_eq!(Span::raw("test content").width(), 12);
}
#[test]
fn stylize() {
let span = Span::raw("test content").green();
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::new().green());
let span = Span::styled("test content", Style::new().green());
let stylized = span.on_yellow().bold();
assert_eq!(stylized.content, Cow::Borrowed("test content"));
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
}
#[test]
fn display_span() {
let span = Span::raw("test content");
assert_eq!(format!("{span}"), "test content");
assert_eq!(format!("{span:.4}"), "test");
}
#[test]
fn display_styled_span() {
let stylized_span = Span::styled("stylized test content", Style::new().green());
assert_eq!(format!("{stylized_span}"), "stylized test content");
assert_eq!(format!("{stylized_span:.8}"), "stylized");
}
#[test]
fn left_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_left_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_centered_line();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_right_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Right));
}
mod widget {
use rstest::rstest;
use super::*;
#[test]
fn render() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
}
#[rstest]
fn render_out_of_bounds(mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
#[test]
fn render_truncates_too_long_content() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
span.render(Rect::new(0, 0, 5, 1), &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
}
#[test]
fn render_patches_existing_style() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
buf.set_style(buf.area, Style::new().italic());
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow().italic(),
" ".italic(),
])]);
assert_eq!(buf, expected);
}
#[test]
fn render_multi_width_symbol() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
assert_eq!(buf, expected);
}
#[test]
fn render_multi_width_symbol_truncates_entire_symbol() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
span.render(buf.area, &mut buf);
let expected =
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
assert_eq!(buf, expected);
}
#[test]
fn render_overflowing_area_truncates() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(Rect::new(10, 0, 20, 1), &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
" ".into(),
"test ".green().on_yellow(),
])]);
assert_eq!(buf, expected);
}
}
}