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 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}