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 const fn char_ascii(&self) -> char {
295 let r = self.raw();
296 let vert = r.top.bits() != 0 || r.bottom.bits() != 0;
297 let horiz = r.left.bits() != 0 || r.right.bits() != 0;
298 match (vert, horiz) {
299 (false, false) => ' ',
300 (true, false) => '|',
301 (false, true) => '-',
302 (true, true) => '+',
303 }
304 }
305 pub fn decode_char(v: char) -> Option<Self> {
306 if v == ' ' {
307 return Some(Self(0));
308 };
309 let v = line_undotted(unround_corner(v));
310 let id = BOX_CHARS_STR.find(v)? / 3;
311 Some(Self::from_raw(
312 Raw::decode(id as u8 + 1).expect("valid idx"),
313 ))
314 }
315 pub const fn from_str(v: &[u8]) -> Self {
316 const fn c(v: &[u8], f: usize, b: u8) -> (u8, usize) {
317 let mut o = 0;
318 if v.len() - f >= 2 {
319 if v[f] == b {
320 o += 1;
321 }
322 if v[f + 1] == b {
323 o += 1;
324 }
325 } else if v.len() - f >= 1 && v[f] == b {
326 o += 1;
327 }
328 (o, f + o as usize)
329 }
330 let (top, f) = c(v, 0, b't');
331 let (right, f) = c(v, f, b'r');
332 let (bottom, f) = c(v, f, b'b');
333 let (left, f) = c(v, f, b'l');
334 assert!(f == v.len(), "invalid box def");
335 Self::from_raw(Raw {
336 top: Width::from_bits(top).expect("v"),
337 right: Width::from_bits(right).expect("v"),
338 bottom: Width::from_bits(bottom).expect("v"),
339 left: Width::from_bits(left).expect("v"),
340 })
341 }
342}
343
344#[macro_export]
345macro_rules! bc {
346 ($i:ident) => {
347 const { $crate::BoxCharacter::from_str(stringify!($i).as_bytes()) }
348 };
349}
350
351#[cfg(test)]
352mod tests {
353 use crate::{BoxCharacter, Raw, Width};
354
355 #[test]
356 fn box_encoding() {
357 let w = [Width::None, Width::Single, Width::Double];
358 for top in w {
359 for right in w {
360 for bottom in w {
361 for left in w {
362 let e = BoxCharacter::from_raw(Raw {
363 top,
364 right,
365 bottom,
366 left,
367 });
368 let c = e.char();
369 let c = BoxCharacter::decode_char(c).expect("from encoded");
370 assert_eq!(e, c);
371 }
372 }
373 }
374 }
375 }
376
377 #[test]
378 fn smoke() {
379 let c = bc!(ttrb);
380 assert_eq!(c.char(), '┞')
381 }
382
383 #[test]
384 fn round_corners() {
385 let c = bc!(tr);
386 assert_eq!(c.char_round(), '╰')
387 }
388
389 #[test]
390 fn dotted() {
391 let c = bc!(tb);
392 assert_eq!(c.char_dotted_w3(), '┆')
393 }
394
395 #[test]
396 fn is_bar() {
397 assert!(bc!(tb).is_vertical_bar());
398 assert!(!bc!(tb).is_horizontal_bar());
399 assert!(bc!(rl).is_horizontal_bar());
400 assert!(!bc!(rl).is_vertical_bar());
401 assert!(!bc!(tr).is_bar())
402 }
403}