radicle_term/
cell.rs

1use std::fmt;
2
3use super::{Color, Filled, Line, Paint};
4
5use unicode_display_width as unicode;
6use unicode_segmentation::UnicodeSegmentation as _;
7
8/// Text that can be displayed on the terminal, measured, truncated and padded.
9pub trait Cell: fmt::Display {
10    /// Type after truncation.
11    type Truncated: Cell;
12    /// Type after padding.
13    type Padded: Cell;
14
15    /// Cell display width in number of terminal columns.
16    fn width(&self) -> usize;
17    /// Background color of cell.
18    fn background(&self) -> Color {
19        Color::Unset
20    }
21    /// Truncate cell if longer than given width. Shows the delimiter if truncated.
22    #[must_use]
23    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated;
24    /// Pad the cell so that it is the given width, while keeping the content left-aligned.
25    #[must_use]
26    fn pad(&self, width: usize) -> Self::Padded;
27}
28
29impl Cell for Paint<String> {
30    type Truncated = Self;
31    type Padded = Self;
32
33    fn width(&self) -> usize {
34        Cell::width(self.content())
35    }
36
37    fn background(&self) -> Color {
38        self.style.background
39    }
40
41    fn truncate(&self, width: usize, delim: &str) -> Self {
42        Self {
43            item: self.item.truncate(width, delim),
44            style: self.style,
45        }
46    }
47
48    fn pad(&self, width: usize) -> Self {
49        Self {
50            item: self.item.pad(width),
51            style: self.style,
52        }
53    }
54}
55
56impl Cell for Line {
57    type Truncated = Line;
58    type Padded = Line;
59
60    fn width(&self) -> usize {
61        Line::width(self)
62    }
63
64    fn pad(&self, width: usize) -> Self::Padded {
65        let mut line = self.clone();
66        Line::pad(&mut line, width);
67        line
68    }
69
70    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
71        let mut line = self.clone();
72        Line::truncate(&mut line, width, delim);
73        line
74    }
75}
76
77impl Cell for Paint<&str> {
78    type Truncated = Paint<String>;
79    type Padded = Paint<String>;
80
81    fn width(&self) -> usize {
82        Cell::width(self.item)
83    }
84
85    fn background(&self) -> Color {
86        self.style.background
87    }
88
89    fn truncate(&self, width: usize, delim: &str) -> Paint<String> {
90        Paint {
91            item: self.item.truncate(width, delim),
92            style: self.style,
93        }
94    }
95
96    fn pad(&self, width: usize) -> Paint<String> {
97        Paint {
98            item: self.item.pad(width),
99            style: self.style,
100        }
101    }
102}
103
104impl Cell for String {
105    type Truncated = Self;
106    type Padded = Self;
107
108    fn width(&self) -> usize {
109        Cell::width(self.as_str())
110    }
111
112    fn truncate(&self, width: usize, delim: &str) -> Self {
113        self.as_str().truncate(width, delim)
114    }
115
116    fn pad(&self, width: usize) -> Self {
117        self.as_str().pad(width)
118    }
119}
120
121impl Cell for str {
122    type Truncated = String;
123    type Padded = String;
124
125    fn width(&self) -> usize {
126        self.graphemes(true)
127            .map(|g| unicode::width(g) as usize)
128            .sum()
129    }
130
131    fn truncate(&self, width: usize, delim: &str) -> String {
132        if width < Cell::width(self) {
133            let d = Cell::width(delim);
134            if width < d {
135                // If we can't even fit the delimiter, just return an empty string.
136                return String::new();
137            }
138            // Find the unicode byte boundary where the display width is the largest,
139            // while being smaller than the given max width.
140            let mut cols = 0; // Number of visual columns we need.
141            let mut boundary = 0; // Boundary in bytes.
142            for g in self.graphemes(true) {
143                let c = Cell::width(g);
144                if cols + c + d > width {
145                    break;
146                }
147                boundary += g.len();
148                cols += c;
149            }
150            // Don't add the delimiter if we just trimmed whitespace.
151            if self[boundary..].trim().is_empty() {
152                self[..boundary + 1].to_owned()
153            } else {
154                format!("{}{delim}", &self[..boundary])
155            }
156        } else {
157            self.to_owned()
158        }
159    }
160
161    fn pad(&self, max: usize) -> String {
162        let width = Cell::width(self);
163
164        if width < max {
165            format!("{self}{}", " ".repeat(max - width))
166        } else {
167            self.to_owned()
168        }
169    }
170}
171
172impl<T: Cell + ?Sized> Cell for &T {
173    type Truncated = T::Truncated;
174    type Padded = T::Padded;
175
176    fn width(&self) -> usize {
177        T::width(self)
178    }
179
180    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
181        T::truncate(self, width, delim)
182    }
183
184    fn pad(&self, width: usize) -> Self::Padded {
185        T::pad(self, width)
186    }
187}
188
189impl<T: Cell + fmt::Display> Cell for Filled<T> {
190    type Truncated = T::Truncated;
191    type Padded = T::Padded;
192
193    fn width(&self) -> usize {
194        T::width(&self.item)
195    }
196
197    fn background(&self) -> Color {
198        self.color
199    }
200
201    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
202        T::truncate(&self.item, width, delim)
203    }
204
205    fn pad(&self, width: usize) -> Self::Padded {
206        T::pad(&self.item, width)
207    }
208}
209
210#[cfg(test)]
211mod test {
212    #[test]
213    fn test_width() {
214        assert_eq!(unicode_display_width::width("❤️"), 2);
215        assert_eq!(unicode_display_width::width("🪵"), 2);
216    }
217}