Skip to main content

rusty_rich/
box_drawing.rs

1//! Box drawing — equivalent to Rich's `box.py`.
2//!
3//! Defines various box styles (ROUNDED, SQUARE, HEAVY, etc.) using Unicode
4//! box-drawing characters, with ASCII-safe fallbacks.
5
6// ---------------------------------------------------------------------------
7// Box — defines characters for drawing a bordered box
8// ---------------------------------------------------------------------------
9
10/// A set of box-drawing characters defining the look of borders.
11///
12/// Layout of the 8-line string that defines a box:
13///
14/// ```text
15/// ┌─┬┐ top
16/// │ ││ head
17/// ├─┼┤ head_row
18/// │ ││ mid
19/// ├─┼┤ row
20/// ├─┼┤ foot_row
21/// │ ││ foot
22/// └─┴┘ bottom
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct BoxStyle {
26    // top row
27    pub top_left: char,
28    pub top: char,
29    pub top_divider: char,
30    pub top_right: char,
31    // head row (where content is on same line as top border)
32    pub head_left: char,
33    pub head_horizontal: char,
34    pub head_vertical: char,
35    pub head_right: char,
36    // head_row (separator after header)
37    pub head_row_left: char,
38    pub head_row_horizontal: char,
39    pub head_row_cross: char,
40    pub head_row_right: char,
41    // mid (between rows when show_lines is off)
42    pub mid_left: char,
43    pub mid_horizontal: char,
44    pub mid_vertical: char,
45    pub mid_right: char,
46    // row (between rows when show_lines is on)
47    pub row_left: char,
48    pub row_horizontal: char,
49    pub row_cross: char,
50    pub row_right: char,
51    // foot_row (separator before footer)
52    pub foot_row_left: char,
53    pub foot_row_horizontal: char,
54    pub foot_row_cross: char,
55    pub foot_row_right: char,
56    // foot
57    pub foot_left: char,
58    pub foot_horizontal: char,
59    pub foot_vertical: char,
60    pub foot_right: char,
61    // bottom row
62    pub bottom_left: char,
63    pub bottom: char,
64    pub bottom_divider: char,
65    pub bottom_right: char,
66    /// True if this box uses only ASCII characters.
67    pub ascii: bool,
68}
69
70impl BoxStyle {
71    /// Returns true if this box has visible outer edges (non-space corners).
72    /// Edge-less styles like SIMPLE, MINIMAL, and MARKDOWN return `false`
73    /// because their corner characters are all spaces — they are designed
74    /// to be used in tables where internal separators provide structure.
75    pub fn has_visible_edges(&self) -> bool {
76        // A visible edge requires at least one non-space corner.
77        self.top_left != ' ' || self.top_right != ' '
78            || self.bottom_left != ' ' || self.bottom_right != ' '
79    }
80
81    /// Parse a box style from an 8-line string.
82    pub fn from_str(box_str: &str, ascii: bool) -> Self {
83        let lines: Vec<&str> = box_str.lines().collect();
84        assert_eq!(lines.len(), 8, "Box definition must have exactly 8 lines");
85
86        let line_chars: Vec<Vec<char>> = lines.iter()
87            .map(|l| l.chars().collect())
88            .collect();
89
90        // Each line should have 4 characters
91        for (i, chars) in line_chars.iter().enumerate() {
92            assert_eq!(chars.len(), 4, "Line {i} must have exactly 4 characters");
93        }
94
95        let l = &line_chars;
96        Self {
97            top_left: l[0][0], top: l[0][1], top_divider: l[0][2], top_right: l[0][3],
98            head_left: l[1][0], head_horizontal: l[1][1], head_vertical: l[1][2], head_right: l[1][3],
99            head_row_left: l[2][0], head_row_horizontal: l[2][1], head_row_cross: l[2][2], head_row_right: l[2][3],
100            mid_left: l[3][0], mid_horizontal: l[3][1], mid_vertical: l[3][2], mid_right: l[3][3],
101            row_left: l[4][0], row_horizontal: l[4][1], row_cross: l[4][2], row_right: l[4][3],
102            foot_row_left: l[5][0], foot_row_horizontal: l[5][1], foot_row_cross: l[5][2], foot_row_right: l[5][3],
103            foot_left: l[6][0], foot_horizontal: l[6][1], foot_vertical: l[6][2], foot_right: l[6][3],
104            bottom_left: l[7][0], bottom: l[7][1], bottom_divider: l[7][2], bottom_right: l[7][3],
105            ascii,
106        }
107    }
108
109    /// Get the plain text representation of the box definition.
110    pub fn to_string(&self) -> String {
111        format!(
112            "{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}",
113            self.top_left, self.top, self.top_divider, self.top_right,
114            self.head_left, self.head_horizontal, self.head_vertical, self.head_right,
115            self.head_row_left, self.head_row_horizontal, self.head_row_cross, self.head_row_right,
116            self.mid_left, self.mid_horizontal, self.mid_vertical, self.mid_right,
117            self.row_left, self.row_horizontal, self.row_cross, self.row_right,
118            self.foot_row_left, self.foot_row_horizontal, self.foot_row_cross, self.foot_row_right,
119            self.foot_left, self.foot_horizontal, self.foot_vertical, self.foot_right,
120            self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right,
121        )
122    }
123}
124
125impl std::fmt::Display for BoxStyle {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "{}", self.to_string())
128    }
129}
130
131// ---------------------------------------------------------------------------
132// Predefined box styles (matching Rich's defaults)
133// ---------------------------------------------------------------------------
134
135/// ASCII-only box.
136pub const ASCII: &str = "\
137+--+
138| ||
139|-+|
140| ||
141|-+|
142|-+|
143| ||
144+--+";
145
146/// ASCII with double edges (no distinct header).
147pub const ASCII2: &str = "\
148+-++
149| ||
150+-++
151| ||
152+-++
153+-++
154| ||
155+-++";
156
157/// Square box with double horizontal header separator.
158pub const SQUARE_DOUBLE_HEAD: &str = "\
159┌─┬┐
160│ ││
161╞═╪╡
162│ ││
163├─┼┤
164├─┼┤
165│ ││
166└─┴┘";
167
168/// Minimal box with double horizontal separator (head row only).
169pub const MINIMAL_DOUBLE_HEAD: &str = "  ╷ \n  │ \n ═╪ \n  │ \n ─┼ \n ─┼ \n  │ \n  ╵ ";
170
171/// Simple box with a single horizontal rule under the header.
172pub const SIMPLE_HEAD: &str = "    \n    \n ── \n    \n    \n    \n    \n    ";
173
174/// ASCII box style with a double header line.
175pub const ASCII_DOUBLE_HEAD: &str = "\
176+-++
177| ||
178+=++
179| ||
180+-++
181+-++
182| ||
183+-++";
184
185/// Rounded corners.
186pub const ROUNDED: &str = "\
187╭─┬╮
188│ ││
189├─┼┤
190│ ││
191├─┼┤
192├─┼┤
193│ ││
194╰─┴╯";
195
196/// Square corners.
197pub const SQUARE: &str = "\
198┌─┬┐
199│ ││
200├─┼┤
201│ ││
202├─┼┤
203├─┼┤
204│ ││
205└─┴┘";
206
207/// Heavy borders.
208pub const HEAVY: &str = "\
209┏━┳┓
210┃ ┃┃
211┣━╋┫
212┃ ┃┃
213┣━╋┫
214┣━╋┫
215┃ ┃┃
216┗━┻┛";
217
218/// Heavy edge, light inner.
219pub const HEAVY_EDGE: &str = "\
220┏━┯┓
221┃ │┃
222┠─┼┨
223┃ │┃
224┠─┼┨
225┠─┼┨
226┃ │┃
227┗━┷┛";
228
229/// Heavy header.
230pub const HEAVY_HEAD: &str = "\
231┏━┳┓
232┃ ┃┃
233┡━╇┩
234│ ││
235├─┼┤
236├─┼┤
237│ ││
238└─┴┘";
239
240/// Double borders.
241pub const DOUBLE: &str = "\
242╔═╦╗
243║ ║║
244╠═╬╣
245║ ║║
246╠═╬╣
247╠═╬╣
248║ ║║
249╚═╩╝";
250
251/// Double edge (like DOUBLE but inner is single).
252pub const DOUBLE_EDGE: &str = "\
253╔═╤╗
254║ │║
255╟─┼╢
256║ │║
257╟─┼╢
258╟─┼╢
259║ │║
260╚═╧╝";
261
262/// Simple (no borders, just vertical separators).
263pub const SIMPLE: &str = "    \n    \n ── \n    \n    \n ── \n    \n    ";
264
265/// Simple with heavy header.
266pub const SIMPLE_HEAVY: &str = "    \n    \n ━━ \n    \n    \n ━━ \n    \n    ";
267
268/// Minimal (thin rule, vertical separators, no outer edges).
269pub const MINIMAL: &str = "  ╷ \n  │ \n╶─┼╴\n  │ \n╶─┼╴\n╶─┼╴\n  │ \n  ╵ ";
270
271/// Minimal with heavy header separator (matches Python Rich MINIMAL_HEAVY_HEAD).
272pub const MINIMAL_HEAVY: &str = "  ╷ \n  │ \n╺━┿╸\n  │ \n╶─┼╴\n╶─┼╴\n  │ \n  ╵ ";
273
274// ---------------------------------------------------------------------------
275// Box style constants (lazily parsed)
276// ---------------------------------------------------------------------------
277
278use once_cell::sync::Lazy;
279
280/// Rounded box (default for Panel).
281pub static BOX_ROUNDED: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ROUNDED, false));
282/// Square-cornered box.
283pub static BOX_SQUARE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SQUARE, false));
284/// Heavy (thick) borders.
285pub static BOX_HEAVY: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(HEAVY, false));
286/// Heavy outer edges with light inner dividers.
287pub static BOX_HEAVY_EDGE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(HEAVY_EDGE, false));
288/// Heavy header row with regular body borders.
289pub static BOX_HEAVY_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(HEAVY_HEAD, false));
290/// Double-line borders.
291pub static BOX_DOUBLE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(DOUBLE, false));
292/// Double outer edge with single inner dividers.
293pub static BOX_DOUBLE_EDGE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(DOUBLE_EDGE, false));
294/// Simple borders (no vertical edges, horizontal rules only).
295pub static BOX_SIMPLE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SIMPLE, false));
296/// Simple borders with heavy horizontal rules.
297pub static BOX_SIMPLE_HEAVY: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SIMPLE_HEAVY, false));
298/// Minimal box (just horizontal separators between header/body).
299pub static BOX_MINIMAL: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MINIMAL, false));
300/// Minimal box with heavy horizontal separators.
301pub static BOX_MINIMAL_HEAVY: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MINIMAL_HEAVY, false));
302/// ASCII-only box (uses `+`, `-`, `|` characters).
303pub static BOX_ASCII: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ASCII, true));
304/// ASCII box with doubled edges.
305pub static BOX_ASCII2: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ASCII2, true));
306/// Square box with a double horizontal header separator.
307pub static BOX_SQUARE_DOUBLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SQUARE_DOUBLE_HEAD, false));
308/// Minimal box with a double horizontal header separator.
309pub static BOX_MINIMAL_DOUBLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MINIMAL_DOUBLE_HEAD, false));
310/// Simple box with a single horizontal rule under the header.
311pub static BOX_SIMPLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SIMPLE_HEAD, false));
312/// ASCII box with a double header line.
313pub static BOX_ASCII_DOUBLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ASCII_DOUBLE_HEAD, true));
314
315// ---------------------------------------------------------------------------
316// MARKDOWN box (no outer border)
317// ---------------------------------------------------------------------------
318
319/// Markdown-style box definition string (no outer borders).
320pub const MARKDOWN: &str = "    \n| ||\n|-||\n| ||\n|-||\n|-||\n| ||\n    ";
321
322/// Markdown-style box (no outer edges, vertical separators only).
323pub static BOX_MARKDOWN: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MARKDOWN, false));
324
325// ---------------------------------------------------------------------------
326// Safe box (for Windows legacy terminals)
327// ---------------------------------------------------------------------------
328
329/// Return an ASCII-safe version of a box if needed.
330pub fn get_safe_box(box_style: &BoxStyle, ascii_only: bool) -> BoxStyle {
331    if ascii_only && !box_style.ascii {
332        BOX_ASCII.clone()
333    } else {
334        box_style.clone()
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_rounded_box() {
344        let b = &*BOX_ROUNDED;
345        assert_eq!(b.top_left, '╭');
346        assert_eq!(b.bottom_right, '╯');
347    }
348
349    #[test]
350    fn test_box_from_str() {
351        let b = BoxStyle::from_str(ROUNDED, false);
352        assert_eq!(b, *BOX_ROUNDED);
353    }
354
355    #[test]
356    fn test_new_box_styles_parse() {
357        // Verify that the new box styles parse without panicking
358        let _ = &*BOX_SQUARE_DOUBLE_HEAD;
359        let _ = &*BOX_MINIMAL_DOUBLE_HEAD;
360        let _ = &*BOX_SIMPLE_HEAD;
361        let _ = &*BOX_ASCII_DOUBLE_HEAD;
362
363        // Spot-check characters
364        let sq = &*BOX_SQUARE_DOUBLE_HEAD;
365        assert_eq!(sq.top_left, '┌');
366        assert_eq!(sq.head_row_horizontal, '═');
367        assert_eq!(sq.head_row_left, '╞');
368
369        let ac = &*BOX_ASCII_DOUBLE_HEAD;
370        assert_eq!(ac.head_row_left, '+');
371        assert_eq!(ac.head_row_horizontal, '=');
372        assert_eq!(ac.row_left, '+');
373    }
374}