radicle_term/
element.rs

1use std::fmt;
2use std::io::IsTerminal;
3use std::ops::Deref;
4use std::{io, vec};
5
6use crate::cell::Cell;
7use crate::{viewport, Color, Filled, Label, Style};
8
9/// Rendering constraint.
10#[derive(Debug, Copy, Clone, PartialEq, Eq)]
11pub struct Constraint {
12    /// Minimum space the element can take.
13    pub min: Size,
14    /// Maximum space the element can take.
15    pub max: Size,
16}
17
18impl Default for Constraint {
19    fn default() -> Self {
20        Self::UNBOUNDED
21    }
22}
23
24impl Constraint {
25    /// Can satisfy any size of object.
26    pub const UNBOUNDED: Self = Self {
27        min: Size::MIN,
28        max: Size::MAX,
29    };
30
31    /// Create a new constraint.
32    pub fn new(min: Size, max: Size) -> Self {
33        assert!(min.cols <= max.cols && min.rows <= max.rows);
34
35        Self { min, max }
36    }
37
38    /// A constraint with only a maximum size.
39    pub fn max(max: Size) -> Self {
40        Self {
41            min: Size::MIN,
42            max,
43        }
44    }
45
46    /// A constraint that can only be satisfied by a single column size.
47    /// The rows are unconstrained.
48    pub fn tight(cols: usize) -> Self {
49        Self {
50            min: Size::new(cols, 1),
51            max: Size::new(cols, usize::MAX),
52        }
53    }
54
55    /// Create a constraint from the terminal environment.
56    /// Returns [`None`] if the output device is not a terminal.
57    pub fn from_env() -> Option<Self> {
58        if io::stdout().is_terminal() {
59            Some(Self::max(viewport().unwrap_or(Size::MAX)))
60        } else {
61            None
62        }
63    }
64}
65
66/// A text element that has a size and can be rendered to the terminal.
67pub trait Element: fmt::Debug + Send + Sync {
68    /// Get the size of the element, in rows and columns.
69    fn size(&self, parent: Constraint) -> Size;
70
71    #[must_use]
72    /// Render the element as lines of text that can be printed.
73    fn render(&self, parent: Constraint) -> Vec<Line>;
74
75    /// Get the number of columns occupied by this element.
76    fn columns(&self, parent: Constraint) -> usize {
77        self.size(parent).cols
78    }
79
80    /// Get the number of rows occupied by this element.
81    fn rows(&self, parent: Constraint) -> usize {
82        self.size(parent).rows
83    }
84
85    /// Print this element to stdout.
86    fn print(&self) {
87        for line in self.render(Constraint::from_env().unwrap_or_default()) {
88            println!("{}", line.to_string().trim_end());
89        }
90    }
91
92    /// Write using the given constraints to `stdout`.
93    fn write(&self, constraints: Constraint) -> io::Result<()>
94    where
95        Self: Sized,
96    {
97        self::write_to(self, &mut io::stdout(), constraints)
98    }
99
100    #[must_use]
101    /// Return a string representation of this element.
102    fn display(&self, constraints: Constraint) -> String {
103        let mut out = String::new();
104        for line in self.render(constraints) {
105            out.extend(line.into_iter().map(|l| l.to_string()));
106            out.push('\n');
107        }
108        out
109    }
110}
111
112impl<'a> Element for Box<dyn Element + 'a> {
113    fn size(&self, parent: Constraint) -> Size {
114        self.deref().size(parent)
115    }
116
117    fn render(&self, parent: Constraint) -> Vec<Line> {
118        self.deref().render(parent)
119    }
120
121    fn print(&self) {
122        self.deref().print()
123    }
124}
125
126impl<T: Element> Element for &T {
127    fn size(&self, parent: Constraint) -> Size {
128        (*self).size(parent)
129    }
130
131    fn render(&self, parent: Constraint) -> Vec<Line> {
132        (*self).render(parent)
133    }
134
135    fn print(&self) {
136        (*self).print()
137    }
138}
139
140/// Write using the given constraints, to a writer.
141pub fn write_to(
142    elem: &impl Element,
143    writer: &mut impl io::Write,
144    constraints: Constraint,
145) -> io::Result<()> {
146    for line in elem.render(constraints) {
147        writeln!(writer, "{}", line.to_string().trim_end())?;
148    }
149    Ok(())
150}
151
152/// A line of text that has styling and can be displayed.
153#[derive(Clone, Default, Debug)]
154pub struct Line {
155    items: Vec<Label>,
156}
157
158impl Line {
159    /// Create a new line.
160    pub fn new(item: impl Into<Label>) -> Self {
161        Self {
162            items: vec![item.into()],
163        }
164    }
165
166    /// Create a blank line.
167    pub fn blank() -> Self {
168        Self { items: vec![] }
169    }
170
171    /// Return a styled line by styling all its labels.
172    pub fn style(self, style: Style) -> Line {
173        Self {
174            items: self
175                .items
176                .into_iter()
177                .map(|l| {
178                    let style = l.paint().style().merge(style);
179                    l.style(style)
180                })
181                .collect(),
182        }
183    }
184
185    /// Return a line with a single space between the given labels.
186    pub fn spaced(items: impl IntoIterator<Item = Label>) -> Self {
187        let mut line = Self::default();
188        for item in items.into_iter() {
189            // Don't create spaces around empty labels.
190            if item.is_blank() {
191                continue;
192            }
193            line.push(item);
194            line.push(Label::space());
195        }
196        line.items.pop();
197        line
198    }
199
200    /// Add an item to this line.
201    pub fn item(mut self, item: impl Into<Label>) -> Self {
202        self.push(item);
203        self
204    }
205
206    /// Add multiple items to this line.
207    pub fn extend(mut self, items: impl IntoIterator<Item = Label>) -> Self {
208        self.items.extend(items);
209        self
210    }
211
212    /// Add an item to this line.
213    pub fn push(&mut self, item: impl Into<Label>) {
214        self.items.push(item.into());
215    }
216
217    /// Pad this line to occupy the given width.
218    pub fn pad(&mut self, width: usize) {
219        let w = self.width();
220
221        if width > w {
222            let pad = width - w;
223            let bg = if let Some(last) = self.items.last() {
224                last.background()
225            } else {
226                Color::Unset
227            };
228            self.items.push(Label::new(" ".repeat(pad).as_str()).bg(bg));
229        }
230    }
231
232    /// Truncate this line to the given width.
233    pub fn truncate(&mut self, width: usize, delim: &str) {
234        while self.width() > width {
235            let total = self.width();
236
237            if total - self.items.last().map_or(0, Cell::width) > width {
238                self.items.pop();
239            } else if let Some(item) = self.items.last_mut() {
240                *item = item.truncate(width - (total - Cell::width(item)), delim);
241            }
242        }
243    }
244
245    /// Get the actual column width of this line.
246    pub fn width(&self) -> usize {
247        self.items.iter().map(Cell::width).sum()
248    }
249
250    /// Create a line that contains a single space.
251    pub fn space(mut self) -> Self {
252        self.items.push(Label::space());
253        self
254    }
255
256    /// Box this line as an [`Element`].
257    pub fn boxed(self) -> Box<dyn Element> {
258        Box::new(self)
259    }
260
261    /// Return a filled line.
262    pub fn filled(self, color: Color) -> Filled<Self> {
263        Filled { item: self, color }
264    }
265}
266
267impl IntoIterator for Line {
268    type Item = Label;
269    type IntoIter = Box<dyn Iterator<Item = Label>>;
270
271    fn into_iter(self) -> Self::IntoIter {
272        Box::new(self.items.into_iter())
273    }
274}
275
276impl<T: Into<Label>> From<T> for Line {
277    fn from(value: T) -> Self {
278        Self::new(value)
279    }
280}
281
282impl From<Vec<Label>> for Line {
283    fn from(items: Vec<Label>) -> Self {
284        Self { items }
285    }
286}
287
288impl Element for Line {
289    fn size(&self, _parent: Constraint) -> Size {
290        Size::new(self.items.iter().map(Cell::width).sum(), 1)
291    }
292
293    fn render(&self, _parent: Constraint) -> Vec<Line> {
294        vec![self.clone()]
295    }
296}
297
298impl Element for Vec<Line> {
299    fn size(&self, parent: Constraint) -> Size {
300        let width = self
301            .iter()
302            .map(|e| e.columns(parent))
303            .max()
304            .unwrap_or_default();
305        let height = self.len();
306
307        Size::new(width, height)
308    }
309
310    fn render(&self, parent: Constraint) -> Vec<Line> {
311        self.iter()
312            .cloned()
313            .flat_map(|l| l.render(parent))
314            .collect()
315    }
316}
317
318impl fmt::Display for Line {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        for item in &self.items {
321            write!(f, "{item}")?;
322        }
323        Ok(())
324    }
325}
326
327/// Size of a text element, in columns and rows.
328#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
329pub struct Size {
330    /// Columns occupied.
331    pub cols: usize,
332    /// Rows occupied.
333    pub rows: usize,
334}
335
336impl Size {
337    /// Minimum size.
338    pub const MIN: Self = Self {
339        cols: usize::MIN,
340        rows: usize::MIN,
341    };
342    /// Maximum size.
343    pub const MAX: Self = Self {
344        cols: usize::MAX,
345        rows: usize::MAX,
346    };
347
348    /// Create a new [`Size`].
349    pub fn new(cols: usize, rows: usize) -> Self {
350        Self { cols, rows }
351    }
352
353    /// Constrain size.
354    pub fn constrain(self, c: Constraint) -> Self {
355        Self {
356            cols: self.cols.clamp(c.min.cols, c.max.cols),
357            rows: self.rows.clamp(c.min.rows, c.max.rows),
358        }
359    }
360}
361
362#[cfg(test)]
363mod test {
364    use super::*;
365
366    #[test]
367    fn test_truncate() {
368        let line = Line::default().item("banana").item("peach").item("apple");
369
370        let mut actual = line.clone();
371        actual = actual.truncate(9, "…");
372        assert_eq!(actual.to_string(), "bananape…");
373
374        let mut actual = line.clone();
375        actual = actual.truncate(7, "…");
376        assert_eq!(actual.to_string(), "banana…");
377
378        let mut actual = line.clone();
379        actual = actual.truncate(1, "…");
380        assert_eq!(actual.to_string(), "…");
381
382        let mut actual = line;
383        actual = actual.truncate(0, "…");
384        assert_eq!(actual.to_string(), "");
385    }
386
387    #[test]
388    fn test_width() {
389        // Nb. This might not display correctly in some editors or terminals.
390        let line = Line::new("Radicle Heartwood Protocol & Stack ❤️🪵");
391        assert_eq!(line.width(), 39, "{line}");
392        let line = Line::new("❤\u{fe0f}");
393        assert_eq!(line.width(), 2, "{line}");
394        let line = Line::new("❤️");
395        assert_eq!(line.width(), 2, "{line}");
396    }
397}