Skip to main content

rich_rs/
box.rs

1//! Box drawing characters for borders, panels, and tables.
2//!
3//! This module provides a comprehensive set of box-drawing character definitions
4//! matching Python Rich's `box.py`. Each `Box` defines 32 characters arranged in
5//! 8 rows that control how borders and dividers are rendered:
6//!
7//! ```text
8//! ┌─┬┐ top
9//! │ ││ head
10//! ├─┼┤ head_row
11//! │ ││ mid
12//! ├─┼┤ row
13//! ├─┼┤ foot_row
14//! │ ││ foot
15//! └─┴┘ bottom
16//! ```
17
18/// Level of a row separator in a table.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RowLevel {
21    /// Header row separator (between header and body).
22    Head,
23    /// Regular row separator.
24    Row,
25    /// Footer row separator (between body and footer).
26    Foot,
27    /// Mid-section (vertical lines only, used for content rows).
28    Mid,
29}
30
31/// A complete set of box-drawing characters for creating borders and tables.
32///
33/// The structure contains 32 characters arranged in 8 logical rows:
34/// - `top`: Top border (corners and dividers)
35/// - `head`: Header content row (vertical lines)
36/// - `head_row`: Header separator row
37/// - `mid`: Mid content row (vertical lines)
38/// - `row`: Body row separator
39/// - `foot_row`: Footer separator row
40/// - `foot`: Footer content row (vertical lines)
41/// - `bottom`: Bottom border (corners and dividers)
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct Box {
44    // Top row: ┌─┬┐
45    /// Top-left corner character.
46    pub top_left: char,
47    /// Top horizontal line character.
48    pub top: char,
49    /// Top divider (T-junction pointing down).
50    pub top_divider: char,
51    /// Top-right corner character.
52    pub top_right: char,
53
54    // Head row: │ ││
55    /// Left edge of header row.
56    pub head_left: char,
57    /// Vertical divider in header.
58    pub head_vertical: char,
59    /// Right edge of header row.
60    pub head_right: char,
61
62    // Head separator row: ├─┼┤
63    /// Left edge of head row separator.
64    pub head_row_left: char,
65    /// Horizontal line in head row separator.
66    pub head_row_horizontal: char,
67    /// Cross junction in head row separator.
68    pub head_row_cross: char,
69    /// Right edge of head row separator.
70    pub head_row_right: char,
71
72    // Mid row: │ ││
73    /// Left edge of mid content row.
74    pub mid_left: char,
75    /// Vertical divider in mid section.
76    pub mid_vertical: char,
77    /// Right edge of mid content row.
78    pub mid_right: char,
79
80    // Row separator: ├─┼┤
81    /// Left edge of row separator.
82    pub row_left: char,
83    /// Horizontal line in row separator.
84    pub row_horizontal: char,
85    /// Cross junction in row separator.
86    pub row_cross: char,
87    /// Right edge of row separator.
88    pub row_right: char,
89
90    // Foot separator row: ├─┼┤
91    /// Left edge of foot row separator.
92    pub foot_row_left: char,
93    /// Horizontal line in foot row separator.
94    pub foot_row_horizontal: char,
95    /// Cross junction in foot row separator.
96    pub foot_row_cross: char,
97    /// Right edge of foot row separator.
98    pub foot_row_right: char,
99
100    // Foot row: │ ││
101    /// Left edge of footer row.
102    pub foot_left: char,
103    /// Vertical divider in footer.
104    pub foot_vertical: char,
105    /// Right edge of footer row.
106    pub foot_right: char,
107
108    // Bottom row: └─┴┘
109    /// Bottom-left corner character.
110    pub bottom_left: char,
111    /// Bottom horizontal line character.
112    pub bottom: char,
113    /// Bottom divider (T-junction pointing up).
114    pub bottom_divider: char,
115    /// Bottom-right corner character.
116    pub bottom_right: char,
117
118    /// Whether this box uses ASCII characters only.
119    pub ascii: bool,
120}
121
122/// ASCII box using + - | characters.
123pub const ASCII: Box = Box {
124    top_left: '+',
125    top: '-',
126    top_divider: '-',
127    top_right: '+',
128    head_left: '|',
129    head_vertical: '|',
130    head_right: '|',
131    head_row_left: '|',
132    head_row_horizontal: '-',
133    head_row_cross: '+',
134    head_row_right: '|',
135    mid_left: '|',
136    mid_vertical: '|',
137    mid_right: '|',
138    row_left: '|',
139    row_horizontal: '-',
140    row_cross: '+',
141    row_right: '|',
142    foot_row_left: '|',
143    foot_row_horizontal: '-',
144    foot_row_cross: '+',
145    foot_row_right: '|',
146    foot_left: '|',
147    foot_vertical: '|',
148    foot_right: '|',
149    bottom_left: '+',
150    bottom: '-',
151    bottom_divider: '-',
152    bottom_right: '+',
153    ascii: true,
154};
155
156/// ASCII box variant 2 with + for all junctions.
157pub const ASCII2: Box = Box {
158    top_left: '+',
159    top: '-',
160    top_divider: '+',
161    top_right: '+',
162    head_left: '|',
163    head_vertical: '|',
164    head_right: '|',
165    head_row_left: '+',
166    head_row_horizontal: '-',
167    head_row_cross: '+',
168    head_row_right: '+',
169    mid_left: '|',
170    mid_vertical: '|',
171    mid_right: '|',
172    row_left: '+',
173    row_horizontal: '-',
174    row_cross: '+',
175    row_right: '+',
176    foot_row_left: '+',
177    foot_row_horizontal: '-',
178    foot_row_cross: '+',
179    foot_row_right: '+',
180    foot_left: '|',
181    foot_vertical: '|',
182    foot_right: '|',
183    bottom_left: '+',
184    bottom: '-',
185    bottom_divider: '+',
186    bottom_right: '+',
187    ascii: true,
188};
189
190/// ASCII box with double-line header separator (= instead of -).
191pub const ASCII_DOUBLE_HEAD: Box = Box {
192    top_left: '+',
193    top: '-',
194    top_divider: '+',
195    top_right: '+',
196    head_left: '|',
197    head_vertical: '|',
198    head_right: '|',
199    head_row_left: '+',
200    head_row_horizontal: '=',
201    head_row_cross: '+',
202    head_row_right: '+',
203    mid_left: '|',
204    mid_vertical: '|',
205    mid_right: '|',
206    row_left: '+',
207    row_horizontal: '-',
208    row_cross: '+',
209    row_right: '+',
210    foot_row_left: '+',
211    foot_row_horizontal: '-',
212    foot_row_cross: '+',
213    foot_row_right: '+',
214    foot_left: '|',
215    foot_vertical: '|',
216    foot_right: '|',
217    bottom_left: '+',
218    bottom: '-',
219    bottom_divider: '+',
220    bottom_right: '+',
221    ascii: true,
222};
223
224/// Square box with thin Unicode lines.
225pub const SQUARE: Box = Box {
226    top_left: '┌',
227    top: '─',
228    top_divider: '┬',
229    top_right: '┐',
230    head_left: '│',
231    head_vertical: '│',
232    head_right: '│',
233    head_row_left: '├',
234    head_row_horizontal: '─',
235    head_row_cross: '┼',
236    head_row_right: '┤',
237    mid_left: '│',
238    mid_vertical: '│',
239    mid_right: '│',
240    row_left: '├',
241    row_horizontal: '─',
242    row_cross: '┼',
243    row_right: '┤',
244    foot_row_left: '├',
245    foot_row_horizontal: '─',
246    foot_row_cross: '┼',
247    foot_row_right: '┤',
248    foot_left: '│',
249    foot_vertical: '│',
250    foot_right: '│',
251    bottom_left: '└',
252    bottom: '─',
253    bottom_divider: '┴',
254    bottom_right: '┘',
255    ascii: false,
256};
257
258/// Square box with double-line header separator.
259pub const SQUARE_DOUBLE_HEAD: Box = Box {
260    top_left: '┌',
261    top: '─',
262    top_divider: '┬',
263    top_right: '┐',
264    head_left: '│',
265    head_vertical: '│',
266    head_right: '│',
267    head_row_left: '╞',
268    head_row_horizontal: '═',
269    head_row_cross: '╪',
270    head_row_right: '╡',
271    mid_left: '│',
272    mid_vertical: '│',
273    mid_right: '│',
274    row_left: '├',
275    row_horizontal: '─',
276    row_cross: '┼',
277    row_right: '┤',
278    foot_row_left: '├',
279    foot_row_horizontal: '─',
280    foot_row_cross: '┼',
281    foot_row_right: '┤',
282    foot_left: '│',
283    foot_vertical: '│',
284    foot_right: '│',
285    bottom_left: '└',
286    bottom: '─',
287    bottom_divider: '┴',
288    bottom_right: '┘',
289    ascii: false,
290};
291
292/// Minimal box with sparse borders.
293pub const MINIMAL: Box = Box {
294    top_left: ' ',
295    top: ' ',
296    top_divider: '╷',
297    top_right: ' ',
298    head_left: ' ',
299    head_vertical: '│',
300    head_right: ' ',
301    head_row_left: '╶',
302    head_row_horizontal: '─',
303    head_row_cross: '┼',
304    head_row_right: '╴',
305    mid_left: ' ',
306    mid_vertical: '│',
307    mid_right: ' ',
308    row_left: '╶',
309    row_horizontal: '─',
310    row_cross: '┼',
311    row_right: '╴',
312    foot_row_left: '╶',
313    foot_row_horizontal: '─',
314    foot_row_cross: '┼',
315    foot_row_right: '╴',
316    foot_left: ' ',
317    foot_vertical: '│',
318    foot_right: ' ',
319    bottom_left: ' ',
320    bottom: ' ',
321    bottom_divider: '╵',
322    bottom_right: ' ',
323    ascii: false,
324};
325
326/// Minimal box with heavy header separator.
327pub const MINIMAL_HEAVY_HEAD: Box = Box {
328    top_left: ' ',
329    top: ' ',
330    top_divider: '╷',
331    top_right: ' ',
332    head_left: ' ',
333    head_vertical: '│',
334    head_right: ' ',
335    head_row_left: '╺',
336    head_row_horizontal: '━',
337    head_row_cross: '┿',
338    head_row_right: '╸',
339    mid_left: ' ',
340    mid_vertical: '│',
341    mid_right: ' ',
342    row_left: '╶',
343    row_horizontal: '─',
344    row_cross: '┼',
345    row_right: '╴',
346    foot_row_left: '╶',
347    foot_row_horizontal: '─',
348    foot_row_cross: '┼',
349    foot_row_right: '╴',
350    foot_left: ' ',
351    foot_vertical: '│',
352    foot_right: ' ',
353    bottom_left: ' ',
354    bottom: ' ',
355    bottom_divider: '╵',
356    bottom_right: ' ',
357    ascii: false,
358};
359
360/// Minimal box with double-line header separator.
361pub const MINIMAL_DOUBLE_HEAD: Box = Box {
362    top_left: ' ',
363    top: ' ',
364    top_divider: '╷',
365    top_right: ' ',
366    head_left: ' ',
367    head_vertical: '│',
368    head_right: ' ',
369    head_row_left: ' ',
370    head_row_horizontal: '═',
371    head_row_cross: '╪',
372    head_row_right: ' ',
373    mid_left: ' ',
374    mid_vertical: '│',
375    mid_right: ' ',
376    row_left: ' ',
377    row_horizontal: '─',
378    row_cross: '┼',
379    row_right: ' ',
380    foot_row_left: ' ',
381    foot_row_horizontal: '─',
382    foot_row_cross: '┼',
383    foot_row_right: ' ',
384    foot_left: ' ',
385    foot_vertical: '│',
386    foot_right: ' ',
387    bottom_left: ' ',
388    bottom: ' ',
389    bottom_divider: '╵',
390    bottom_right: ' ',
391    ascii: false,
392};
393
394/// Simple box with only header and footer separators.
395pub const SIMPLE: Box = Box {
396    top_left: ' ',
397    top: ' ',
398    top_divider: ' ',
399    top_right: ' ',
400    head_left: ' ',
401    head_vertical: ' ',
402    head_right: ' ',
403    head_row_left: ' ',
404    head_row_horizontal: '─',
405    head_row_cross: '─',
406    head_row_right: ' ',
407    mid_left: ' ',
408    mid_vertical: ' ',
409    mid_right: ' ',
410    row_left: ' ',
411    row_horizontal: ' ',
412    row_cross: ' ',
413    row_right: ' ',
414    foot_row_left: ' ',
415    foot_row_horizontal: '─',
416    foot_row_cross: '─',
417    foot_row_right: ' ',
418    foot_left: ' ',
419    foot_vertical: ' ',
420    foot_right: ' ',
421    bottom_left: ' ',
422    bottom: ' ',
423    bottom_divider: ' ',
424    bottom_right: ' ',
425    ascii: false,
426};
427
428/// Simple box with only header separator.
429pub const SIMPLE_HEAD: Box = Box {
430    top_left: ' ',
431    top: ' ',
432    top_divider: ' ',
433    top_right: ' ',
434    head_left: ' ',
435    head_vertical: ' ',
436    head_right: ' ',
437    head_row_left: ' ',
438    head_row_horizontal: '─',
439    head_row_cross: '─',
440    head_row_right: ' ',
441    mid_left: ' ',
442    mid_vertical: ' ',
443    mid_right: ' ',
444    row_left: ' ',
445    row_horizontal: ' ',
446    row_cross: ' ',
447    row_right: ' ',
448    foot_row_left: ' ',
449    foot_row_horizontal: ' ',
450    foot_row_cross: ' ',
451    foot_row_right: ' ',
452    foot_left: ' ',
453    foot_vertical: ' ',
454    foot_right: ' ',
455    bottom_left: ' ',
456    bottom: ' ',
457    bottom_divider: ' ',
458    bottom_right: ' ',
459    ascii: false,
460};
461
462/// Simple box with heavy (thick) separators.
463pub const SIMPLE_HEAVY: Box = Box {
464    top_left: ' ',
465    top: ' ',
466    top_divider: ' ',
467    top_right: ' ',
468    head_left: ' ',
469    head_vertical: ' ',
470    head_right: ' ',
471    head_row_left: ' ',
472    head_row_horizontal: '━',
473    head_row_cross: '━',
474    head_row_right: ' ',
475    mid_left: ' ',
476    mid_vertical: ' ',
477    mid_right: ' ',
478    row_left: ' ',
479    row_horizontal: ' ',
480    row_cross: ' ',
481    row_right: ' ',
482    foot_row_left: ' ',
483    foot_row_horizontal: '━',
484    foot_row_cross: '━',
485    foot_row_right: ' ',
486    foot_left: ' ',
487    foot_vertical: ' ',
488    foot_right: ' ',
489    bottom_left: ' ',
490    bottom: ' ',
491    bottom_divider: ' ',
492    bottom_right: ' ',
493    ascii: false,
494};
495
496/// Horizontals-only box with lines at top, head, row, foot, and bottom.
497pub const HORIZONTALS: Box = Box {
498    top_left: ' ',
499    top: '─',
500    top_divider: '─',
501    top_right: ' ',
502    head_left: ' ',
503    head_vertical: ' ',
504    head_right: ' ',
505    head_row_left: ' ',
506    head_row_horizontal: '─',
507    head_row_cross: '─',
508    head_row_right: ' ',
509    mid_left: ' ',
510    mid_vertical: ' ',
511    mid_right: ' ',
512    row_left: ' ',
513    row_horizontal: '─',
514    row_cross: '─',
515    row_right: ' ',
516    foot_row_left: ' ',
517    foot_row_horizontal: '─',
518    foot_row_cross: '─',
519    foot_row_right: ' ',
520    foot_left: ' ',
521    foot_vertical: ' ',
522    foot_right: ' ',
523    bottom_left: ' ',
524    bottom: '─',
525    bottom_divider: '─',
526    bottom_right: ' ',
527    ascii: false,
528};
529
530/// Rounded box with curved corners.
531pub const ROUNDED: Box = Box {
532    top_left: '╭',
533    top: '─',
534    top_divider: '┬',
535    top_right: '╮',
536    head_left: '│',
537    head_vertical: '│',
538    head_right: '│',
539    head_row_left: '├',
540    head_row_horizontal: '─',
541    head_row_cross: '┼',
542    head_row_right: '┤',
543    mid_left: '│',
544    mid_vertical: '│',
545    mid_right: '│',
546    row_left: '├',
547    row_horizontal: '─',
548    row_cross: '┼',
549    row_right: '┤',
550    foot_row_left: '├',
551    foot_row_horizontal: '─',
552    foot_row_cross: '┼',
553    foot_row_right: '┤',
554    foot_left: '│',
555    foot_vertical: '│',
556    foot_right: '│',
557    bottom_left: '╰',
558    bottom: '─',
559    bottom_divider: '┴',
560    bottom_right: '╯',
561    ascii: false,
562};
563
564/// Heavy box with thick lines throughout.
565pub const HEAVY: Box = Box {
566    top_left: '┏',
567    top: '━',
568    top_divider: '┳',
569    top_right: '┓',
570    head_left: '┃',
571    head_vertical: '┃',
572    head_right: '┃',
573    head_row_left: '┣',
574    head_row_horizontal: '━',
575    head_row_cross: '╋',
576    head_row_right: '┫',
577    mid_left: '┃',
578    mid_vertical: '┃',
579    mid_right: '┃',
580    row_left: '┣',
581    row_horizontal: '━',
582    row_cross: '╋',
583    row_right: '┫',
584    foot_row_left: '┣',
585    foot_row_horizontal: '━',
586    foot_row_cross: '╋',
587    foot_row_right: '┫',
588    foot_left: '┃',
589    foot_vertical: '┃',
590    foot_right: '┃',
591    bottom_left: '┗',
592    bottom: '━',
593    bottom_divider: '┻',
594    bottom_right: '┛',
595    ascii: false,
596};
597
598/// Heavy edge box (thick outer, thin inner lines).
599pub const HEAVY_EDGE: Box = Box {
600    top_left: '┏',
601    top: '━',
602    top_divider: '┯',
603    top_right: '┓',
604    head_left: '┃',
605    head_vertical: '│',
606    head_right: '┃',
607    head_row_left: '┠',
608    head_row_horizontal: '─',
609    head_row_cross: '┼',
610    head_row_right: '┨',
611    mid_left: '┃',
612    mid_vertical: '│',
613    mid_right: '┃',
614    row_left: '┠',
615    row_horizontal: '─',
616    row_cross: '┼',
617    row_right: '┨',
618    foot_row_left: '┠',
619    foot_row_horizontal: '─',
620    foot_row_cross: '┼',
621    foot_row_right: '┨',
622    foot_left: '┃',
623    foot_vertical: '│',
624    foot_right: '┃',
625    bottom_left: '┗',
626    bottom: '━',
627    bottom_divider: '┷',
628    bottom_right: '┛',
629    ascii: false,
630};
631
632/// Heavy head box (thick header, thin body).
633pub const HEAVY_HEAD: Box = Box {
634    top_left: '┏',
635    top: '━',
636    top_divider: '┳',
637    top_right: '┓',
638    head_left: '┃',
639    head_vertical: '┃',
640    head_right: '┃',
641    head_row_left: '┡',
642    head_row_horizontal: '━',
643    head_row_cross: '╇',
644    head_row_right: '┩',
645    mid_left: '│',
646    mid_vertical: '│',
647    mid_right: '│',
648    row_left: '├',
649    row_horizontal: '─',
650    row_cross: '┼',
651    row_right: '┤',
652    foot_row_left: '├',
653    foot_row_horizontal: '─',
654    foot_row_cross: '┼',
655    foot_row_right: '┤',
656    foot_left: '│',
657    foot_vertical: '│',
658    foot_right: '│',
659    bottom_left: '└',
660    bottom: '─',
661    bottom_divider: '┴',
662    bottom_right: '┘',
663    ascii: false,
664};
665
666/// Double-line box throughout.
667pub const DOUBLE: Box = Box {
668    top_left: '╔',
669    top: '═',
670    top_divider: '╦',
671    top_right: '╗',
672    head_left: '║',
673    head_vertical: '║',
674    head_right: '║',
675    head_row_left: '╠',
676    head_row_horizontal: '═',
677    head_row_cross: '╬',
678    head_row_right: '╣',
679    mid_left: '║',
680    mid_vertical: '║',
681    mid_right: '║',
682    row_left: '╠',
683    row_horizontal: '═',
684    row_cross: '╬',
685    row_right: '╣',
686    foot_row_left: '╠',
687    foot_row_horizontal: '═',
688    foot_row_cross: '╬',
689    foot_row_right: '╣',
690    foot_left: '║',
691    foot_vertical: '║',
692    foot_right: '║',
693    bottom_left: '╚',
694    bottom: '═',
695    bottom_divider: '╩',
696    bottom_right: '╝',
697    ascii: false,
698};
699
700/// Double edge box (double outer, single inner lines).
701pub const DOUBLE_EDGE: Box = Box {
702    top_left: '╔',
703    top: '═',
704    top_divider: '╤',
705    top_right: '╗',
706    head_left: '║',
707    head_vertical: '│',
708    head_right: '║',
709    head_row_left: '╟',
710    head_row_horizontal: '─',
711    head_row_cross: '┼',
712    head_row_right: '╢',
713    mid_left: '║',
714    mid_vertical: '│',
715    mid_right: '║',
716    row_left: '╟',
717    row_horizontal: '─',
718    row_cross: '┼',
719    row_right: '╢',
720    foot_row_left: '╟',
721    foot_row_horizontal: '─',
722    foot_row_cross: '┼',
723    foot_row_right: '╢',
724    foot_left: '║',
725    foot_vertical: '│',
726    foot_right: '║',
727    bottom_left: '╚',
728    bottom: '═',
729    bottom_divider: '╧',
730    bottom_right: '╝',
731    ascii: false,
732};
733
734/// Markdown-compatible table format.
735pub const MARKDOWN: Box = Box {
736    top_left: ' ',
737    top: ' ',
738    top_divider: ' ',
739    top_right: ' ',
740    head_left: '|',
741    head_vertical: '|',
742    head_right: '|',
743    head_row_left: '|',
744    head_row_horizontal: '-',
745    head_row_cross: '|',
746    head_row_right: '|',
747    mid_left: '|',
748    mid_vertical: '|',
749    mid_right: '|',
750    row_left: '|',
751    row_horizontal: '-',
752    row_cross: '|',
753    row_right: '|',
754    foot_row_left: '|',
755    foot_row_horizontal: '-',
756    foot_row_cross: '|',
757    foot_row_right: '|',
758    foot_left: '|',
759    foot_vertical: '|',
760    foot_right: '|',
761    bottom_left: ' ',
762    bottom: ' ',
763    bottom_divider: ' ',
764    bottom_right: ' ',
765    ascii: true,
766};
767
768impl Box {
769    /// Substitute this box for another if it won't render due to platform issues.
770    ///
771    /// # Arguments
772    ///
773    /// * `legacy_windows` - If true, substitute boxes that don't render well with
774    ///   legacy Windows console (raster fonts).
775    /// * `ascii_only` - If true, substitute non-ASCII boxes with ASCII equivalent.
776    ///
777    /// # Returns
778    ///
779    /// A compatible `Box`. For known box constants that need substitution, returns
780    /// the appropriate fallback. For custom boxes, returns `self` unchanged unless
781    /// `ascii_only` is true and `self.ascii` is false, in which case returns `ASCII`.
782    pub fn substitute(&self, legacy_windows: bool, ascii_only: bool) -> Box {
783        let mut result = *self;
784
785        if legacy_windows {
786            // Only substitute known box constants that have rendering issues
787            // Group boxes by their substitution target
788            result =
789                if *self == ROUNDED || *self == HEAVY || *self == HEAVY_EDGE || *self == HEAVY_HEAD
790                {
791                    SQUARE
792                } else if *self == MINIMAL_HEAVY_HEAD {
793                    MINIMAL
794                } else if *self == SIMPLE_HEAVY {
795                    SIMPLE
796                } else {
797                    result
798                };
799        }
800
801        if ascii_only && !result.ascii {
802            return ASCII;
803        }
804
805        result
806    }
807
808    /// If this box uses special characters for the header borders, return the
809    /// equivalent box without special header characters.
810    ///
811    /// # Returns
812    ///
813    /// The equivalent plain-headed `Box`, or `self` if already plain.
814    /// For custom boxes, returns `self` unchanged.
815    pub fn get_plain_headed_box(&self) -> Box {
816        // Group boxes by their substitution target
817        if *self == HEAVY_HEAD || *self == SQUARE_DOUBLE_HEAD {
818            SQUARE
819        } else if *self == MINIMAL_DOUBLE_HEAD || *self == MINIMAL_HEAVY_HEAD {
820            MINIMAL
821        } else if *self == ASCII_DOUBLE_HEAD {
822            ASCII2
823        } else {
824            *self
825        }
826    }
827
828    /// Generate the top border of a box.
829    ///
830    /// # Arguments
831    ///
832    /// * `widths` - Slice of column widths.
833    ///
834    /// # Returns
835    ///
836    /// A string representing the top border.
837    ///
838    /// # Example
839    ///
840    /// ```
841    /// use rich_rs::r#box::SQUARE;
842    ///
843    /// let top = SQUARE.get_top(&[5, 10, 5]);
844    /// assert_eq!(top, "┌─────┬──────────┬─────┐");
845    /// ```
846    pub fn get_top(&self, widths: &[usize]) -> String {
847        let mut parts = String::new();
848        parts.push(self.top_left);
849
850        for (i, &width) in widths.iter().enumerate() {
851            for _ in 0..width {
852                parts.push(self.top);
853            }
854            if i < widths.len() - 1 {
855                parts.push(self.top_divider);
856            }
857        }
858
859        parts.push(self.top_right);
860        parts
861    }
862
863    /// Generate a row separator line.
864    ///
865    /// # Arguments
866    ///
867    /// * `widths` - Slice of column widths.
868    /// * `level` - The type of row separator (Head, Row, Foot, or Mid).
869    /// * `edge` - Whether to include edge characters (left and right borders).
870    ///
871    /// # Returns
872    ///
873    /// A string representing the row separator.
874    ///
875    /// # Example
876    ///
877    /// ```
878    /// use rich_rs::r#box::{SQUARE, RowLevel};
879    ///
880    /// let row = SQUARE.get_row(&[5, 10], RowLevel::Head, true);
881    /// assert_eq!(row, "├─────┼──────────┤");
882    /// ```
883    pub fn get_row(&self, widths: &[usize], level: RowLevel, edge: bool) -> String {
884        let (left, horizontal, cross, right) = match level {
885            RowLevel::Head => (
886                self.head_row_left,
887                self.head_row_horizontal,
888                self.head_row_cross,
889                self.head_row_right,
890            ),
891            RowLevel::Row => (
892                self.row_left,
893                self.row_horizontal,
894                self.row_cross,
895                self.row_right,
896            ),
897            RowLevel::Foot => (
898                self.foot_row_left,
899                self.foot_row_horizontal,
900                self.foot_row_cross,
901                self.foot_row_right,
902            ),
903            RowLevel::Mid => (self.mid_left, ' ', self.mid_vertical, self.mid_right),
904        };
905
906        let mut parts = String::new();
907
908        if edge {
909            parts.push(left);
910        }
911
912        for (i, &width) in widths.iter().enumerate() {
913            for _ in 0..width {
914                parts.push(horizontal);
915            }
916            if i < widths.len() - 1 {
917                parts.push(cross);
918            }
919        }
920
921        if edge {
922            parts.push(right);
923        }
924
925        parts
926    }
927
928    /// Generate the bottom border of a box.
929    ///
930    /// # Arguments
931    ///
932    /// * `widths` - Slice of column widths.
933    ///
934    /// # Returns
935    ///
936    /// A string representing the bottom border.
937    ///
938    /// # Example
939    ///
940    /// ```
941    /// use rich_rs::r#box::SQUARE;
942    ///
943    /// let bottom = SQUARE.get_bottom(&[5, 10, 5]);
944    /// assert_eq!(bottom, "└─────┴──────────┴─────┘");
945    /// ```
946    pub fn get_bottom(&self, widths: &[usize]) -> String {
947        let mut parts = String::new();
948        parts.push(self.bottom_left);
949
950        for (i, &width) in widths.iter().enumerate() {
951            for _ in 0..width {
952                parts.push(self.bottom);
953            }
954            if i < widths.len() - 1 {
955                parts.push(self.bottom_divider);
956            }
957        }
958
959        parts.push(self.bottom_right);
960        parts
961    }
962
963    /// Get a string for the top edge of a simple box (single column).
964    ///
965    /// This is a convenience method for backward compatibility with the
966    /// original `BoxChars` implementation.
967    ///
968    /// # Arguments
969    ///
970    /// * `width` - The width of the box interior.
971    ///
972    /// # Returns
973    ///
974    /// A string representing the top edge.
975    pub fn top_edge(&self, width: usize) -> String {
976        self.get_top(&[width])
977    }
978
979    /// Get a string for the bottom edge of a simple box (single column).
980    ///
981    /// This is a convenience method for backward compatibility with the
982    /// original `BoxChars` implementation.
983    ///
984    /// # Arguments
985    ///
986    /// * `width` - The width of the box interior.
987    ///
988    /// # Returns
989    ///
990    /// A string representing the bottom edge.
991    pub fn bottom_edge(&self, width: usize) -> String {
992        self.get_bottom(&[width])
993    }
994}
995
996// ============================================================================
997// Backward Compatibility
998// ============================================================================
999
1000/// Deprecated alias for `Box`.
1001///
1002/// This type alias is provided for backward compatibility with code that
1003/// used the original `BoxChars` struct.
1004#[deprecated(since = "0.2.0", note = "Use `Box` instead")]
1005pub type BoxChars = Box;
1006
1007// ============================================================================
1008// Tests
1009// ============================================================================
1010
1011#[cfg(test)]
1012mod tests {
1013    use super::*;
1014
1015    #[test]
1016    fn test_box_top_edge() {
1017        assert_eq!(ROUNDED.top_edge(3), "╭───╮");
1018        assert_eq!(ROUNDED.bottom_edge(3), "╰───╯");
1019    }
1020
1021    #[test]
1022    fn test_ascii_box() {
1023        assert_eq!(ASCII.top_edge(3), "+---+");
1024        assert_eq!(ASCII.bottom_edge(3), "+---+");
1025    }
1026
1027    #[test]
1028    fn test_square_box() {
1029        assert_eq!(SQUARE.top_edge(5), "┌─────┐");
1030        assert_eq!(SQUARE.bottom_edge(5), "└─────┘");
1031    }
1032
1033    #[test]
1034    fn test_heavy_box() {
1035        assert_eq!(HEAVY.top_edge(4), "┏━━━━┓");
1036        assert_eq!(HEAVY.bottom_edge(4), "┗━━━━┛");
1037    }
1038
1039    #[test]
1040    fn test_double_box() {
1041        assert_eq!(DOUBLE.top_edge(3), "╔═══╗");
1042        assert_eq!(DOUBLE.bottom_edge(3), "╚═══╝");
1043    }
1044
1045    #[test]
1046    fn test_get_top_multiple_columns() {
1047        assert_eq!(SQUARE.get_top(&[3, 5, 2]), "┌───┬─────┬──┐");
1048        // ASCII uses '-' for both top and top_divider, so no visible column separators
1049        assert_eq!(ASCII.get_top(&[3, 5, 2]), "+------------+");
1050        // ASCII2 uses '+' for top_divider, showing column separators
1051        assert_eq!(ASCII2.get_top(&[3, 5, 2]), "+---+-----+--+");
1052        assert_eq!(HEAVY.get_top(&[2, 2]), "┏━━┳━━┓");
1053    }
1054
1055    #[test]
1056    fn test_get_bottom_multiple_columns() {
1057        assert_eq!(SQUARE.get_bottom(&[3, 5, 2]), "└───┴─────┴──┘");
1058        // ASCII uses '-' for both bottom and bottom_divider
1059        assert_eq!(ASCII.get_bottom(&[3, 5, 2]), "+------------+");
1060        // ASCII2 uses '+' for bottom_divider
1061        assert_eq!(ASCII2.get_bottom(&[3, 5, 2]), "+---+-----+--+");
1062        assert_eq!(DOUBLE.get_bottom(&[4, 4]), "╚════╩════╝");
1063    }
1064
1065    #[test]
1066    fn test_get_row_head() {
1067        assert_eq!(SQUARE.get_row(&[3, 5], RowLevel::Head, true), "├───┼─────┤");
1068        assert_eq!(HEAVY.get_row(&[3, 5], RowLevel::Head, true), "┣━━━╋━━━━━┫");
1069    }
1070
1071    #[test]
1072    fn test_get_row_regular() {
1073        assert_eq!(SQUARE.get_row(&[4, 4], RowLevel::Row, true), "├────┼────┤");
1074        assert_eq!(ASCII.get_row(&[3, 3], RowLevel::Row, true), "|---+---|");
1075    }
1076
1077    #[test]
1078    fn test_get_row_foot() {
1079        assert_eq!(SQUARE.get_row(&[3, 3], RowLevel::Foot, true), "├───┼───┤");
1080    }
1081
1082    #[test]
1083    fn test_get_row_mid() {
1084        assert_eq!(SQUARE.get_row(&[3, 3], RowLevel::Mid, true), "│   │   │");
1085        assert_eq!(HEAVY.get_row(&[2, 2], RowLevel::Mid, true), "┃  ┃  ┃");
1086    }
1087
1088    #[test]
1089    fn test_get_row_no_edge() {
1090        assert_eq!(SQUARE.get_row(&[3, 3], RowLevel::Row, false), "───┼───");
1091        assert_eq!(ASCII.get_row(&[2, 2], RowLevel::Row, false), "--+--");
1092    }
1093
1094    #[test]
1095    fn test_substitute_legacy_windows() {
1096        // ROUNDED should become SQUARE on legacy Windows
1097        let result = ROUNDED.substitute(true, false);
1098        assert_eq!(result, SQUARE);
1099
1100        // HEAVY should become SQUARE
1101        let result = HEAVY.substitute(true, false);
1102        assert_eq!(result, SQUARE);
1103
1104        // MINIMAL_HEAVY_HEAD should become MINIMAL
1105        let result = MINIMAL_HEAVY_HEAD.substitute(true, false);
1106        assert_eq!(result, MINIMAL);
1107
1108        // SIMPLE_HEAVY should become SIMPLE
1109        let result = SIMPLE_HEAVY.substitute(true, false);
1110        assert_eq!(result, SIMPLE);
1111
1112        // SQUARE stays SQUARE
1113        let result = SQUARE.substitute(true, false);
1114        assert_eq!(result, SQUARE);
1115    }
1116
1117    #[test]
1118    fn test_substitute_ascii_only() {
1119        // Non-ASCII boxes should become ASCII
1120        let result = SQUARE.substitute(false, true);
1121        assert_eq!(result, ASCII);
1122
1123        let result = ROUNDED.substitute(false, true);
1124        assert_eq!(result, ASCII);
1125
1126        // ASCII boxes stay ASCII
1127        let result = ASCII.substitute(false, true);
1128        assert_eq!(result, ASCII);
1129
1130        let result = ASCII2.substitute(false, true);
1131        assert_eq!(result, ASCII2);
1132
1133        let result = MARKDOWN.substitute(false, true);
1134        assert_eq!(result, MARKDOWN);
1135    }
1136
1137    #[test]
1138    fn test_substitute_both_flags() {
1139        // Legacy Windows + ASCII only: ROUNDED -> SQUARE -> ASCII
1140        let result = ROUNDED.substitute(true, true);
1141        assert_eq!(result, ASCII);
1142    }
1143
1144    #[test]
1145    fn test_substitute_custom_box() {
1146        // Custom boxes should be returned unchanged when not needing substitution
1147        let custom = Box {
1148            top_left: '*',
1149            top: '*',
1150            top_divider: '*',
1151            top_right: '*',
1152            head_left: '*',
1153            head_vertical: '*',
1154            head_right: '*',
1155            head_row_left: '*',
1156            head_row_horizontal: '*',
1157            head_row_cross: '*',
1158            head_row_right: '*',
1159            mid_left: '*',
1160            mid_vertical: '*',
1161            mid_right: '*',
1162            row_left: '*',
1163            row_horizontal: '*',
1164            row_cross: '*',
1165            row_right: '*',
1166            foot_row_left: '*',
1167            foot_row_horizontal: '*',
1168            foot_row_cross: '*',
1169            foot_row_right: '*',
1170            foot_left: '*',
1171            foot_vertical: '*',
1172            foot_right: '*',
1173            bottom_left: '*',
1174            bottom: '*',
1175            bottom_divider: '*',
1176            bottom_right: '*',
1177            ascii: true,
1178        };
1179
1180        // Custom box should be unchanged
1181        let result = custom.substitute(false, false);
1182        assert_eq!(result, custom);
1183
1184        // Custom ASCII box stays custom even with legacy_windows
1185        let result = custom.substitute(true, false);
1186        assert_eq!(result, custom);
1187
1188        // Custom ASCII box stays custom with ascii_only (since it is ASCII)
1189        let result = custom.substitute(false, true);
1190        assert_eq!(result, custom);
1191
1192        // Non-ASCII custom box becomes ASCII when ascii_only=true
1193        let custom_unicode = Box {
1194            ascii: false,
1195            ..custom
1196        };
1197        let result = custom_unicode.substitute(false, true);
1198        assert_eq!(result, ASCII);
1199    }
1200
1201    #[test]
1202    fn test_get_plain_headed_box() {
1203        assert_eq!(HEAVY_HEAD.get_plain_headed_box(), SQUARE);
1204        assert_eq!(SQUARE_DOUBLE_HEAD.get_plain_headed_box(), SQUARE);
1205        assert_eq!(MINIMAL_DOUBLE_HEAD.get_plain_headed_box(), MINIMAL);
1206        assert_eq!(MINIMAL_HEAVY_HEAD.get_plain_headed_box(), MINIMAL);
1207        assert_eq!(ASCII_DOUBLE_HEAD.get_plain_headed_box(), ASCII2);
1208
1209        // Plain boxes return themselves
1210        assert_eq!(SQUARE.get_plain_headed_box(), SQUARE);
1211        assert_eq!(ASCII.get_plain_headed_box(), ASCII);
1212        assert_eq!(ROUNDED.get_plain_headed_box(), ROUNDED);
1213    }
1214
1215    #[test]
1216    fn test_get_plain_headed_box_custom() {
1217        // Custom boxes should be returned unchanged
1218        let custom = Box {
1219            top_left: '#',
1220            top: '#',
1221            top_divider: '#',
1222            top_right: '#',
1223            head_left: '#',
1224            head_vertical: '#',
1225            head_right: '#',
1226            head_row_left: '#',
1227            head_row_horizontal: '#',
1228            head_row_cross: '#',
1229            head_row_right: '#',
1230            mid_left: '#',
1231            mid_vertical: '#',
1232            mid_right: '#',
1233            row_left: '#',
1234            row_horizontal: '#',
1235            row_cross: '#',
1236            row_right: '#',
1237            foot_row_left: '#',
1238            foot_row_horizontal: '#',
1239            foot_row_cross: '#',
1240            foot_row_right: '#',
1241            foot_left: '#',
1242            foot_vertical: '#',
1243            foot_right: '#',
1244            bottom_left: '#',
1245            bottom: '#',
1246            bottom_divider: '#',
1247            bottom_right: '#',
1248            ascii: true,
1249        };
1250        assert_eq!(custom.get_plain_headed_box(), custom);
1251    }
1252
1253    #[test]
1254    fn test_all_boxes_defined() {
1255        // Verify all 19 boxes are defined and have the expected ascii flag
1256        assert!(ASCII.ascii);
1257        assert!(ASCII2.ascii);
1258        assert!(ASCII_DOUBLE_HEAD.ascii);
1259        assert!(!SQUARE.ascii);
1260        assert!(!SQUARE_DOUBLE_HEAD.ascii);
1261        assert!(!MINIMAL.ascii);
1262        assert!(!MINIMAL_HEAVY_HEAD.ascii);
1263        assert!(!MINIMAL_DOUBLE_HEAD.ascii);
1264        assert!(!SIMPLE.ascii);
1265        assert!(!SIMPLE_HEAD.ascii);
1266        assert!(!SIMPLE_HEAVY.ascii);
1267        assert!(!HORIZONTALS.ascii);
1268        assert!(!ROUNDED.ascii);
1269        assert!(!HEAVY.ascii);
1270        assert!(!HEAVY_EDGE.ascii);
1271        assert!(!HEAVY_HEAD.ascii);
1272        assert!(!DOUBLE.ascii);
1273        assert!(!DOUBLE_EDGE.ascii);
1274        assert!(MARKDOWN.ascii);
1275    }
1276
1277    #[test]
1278    fn test_box_equality() {
1279        // Each box should equal itself
1280        assert_eq!(SQUARE, SQUARE);
1281        assert_eq!(ROUNDED, ROUNDED);
1282
1283        // Different boxes should not be equal
1284        assert_ne!(SQUARE, ROUNDED);
1285        assert_ne!(ASCII, MARKDOWN);
1286    }
1287
1288    #[test]
1289    fn test_single_column() {
1290        // Single column should work without dividers
1291        assert_eq!(SQUARE.get_top(&[5]), "┌─────┐");
1292        assert_eq!(SQUARE.get_bottom(&[5]), "└─────┘");
1293        assert_eq!(SQUARE.get_row(&[5], RowLevel::Row, true), "├─────┤");
1294    }
1295
1296    #[test]
1297    fn test_empty_widths() {
1298        // Empty widths should produce minimal output
1299        assert_eq!(SQUARE.get_top(&[]), "┌┐");
1300        assert_eq!(SQUARE.get_bottom(&[]), "└┘");
1301        assert_eq!(SQUARE.get_row(&[], RowLevel::Row, true), "├┤");
1302    }
1303
1304    #[test]
1305    fn test_zero_width_column() {
1306        // Zero-width columns are valid
1307        assert_eq!(SQUARE.get_top(&[0, 3, 0]), "┌┬───┬┐");
1308    }
1309
1310    #[test]
1311    fn test_markdown_box() {
1312        // Markdown has space for top/bottom (no visible borders)
1313        // top_left=' ', top=' ', top_right=' '
1314        // For width 5: ' ' + 5*' ' + ' ' = 7 spaces
1315        assert_eq!(MARKDOWN.get_top(&[5]), "       ");
1316        assert_eq!(
1317            MARKDOWN.get_row(&[5, 5], RowLevel::Head, true),
1318            "|-----|-----|"
1319        );
1320        assert_eq!(MARKDOWN.get_bottom(&[5]), "       ");
1321        // With two columns of width 5: ' ' + '     ' + ' ' + '     ' + ' ' = 13 chars
1322        assert_eq!(MARKDOWN.get_top(&[5, 5]), "             ");
1323    }
1324
1325    #[test]
1326    fn test_horizontals_box() {
1327        // HORIZONTALS has lines at top and bottom, no corner chars
1328        // top_left=' ', top='─', top_divider='─', top_right=' '
1329        assert_eq!(HORIZONTALS.get_top(&[5]), " ───── ");
1330        assert_eq!(HORIZONTALS.get_bottom(&[5]), " ───── ");
1331    }
1332
1333    #[test]
1334    fn test_minimal_box() {
1335        // MINIMAL has mostly spaces, dividers only appear between columns
1336        // top_left=' ', top=' ', top_divider='╷', top_right=' '
1337        // For width 3: ' ' + 3*' ' + ' ' = 5 spaces (no divider for single column)
1338        assert_eq!(MINIMAL.get_top(&[3]), "     ");
1339        assert_eq!(MINIMAL.get_bottom(&[3]), "     ");
1340        // With two columns: ' ' + '   ' + '╷' + '   ' + ' '
1341        assert_eq!(MINIMAL.get_top(&[3, 3]), "    ╷    ");
1342    }
1343
1344    #[test]
1345    fn test_simple_box() {
1346        // SIMPLE has mostly spaces
1347        assert_eq!(SIMPLE.get_top(&[5]), "       ");
1348        assert_eq!(SIMPLE.get_row(&[5], RowLevel::Head, true), " ───── ");
1349    }
1350
1351    #[test]
1352    fn test_double_headed_boxes() {
1353        // Verify the header row uses different characters
1354        assert_eq!(
1355            SQUARE_DOUBLE_HEAD.get_row(&[3], RowLevel::Head, true),
1356            "╞═══╡"
1357        );
1358        assert_eq!(
1359            ASCII_DOUBLE_HEAD.get_row(&[3], RowLevel::Head, true),
1360            "+===+"
1361        );
1362    }
1363
1364    #[test]
1365    fn test_heavy_variations() {
1366        // HEAVY_HEAD has thick header, thin body
1367        assert_eq!(HEAVY_HEAD.get_top(&[3]), "┏━━━┓");
1368        assert_eq!(HEAVY_HEAD.get_row(&[3], RowLevel::Row, true), "├───┤");
1369
1370        // HEAVY_EDGE has thick edges, thin interior
1371        assert_eq!(HEAVY_EDGE.get_top(&[3, 3]), "┏━━━┯━━━┓");
1372        assert_eq!(
1373            HEAVY_EDGE.get_row(&[3, 3], RowLevel::Row, true),
1374            "┠───┼───┨"
1375        );
1376    }
1377}