use std::sync::Arc;
use ratatui_core::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
use crate::children::DataChildren;
use crate::components::Canvas;
use crate::element::Elements;
use crate::wrap;
#[derive(Clone, Debug, Default, typed_builder::TypedBuilder)]
pub struct Span {
#[builder(default, setter(into))]
pub text: String,
#[builder(default, setter(into))]
pub style: Style,
}
pub enum TextChild {
Span(Span),
}
impl From<Span> for TextChild {
fn from(s: Span) -> Self {
TextChild::Span(s)
}
}
impl From<String> for TextChild {
fn from(s: String) -> Self {
TextChild::Span(Span {
text: s,
style: Style::default(),
})
}
}
impl From<&str> for TextChild {
fn from(s: &str) -> Self {
TextChild::Span(Span {
text: s.to_string(),
style: Style::default(),
})
}
}
#[derive(Default, typed_builder::TypedBuilder)]
pub struct Text {
#[builder(default, setter(into))]
pub style: Style,
}
impl Text {
pub fn unstyled(content: impl Into<String>) -> impl crate::component::Component<State = ()> {
let mut collector = DataChildren::default();
crate::children::AddTo::add_to(content.into(), &mut collector);
<Text as crate::children::ChildCollector>::finish(Text::default(), collector)
}
pub fn styled(
content: impl Into<String>,
style: Style,
) -> impl crate::component::Component<State = ()> {
let mut collector = DataChildren::default();
crate::children::AddTo::add_to(
Span {
text: content.into(),
style,
},
&mut collector,
);
<Text as crate::children::ChildCollector>::finish(Text::default(), collector)
}
}
fn build_line(children: &[TextChild], base_style: Style) -> ratatui_core::text::Line<'static> {
let spans: Vec<ratatui_core::text::Span<'static>> = children
.iter()
.map(|c| match c {
TextChild::Span(s) => {
let effective_style = base_style.patch(s.style);
ratatui_core::text::Span::styled(s.text.clone(), effective_style)
}
})
.collect();
ratatui_core::text::Line::from(spans)
}
#[eye_declare_macros::component(props = Text, children = DataChildren<TextChild>, crate_path = crate)]
fn text(props: &Text, children: &DataChildren<TextChild>) -> Elements {
let spans = children.as_slice();
if spans.is_empty() {
return Elements::new();
}
let line = Arc::new(build_line(spans, props.style));
let line_for_height = line.clone();
let mut els = Elements::new();
els.add(
Canvas::builder()
.render_fn(move |area: Rect, buf: &mut Buffer| {
let text = ratatui_core::text::Text::from(vec![(*line).clone()]);
wrap::wrapping_paragraph(text).render(area, buf);
})
.desired_height_fn(move |width: u16| {
let text = ratatui_core::text::Text::from(vec![(*line_for_height).clone()]);
wrap::wrapped_line_count(&text, width)
})
.build(),
);
els
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hooks::Hooks;
use ratatui_core::style::Color;
#[test]
fn text_renders_single_span() {
let mut collector = DataChildren::<TextChild>::default();
crate::children::AddTo::add_to(String::from("hello"), &mut collector);
let wrapper = <Text as crate::children::ChildCollector>::finish(Text::default(), collector);
use crate::component::Component;
let hooks_output = {
let mut hooks = Hooks::new();
let result = wrapper.update(&mut hooks, &(), Elements::new());
(hooks.decompose(), result)
};
let elements = hooks_output.1;
assert!(!elements.is_empty());
}
#[test]
fn text_renders_multiple_spans() {
let mut collector = DataChildren::<TextChild>::default();
crate::children::AddTo::add_to(
Span {
text: "hello ".into(),
style: Style::default(),
},
&mut collector,
);
crate::children::AddTo::add_to(
Span {
text: "world".into(),
style: Style::default().fg(Color::Green),
},
&mut collector,
);
let wrapper = <Text as crate::children::ChildCollector>::finish(Text::default(), collector);
use crate::component::Component;
let mut hooks = Hooks::new();
let elements = wrapper.update(&mut hooks, &(), Elements::new());
assert!(!elements.is_empty());
}
#[test]
fn text_with_base_style() {
let base = Style::default().fg(Color::Red);
let children = vec![TextChild::Span(Span {
text: "hello".into(),
style: Style::default(),
})];
let line = build_line(&children, base);
assert_eq!(line.spans[0].style.fg, Some(Color::Red));
}
#[test]
fn span_style_overrides_base() {
let base = Style::default().fg(Color::Red);
let children = vec![TextChild::Span(Span {
text: "hello".into(),
style: Style::default().fg(Color::Green),
})];
let line = build_line(&children, base);
assert_eq!(line.spans[0].style.fg, Some(Color::Green));
}
#[test]
fn empty_children_produces_no_elements() {
use crate::component::Component;
let text = Text::default();
let mut hooks = Hooks::new();
let elements = text.update(&mut hooks, &(), Elements::new());
assert!(elements.is_empty());
}
}