Skip to main content

altui_core/
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 altui_core::widgets::Block;
23//! # use altui_core::text::{Span, Spans};
24//! # use altui_core::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 altui_core::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 altui_core::text::Span;
94    /// # use altui_core::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    /// Span::styled("Short style", Color::Blue);
99    /// ```
100    pub fn styled<T>(content: T, style: impl Into<Style>) -> Span<'a>
101    where
102        T: Into<Cow<'a, str>>,
103    {
104        Span {
105            content: content.into(),
106            style: style.into(),
107        }
108    }
109
110    /// Returns the width of the content held by this span.
111    pub fn width(&self) -> usize {
112        self.content.width()
113    }
114
115    /// Returns an iterator over the graphemes held by this span.
116    ///
117    /// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
118    /// the resulting [`Style`].
119    ///
120    /// ## Examples
121    ///
122    /// ```rust
123    /// # use altui_core::text::{Span, StyledGrapheme};
124    /// # use altui_core::style::{Color, Modifier, Style};
125    /// # use std::iter::Iterator;
126    /// let style = Style::default().fg(Color::Yellow);
127    /// let span = Span::styled("Text", style);
128    /// let style = Style::default().fg(Color::Green).bg(Color::Black);
129    /// let styled_graphemes = span.styled_graphemes(style);
130    /// assert_eq!(
131    ///     vec![
132    ///         StyledGrapheme {
133    ///             symbol: "T",
134    ///             style: Style {
135    ///                 fg: Some(Color::Yellow),
136    ///                 bg: Some(Color::Black),
137    ///                 add_modifier: Modifier::empty(),
138    ///                 sub_modifier: Modifier::empty(),
139    ///             },
140    ///         },
141    ///         StyledGrapheme {
142    ///             symbol: "e",
143    ///             style: Style {
144    ///                 fg: Some(Color::Yellow),
145    ///                 bg: Some(Color::Black),
146    ///                 add_modifier: Modifier::empty(),
147    ///                 sub_modifier: Modifier::empty(),
148    ///             },
149    ///         },
150    ///         StyledGrapheme {
151    ///             symbol: "x",
152    ///             style: Style {
153    ///                 fg: Some(Color::Yellow),
154    ///                 bg: Some(Color::Black),
155    ///                 add_modifier: Modifier::empty(),
156    ///                 sub_modifier: Modifier::empty(),
157    ///             },
158    ///         },
159    ///         StyledGrapheme {
160    ///             symbol: "t",
161    ///             style: Style {
162    ///                 fg: Some(Color::Yellow),
163    ///                 bg: Some(Color::Black),
164    ///                 add_modifier: Modifier::empty(),
165    ///                 sub_modifier: Modifier::empty(),
166    ///             },
167    ///         },
168    ///     ],
169    ///     styled_graphemes.collect::<Vec<StyledGrapheme>>()
170    /// );
171    /// ```
172    pub fn styled_graphemes(
173        &'a self,
174        base_style: Style,
175    ) -> impl Iterator<Item = StyledGrapheme<'a>> {
176        UnicodeSegmentation::graphemes(self.content.as_ref(), true)
177            .map(move |g| StyledGrapheme {
178                symbol: g,
179                style: base_style.patch(self.style),
180            })
181            .filter(|s| s.symbol != "\n")
182    }
183}
184
185impl<'a> From<String> for Span<'a> {
186    fn from(s: String) -> Span<'a> {
187        Span::raw(s)
188    }
189}
190
191impl<'a> From<&'a str> for Span<'a> {
192    fn from(s: &'a str) -> Span<'a> {
193        Span::raw(s)
194    }
195}
196
197/// A string composed of clusters of graphemes, each with their own style.
198#[derive(Debug, Clone, PartialEq, Default, Eq)]
199pub struct Spans<'a>(pub Vec<Span<'a>>);
200
201impl<'a> Spans<'a> {
202    /// Returns the width of the underlying string.
203    ///
204    /// ## Examples
205    ///
206    /// ```rust
207    /// # use altui_core::text::{Span, Spans};
208    /// # use altui_core::style::{Color, Style};
209    /// let spans = Spans::from(vec![
210    ///     Span::styled("My", Style::default().fg(Color::Yellow)),
211    ///     Span::raw(" text"),
212    /// ]);
213    /// assert_eq!(7, spans.width());
214    /// ```
215    pub fn width(&self) -> usize {
216        self.0.iter().map(Span::width).sum()
217    }
218}
219
220impl<'a> From<String> for Spans<'a> {
221    fn from(s: String) -> Spans<'a> {
222        Spans(vec![Span::from(s)])
223    }
224}
225
226impl<'a> From<&'a str> for Spans<'a> {
227    fn from(s: &'a str) -> Spans<'a> {
228        Spans(vec![Span::from(s)])
229    }
230}
231
232impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
233    fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
234        Spans(spans)
235    }
236}
237
238impl<'a> From<Span<'a>> for Spans<'a> {
239    fn from(span: Span<'a>) -> Spans<'a> {
240        Spans(vec![span])
241    }
242}
243
244impl<'a> From<Spans<'a>> for String {
245    fn from(line: Spans<'a>) -> String {
246        line.0.iter().fold(String::new(), |mut acc, s| {
247            acc.push_str(s.content.as_ref());
248            acc
249        })
250    }
251}
252
253/// A string split over multiple lines where each line is composed of several clusters, each with
254/// their own style.
255///
256/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
257/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
258/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
259///
260/// ```rust
261/// # use altui_core::text::Text;
262/// # use altui_core::style::{Color, Modifier, Style};
263/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
264///
265/// // An initial two lines of `Text` built from a `&str`
266/// let mut text = Text::from("The first line\nThe second line");
267/// assert_eq!(2, text.height());
268///
269/// // Adding two more unstyled lines
270/// text.extend(Text::raw("These are two\nmore lines!"));
271/// assert_eq!(4, text.height());
272///
273/// // Adding a final two styled lines
274/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
275/// assert_eq!(6, text.height());
276/// ```
277#[derive(Debug, Clone, PartialEq, Default, Eq)]
278pub struct Text<'a> {
279    pub lines: Vec<Spans<'a>>,
280}
281
282impl<'a> Text<'a> {
283    /// Create some text (potentially multiple lines) with no style.
284    ///
285    /// ## Examples
286    ///
287    /// ```rust
288    /// # use altui_core::text::Text;
289    /// Text::raw("The first line\nThe second line");
290    /// Text::raw(String::from("The first line\nThe second line"));
291    /// ```
292    pub fn raw<T>(content: T) -> Text<'a>
293    where
294        T: Into<Cow<'a, str>>,
295    {
296        Text {
297            lines: match content.into() {
298                Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
299                Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
300            },
301        }
302    }
303
304    /// Create some text (potentially multiple lines) with a style.
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// # use altui_core::text::Text;
310    /// # use altui_core::style::{Color, Modifier, Style};
311    /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
312    /// Text::styled("The first line\nThe second line", style);
313    /// Text::styled(String::from("The first line\nThe second line"), style);
314    /// ```
315    pub fn styled<T>(content: T, style: impl Into<Style>) -> Text<'a>
316    where
317        T: Into<Cow<'a, str>>,
318    {
319        let mut text = Text::raw(content);
320        text.patch_style(style.into());
321        text
322    }
323
324    /// Returns the max width of all the lines.
325    ///
326    /// ## Examples
327    ///
328    /// ```rust
329    /// use altui_core::text::Text;
330    /// let text = Text::from("The first line\nThe second line");
331    /// assert_eq!(15, text.width());
332    /// ```
333    pub fn width(&self) -> usize {
334        self.lines
335            .iter()
336            .map(Spans::width)
337            .max()
338            .unwrap_or_default()
339    }
340
341    /// Returns the height.
342    ///
343    /// ## Examples
344    ///
345    /// ```rust
346    /// use altui_core::text::Text;
347    /// let text = Text::from("The first line\nThe second line");
348    /// assert_eq!(2, text.height());
349    /// ```
350    pub fn height(&self) -> usize {
351        self.lines.len()
352    }
353
354    /// Apply a new style to existing text.
355    ///
356    /// # Examples
357    ///
358    /// ```rust
359    /// # use altui_core::text::Text;
360    /// # use altui_core::style::{Color, Modifier, Style};
361    /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
362    /// let mut raw_text = Text::raw("The first line\nThe second line");
363    /// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
364    /// assert_ne!(raw_text, styled_text);
365    ///
366    /// raw_text.patch_style(style);
367    /// assert_eq!(raw_text, styled_text);
368    /// ```
369    pub fn patch_style(&mut self, style: Style) {
370        for line in &mut self.lines {
371            for span in &mut line.0 {
372                span.style = span.style.patch(style);
373            }
374        }
375    }
376}
377
378impl<'a> From<String> for Text<'a> {
379    fn from(s: String) -> Text<'a> {
380        Text::raw(s)
381    }
382}
383
384impl<'a> From<&'a str> for Text<'a> {
385    fn from(s: &'a str) -> Text<'a> {
386        Text::raw(s)
387    }
388}
389
390impl<'a> From<Cow<'a, str>> for Text<'a> {
391    fn from(s: Cow<'a, str>) -> Text<'a> {
392        Text::raw(s)
393    }
394}
395
396impl<'a> From<Span<'a>> for Text<'a> {
397    fn from(span: Span<'a>) -> Text<'a> {
398        Text {
399            lines: vec![Spans::from(span)],
400        }
401    }
402}
403
404impl<'a> From<Spans<'a>> for Text<'a> {
405    fn from(spans: Spans<'a>) -> Text<'a> {
406        Text { lines: vec![spans] }
407    }
408}
409
410impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
411    fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
412        Text { lines }
413    }
414}
415
416impl<'a> IntoIterator for Text<'a> {
417    type Item = Spans<'a>;
418    type IntoIter = std::vec::IntoIter<Self::Item>;
419
420    fn into_iter(self) -> Self::IntoIter {
421        self.lines.into_iter()
422    }
423}
424
425impl<'a> Extend<Spans<'a>> for Text<'a> {
426    fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
427        self.lines.extend(iter);
428    }
429}