tui_realm_stdlib/components/
span.rs

1//! ## Span
2//!
3//! `Span` represents a read-only text component without any container, but with the possibility to define multiple text parts.
4//! The main difference with `Label` is that the Span allows different styles inside the same component for the texsts.
5
6use tuirealm::command::{Cmd, CmdResult};
7use tuirealm::props::{
8    Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, Props, Style, TextModifiers,
9    TextSpan,
10};
11use tuirealm::ratatui::text::Line;
12use tuirealm::ratatui::{
13    layout::Rect,
14    text::{Span as TuiSpan, Text},
15    widgets::Paragraph,
16};
17use tuirealm::{Frame, MockComponent, State};
18
19// -- Component
20
21/// ## Span
22///
23/// represents a read-only text component without any container, but with multy-style text parts
24#[derive(Default)]
25#[must_use]
26pub struct Span {
27    props: Props,
28}
29
30impl Span {
31    pub fn foreground(mut self, fg: Color) -> Self {
32        self.attr(Attribute::Foreground, AttrValue::Color(fg));
33        self
34    }
35
36    pub fn background(mut self, bg: Color) -> Self {
37        self.attr(Attribute::Background, AttrValue::Color(bg));
38        self
39    }
40
41    pub fn modifiers(mut self, m: TextModifiers) -> Self {
42        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
43        self
44    }
45
46    pub fn alignment(mut self, a: Alignment) -> Self {
47        self.attr(Attribute::Alignment, AttrValue::Alignment(a));
48        self
49    }
50
51    pub fn spans(mut self, s: impl IntoIterator<Item = TextSpan>) -> Self {
52        self.attr(
53            Attribute::Text,
54            AttrValue::Payload(PropPayload::Vec(
55                s.into_iter().map(PropValue::TextSpan).collect(),
56            )),
57        );
58        self
59    }
60}
61
62impl MockComponent for Span {
63    fn view(&mut self, render: &mut Frame, area: Rect) {
64        // Make a Span
65        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
66            // Make text
67            let foreground = self
68                .props
69                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
70                .unwrap_color();
71            let background = self
72                .props
73                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
74                .unwrap_color();
75            // binding required as "spans" is a reference and otherwise would not live long enough
76            let payload = self
77                .props
78                .get_ref(Attribute::Text)
79                .and_then(|x| x.as_payload());
80            let spans: Vec<TuiSpan> = match payload {
81                Some(PropPayload::Vec(spans)) => spans
82                    .iter()
83                    // this will skip any "PropValue" that is not a "TextSpan", instead of panicing
84                    .filter_map(|x| x.as_text_span())
85                    .map(|x| {
86                        // Keep colors and modifiers, or use default
87                        let (fg, bg, modifiers) =
88                            crate::utils::use_or_default_styles(&self.props, x);
89                        TuiSpan::styled(
90                            &x.content,
91                            Style::default().add_modifier(modifiers).fg(fg).bg(bg),
92                        )
93                    })
94                    .collect(),
95                _ => Vec::new(),
96            };
97            let text: Text = Text::from(Line::from(spans));
98            // Text properties
99            let alignment: Alignment = self
100                .props
101                .get_or(Attribute::Alignment, AttrValue::Alignment(Alignment::Left))
102                .unwrap_alignment();
103            render.render_widget(
104                Paragraph::new(text)
105                    .alignment(alignment)
106                    .style(Style::default().bg(background).fg(foreground)),
107                area,
108            );
109        }
110    }
111
112    fn query(&self, attr: Attribute) -> Option<AttrValue> {
113        self.props.get(attr)
114    }
115
116    fn attr(&mut self, attr: Attribute, value: AttrValue) {
117        self.props.set(attr, value);
118    }
119
120    fn state(&self) -> State {
121        State::None
122    }
123
124    fn perform(&mut self, _cmd: Cmd) -> CmdResult {
125        CmdResult::None
126    }
127}
128
129#[cfg(test)]
130mod tests {
131
132    use super::*;
133
134    use pretty_assertions::assert_eq;
135
136    #[test]
137    fn test_components_span() {
138        let component = Span::default()
139            .background(Color::Blue)
140            .foreground(Color::Red)
141            .modifiers(TextModifiers::BOLD)
142            .alignment(Alignment::Center)
143            .spans([
144                TextSpan::from("Press "),
145                TextSpan::from("<ESC>").fg(Color::Cyan).bold(),
146                TextSpan::from(" to quit"),
147            ]);
148        // Get value
149        assert_eq!(component.state(), State::None);
150    }
151
152    #[test]
153    fn various_spans_types() {
154        // Vec
155        let _ = Span::default().spans(vec![TextSpan::new("hello")]);
156        // static array
157        let _ = Span::default().spans([TextSpan::new("hello")]);
158        // boxed array
159        let _ = Span::default().spans(vec![TextSpan::new("hello")].into_boxed_slice());
160        // already a iterator
161        let _ = Span::default().spans(["Hello"].map(TextSpan::new));
162    }
163}