use crate::core::{Color, Element, ElementType, Style, TextWrap};
macro_rules! style_setters {
(color $name:ident => $field:ident, $doc:literal) => {
#[doc = $doc]
pub fn $name(mut self, color: Color) -> Self {
self.style.$field = Some(color);
self
}
};
(bool $name:ident => $field:ident, $doc:literal) => {
#[doc = $doc]
pub fn $name(mut self) -> Self {
self.style.$field = true;
self
}
};
}
macro_rules! text_style_setters {
(color $name:ident => $field:ident, $doc:literal) => {
#[doc = $doc]
pub fn $name(mut self, color: Color) -> Self {
self.style.$field = Some(color);
self.for_each_span_mut(|span| {
if span.style.$field.is_none() {
span.style.$field = Some(color);
}
});
self
}
};
(bool $name:ident => $field:ident, $doc:literal) => {
#[doc = $doc]
pub fn $name(mut self) -> Self {
self.style.$field = true;
self.for_each_span_mut(|span| span.style.$field = true);
self
}
};
}
#[derive(Debug, Clone)]
pub struct Span {
pub content: String,
pub style: Style,
}
impl Span {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
style: Style::new(),
}
}
pub fn raw(content: impl Into<String>) -> Self {
Self::new(content)
}
pub fn styled(content: impl Into<String>, style: Style) -> Self {
Self {
content: content.into(),
style,
}
}
style_setters!(color color => color, "Set text color");
style_setters!(color background => background_color, "Set background color");
style_setters!(bool bold => bold, "Set bold");
style_setters!(bool italic => italic, "Set italic");
style_setters!(bool underline => underline, "Set underline");
style_setters!(bool strikethrough => strikethrough, "Set strikethrough");
style_setters!(bool dim => dim, "Set dim");
style_setters!(bool inverse => inverse, "Set inverse");
pub fn fg(self, color: Color) -> Self {
self.color(color)
}
pub fn bg(self, color: Color) -> Self {
self.background(color)
}
pub fn width(&self) -> usize {
use unicode_width::UnicodeWidthStr;
self.content.width()
}
}
impl<T: Into<String>> From<T> for Span {
fn from(s: T) -> Self {
Span::new(s)
}
}
#[derive(Debug, Clone, Default)]
pub struct Line {
pub spans: Vec<Span>,
}
impl Line {
pub fn new() -> Self {
Self { spans: Vec::new() }
}
pub fn from_spans(spans: Vec<Span>) -> Self {
Self { spans }
}
pub fn raw(content: impl Into<String>) -> Self {
Self {
spans: vec![Span::new(content)],
}
}
pub fn span(mut self, span: impl Into<Span>) -> Self {
self.spans.push(span.into());
self
}
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.content.is_empty())
}
}
impl From<&str> for Line {
fn from(s: &str) -> Self {
Line::from_spans(vec![Span::new(s)])
}
}
impl From<String> for Line {
fn from(s: String) -> Self {
Line::from_spans(vec![Span::new(s)])
}
}
impl From<Span> for Line {
fn from(s: Span) -> Self {
Line::from_spans(vec![s])
}
}
impl From<Vec<Span>> for Line {
fn from(spans: Vec<Span>) -> Self {
Line::from_spans(spans)
}
}
#[derive(Debug, Clone)]
pub struct Text {
lines: Vec<Line>,
style: Style,
key: Option<String>,
}
impl Text {
pub fn new(content: impl Into<String>) -> Self {
let content_str: String = content.into();
let lines: Vec<Line> = content_str.lines().map(Line::raw).collect();
Self {
lines: if lines.is_empty() {
vec![Line::raw("")]
} else {
lines
},
style: Style::new(),
key: None,
}
}
pub fn spans(spans: Vec<Span>) -> Self {
Self {
lines: vec![Line::from_spans(spans)],
style: Style::new(),
key: None,
}
}
pub fn line(line: Line) -> Self {
Self {
lines: vec![line],
style: Style::new(),
key: None,
}
}
pub fn from_lines(lines: Vec<Line>) -> Self {
Self {
lines,
style: Style::new(),
key: None,
}
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn get_lines(&self) -> &[Line] {
&self.lines
}
fn for_each_span_mut(&mut self, mut apply: impl FnMut(&mut Span)) {
for line in &mut self.lines {
for span in &mut line.spans {
apply(span);
}
}
}
text_style_setters!(color color => color, "Set text color");
text_style_setters!(color background => background_color, "Set background color");
text_style_setters!(bool bold => bold, "Set bold");
text_style_setters!(bool italic => italic, "Set italic");
text_style_setters!(bool underline => underline, "Set underline");
text_style_setters!(bool strikethrough => strikethrough, "Set strikethrough");
text_style_setters!(bool dim => dim, "Set dim (less bright)");
text_style_setters!(bool inverse => inverse, "Set inverse (swap foreground and background)");
pub fn bg(self, color: Color) -> Self {
self.background(color)
}
pub fn wrap(mut self, wrap: TextWrap) -> Self {
self.style.text_wrap = wrap;
self
}
pub fn error(self) -> Self {
self.color(Color::Red)
}
pub fn success(self) -> Self {
self.color(Color::Green)
}
pub fn warning(self) -> Self {
self.color(Color::Yellow)
}
pub fn info(self) -> Self {
self.color(Color::Blue)
}
pub fn muted(self) -> Self {
self.dim()
}
pub fn into_element(self) -> Element {
let mut element = Element::new(ElementType::Text);
element.style = self.style;
element.key = self.key;
let is_simple = self.lines.len() == 1 && self.lines[0].spans.len() == 1;
if is_simple {
element.text_content = Some(self.lines[0].spans[0].content.clone());
let span_style = &self.lines[0].spans[0].style;
element.style = element.style.merge(span_style);
} else {
let mut full_text = String::new();
for (i, line) in self.lines.iter().enumerate() {
if i > 0 {
full_text.push('\n');
}
for span in &line.spans {
full_text.push_str(&span.content);
}
}
element.text_content = Some(full_text);
element.spans = Some(self.lines);
}
element
}
}
impl Default for Text {
fn default() -> Self {
Self::new("")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_creation() {
let element = Text::new("Hello").into_element();
assert_eq!(element.get_text(), Some("Hello"));
}
#[test]
fn test_text_styles() {
let element = Text::new("Styled")
.color(Color::Green)
.bold()
.underline()
.into_element();
assert_eq!(element.style.color, Some(Color::Green));
assert!(element.style.bold);
assert!(element.style.underline);
}
#[test]
fn test_text_convenience_methods() {
let error = Text::new("Error").error().into_element();
assert_eq!(error.style.color, Some(Color::Red));
let success = Text::new("Success").success().into_element();
assert_eq!(success.style.color, Some(Color::Green));
}
#[test]
fn test_span_creation() {
let span = Span::new("Hello").color(Color::Green).bold();
assert_eq!(span.content, "Hello");
assert_eq!(span.style.color, Some(Color::Green));
assert!(span.style.bold);
}
#[test]
fn test_text_with_spans() {
let text = Text::spans(vec![
Span::new("Hello ").color(Color::White),
Span::new("World").color(Color::Green).bold(),
]);
assert_eq!(text.lines.len(), 1);
assert_eq!(text.lines[0].spans.len(), 2);
assert_eq!(text.lines[0].spans[0].content, "Hello ");
assert_eq!(text.lines[0].spans[1].content, "World");
}
#[test]
fn test_text_spans_element() {
let element = Text::spans(vec![
Span::new("Hello ").color(Color::White),
Span::new("World").color(Color::Green),
])
.into_element();
assert!(element.spans.is_some());
let spans = element.spans.as_ref().unwrap();
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].spans.len(), 2);
}
#[test]
fn test_line_creation() {
let line = Line::new()
.span(Span::new("Part 1").color(Color::Red))
.span(Span::new(" - "))
.span(Span::new("Part 2").color(Color::Blue));
assert_eq!(line.spans.len(), 3);
assert_eq!(line.width(), 15); }
#[test]
fn test_multiline_text() {
let text = Text::from_lines(vec![
Line::from_spans(vec![Span::new("Line 1").bold()]),
Line::from_spans(vec![Span::new("Line 2").italic()]),
]);
assert_eq!(text.lines.len(), 2);
}
}