tui/
text.rs

1//! Primitives for styled text.
2//!
3//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
4//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
5//! - A single line string where all graphemes have the same style is represented by a [`Span`].
6//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
7//! - A multiple line string where each grapheme may have its own style is represented by a
8//! [`Text`].
9//!
10//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
11//! is a [`Spans`].
12//!
13//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
14//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
15//! that you can start by using simple `String` or `&str` and then promote them to the previous
16//! primitives when you need additional styling capabilities.
17//!
18//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
19//! its `title` property (which is a [`Spans`] under the hood):
20//!
21//! ```rust
22//! # use tui::widgets::Block;
23//! # use tui::text::{Span, Spans};
24//! # use tui::style::{Color, Style};
25//! // A simple string with no styling.
26//! // Converted to Spans(vec![
27//! //   Span { content: Cow::Borrowed("My title"), style: Style { .. } }
28//! // ])
29//! let block = Block::default().title("My title");
30//!
31//! // A simple string with a unique style.
32//! // Converted to Spans(vec![
33//! //   Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
34//! // ])
35//! let block = Block::default().title(
36//!     Span::styled("My title", Style::default().fg(Color::Yellow))
37//! );
38//!
39//! // A string with multiple styles.
40//! // Converted to Spans(vec![
41//! //   Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
42//! //   Span { content: Cow::Borrowed(" title"), .. }
43//! // ])
44//! let block = Block::default().title(vec![
45//!     Span::styled("My", Style::default().fg(Color::Yellow)),
46//!     Span::raw(" title"),
47//! ]);
48//! ```
49use crate::style::Style;
50use std::borrow::Cow;
51use unicode_segmentation::UnicodeSegmentation;
52use unicode_width::UnicodeWidthStr;
53
54/// A grapheme associated to a style.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct StyledGrapheme<'a> {
57    pub symbol: &'a str,
58    pub style: Style,
59}
60
61/// A string where all graphemes have the same style.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Span<'a> {
64    pub content: Cow<'a, str>,
65    pub style: Style,
66}
67
68impl<'a> Span<'a> {
69    /// Create a span with no style.
70    ///
71    /// ## Examples
72    ///
73    /// ```rust
74    /// # use tui::text::Span;
75    /// Span::raw("My text");
76    /// Span::raw(String::from("My text"));
77    /// ```
78    pub fn raw<T>(content: T) -> Span<'a>
79    where
80        T: Into<Cow<'a, str>>,
81    {
82        Span {
83            content: content.into(),
84            style: Style::default(),
85        }
86    }
87
88    /// Create a span with a style.
89    ///
90    /// # Examples
91    ///
92    /// ```rust
93    /// # use tui::text::Span;
94    /// # use tui::style::{Color, Modifier, Style};
95    /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
96    /// Span::styled("My text", style);
97    /// Span::styled(String::from("My text"), style);
98    /// ```
99    pub fn styled<T>(content: T, style: Style) -> Span<'a>
100    where
101        T: Into<Cow<'a, str>>,
102    {
103        Span {
104            content: content.into(),
105            style,
106        }
107    }
108
109    /// Returns the width of the content held by this span.
110    pub fn width(&self) -> usize {
111        self.content.width()
112    }
113
114    /// Returns an iterator over the graphemes held by this span.
115    ///
116    /// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
117    /// the resulting [`Style`].
118    ///
119    /// ## Examples
120    ///
121    /// ```rust
122    /// # use tui::text::{Span, StyledGrapheme};
123    /// # use tui::style::{Color, Modifier, Style};
124    /// # use std::iter::Iterator;
125    /// let style = Style::default().fg(Color::Yellow);
126    /// let span = Span::styled("Text", style);
127    /// let style = Style::default().fg(Color::Green).bg(Color::Black);
128    /// let styled_graphemes = span.styled_graphemes(style);
129    /// assert_eq!(
130    ///     vec![
131    ///         StyledGrapheme {
132    ///             symbol: "T",
133    ///             style: Style {
134    ///                 fg: Some(Color::Yellow),
135    ///                 bg: Some(Color::Black),
136    ///                 add_modifier: Modifier::empty(),
137    ///                 sub_modifier: Modifier::empty(),
138    ///             },
139    ///         },
140    ///         StyledGrapheme {
141    ///             symbol: "e",
142    ///             style: Style {
143    ///                 fg: Some(Color::Yellow),
144    ///                 bg: Some(Color::Black),
145    ///                 add_modifier: Modifier::empty(),
146    ///                 sub_modifier: Modifier::empty(),
147    ///             },
148    ///         },
149    ///         StyledGrapheme {
150    ///             symbol: "x",
151    ///             style: Style {
152    ///                 fg: Some(Color::Yellow),
153    ///                 bg: Some(Color::Black),
154    ///                 add_modifier: Modifier::empty(),
155    ///                 sub_modifier: Modifier::empty(),
156    ///             },
157    ///         },
158    ///         StyledGrapheme {
159    ///             symbol: "t",
160    ///             style: Style {
161    ///                 fg: Some(Color::Yellow),
162    ///                 bg: Some(Color::Black),
163    ///                 add_modifier: Modifier::empty(),
164    ///                 sub_modifier: Modifier::empty(),
165    ///             },
166    ///         },
167    ///     ],
168    ///     styled_graphemes.collect::<Vec<StyledGrapheme>>()
169    /// );
170    /// ```
171    pub fn styled_graphemes(
172        &'a self,
173        base_style: Style,
174    ) -> impl Iterator<Item = StyledGrapheme<'a>> {
175        UnicodeSegmentation::graphemes(self.content.as_ref(), true)
176            .map(move |g| StyledGrapheme {
177                symbol: g,
178                style: base_style.patch(self.style),
179            })
180            .filter(|s| s.symbol != "\n")
181    }
182}
183
184impl<'a> From<String> for Span<'a> {
185    fn from(s: String) -> Span<'a> {
186        Span::raw(s)
187    }
188}
189
190impl<'a> From<&'a str> for Span<'a> {
191    fn from(s: &'a str) -> Span<'a> {
192        Span::raw(s)
193    }
194}
195
196/// A string composed of clusters of graphemes, each with their own style.
197#[derive(Debug, Clone, PartialEq, Default, Eq)]
198pub struct Spans<'a>(pub Vec<Span<'a>>);
199
200impl<'a> Spans<'a> {
201    /// Returns the width of the underlying string.
202    ///
203    /// ## Examples
204    ///
205    /// ```rust
206    /// # use tui::text::{Span, Spans};
207    /// # use tui::style::{Color, Style};
208    /// let spans = Spans::from(vec![
209    ///     Span::styled("My", Style::default().fg(Color::Yellow)),
210    ///     Span::raw(" text"),
211    /// ]);
212    /// assert_eq!(7, spans.width());
213    /// ```
214    pub fn width(&self) -> usize {
215        self.0.iter().map(Span::width).sum()
216    }
217}
218
219impl<'a> From<String> for Spans<'a> {
220    fn from(s: String) -> Spans<'a> {
221        Spans(vec![Span::from(s)])
222    }
223}
224
225impl<'a> From<&'a str> for Spans<'a> {
226    fn from(s: &'a str) -> Spans<'a> {
227        Spans(vec![Span::from(s)])
228    }
229}
230
231impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
232    fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
233        Spans(spans)
234    }
235}
236
237impl<'a> From<Span<'a>> for Spans<'a> {
238    fn from(span: Span<'a>) -> Spans<'a> {
239        Spans(vec![span])
240    }
241}
242
243impl<'a> From<Spans<'a>> for String {
244    fn from(line: Spans<'a>) -> String {
245        line.0.iter().fold(String::new(), |mut acc, s| {
246            acc.push_str(s.content.as_ref());
247            acc
248        })
249    }
250}
251
252/// A string split over multiple lines where each line is composed of several clusters, each with
253/// their own style.
254///
255/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
256/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
257/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
258///
259/// ```rust
260/// # use tui::text::Text;
261/// # use tui::style::{Color, Modifier, Style};
262/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
263///
264/// // An initial two lines of `Text` built from a `&str`
265/// let mut text = Text::from("The first line\nThe second line");
266/// assert_eq!(2, text.height());
267///
268/// // Adding two more unstyled lines
269/// text.extend(Text::raw("These are two\nmore lines!"));
270/// assert_eq!(4, text.height());
271///
272/// // Adding a final two styled lines
273/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
274/// assert_eq!(6, text.height());
275/// ```
276#[derive(Debug, Clone, PartialEq, Default, Eq)]
277pub struct Text<'a> {
278    pub lines: Vec<Spans<'a>>,
279}
280
281impl<'a> Text<'a> {
282    /// Create some text (potentially multiple lines) with no style.
283    ///
284    /// ## Examples
285    ///
286    /// ```rust
287    /// # use tui::text::Text;
288    /// Text::raw("The first line\nThe second line");
289    /// Text::raw(String::from("The first line\nThe second line"));
290    /// ```
291    pub fn raw<T>(content: T) -> Text<'a>
292    where
293        T: Into<Cow<'a, str>>,
294    {
295        Text {
296            lines: match content.into() {
297                Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
298                Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
299            },
300        }
301    }
302
303    /// Create some text (potentially multiple lines) with a style.
304    ///
305    /// # Examples
306    ///
307    /// ```rust
308    /// # use tui::text::Text;
309    /// # use tui::style::{Color, Modifier, Style};
310    /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
311    /// Text::styled("The first line\nThe second line", style);
312    /// Text::styled(String::from("The first line\nThe second line"), style);
313    /// ```
314    pub fn styled<T>(content: T, style: Style) -> Text<'a>
315    where
316        T: Into<Cow<'a, str>>,
317    {
318        let mut text = Text::raw(content);
319        text.patch_style(style);
320        text
321    }
322
323    /// Returns the max width of all the lines.
324    ///
325    /// ## Examples
326    ///
327    /// ```rust
328    /// use tui::text::Text;
329    /// let text = Text::from("The first line\nThe second line");
330    /// assert_eq!(15, text.width());
331    /// ```
332    pub fn width(&self) -> usize {
333        self.lines
334            .iter()
335            .map(Spans::width)
336            .max()
337            .unwrap_or_default()
338    }
339
340    /// Returns the height.
341    ///
342    /// ## Examples
343    ///
344    /// ```rust
345    /// use tui::text::Text;
346    /// let text = Text::from("The first line\nThe second line");
347    /// assert_eq!(2, text.height());
348    /// ```
349    pub fn height(&self) -> usize {
350        self.lines.len()
351    }
352
353    /// Apply a new style to existing text.
354    ///
355    /// # Examples
356    ///
357    /// ```rust
358    /// # use tui::text::Text;
359    /// # use tui::style::{Color, Modifier, Style};
360    /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
361    /// let mut raw_text = Text::raw("The first line\nThe second line");
362    /// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
363    /// assert_ne!(raw_text, styled_text);
364    ///
365    /// raw_text.patch_style(style);
366    /// assert_eq!(raw_text, styled_text);
367    /// ```
368    pub fn patch_style(&mut self, style: Style) {
369        for line in &mut self.lines {
370            for span in &mut line.0 {
371                span.style = span.style.patch(style);
372            }
373        }
374    }
375}
376
377impl<'a> From<String> for Text<'a> {
378    fn from(s: String) -> Text<'a> {
379        Text::raw(s)
380    }
381}
382
383impl<'a> From<&'a str> for Text<'a> {
384    fn from(s: &'a str) -> Text<'a> {
385        Text::raw(s)
386    }
387}
388
389impl<'a> From<Cow<'a, str>> for Text<'a> {
390    fn from(s: Cow<'a, str>) -> Text<'a> {
391        Text::raw(s)
392    }
393}
394
395impl<'a> From<Span<'a>> for Text<'a> {
396    fn from(span: Span<'a>) -> Text<'a> {
397        Text {
398            lines: vec![Spans::from(span)],
399        }
400    }
401}
402
403impl<'a> From<Spans<'a>> for Text<'a> {
404    fn from(spans: Spans<'a>) -> Text<'a> {
405        Text { lines: vec![spans] }
406    }
407}
408
409impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
410    fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
411        Text { lines }
412    }
413}
414
415impl<'a> IntoIterator for Text<'a> {
416    type Item = Spans<'a>;
417    type IntoIter = std::vec::IntoIter<Self::Item>;
418
419    fn into_iter(self) -> Self::IntoIter {
420        self.lines.into_iter()
421    }
422}
423
424impl<'a> Extend<Spans<'a>> for Text<'a> {
425    fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
426        self.lines.extend(iter);
427    }
428}