aboutsummaryrefslogtreecommitdiff
path: root/helix-tui/src/text.rs
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-05-09 08:52:55 +0000
committerBlaž Hrastnik2021-05-09 08:52:55 +0000
commit35606a3daa7ee273845a12f3e03728e0ae23928e (patch)
tree643684eaff6627dbebc4156d33fdb541bf87bbd9 /helix-tui/src/text.rs
parent6c705f09e88a4b63c4ed854bc9e956b0539ca8af (diff)
Inline tui as helix-tui fork.
We only rely on some of the rendering primitives and implement our Cursive-style compositor on top.
Diffstat (limited to 'helix-tui/src/text.rs')
-rw-r--r--helix-tui/src/text.rs434
1 files changed, 434 insertions, 0 deletions
diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs
new file mode 100644
index 00000000..c671e918
--- /dev/null
+++ b/helix-tui/src/text.rs
@@ -0,0 +1,434 @@
+//! Primitives for styled text.
+//!
+//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
+//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
+//! - A single line string where all graphemes have the same style is represented by a [`Span`].
+//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
+//! - A multiple line string where each grapheme may have its own style is represented by a
+//! [`Text`].
+//!
+//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
+//! is a [`Spans`].
+//!
+//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
+//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
+//! that you can start by using simple `String` or `&str` and then promote them to the previous
+//! primitives when you need additional styling capabilities.
+//!
+//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
+//! its `title` property (which is a [`Spans`] under the hood):
+//!
+//! ```rust
+//! # use helix_tui::widgets::Block;
+//! # use helix_tui::text::{Span, Spans};
+//! # use helix_tui::style::{Color, Style};
+//! // A simple string with no styling.
+//! // Converted to Spans(vec![
+//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
+//! // ])
+//! let block = Block::default().title("My title");
+//!
+//! // A simple string with a unique style.
+//! // Converted to Spans(vec![
+//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
+//! // ])
+//! let block = Block::default().title(
+//! Span::styled("My title", Style::default().fg(Color::Yellow))
+//! );
+//!
+//! // A string with multiple styles.
+//! // Converted to Spans(vec![
+//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
+//! // Span { content: Cow::Borrowed(" title"), .. }
+//! // ])
+//! let block = Block::default().title(vec![
+//! Span::styled("My", Style::default().fg(Color::Yellow)),
+//! Span::raw(" title"),
+//! ]);
+//! ```
+use crate::style::Style;
+use std::borrow::Cow;
+use unicode_segmentation::UnicodeSegmentation;
+use unicode_width::UnicodeWidthStr;
+
+/// A grapheme associated to a style.
+#[derive(Debug, Clone, PartialEq)]
+pub struct StyledGrapheme<'a> {
+ pub symbol: &'a str,
+ pub style: Style,
+}
+
+/// A string where all graphemes have the same style.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Span<'a> {
+ pub content: Cow<'a, str>,
+ pub style: Style,
+}
+
+impl<'a> Span<'a> {
+ /// Create a span with no style.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::Span;
+ /// Span::raw("My text");
+ /// Span::raw(String::from("My text"));
+ /// ```
+ pub fn raw<T>(content: T) -> Span<'a>
+ where
+ T: Into<Cow<'a, str>>,
+ {
+ Span {
+ content: content.into(),
+ style: Style::default(),
+ }
+ }
+
+ /// Create a span with a style.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::Span;
+ /// # use helix_tui::style::{Color, Modifier, Style};
+ /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
+ /// Span::styled("My text", style);
+ /// Span::styled(String::from("My text"), style);
+ /// ```
+ pub fn styled<T>(content: T, style: Style) -> Span<'a>
+ where
+ T: Into<Cow<'a, str>>,
+ {
+ Span {
+ content: content.into(),
+ style,
+ }
+ }
+
+ /// Returns the width of the content held by this span.
+ pub fn width(&self) -> usize {
+ self.content.width()
+ }
+
+ /// Returns an iterator over the graphemes held by this span.
+ ///
+ /// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
+ /// the resulting [`Style`].
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::{Span, StyledGrapheme};
+ /// # use helix_tui::style::{Color, Modifier, Style};
+ /// # use std::iter::Iterator;
+ /// let style = Style::default().fg(Color::Yellow);
+ /// let span = Span::styled("Text", style);
+ /// let style = Style::default().fg(Color::Green).bg(Color::Black);
+ /// let styled_graphemes = span.styled_graphemes(style);
+ /// assert_eq!(
+ /// vec![
+ /// StyledGrapheme {
+ /// symbol: "T",
+ /// style: Style {
+ /// fg: Some(Color::Yellow),
+ /// bg: Some(Color::Black),
+ /// add_modifier: Modifier::empty(),
+ /// sub_modifier: Modifier::empty(),
+ /// },
+ /// },
+ /// StyledGrapheme {
+ /// symbol: "e",
+ /// style: Style {
+ /// fg: Some(Color::Yellow),
+ /// bg: Some(Color::Black),
+ /// add_modifier: Modifier::empty(),
+ /// sub_modifier: Modifier::empty(),
+ /// },
+ /// },
+ /// StyledGrapheme {
+ /// symbol: "x",
+ /// style: Style {
+ /// fg: Some(Color::Yellow),
+ /// bg: Some(Color::Black),
+ /// add_modifier: Modifier::empty(),
+ /// sub_modifier: Modifier::empty(),
+ /// },
+ /// },
+ /// StyledGrapheme {
+ /// symbol: "t",
+ /// style: Style {
+ /// fg: Some(Color::Yellow),
+ /// bg: Some(Color::Black),
+ /// add_modifier: Modifier::empty(),
+ /// sub_modifier: Modifier::empty(),
+ /// },
+ /// },
+ /// ],
+ /// styled_graphemes.collect::<Vec<StyledGrapheme>>()
+ /// );
+ /// ```
+ pub fn styled_graphemes(
+ &'a self,
+ base_style: Style,
+ ) -> impl Iterator<Item = StyledGrapheme<'a>> {
+ UnicodeSegmentation::graphemes(self.content.as_ref(), true)
+ .map(move |g| StyledGrapheme {
+ symbol: g,
+ style: base_style.patch(self.style),
+ })
+ .filter(|s| s.symbol != "\n")
+ }
+}
+
+impl<'a> From<String> for Span<'a> {
+ fn from(s: String) -> Span<'a> {
+ Span::raw(s)
+ }
+}
+
+impl<'a> From<&'a str> for Span<'a> {
+ fn from(s: &'a str) -> Span<'a> {
+ Span::raw(s)
+ }
+}
+
+/// A string composed of clusters of graphemes, each with their own style.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Spans<'a>(pub Vec<Span<'a>>);
+
+impl<'a> Default for Spans<'a> {
+ fn default() -> Spans<'a> {
+ Spans(Vec::new())
+ }
+}
+
+impl<'a> Spans<'a> {
+ /// Returns the width of the underlying string.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::{Span, Spans};
+ /// # use helix_tui::style::{Color, Style};
+ /// let spans = Spans::from(vec![
+ /// Span::styled("My", Style::default().fg(Color::Yellow)),
+ /// Span::raw(" text"),
+ /// ]);
+ /// assert_eq!(7, spans.width());
+ /// ```
+ pub fn width(&self) -> usize {
+ self.0.iter().map(Span::width).sum()
+ }
+}
+
+impl<'a> From<String> for Spans<'a> {
+ fn from(s: String) -> Spans<'a> {
+ Spans(vec![Span::from(s)])
+ }
+}
+
+impl<'a> From<&'a str> for Spans<'a> {
+ fn from(s: &'a str) -> Spans<'a> {
+ Spans(vec![Span::from(s)])
+ }
+}
+
+impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
+ fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
+ Spans(spans)
+ }
+}
+
+impl<'a> From<Span<'a>> for Spans<'a> {
+ fn from(span: Span<'a>) -> Spans<'a> {
+ Spans(vec![span])
+ }
+}
+
+impl<'a> From<Spans<'a>> for String {
+ fn from(line: Spans<'a>) -> String {
+ line.0.iter().fold(String::new(), |mut acc, s| {
+ acc.push_str(s.content.as_ref());
+ acc
+ })
+ }
+}
+
+/// A string split over multiple lines where each line is composed of several clusters, each with
+/// their own style.
+///
+/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
+/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
+/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
+///
+/// ```rust
+/// # use helix_tui::text::Text;
+/// # use helix_tui::style::{Color, Modifier, Style};
+/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
+///
+/// // An initial two lines of `Text` built from a `&str`
+/// let mut text = Text::from("The first line\nThe second line");
+/// assert_eq!(2, text.height());
+///
+/// // Adding two more unstyled lines
+/// text.extend(Text::raw("These are two\nmore lines!"));
+/// assert_eq!(4, text.height());
+///
+/// // Adding a final two styled lines
+/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
+/// assert_eq!(6, text.height());
+/// ```
+#[derive(Debug, Clone, PartialEq)]
+pub struct Text<'a> {
+ pub lines: Vec<Spans<'a>>,
+}
+
+impl<'a> Default for Text<'a> {
+ fn default() -> Text<'a> {
+ Text { lines: Vec::new() }
+ }
+}
+
+impl<'a> Text<'a> {
+ /// Create some text (potentially multiple lines) with no style.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::Text;
+ /// Text::raw("The first line\nThe second line");
+ /// Text::raw(String::from("The first line\nThe second line"));
+ /// ```
+ pub fn raw<T>(content: T) -> Text<'a>
+ where
+ T: Into<Cow<'a, str>>,
+ {
+ Text {
+ lines: match content.into() {
+ Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
+ Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
+ },
+ }
+ }
+
+ /// Create some text (potentially multiple lines) with a style.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::Text;
+ /// # use helix_tui::style::{Color, Modifier, Style};
+ /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
+ /// Text::styled("The first line\nThe second line", style);
+ /// Text::styled(String::from("The first line\nThe second line"), style);
+ /// ```
+ pub fn styled<T>(content: T, style: Style) -> Text<'a>
+ where
+ T: Into<Cow<'a, str>>,
+ {
+ let mut text = Text::raw(content);
+ text.patch_style(style);
+ text
+ }
+
+ /// Returns the max width of all the lines.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// use helix_tui::text::Text;
+ /// let text = Text::from("The first line\nThe second line");
+ /// assert_eq!(15, text.width());
+ /// ```
+ pub fn width(&self) -> usize {
+ self.lines
+ .iter()
+ .map(Spans::width)
+ .max()
+ .unwrap_or_default()
+ }
+
+ /// Returns the height.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// use helix_tui::text::Text;
+ /// let text = Text::from("The first line\nThe second line");
+ /// assert_eq!(2, text.height());
+ /// ```
+ pub fn height(&self) -> usize {
+ self.lines.len()
+ }
+
+ /// Apply a new style to existing text.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::Text;
+ /// # use helix_tui::style::{Color, Modifier, Style};
+ /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
+ /// let mut raw_text = Text::raw("The first line\nThe second line");
+ /// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
+ /// assert_ne!(raw_text, styled_text);
+ ///
+ /// raw_text.patch_style(style);
+ /// assert_eq!(raw_text, styled_text);
+ /// ```
+ pub fn patch_style(&mut self, style: Style) {
+ for line in &mut self.lines {
+ for span in &mut line.0 {
+ span.style = span.style.patch(style);
+ }
+ }
+ }
+}
+
+impl<'a> From<String> for Text<'a> {
+ fn from(s: String) -> Text<'a> {
+ Text::raw(s)
+ }
+}
+
+impl<'a> From<&'a str> for Text<'a> {
+ fn from(s: &'a str) -> Text<'a> {
+ Text::raw(s)
+ }
+}
+
+impl<'a> From<Span<'a>> for Text<'a> {
+ fn from(span: Span<'a>) -> Text<'a> {
+ Text {
+ lines: vec![Spans::from(span)],
+ }
+ }
+}
+
+impl<'a> From<Spans<'a>> for Text<'a> {
+ fn from(spans: Spans<'a>) -> Text<'a> {
+ Text { lines: vec![spans] }
+ }
+}
+
+impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
+ fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
+ Text { lines }
+ }
+}
+
+impl<'a> IntoIterator for Text<'a> {
+ type Item = Spans<'a>;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.lines.into_iter()
+ }
+}
+
+impl<'a> Extend<Spans<'a>> for Text<'a> {
+ fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
+ self.lines.extend(iter);
+ }
+}