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 Element for Box<dyn Element + '_> {
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    // TODO: Make this impl trivial once [`Iterator::intersperse`] is stable.
187    pub fn spaced(items: impl IntoIterator<Item = Label>) -> Self {
188        let iter = items.into_iter();
189
190        let mut line = Self {
191            items: Vec::with_capacity({
192                let (min, max) = iter.size_hint();
193                let likely = max.unwrap_or(min);
194
195                // Technically (likely + (likely - 1)), but we push the last space before
196                // we pop it again, so we need that additional space anyways.
197                likely * 2
198            }),
199        };
200
201        // Don't create spaces around empty labels.
202        for item in iter.filter(|i| !i.is_blank()) {
203            line.push(item);
204            line.push(Label::space());
205        }
206        line.items.pop();
207        line
208    }
209
210    /// Add an item to this line.
211    pub fn item(mut self, item: impl Into<Label>) -> Self {
212        self.push(item);
213        self
214    }
215
216    /// Add multiple items to this line.
217    pub fn extend(mut self, items: impl IntoIterator<Item = Label>) -> Self {
218        self.items.extend(items);
219        self
220    }
221
222    /// Add an item to this line.
223    pub fn push(&mut self, item: impl Into<Label>) {
224        self.items.push(item.into());
225    }
226
227    /// Pad this line to occupy the given width.
228    pub fn pad(&mut self, width: usize) {
229        let w = self.width();
230
231        if width > w {
232            let pad = width - w;
233            let bg = if let Some(last) = self.items.last() {
234                last.background()
235            } else {
236                Color::Unset
237            };
238            self.items.push(Label::new(" ".repeat(pad).as_str()).bg(bg));
239        }
240    }
241
242    /// Truncate this line to the given width.
243    pub fn truncate(&mut self, width: usize, delim: &str) {
244        while self.width() > width {
245            let total = self.width();
246
247            if total - self.items.last().map_or(0, Cell::width) > width {
248                self.items.pop();
249            } else if let Some(item) = self.items.last_mut() {
250                *item = item.truncate(width - (total - Cell::width(item)), delim);
251            }
252        }
253    }
254
255    /// Get the actual column width of this line.
256    pub fn width(&self) -> usize {
257        self.items.iter().map(Cell::width).sum()
258    }
259
260    /// Create a line that contains a single space.
261    pub fn space(mut self) -> Self {
262        self.items.push(Label::space());
263        self
264    }
265
266    /// Box this line as an [`Element`].
267    pub fn boxed(self) -> Box<dyn Element> {
268        Box::new(self)
269    }
270
271    /// Return a filled line.
272    pub fn filled(self, color: Color) -> Filled<Self> {
273        Filled { item: self, color }
274    }
275}
276
277impl IntoIterator for Line {
278    type Item = Label;
279    type IntoIter = Box<dyn Iterator<Item = Label>>;
280
281    fn into_iter(self) -> Self::IntoIter {
282        Box::new(self.items.into_iter())
283    }
284}
285
286impl<T: Into<Label>> From<T> for Line {
287    fn from(value: T) -> Self {
288        Self::new(value)
289    }
290}
291
292impl From<Vec<Label>> for Line {
293    fn from(items: Vec<Label>) -> Self {
294        Self { items }
295    }
296}
297
298impl Element for Line {
299    fn size(&self, _parent: Constraint) -> Size {
300        Size::new(self.items.iter().map(Cell::width).sum(), 1)
301    }
302
303    fn render(&self, _parent: Constraint) -> Vec<Line> {
304        vec![self.clone()]
305    }
306}
307
308impl Element for Vec<Line> {
309    fn size(&self, parent: Constraint) -> Size {
310        let width = self
311            .iter()
312            .map(|e| e.columns(parent))
313            .max()
314            .unwrap_or_default();
315        let height = self.len();
316
317        Size::new(width, height)
318    }
319
320    fn render(&self, parent: Constraint) -> Vec<Line> {
321        self.iter()
322            .cloned()
323            .flat_map(|l| l.render(parent))
324            .collect()
325    }
326}
327
328impl fmt::Display for Line {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        for item in &self.items {
331            write!(f, "{item}")?;
332        }
333        Ok(())
334    }
335}
336
337/// Size of a text element, in columns and rows.
338#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
339pub struct Size {
340    /// Columns occupied.
341    pub cols: usize,
342    /// Rows occupied.
343    pub rows: usize,
344}
345
346impl Size {
347    /// Minimum size.
348    pub const MIN: Self = Self {
349        cols: usize::MIN,
350        rows: usize::MIN,
351    };
352    /// Maximum size.
353    pub const MAX: Self = Self {
354        cols: usize::MAX,
355        rows: usize::MAX,
356    };
357
358    /// Create a new [`Size`].
359    pub fn new(cols: usize, rows: usize) -> Self {
360        Self { cols, rows }
361    }
362
363    /// Constrain size.
364    pub fn constrain(self, c: Constraint) -> Self {
365        Self {
366            cols: self.cols.clamp(c.min.cols, c.max.cols),
367            rows: self.rows.clamp(c.min.rows, c.max.rows),
368        }
369    }
370}
371
372#[cfg(test)]
373mod test {
374    use super::*;
375
376    #[test]
377    fn test_truncate() {
378        let line = Line::default().item("banana").item("peach").item("apple");
379
380        let mut actual = line.clone();
381        actual = actual.truncate(9, "…");
382        assert_eq!(actual.to_string(), "bananape…");
383
384        let mut actual = line.clone();
385        actual = actual.truncate(7, "…");
386        assert_eq!(actual.to_string(), "banana…");
387
388        let mut actual = line.clone();
389        actual = actual.truncate(1, "…");
390        assert_eq!(actual.to_string(), "…");
391
392        let mut actual = line;
393        actual = actual.truncate(0, "…");
394        assert_eq!(actual.to_string(), "");
395    }
396
397    #[test]
398    fn test_width() {
399        // Nb. This might not display correctly in some editors or terminals.
400        let line = Line::new("Radicle Heartwood Protocol & Stack ❤️🪵");
401        assert_eq!(line.width(), 39, "{line}");
402        let line = Line::new("❤\u{fe0f}");
403        assert_eq!(line.width(), 2, "{line}");
404        let line = Line::new("❤️");
405        assert_eq!(line.width(), 2, "{line}");
406    }
407
408    #[test]
409    fn test_spaced() {
410        let line = Line::spaced(["banana", "peach", "apple"].into_iter().map(Label::new));
411
412        let iterated: Vec<_> = line.into_iter().collect();
413        assert_eq!(
414            iterated
415                .into_iter()
416                .map(|l| l.to_string())
417                .collect::<Vec<_>>(),
418            ["banana", " ", "peach", " ", "apple"]
419        );
420    }
421}