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}