use crate::{
components::text::{Text, TextAlign, TextDecoration, TextDrawer, TextWrap},
segmented_string::SegmentedString,
strip_ansi::strip_ansi,
CanvasTextStyle, Color, Component, ComponentDrawer, ComponentUpdater, Hooks, Props, Weight,
};
#[non_exhaustive]
#[derive(Default, Clone)]
pub struct MixedTextContent {
pub text: String,
pub color: Option<Color>,
pub weight: Weight,
pub decoration: TextDecoration,
pub italic: bool,
}
impl MixedTextContent {
pub fn new<S: ToString>(text: S) -> Self {
Self {
text: text.to_string(),
..Default::default()
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn weight(mut self, weight: Weight) -> Self {
self.weight = weight;
self
}
pub fn decoration(mut self, decoration: TextDecoration) -> Self {
self.decoration = decoration;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
}
#[non_exhaustive]
#[derive(Default, Props)]
pub struct MixedTextProps {
pub contents: Vec<MixedTextContent>,
pub wrap: TextWrap,
pub align: TextAlign,
}
#[derive(Default)]
pub struct MixedText {
contents: Vec<MixedTextContent>,
wrap: TextWrap,
align: TextAlign,
}
impl Component for MixedText {
type Props<'a> = MixedTextProps;
fn new(_props: &Self::Props<'_>) -> Self {
Self::default()
}
fn update(
&mut self,
props: &mut Self::Props<'_>,
_hooks: Hooks,
updater: &mut ComponentUpdater,
) {
for content in props.contents.iter_mut() {
content.text = strip_ansi(&content.text).into_owned();
}
let plaintext = props
.contents
.iter()
.map(|content| content.text.as_str())
.collect::<Vec<_>>()
.join("");
self.contents = props.contents.clone();
self.wrap = props.wrap;
self.align = props.align;
updater.set_measure_func(Text::measure_func(plaintext, props.wrap));
}
fn draw(&mut self, drawer: &mut ComponentDrawer<'_>) {
let width = drawer.layout().size.width;
let segmented_string: SegmentedString = self
.contents
.iter()
.map(|content| content.text.as_str())
.collect();
let lines = segmented_string.wrap(match self.wrap {
TextWrap::Wrap => width as usize,
TextWrap::NoWrap => usize::MAX,
});
let paddings = lines
.iter()
.map(|line| Text::alignment_padding(line.width, self.align, width as _))
.collect::<Vec<_>>();
let x_offset = paddings.iter().copied().min().unwrap_or(0);
let mut drawer = TextDrawer::new(drawer, x_offset, self.align != TextAlign::Left);
for (mut line, padding) in lines.into_iter().zip(paddings) {
if self.wrap == TextWrap::Wrap {
line.trim_end();
}
let additional_padding = padding - x_offset;
if additional_padding > 0 {
drawer.append_lines(
[format!("{:width$}", "", width = additional_padding as usize).as_str()],
CanvasTextStyle::default(),
);
}
let mut segments = line.segments.into_iter().peekable();
while let Some(segment) = segments.next() {
let content = &self.contents[segment.index];
let style = CanvasTextStyle {
color: content.color,
weight: content.weight,
underline: content.decoration == TextDecoration::Underline,
italic: content.italic,
};
if segments.peek().is_some() {
drawer.append_lines([segment.text], style);
} else {
drawer.append_lines([segment.text, ""], style);
}
}
}
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[test]
fn test_mixed_text() {
assert_eq!(element!(MixedText).to_string(), "\n");
assert_eq!(
element! {
View(width: 14) {
MixedText(contents: vec![
MixedTextContent::new("this is ").color(Color::Red).weight(Weight::Bold).italic(),
MixedTextContent::new("a wrapping test").decoration(TextDecoration::Underline),
])
}
}
.to_string(),
"this is a\nwrapping test\n"
);
}
#[test]
fn test_mixed_text_strips_ansi() {
assert_eq!(
element! {
View(width: 14) {
MixedText(contents: vec![
MixedTextContent::new("\x1b[31mthis is \x1b[0m"),
MixedTextContent::new("\x1b[1ma wrapping test\x1b[0m"),
])
}
}
.to_string(),
"this is a\nwrapping test\n"
);
}
}