use super::Style;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Span {
pub content: String,
pub style: Style,
}
impl Span {
pub fn raw<S: Into<String>>(content: S) -> Self {
Self {
content: content.into(),
style: Style::default(),
}
}
pub fn styled<S: Into<String>>(content: S, style: Style) -> Self {
Self {
content: content.into(),
style,
}
}
pub fn width(&self) -> usize {
UnicodeWidthStr::width(self.content.as_str())
}
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl<S: Into<String>> From<S> for Span {
fn from(s: S) -> Self {
Self::raw(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Line {
pub spans: Vec<Span>,
}
impl Line {
pub fn empty() -> Self {
Self { spans: Vec::new() }
}
pub fn raw<S: Into<String>>(content: S) -> Self {
Self {
spans: vec![Span::raw(content)],
}
}
pub fn styled<S: Into<String>>(content: S, style: Style) -> Self {
Self {
spans: vec![Span::styled(content, style)],
}
}
pub fn from_spans<I: IntoIterator<Item = Span>>(spans: I) -> Self {
Self {
spans: spans.into_iter().collect(),
}
}
pub fn width(&self) -> usize {
self.spans.iter().map(|s| s.width()).sum()
}
pub fn is_empty(&self) -> bool {
self.spans.is_empty() || self.spans.iter().all(|s| s.is_empty())
}
pub fn push(&mut self, span: Span) {
self.spans.push(span);
}
pub fn style(mut self, style: Style) -> Self {
for span in &mut self.spans {
span.style = style.patch(span.style);
}
self
}
pub fn styled_chars(&self) -> impl Iterator<Item = (char, Style)> + '_ {
self.spans
.iter()
.flat_map(|span| span.content.chars().map(move |c| (c, span.style)))
}
}
impl<S: Into<String>> From<S> for Line {
fn from(s: S) -> Self {
Self::raw(s)
}
}
impl FromIterator<Span> for Line {
fn from_iter<I: IntoIterator<Item = Span>>(iter: I) -> Self {
Self::from_spans(iter)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Text {
pub lines: Vec<Line>,
}
impl Text {
pub fn empty() -> Self {
Self { lines: Vec::new() }
}
pub fn raw<S: AsRef<str>>(content: S) -> Self {
Self {
lines: content
.as_ref()
.lines()
.map(|l| Line::raw(l.to_string()))
.collect(),
}
}
pub fn styled<S: AsRef<str>>(content: S, style: Style) -> Self {
Self {
lines: content
.as_ref()
.lines()
.map(|l| Line::styled(l.to_string(), style))
.collect(),
}
}
pub fn from_lines<I: IntoIterator<Item = Line>>(lines: I) -> Self {
Self {
lines: lines.into_iter().collect(),
}
}
pub fn height(&self) -> usize {
self.lines.len()
}
pub fn width(&self) -> usize {
self.lines.iter().map(|l| l.width()).max().unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty() || self.lines.iter().all(|l| l.is_empty())
}
pub fn push(&mut self, line: Line) {
self.lines.push(line);
}
pub fn style(mut self, style: Style) -> Self {
self.lines = self.lines.into_iter().map(|l| l.style(style)).collect();
self
}
}
impl<S: AsRef<str>> From<S> for Text {
fn from(s: S) -> Self {
Self::raw(s)
}
}
impl FromIterator<Line> for Text {
fn from_iter<I: IntoIterator<Item = Line>>(iter: I) -> Self {
Self::from_lines(iter)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec_ai_tui::style::Color;
#[test]
fn test_span_width() {
let s = Span::raw("hello");
assert_eq!(s.width(), 5);
let s = Span::raw("日本語"); assert_eq!(s.width(), 6);
}
#[test]
fn test_line_width() {
let line = Line::from_spans(vec![Span::raw("hello"), Span::raw(" "), Span::raw("world")]);
assert_eq!(line.width(), 11);
}
#[test]
fn test_text_raw() {
let text = Text::raw("line1\nline2\nline3");
assert_eq!(text.height(), 3);
assert_eq!(text.lines[0].spans[0].content, "line1");
assert_eq!(text.lines[1].spans[0].content, "line2");
assert_eq!(text.lines[2].spans[0].content, "line3");
}
#[test]
fn test_line_style() {
let line = Line::raw("test").style(Style::new().fg(Color::Red));
assert_eq!(line.spans[0].style.fg, Color::Red);
}
#[test]
fn test_styled_chars() {
let line = Line::from_spans(vec![
Span::styled("ab", Style::new().fg(Color::Red)),
Span::styled("cd", Style::new().fg(Color::Blue)),
]);
let chars: Vec<_> = line.styled_chars().collect();
assert_eq!(chars.len(), 4);
assert_eq!(chars[0], ('a', Style::new().fg(Color::Red)));
assert_eq!(chars[1], ('b', Style::new().fg(Color::Red)));
assert_eq!(chars[2], ('c', Style::new().fg(Color::Blue)));
assert_eq!(chars[3], ('d', Style::new().fg(Color::Blue)));
}
}