on 2158366 help sign in
435 lines 13 kB history raw
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 helix_tui::widgets::Block;
23//! # use helix_tui::text::{Span, Spans};
24//! # use helix_view::graphics::{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 helix_core::line_ending::str_is_line_ending;
50use helix_core::unicode::width::UnicodeWidthStr;
51use helix_view::graphics::Style;
52use std::borrow::Cow;
53use unicode_segmentation::UnicodeSegmentation;
55/// A grapheme associated to a style.
56#[derive(Debug, Clone, PartialEq)]
57pub struct StyledGrapheme<'a> {
58 pub symbol: &'a str,
59 pub style: Style,
62/// A string where all graphemes have the same style.
63#[derive(Debug, Clone, PartialEq)]
64pub struct Span<'a> {
65 pub content: Cow<'a, str>,
66 pub style: Style,
69impl<'a> Span<'a> {
70 /// Create a span with no style.
71 ///
72 /// ## Examples
73 ///
74 /// ```rust
75 /// # use helix_tui::text::Span;
76 /// Span::raw("My text");
77 /// Span::raw(String::from("My text"));
78 /// ```
79 pub fn raw<T>(content: T) -> Span<'a>
80 where
81 T: Into<Cow<'a, str>>,
82 {
83 Span {
84 content: content.into(),
85 style: Style::default(),
86 }
87 }
89 /// Create a span with a style.
90 ///
91 /// # Examples
92 ///
93 /// ```rust
94 /// # use helix_tui::text::Span;
95 /// # use helix_view::graphics::{Color, Modifier, Style};
96 /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
97 /// Span::styled("My text", style);
98 /// Span::styled(String::from("My text"), style);
99 /// ```
100 pub fn styled<T>(content: T, style: Style) -> Span<'a>
101 where
102 T: Into<Cow<'a, str>>,
103 {
104 Span {
105 content: content.into(),
106 style,
107 }
108 }
110 /// Returns the width of the content held by this span.
111 pub fn width(&self) -> usize {
112 self.content.width()
113 }
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 helix_tui::text::{Span, StyledGrapheme};
124 /// # use helix_view::graphics::{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| !str_is_line_ending(s.symbol))
182 }
185impl<'a> From<String> for Span<'a> {
186 fn from(s: String) -> Span<'a> {
187 Span::raw(s)
188 }
191impl<'a> From<&'a str> for Span<'a> {
192 fn from(s: &'a str) -> Span<'a> {
193 Span::raw(s)
194 }
197/// A string composed of clusters of graphemes, each with their own style.
198#[derive(Debug, Clone, PartialEq)]
199pub struct Spans<'a>(pub Vec<Span<'a>>);
201impl<'a> Default for Spans<'a> {
202 fn default() -> Spans<'a> {
203 Spans(Vec::new())
204 }
207impl<'a> Spans<'a> {
208 /// Returns the width of the underlying string.
209 ///
210 /// ## Examples
211 ///
212 /// ```rust
213 /// # use helix_tui::text::{Span, Spans};
214 /// # use helix_view::graphics::{Color, Style};
215 /// let spans = Spans::from(vec![
216 /// Span::styled("My", Style::default().fg(Color::Yellow)),
217 /// Span::raw(" text"),
218 /// ]);
219 /// assert_eq!(7, spans.width());
220 /// ```
221 pub fn width(&self) -> usize {
222 self.0.iter().map(Span::width).sum()
223 }
226impl<'a> From<String> for Spans<'a> {
227 fn from(s: String) -> Spans<'a> {
228 Spans(vec![Span::from(s)])
229 }
232impl<'a> From<&'a str> for Spans<'a> {
233 fn from(s: &'a str) -> Spans<'a> {
234 Spans(vec![Span::from(s)])
235 }
238impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
239 fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
240 Spans(spans)
241 }
244impl<'a> From<Span<'a>> for Spans<'a> {
245 fn from(span: Span<'a>) -> Spans<'a> {
246 Spans(vec![span])
247 }
250impl<'a> From<Spans<'a>> for String {
251 fn from(line: Spans<'a>) -> String {
252 line.0.iter().fold(String::new(), |mut acc, s| {
253 acc.push_str(s.content.as_ref());
254 acc
255 })
256 }
259/// A string split over multiple lines where each line is composed of several clusters, each with
260/// their own style.
261///
262/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
263/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
264/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
265///
266/// ```rust
267/// # use helix_tui::text::Text;
268/// # use helix_view::graphics::{Color, Modifier, Style};
269/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
270///
271/// // An initial two lines of `Text` built from a `&str`
272/// let mut text = Text::from("The first line\nThe second line");
273/// assert_eq!(2, text.height());
274///
275/// // Adding two more unstyled lines
276/// text.extend(Text::raw("These are two\nmore lines!"));
277/// assert_eq!(4, text.height());
278///
279/// // Adding a final two styled lines
280/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
281/// assert_eq!(6, text.height());
282/// ```
283#[derive(Debug, Clone, PartialEq)]
284pub struct Text<'a> {
285 pub lines: Vec<Spans<'a>>,
288impl<'a> Default for Text<'a> {
289 fn default() -> Text<'a> {
290 Text { lines: Vec::new() }
291 }
294impl<'a> Text<'a> {
295 /// Create some text (potentially multiple lines) with no style.
296 ///
297 /// ## Examples
298 ///
299 /// ```rust
300 /// # use helix_tui::text::Text;
301 /// Text::raw("The first line\nThe second line");
302 /// Text::raw(String::from("The first line\nThe second line"));
303 /// ```
304 pub fn raw<T>(content: T) -> Text<'a>
305 where
306 T: Into<Cow<'a, str>>,
307 {
308 Text {
309 lines: match content.into() {
310 Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
311 Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
312 },
313 }
314 }
316 /// Create some text (potentially multiple lines) with a style.
317 ///
318 /// # Examples
319 ///
320 /// ```rust
321 /// # use helix_tui::text::Text;
322 /// # use helix_view::graphics::{Color, Modifier, Style};
323 /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
324 /// Text::styled("The first line\nThe second line", style);
325 /// Text::styled(String::from("The first line\nThe second line"), style);
326 /// ```
327 pub fn styled<T>(content: T, style: Style) -> Text<'a>
328 where
329 T: Into<Cow<'a, str>>,
330 {
331 let mut text = Text::raw(content);
332 text.patch_style(style);
333 text
334 }
336 /// Returns the max width of all the lines.
337 ///
338 /// ## Examples
339 ///
340 /// ```rust
341 /// use helix_tui::text::Text;
342 /// let text = Text::from("The first line\nThe second line");
343 /// assert_eq!(15, text.width());
344 /// ```
345 pub fn width(&self) -> usize {
346 self.lines
347 .iter()
348 .map(Spans::width)
349 .max()
350 .unwrap_or_default()
351 }
353 /// Returns the height.
354 ///
355 /// ## Examples
356 ///
357 /// ```rust
358 /// use helix_tui::text::Text;
359 /// let text = Text::from("The first line\nThe second line");
360 /// assert_eq!(2, text.height());
361 /// ```
362 pub fn height(&self) -> usize {
363 self.lines.len()
364 }
366 /// Apply a new style to existing text.
367 ///
368 /// # Examples
369 ///
370 /// ```rust
371 /// # use helix_tui::text::Text;
372 /// # use helix_view::graphics::{Color, Modifier, Style};
373 /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
374 /// let mut raw_text = Text::raw("The first line\nThe second line");
375 /// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
376 /// assert_ne!(raw_text, styled_text);
377 ///
378 /// raw_text.patch_style(style);
379 /// assert_eq!(raw_text, styled_text);
380 /// ```
381 pub fn patch_style(&mut self, style: Style) {
382 for line in &mut self.lines {
383 for span in &mut line.0 {
384 span.style = span.style.patch(style);
385 }
386 }
387 }
390impl<'a> From<String> for Text<'a> {
391 fn from(s: String) -> Text<'a> {
392 Text::raw(s)
393 }
396impl<'a> From<&'a str> for Text<'a> {
397 fn from(s: &'a str) -> Text<'a> {
398 Text::raw(s)
399 }
402impl<'a> From<Span<'a>> for Text<'a> {
403 fn from(span: Span<'a>) -> Text<'a> {
404 Text {
405 lines: vec![Spans::from(span)],
406 }
407 }
410impl<'a> From<Spans<'a>> for Text<'a> {
411 fn from(spans: Spans<'a>) -> Text<'a> {
412 Text { lines: vec![spans] }
413 }
416impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
417 fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
418 Text { lines }
419 }
422impl<'a> IntoIterator for Text<'a> {
423 type Item = Spans<'a>;
424 type IntoIter = std::vec::IntoIter<Self::Item>;
426 fn into_iter(self) -> Self::IntoIter {
427 self.lines.into_iter()
428 }
431impl<'a> Extend<Spans<'a>> for Text<'a> {
432 fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
433 self.lines.extend(iter);
434 }