1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct BorderSet {
6 pub vertical: char,
8 pub horizontal: char,
10 pub top_left: char,
12 pub top_right: char,
14 pub bottom_left: char,
16 pub bottom_right: char,
18 pub tee_up: char,
20 pub tee_down: char,
22 pub tee_left: char,
24 pub tee_right: char,
26 pub cross: char,
28}
29
30impl BorderSet {
31 pub const ASCII: Self = Self {
33 vertical: '|',
34 horizontal: '-',
35 top_left: '+',
36 top_right: '+',
37 bottom_left: '+',
38 bottom_right: '+',
39 tee_up: '+',
40 tee_down: '+',
41 tee_left: '+',
42 tee_right: '+',
43 cross: '+',
44 };
45
46 pub const ROUNDED: Self = Self {
48 vertical: '│',
49 horizontal: '─',
50 top_left: '╭',
51 top_right: '╮',
52 bottom_left: '╰',
53 bottom_right: '╯',
54 tee_up: '┴',
55 tee_down: '┬',
56 tee_left: '┤',
57 tee_right: '├',
58 cross: '┼',
59 };
60
61 pub const SQUARE: Self = Self {
63 vertical: '│',
64 horizontal: '─',
65 top_left: '┌',
66 top_right: '┐',
67 bottom_left: '└',
68 bottom_right: '┘',
69 tee_up: '┴',
70 tee_down: '┬',
71 tee_left: '┤',
72 tee_right: '├',
73 cross: '┼',
74 };
75
76 pub const DOUBLE: Self = Self {
78 vertical: '║',
79 horizontal: '═',
80 top_left: '╔',
81 top_right: '╗',
82 bottom_left: '╚',
83 bottom_right: '╝',
84 tee_up: '╩',
85 tee_down: '╦',
86 tee_left: '╣',
87 tee_right: '╠',
88 cross: '╬',
89 };
90
91 pub const HEAVY: Self = Self {
93 vertical: '┃',
94 horizontal: '━',
95 top_left: '┏',
96 top_right: '┓',
97 bottom_left: '┗',
98 bottom_right: '┛',
99 tee_up: '┻',
100 tee_down: '┳',
101 tee_left: '┫',
102 tee_right: '┣',
103 cross: '╋',
104 };
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109pub enum BorderType {
110 #[default]
112 Square,
113 Ascii,
115 Rounded,
117 Double,
119 Heavy,
121 Custom(BorderSet),
123}
124
125impl BorderType {
126 pub fn to_border_set(&self) -> BorderSet {
128 match self {
129 BorderType::Square => BorderSet::SQUARE,
130 BorderType::Ascii => BorderSet::ASCII,
131 BorderType::Rounded => BorderSet::ROUNDED,
132 BorderType::Double => BorderSet::DOUBLE,
133 BorderType::Heavy => BorderSet::HEAVY,
134 BorderType::Custom(set) => *set,
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn ascii_is_ascii_only() {
145 let set = BorderSet::ASCII;
146 let chars = [
147 set.vertical,
148 set.horizontal,
149 set.top_left,
150 set.top_right,
151 set.bottom_left,
152 set.bottom_right,
153 set.tee_up,
154 set.tee_down,
155 set.tee_left,
156 set.tee_right,
157 set.cross,
158 ];
159 assert!(chars.iter().all(|c| c.is_ascii()));
160 }
161
162 #[test]
163 fn square_has_box_drawing() {
164 let set = BorderSet::SQUARE;
165 assert_eq!(set.horizontal, '─');
166 assert_eq!(set.vertical, '│');
167 assert_eq!(set.cross, '┼');
168 }
169
170 #[test]
171 fn rounded_has_round_corners() {
172 let set = BorderSet::ROUNDED;
173 assert_eq!(set.top_left, '╭');
174 assert_eq!(set.top_right, '╮');
175 assert_eq!(set.bottom_left, '╰');
176 assert_eq!(set.bottom_right, '╯');
177 assert_eq!(set.horizontal, '─');
178 assert_eq!(set.vertical, '│');
179 }
180
181 #[test]
182 fn double_has_double_lines() {
183 let set = BorderSet::DOUBLE;
184 assert_eq!(set.horizontal, '═');
185 assert_eq!(set.vertical, '║');
186 assert_eq!(set.top_left, '╔');
187 assert_eq!(set.top_right, '╗');
188 assert_eq!(set.bottom_left, '╚');
189 assert_eq!(set.bottom_right, '╝');
190 assert_eq!(set.cross, '╬');
191 }
192
193 #[test]
194 fn heavy_has_heavy_lines() {
195 let set = BorderSet::HEAVY;
196 assert_eq!(set.horizontal, '━');
197 assert_eq!(set.vertical, '┃');
198 assert_eq!(set.top_left, '┏');
199 assert_eq!(set.top_right, '┓');
200 assert_eq!(set.bottom_left, '┗');
201 assert_eq!(set.bottom_right, '┛');
202 assert_eq!(set.cross, '╋');
203 }
204
205 #[test]
206 fn all_border_sets_have_11_fields() {
207 for set in [
208 BorderSet::ASCII,
209 BorderSet::ROUNDED,
210 BorderSet::SQUARE,
211 BorderSet::DOUBLE,
212 BorderSet::HEAVY,
213 ] {
214 let chars = [
215 set.vertical,
216 set.horizontal,
217 set.top_left,
218 set.top_right,
219 set.bottom_left,
220 set.bottom_right,
221 set.tee_up,
222 set.tee_down,
223 set.tee_left,
224 set.tee_right,
225 set.cross,
226 ];
227 assert_eq!(chars.len(), 11);
228 assert_ne!(set.horizontal, set.vertical);
230 }
231 }
232
233 #[test]
234 fn box_drawing_sets_have_distinct_corners() {
235 for set in [
237 BorderSet::ROUNDED,
238 BorderSet::SQUARE,
239 BorderSet::DOUBLE,
240 BorderSet::HEAVY,
241 ] {
242 let corners = [
243 set.top_left,
244 set.top_right,
245 set.bottom_left,
246 set.bottom_right,
247 ];
248 for (i, a) in corners.iter().enumerate() {
249 for (j, b) in corners.iter().enumerate() {
250 if i != j {
251 assert_ne!(a, b, "corners {i} and {j} should differ");
252 }
253 }
254 }
255 }
256 }
257
258 #[test]
259 fn ascii_set_reuses_plus_for_junctions() {
260 let set = BorderSet::ASCII;
261 assert_eq!(set.top_left, '+');
263 assert_eq!(set.top_right, '+');
264 assert_eq!(set.bottom_left, '+');
265 assert_eq!(set.bottom_right, '+');
266 assert_eq!(set.tee_up, '+');
267 assert_eq!(set.tee_down, '+');
268 assert_eq!(set.tee_left, '+');
269 assert_eq!(set.tee_right, '+');
270 assert_eq!(set.cross, '+');
271 }
272
273 #[test]
274 fn border_type_to_border_set_roundtrip() {
275 assert_eq!(BorderType::Square.to_border_set(), BorderSet::SQUARE);
276 assert_eq!(BorderType::Ascii.to_border_set(), BorderSet::ASCII);
277 assert_eq!(BorderType::Rounded.to_border_set(), BorderSet::ROUNDED);
278 assert_eq!(BorderType::Double.to_border_set(), BorderSet::DOUBLE);
279 assert_eq!(BorderType::Heavy.to_border_set(), BorderSet::HEAVY);
280 }
281
282 #[test]
283 fn border_type_default_is_square() {
284 assert_eq!(BorderType::default(), BorderType::Square);
285 }
286
287 #[test]
288 fn border_type_custom_uses_provided_set() {
289 let custom = BorderSet {
290 vertical: '!',
291 horizontal: '-',
292 top_left: '/',
293 top_right: '\\',
294 bottom_left: '\\',
295 bottom_right: '/',
296 tee_up: '+',
297 tee_down: '+',
298 tee_left: '+',
299 tee_right: '+',
300 cross: '*',
301 };
302
303 assert_eq!(BorderType::Custom(custom).to_border_set(), custom);
304 }
305
306 #[test]
307 fn borders_none_is_zero() {
308 assert!(Borders::NONE.is_empty());
309 assert_eq!(Borders::NONE.bits(), 0);
310 }
311
312 #[test]
313 fn borders_all_contains_all_sides() {
314 assert!(Borders::ALL.contains(Borders::TOP));
315 assert!(Borders::ALL.contains(Borders::RIGHT));
316 assert!(Borders::ALL.contains(Borders::BOTTOM));
317 assert!(Borders::ALL.contains(Borders::LEFT));
318 }
319
320 #[test]
321 fn borders_individual_bits_are_distinct() {
322 let sides = [Borders::TOP, Borders::RIGHT, Borders::BOTTOM, Borders::LEFT];
323 for (i, a) in sides.iter().enumerate() {
324 for (j, b) in sides.iter().enumerate() {
325 if i != j {
326 assert!(!a.contains(*b), "side {i} should not contain side {j}");
327 }
328 }
329 }
330 }
331
332 #[test]
333 fn borders_union_and_intersection() {
334 let top_left = Borders::TOP | Borders::LEFT;
335 assert!(top_left.contains(Borders::TOP));
336 assert!(top_left.contains(Borders::LEFT));
337 assert!(!top_left.contains(Borders::RIGHT));
338
339 let top_right = Borders::TOP | Borders::RIGHT;
340 let intersection = top_left & top_right;
341 assert!(intersection.contains(Borders::TOP));
342 assert!(!intersection.contains(Borders::LEFT));
343 assert!(!intersection.contains(Borders::RIGHT));
344 }
345
346 #[test]
347 fn borders_default_is_none() {
348 assert_eq!(Borders::default(), Borders::NONE);
349 }
350
351 #[test]
352 fn non_ascii_sets_have_no_ascii_chars() {
353 for set in [
354 BorderSet::ROUNDED,
355 BorderSet::SQUARE,
356 BorderSet::DOUBLE,
357 BorderSet::HEAVY,
358 ] {
359 let chars = [
360 set.vertical,
361 set.horizontal,
362 set.top_left,
363 set.top_right,
364 set.bottom_left,
365 set.bottom_right,
366 set.tee_up,
367 set.tee_down,
368 set.tee_left,
369 set.tee_right,
370 set.cross,
371 ];
372 assert!(
373 chars.iter().all(|c| !c.is_ascii()),
374 "non-ASCII border set should have no ASCII chars"
375 );
376 }
377 }
378
379 #[test]
380 fn border_set_tees_are_consistent() {
381 let set = BorderSet::SQUARE;
383 let tees = [set.tee_up, set.tee_down, set.tee_left, set.tee_right];
387 for (i, a) in tees.iter().enumerate() {
388 for (j, b) in tees.iter().enumerate() {
389 if i != j {
390 assert_ne!(a, b, "tees {i} and {j} should differ");
391 }
392 }
393 }
394 }
395}
396
397bitflags::bitflags! {
398 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
400 pub struct Borders: u8 {
401 const NONE = 0b0000;
403 const TOP = 0b0001;
405 const RIGHT = 0b0010;
407 const BOTTOM = 0b0100;
409 const LEFT = 0b1000;
411 const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
413 }
414}