Skip to main content

unicode_box_drawing/
lib.rs

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		// .chars() iterator is not const, and for some reason there is no longer char::decode_utf8
278		// function in stdlib, thus here I inlined utf-8 decoding of 3 bytes...
279		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}