Skip to main content

ctl_text/
text.rs

1//! Styled text that spans lines.
2
3use std::num::NonZeroU16;
4
5use anstyle::Style;
6
7use crate::line::Line;
8use crate::protocol::{Context, Measurement, Renderable};
9use crate::sanitize::{Breaks, clean, expand_tabs};
10use crate::span::Span;
11use crate::wrap::Overflow;
12
13#[derive(Clone, Debug, Eq, Hash, PartialEq)]
14struct Segment {
15    text: String,
16    style: Style,
17}
18
19/// Styled text of any length: a paragraph, a table cell, a title.
20///
21/// Text from outside the program is safe to put here. Every character that
22/// [`is_unsafe`](crate::unsafe_chars::is_unsafe) refuses is removed, except
23/// newline and tab.
24///
25/// Every other line break becomes a newline: carriage return, vertical tab,
26/// form feed, next line, and the line and paragraph separators. A carriage
27/// return before a line feed is one break with it. A newline starts a new
28/// line. A tab expands to the next tab stop of the [`Context`] it is rendered
29/// in, counted from the start of its line.
30///
31/// Two texts are equal when their content, styles, and [`Overflow`] are.
32/// Empty pieces are dropped and neighbours in one style are joined, however
33/// the text was pushed.
34///
35/// `Text` is [`Renderable`]: [`Overflow`] decides how it fits a width.
36#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
37pub struct Text {
38    segments: Vec<Segment>,
39    overflow: Overflow,
40}
41
42impl Text {
43    /// Text with nothing in it. It renders to no lines.
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// `text` in the default style.
50    #[must_use]
51    pub fn plain(text: impl AsRef<str>) -> Self {
52        Self::styled(text, Style::new())
53    }
54
55    /// `text` in `style`.
56    #[must_use]
57    pub fn styled(text: impl AsRef<str>, style: Style) -> Self {
58        let mut created = Self::new();
59        created.push(text, style);
60        created
61    }
62
63    /// Adds `text` in `style` after what is already there. A newline in
64    /// `text` starts a new line.
65    pub fn push(&mut self, text: impl AsRef<str>, style: Style) {
66        self.push_str(text.as_ref(), style);
67    }
68
69    fn push_str(&mut self, text: &str, style: Style) {
70        self.push_segment(clean(text, Breaks::Newline), style);
71    }
72
73    /// Adds `text`, which is already clean, unless it is empty; joins the last
74    /// segment when it has the same style.
75    fn push_segment(&mut self, text: String, style: Style) {
76        if text.is_empty() {
77            return;
78        }
79        match self.segments.last_mut() {
80            Some(last) if last.style == style => last.text.push_str(&text),
81            Some(_) | None => self.segments.push(Segment { text, style }),
82        }
83    }
84
85    /// Sets what to do with a line wider than the width it renders at.
86    /// The default is [`Overflow::KeepWords`].
87    #[must_use]
88    pub fn overflow(mut self, overflow: Overflow) -> Self {
89        self.overflow = overflow;
90        self
91    }
92
93    /// The text as lines, with tabs expanded to multiples of `tab_width`.
94    ///
95    /// Nothing is wrapped. A text that ends in a newline ends in an empty
96    /// line.
97    #[must_use]
98    pub fn lines(&self, tab_width: NonZeroU16) -> Vec<Line> {
99        let mut lines: Vec<Line> = Vec::new();
100        for segment in &self.segments {
101            for (index, part) in segment.text.split('\n').enumerate() {
102                let mut line = if index == 0 {
103                    lines.pop().unwrap_or_default()
104                } else {
105                    Line::new()
106                };
107                append_expanded(&mut line, part, segment.style, tab_width);
108                lines.push(line);
109            }
110        }
111        lines
112    }
113
114    /// The lines of the text with the spaces at the end of each removed.
115    fn trimmed_lines(&self, context: &Context) -> Vec<Line> {
116        let mut lines = self.lines(context.tab_width());
117        for line in &mut lines {
118            line.trim_end();
119        }
120        lines
121    }
122}
123
124/// Adds `part`, which holds no newline, to `line`, with each tab expanded.
125///
126/// The text before the first tab goes in first, so a tab counts from the
127/// line's own width. That text can continue the line's last cluster, as a
128/// variation selector after a heart does. It then widens the line before the
129/// tab is measured.
130fn append_expanded(line: &mut Line, part: &str, style: Style, tab_width: NonZeroU16) {
131    let (head, tabbed) = part.split_at(part.find('\t').unwrap_or(part.len()));
132    line.append(head, style);
133    if !tabbed.is_empty() {
134        let column = usize::from(line.width());
135        line.append(&expand_tabs(tabbed, column, tab_width), style);
136    }
137}
138
139impl From<&str> for Text {
140    fn from(text: &str) -> Self {
141        Self::plain(text)
142    }
143}
144
145impl From<String> for Text {
146    fn from(text: String) -> Self {
147        Self::plain(text)
148    }
149}
150
151impl From<Span> for Text {
152    fn from(span: Span) -> Self {
153        let mut text = Self::new();
154        text.push_segment(span.text, span.style);
155        text
156    }
157}
158
159impl Text {
160    /// [`measure`](Renderable::measure) as if the text's overflow were
161    /// `overflow`, so a container that decides overflow per column measures a
162    /// table cell without cloning it.
163    #[must_use]
164    pub fn measure_as(&self, context: &Context, overflow: Overflow) -> Measurement {
165        let lines = self.trimmed_lines(context);
166        let maximum = lines.iter().map(Line::width).max().unwrap_or(0);
167        let minimum = lines
168            .iter()
169            .map(|line| line.minimum_width(overflow))
170            .max()
171            .unwrap_or(0);
172        Measurement::new(minimum, maximum)
173    }
174
175    /// [`render`](Renderable::render) as if the text's overflow were
176    /// `overflow`, so a container that decides overflow per column renders a
177    /// table cell without cloning it.
178    #[must_use]
179    pub fn render_as(&self, context: &Context, overflow: Overflow) -> Vec<Line> {
180        self.trimmed_lines(context)
181            .iter()
182            .flat_map(|line| line.fit(context.width(), overflow))
183            .collect()
184    }
185}
186
187impl Renderable for Text {
188    fn measure(&self, context: &Context) -> Measurement {
189        self.measure_as(context, self.overflow)
190    }
191
192    fn render(&self, context: &Context) -> Vec<Line> {
193        self.render_as(context, self.overflow)
194    }
195}