1#![no_std]
2
3use core::fmt;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum Width {
7 None,
8 Single,
9 Double,
10}
11impl Width {
12 fn inc(self) -> Self {
13 match self {
14 Width::None => Self::Single,
15 Width::Single => Self::Double,
16 Width::Double => Self::Double,
17 }
18 }
19 pub fn is_none(&self) -> bool {
20 matches!(self, Self::None)
21 }
22 const fn bits(&self) -> u8 {
23 match self {
24 Width::None => 0,
25 Width::Single => 1,
26 Width::Double => 2,
27 }
28 }
29 const fn from_bits(v: u8) -> Option<Self> {
30 Some(match v {
31 0 => Self::None,
32 1 => Self::Single,
33 2 => Self::Double,
34 _ => return None,
35 })
36 }
37}
38
39const BOX_CHAR_BYTES: usize = 3;
40const BOX_CHARS_STR: &str = {
41 let c = "╴╸╷┐┑╻┒┓╶─╾┌┬┭┎┰┱╺╼━┍┮┯┏┲┳╵┘┙│┤┥╽┧┪└┴┵├┼┽┟╁╅┕┶┷┝┾┿┢╆╈╹┚┛╿┦┩┃┨┫┖┸┹┞╀╃┠╂╉┗┺┻┡╄╇┣╊╋";
42 assert!(c.len() == 80 * BOX_CHAR_BYTES);
43 c
44};
45const BOX_CHARS: &[u8] = BOX_CHARS_STR.as_bytes();
46const fn corner_round(c: char) -> char {
47 match c {
48 '┌' => '╭',
49 '└' => '╰',
50 '┐' => '╮',
51 '┘' => '╯',
52 _ => c,
53 }
54}
55fn unround_corner(c: char) -> char {
56 match c {
57 '╭' => '┌',
58 '╰' => '└',
59 '╮' => '┐',
60 '╯' => '┘',
61 _ => c,
62 }
63}
64
65const LINES_NORMAL: [char; 4] = ['│', '┃', '─', '━'];
66const LINES_DOT_W2: [char; 4] = ['╎', '╏', '╌', '╍'];
67const LINES_DOT_W3: [char; 4] = ['┆', '┇', '┄', '┅'];
68const LINES_DOT_W4: [char; 4] = ['┊', '┋', '┈', '┉'];
69const fn index_of_4(v: &[char; 4], c: char) -> Option<usize> {
70 let mut i = 0;
71 while i < 4 {
72 if v[i] == c {
73 return Some(i);
74 }
75 i += 1;
76 }
77 None
78}
79const fn line_dotted_w2(c: char) -> char {
80 if let Some(v) = index_of_4(&LINES_NORMAL, c) {
81 LINES_DOT_W2[v]
82 } else {
83 c
84 }
85}
86const fn line_dotted_w3(c: char) -> char {
87 if let Some(v) = index_of_4(&LINES_NORMAL, c) {
88 LINES_DOT_W3[v]
89 } else {
90 c
91 }
92}
93const fn line_dotted_w4(c: char) -> char {
94 if let Some(v) = index_of_4(&LINES_NORMAL, c) {
95 LINES_DOT_W4[v]
96 } else {
97 c
98 }
99}
100const fn line_undotted(c: char) -> char {
101 if let Some(v) = index_of_4(&LINES_DOT_W2, c) {
102 LINES_NORMAL[v]
103 } else if let Some(v) = index_of_4(&LINES_DOT_W3, c) {
104 LINES_NORMAL[v]
105 } else if let Some(v) = index_of_4(&LINES_DOT_W4, c) {
106 LINES_NORMAL[v]
107 } else {
108 c
109 }
110}
111
112const fn div_rem(a: u8, b: u8) -> (u8, u8) {
113 (a / b, a % b)
114}
115
116#[derive(Clone, Copy, PartialEq, Eq, Debug)]
117struct Raw {
118 top: Width,
119 right: Width,
120 bottom: Width,
121 left: Width,
122}
123impl Raw {
124 const fn encode(&self) -> u8 {
125 self.top.bits() * 27 + self.right.bits() * 9 + self.bottom.bits() * 3 + self.left.bits()
126 }
127 const fn decode(v: u8) -> Option<Self> {
128 let (v, left) = div_rem(v, 3);
129 let (v, bottom) = div_rem(v, 3);
130 let (v, right) = div_rem(v, 3);
131 let (v, top) = div_rem(v, 3);
132 if v != 0 {
133 return None;
134 }
135 Some(Self {
136 top: Width::from_bits(top).expect("valid"),
137 right: Width::from_bits(right).expect("valid"),
138 bottom: Width::from_bits(bottom).expect("valid"),
139 left: Width::from_bits(left).expect("valid"),
140 })
141 }
142}
143
144#[derive(Clone, Copy, PartialEq, Eq)]
145pub struct BoxCharacter(u8);
146impl fmt::Debug for BoxCharacter {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 struct Inner(Raw);
149 impl fmt::Debug for Inner {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 fn fmt(f: &mut fmt::Formatter<'_>, width: Width, c: char) -> fmt::Result {
152 match width {
153 Width::None => Ok(()),
154 Width::Single => write!(f, "{c}"),
155 Width::Double => write!(f, "{c}{c}"),
156 }
157 }
158 fmt(f, self.0.top, 't')?;
159 fmt(f, self.0.right, 'r')?;
160 fmt(f, self.0.bottom, 'b')?;
161 fmt(f, self.0.left, 'l')
162 }
163 }
164 let mut d = f.debug_tuple("BoxCharacter");
165 d.field(&Inner(self.raw()));
166 d.finish()
167 }
168}
169impl fmt::Display for BoxCharacter {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 write!(f, "{}", self.char())
172 }
173}
174
175impl BoxCharacter {
176 pub fn is_vertical_bar(self) -> bool {
177 !self.top().is_none()
178 && !self.bottom().is_none()
179 && self.left().is_none()
180 && self.right().is_none()
181 }
182 pub fn is_horizontal_bar(self) -> bool {
183 self.rotate_clockwise().is_vertical_bar()
184 }
185 pub fn is_bar(self) -> bool {
186 self.is_vertical_bar() || self.is_horizontal_bar()
187 }
188 pub fn rotate_clockwise(self) -> Self {
189 let r = self.raw();
190 Self::from_raw(Raw {
191 right: r.top,
192 bottom: r.right,
193 left: r.bottom,
194 top: r.left,
195 })
196 }
197 pub fn top(self) -> Width {
198 self.raw().top
199 }
200 pub fn right(self) -> Width {
201 self.raw().right
202 }
203 pub fn bottom(self) -> Width {
204 self.raw().bottom
205 }
206 pub fn left(self) -> Width {
207 self.raw().left
208 }
209 pub fn new(top: Width, right: Width, bottom: Width, left: Width) -> Self {
210 Self::from_raw(Raw {
211 top,
212 right,
213 bottom,
214 left,
215 })
216 }
217 const fn raw(self) -> Raw {
218 Raw::decode(self.0).expect("BoxData items are properly encoded")
219 }
220 const fn from_raw(r: Raw) -> Self {
221 Self(r.encode())
222 }
223 pub fn with_top(self) -> Self {
224 let r = self.raw();
225 Self::from_raw(Raw {
226 top: r.top.inc(),
227 ..r
228 })
229 }
230 pub fn with_right(self) -> Self {
231 let r = self.raw();
232 Self::from_raw(Raw {
233 right: r.right.inc(),
234 ..r
235 })
236 }
237 pub fn with_bottom(self) -> Self {
238 let r = self.raw();
239 Self::from_raw(Raw {
240 bottom: r.bottom.inc(),
241 ..r
242 })
243 }
244 pub fn with_left(self) -> Self {
245 let r = self.raw();
246 Self::from_raw(Raw {
247 left: r.left.inc(),
248 ..r
249 })
250 }
251 pub const fn mirror_vertical(self) -> Self {
252 let r = self.raw();
253 Self::from_raw(Raw {
254 top: r.bottom,
255 bottom: r.top,
256 ..r
257 })
258 }
259 pub const fn mirror_horizontal(self) -> Self {
260 let r = self.raw();
261 Self::from_raw(Raw {
262 left: r.right,
263 right: r.left,
264 ..r
265 })
266 }
267 pub const fn char(&self) -> char {
268 let Some(i) = self.0.checked_sub(1) else {
269 return ' ';
270 };
271 let i = i as usize;
272
273 let a = BOX_CHARS[i * BOX_CHAR_BYTES] as u32;
274 let b = BOX_CHARS[i * BOX_CHAR_BYTES + 1] as u32;
275 let c = BOX_CHARS[i * BOX_CHAR_BYTES + 2] as u32;
276
277 let c = ((a & 0x0f) << 12) | ((b & 0x3f) << 6) | (c & 0x3f);
280 char::from_u32(c).expect("char")
281 }
282 pub const fn char_round(&self) -> char {
283 corner_round(self.char())
284 }
285 pub const fn char_dotted_w2(&self) -> char {
286 line_dotted_w2(self.char())
287 }
288 pub const fn char_dotted_w3(&self) -> char {
289 line_dotted_w3(self.char())
290 }
291 pub const fn char_dotted_w4(&self) -> char {
292 line_dotted_w4(self.char())
293 }
294 pub fn decode_char(v: char) -> Option<Self> {
295 if v == ' ' {
296 return Some(Self(0));
297 };
298 let v = line_undotted(unround_corner(v));
299 let id = BOX_CHARS_STR.find(v)? / 3;
300 Some(Self::from_raw(
301 Raw::decode(id as u8 + 1).expect("valid idx"),
302 ))
303 }
304 pub const fn from_str(v: &[u8]) -> Self {
305 const fn c(v: &[u8], f: usize, b: u8) -> (u8, usize) {
306 let mut o = 0;
307 if v.len() - f >= 2 {
308 if v[f] == b {
309 o += 1;
310 }
311 if v[f + 1] == b {
312 o += 1;
313 }
314 } else if v.len() - f >= 1 && v[f] == b {
315 o += 1;
316 }
317 (o, f + o as usize)
318 }
319 let (top, f) = c(v, 0, b't');
320 let (right, f) = c(v, f, b'r');
321 let (bottom, f) = c(v, f, b'b');
322 let (left, f) = c(v, f, b'l');
323 assert!(f == v.len(), "invalid box def");
324 Self::from_raw(Raw {
325 top: Width::from_bits(top).expect("v"),
326 right: Width::from_bits(right).expect("v"),
327 bottom: Width::from_bits(bottom).expect("v"),
328 left: Width::from_bits(left).expect("v"),
329 })
330 }
331}
332
333#[macro_export]
334macro_rules! bc {
335 ($i:ident) => {
336 const { $crate::BoxCharacter::from_str(stringify!($i).as_bytes()) }
337 };
338}
339
340#[cfg(test)]
341mod tests {
342 use crate::{BoxCharacter, Raw, Width};
343
344 #[test]
345 fn box_encoding() {
346 let w = [Width::None, Width::Single, Width::Double];
347 for top in w {
348 for right in w {
349 for bottom in w {
350 for left in w {
351 let e = BoxCharacter::from_raw(Raw {
352 top,
353 right,
354 bottom,
355 left,
356 });
357 let c = e.char();
358 let c = BoxCharacter::decode_char(c).expect("from encoded");
359 assert_eq!(e, c);
360 }
361 }
362 }
363 }
364 }
365
366 #[test]
367 fn smoke() {
368 let c = bc!(ttrb);
369 assert_eq!(c.char(), '┞')
370 }
371
372 #[test]
373 fn round_corners() {
374 let c = bc!(tr);
375 assert_eq!(c.char_round(), '╰')
376 }
377
378 #[test]
379 fn dotted() {
380 let c = bc!(tb);
381 assert_eq!(c.char_dotted_w3(), '┆')
382 }
383
384 #[test]
385 fn is_bar() {
386 assert!(bc!(tb).is_vertical_bar());
387 assert!(!bc!(tb).is_horizontal_bar());
388 assert!(bc!(rl).is_horizontal_bar());
389 assert!(!bc!(rl).is_vertical_bar());
390 assert!(!bc!(tr).is_bar())
391 }
392}