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}