mirror of https://github.com/helix-editor/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
435 lines
13 KiB
Rust
435 lines
13 KiB
Rust
//! 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);
|
|
}
|
|
}
|