Skip to main content

oxstack_gfx/
text.rs

1use core::fmt;
2use core::ops::Deref;
3
4/// A fixed-capacity, stack-allocated string for formatting text to draw.
5///
6/// `write!` into it never fails: text that does not fit is dropped at a
7/// character boundary and [`truncated`](Self::truncated) is set. That makes
8/// it safe for status lines, where a long SSID should not take the whole
9/// screen down with it.
10///
11/// ```
12/// use core::fmt::Write as _;
13/// use oxstack_gfx::TextBuf;
14///
15/// let mut line = TextBuf::<16>::new();
16/// write!(line, "{:>4} dBm ch {}", -61, 6).unwrap();
17/// assert_eq!(line.as_str(), " -61 dBm ch 6");
18///
19/// let long = oxstack_gfx::text!(8, "{}", "0123456789");
20/// assert_eq!(long.as_str(), "01234567");
21/// assert!(long.truncated());
22/// ```
23#[derive(Clone, Copy)]
24pub struct TextBuf<const N: usize> {
25    buf: [u8; N],
26    len: usize,
27    truncated: bool,
28}
29
30impl<const N: usize> TextBuf<N> {
31    /// An empty buffer.
32    pub const fn new() -> Self {
33        Self {
34            buf: [0; N],
35            len: 0,
36            truncated: false,
37        }
38    }
39
40    /// Formats `args` into a new buffer: `TextBuf::<40>::format(format_args!(…))`,
41    /// or the [`text!`](crate::text) macro.
42    pub fn format(args: fmt::Arguments<'_>) -> Self {
43        let mut s = Self::new();
44        let _ = fmt::Write::write_fmt(&mut s, args);
45        s
46    }
47
48    /// The text.
49    pub fn as_str(&self) -> &str {
50        // Only whole UTF-8 characters are ever copied in.
51        core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
52    }
53
54    /// Empties the buffer and clears the truncation flag.
55    pub fn clear(&mut self) {
56        self.len = 0;
57        self.truncated = false;
58    }
59
60    /// Length in bytes.
61    pub fn len(&self) -> usize {
62        self.len
63    }
64
65    /// Whether nothing has been written.
66    pub fn is_empty(&self) -> bool {
67        self.len == 0
68    }
69
70    /// Capacity in bytes.
71    pub const fn capacity(&self) -> usize {
72        N
73    }
74
75    /// Whether anything written since the last [`clear`](Self::clear) did
76    /// not fit.
77    pub fn truncated(&self) -> bool {
78        self.truncated
79    }
80
81    /// Appends spaces until the text is at least `width` characters long,
82    /// so that drawing it with a background colour overwrites whatever
83    /// shorter text was there before.
84    pub fn pad(&mut self, width: usize) -> &mut Self {
85        let chars = self.as_str().chars().count();
86        for _ in chars..width {
87            if self.len == N {
88                break;
89            }
90            self.buf[self.len] = b' ';
91            self.len += 1;
92        }
93        self
94    }
95}
96
97impl<const N: usize> Default for TextBuf<N> {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl<const N: usize> fmt::Write for TextBuf<N> {
104    fn write_str(&mut self, s: &str) -> fmt::Result {
105        let room = N - self.len;
106        let take = if s.len() <= room {
107            s.len()
108        } else {
109            self.truncated = true;
110            // Back off to a character boundary.
111            let mut n = room;
112            while !s.is_char_boundary(n) {
113                n -= 1;
114            }
115            n
116        };
117        self.buf[self.len..self.len + take].copy_from_slice(&s.as_bytes()[..take]);
118        self.len += take;
119        Ok(())
120    }
121}
122
123impl<const N: usize> Deref for TextBuf<N> {
124    type Target = str;
125    fn deref(&self) -> &str {
126        self.as_str()
127    }
128}
129
130impl<const N: usize> AsRef<str> for TextBuf<N> {
131    fn as_ref(&self) -> &str {
132        self.as_str()
133    }
134}
135
136impl<const N: usize> fmt::Display for TextBuf<N> {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.write_str(self.as_str())
139    }
140}
141
142impl<const N: usize> fmt::Debug for TextBuf<N> {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        fmt::Debug::fmt(self.as_str(), f)
145    }
146}
147
148impl<const N: usize> PartialEq<str> for TextBuf<N> {
149    fn eq(&self, other: &str) -> bool {
150        self.as_str() == other
151    }
152}
153
154impl<const N: usize> PartialEq<&str> for TextBuf<N> {
155    fn eq(&self, other: &&str) -> bool {
156        self.as_str() == *other
157    }
158}
159
160#[cfg(feature = "defmt")]
161impl<const N: usize> defmt::Format for TextBuf<N> {
162    fn format(&self, f: defmt::Formatter<'_>) {
163        defmt::write!(f, "{}", self.as_str())
164    }
165}
166
167/// Formats into a [`TextBuf`] of the given capacity:
168/// `text!(40, "{} dBm", rssi)`.
169#[macro_export]
170macro_rules! text {
171    ($n:literal, $($arg:tt)*) => {
172        $crate::TextBuf::<$n>::format(::core::format_args!($($arg)*))
173    };
174}
175
176#[cfg(test)]
177mod tests {
178    use core::fmt::Write as _;
179
180    use super::*;
181
182    #[test]
183    fn truncates_at_char_boundary() {
184        let mut t = TextBuf::<5>::new();
185        write!(t, "ab€cd").unwrap(); // € is 3 bytes: "ab€" is 5 bytes
186        assert_eq!(t, "ab€");
187        assert!(t.truncated());
188        let mut t = TextBuf::<4>::new();
189        write!(t, "ab€cd").unwrap(); // only 4 bytes: € doesn't fit
190        assert_eq!(t, "ab");
191        assert!(t.truncated());
192    }
193
194    #[test]
195    fn pad_counts_characters() {
196        let mut t = text!(8, "é");
197        t.pad(3);
198        assert_eq!(t, "é  ");
199        assert_eq!(t.len(), 4);
200        t.pad(100);
201        assert_eq!(t.len(), 8);
202    }
203
204    #[test]
205    fn clear_resets_everything() {
206        let mut t = text!(2, "abc");
207        assert!(t.truncated());
208        t.clear();
209        assert!(t.is_empty() && !t.truncated());
210        write!(t, "x").unwrap();
211        assert_eq!(&*t, "x");
212    }
213}