ratatui_core/symbols/
border.rs

1use crate::symbols::{block, line};
2
3#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
4pub struct Set<'a> {
5    pub top_left: &'a str,
6    pub top_right: &'a str,
7    pub bottom_left: &'a str,
8    pub bottom_right: &'a str,
9    pub vertical_left: &'a str,
10    pub vertical_right: &'a str,
11    pub horizontal_top: &'a str,
12    pub horizontal_bottom: &'a str,
13}
14
15impl Default for Set<'_> {
16    fn default() -> Self {
17        PLAIN
18    }
19}
20
21// Helper function to convert a line set to a border set
22const fn from_line_set(line_set: line::Set<'_>) -> Set<'_> {
23    Set {
24        top_left: line_set.top_left,
25        top_right: line_set.top_right,
26        bottom_left: line_set.bottom_left,
27        bottom_right: line_set.bottom_right,
28        vertical_left: line_set.vertical,
29        vertical_right: line_set.vertical,
30        horizontal_top: line_set.horizontal,
31        horizontal_bottom: line_set.horizontal,
32    }
33}
34
35/// Border Set with a single line width
36///
37/// ```text
38/// ┌─────┐
39/// │xxxxx│
40/// │xxxxx│
41/// └─────┘
42/// ```
43pub const PLAIN: Set = Set {
44    top_left: line::NORMAL.top_left,
45    top_right: line::NORMAL.top_right,
46    bottom_left: line::NORMAL.bottom_left,
47    bottom_right: line::NORMAL.bottom_right,
48    vertical_left: line::NORMAL.vertical,
49    vertical_right: line::NORMAL.vertical,
50    horizontal_top: line::NORMAL.horizontal,
51    horizontal_bottom: line::NORMAL.horizontal,
52};
53
54/// Border Set with a single line width and rounded corners
55///
56/// ```text
57/// ╭─────╮
58/// │xxxxx│
59/// │xxxxx│
60/// ╰─────╯
61/// ```
62pub const ROUNDED: Set = Set {
63    top_left: line::ROUNDED.top_left,
64    top_right: line::ROUNDED.top_right,
65    bottom_left: line::ROUNDED.bottom_left,
66    bottom_right: line::ROUNDED.bottom_right,
67    vertical_left: line::ROUNDED.vertical,
68    vertical_right: line::ROUNDED.vertical,
69    horizontal_top: line::ROUNDED.horizontal,
70    horizontal_bottom: line::ROUNDED.horizontal,
71};
72
73/// Border Set with a double line width
74///
75/// ```text
76/// ╔═════╗
77/// ║xxxxx║
78/// ║xxxxx║
79/// ╚═════╝
80/// ```
81pub const DOUBLE: Set = Set {
82    top_left: line::DOUBLE.top_left,
83    top_right: line::DOUBLE.top_right,
84    bottom_left: line::DOUBLE.bottom_left,
85    bottom_right: line::DOUBLE.bottom_right,
86    vertical_left: line::DOUBLE.vertical,
87    vertical_right: line::DOUBLE.vertical,
88    horizontal_top: line::DOUBLE.horizontal,
89    horizontal_bottom: line::DOUBLE.horizontal,
90};
91
92/// Border Set with a thick line width
93///
94/// ```text
95/// ┏━━━━━┓
96/// ┃xxxxx┃
97/// ┃xxxxx┃
98/// ┗━━━━━┛
99/// ```
100pub const THICK: Set = Set {
101    top_left: line::THICK.top_left,
102    top_right: line::THICK.top_right,
103    bottom_left: line::THICK.bottom_left,
104    bottom_right: line::THICK.bottom_right,
105    vertical_left: line::THICK.vertical,
106    vertical_right: line::THICK.vertical,
107    horizontal_top: line::THICK.horizontal,
108    horizontal_bottom: line::THICK.horizontal,
109};
110
111/// Border Set with light double-dashed border lines
112///
113/// ```text
114/// ┌╌╌╌╌╌┐
115/// ╎xxxxx╎
116/// ╎xxxxx╎
117/// └╌╌╌╌╌┘
118/// ```
119pub const LIGHT_DOUBLE_DASHED: Set = from_line_set(line::LIGHT_DOUBLE_DASHED);
120
121/// Border Set with thick double-dashed border lines
122///
123/// ```text
124/// ┏╍╍╍╍╍┓
125/// ╏xxxxx╏
126/// ╏xxxxx╏
127/// ┗╍╍╍╍╍┛
128/// ```
129pub const HEAVY_DOUBLE_DASHED: Set = from_line_set(line::HEAVY_DOUBLE_DASHED);
130
131/// Border Set with light triple-dashed border lines
132///
133/// ```text
134/// ┌┄┄┄┄┄┐
135/// ┆xxxxx┆
136/// ┆xxxxx┆
137/// └┄┄┄┄┄┘
138/// ```
139pub const LIGHT_TRIPLE_DASHED: Set = from_line_set(line::LIGHT_TRIPLE_DASHED);
140
141/// Border Set with thick triple-dashed border lines
142///
143/// ```text
144/// ┏┅┅┅┅┅┓
145/// ┇xxxxx┇
146/// ┇xxxxx┇
147/// ┗┅┅┅┅┅┛
148/// ```
149pub const HEAVY_TRIPLE_DASHED: Set = from_line_set(line::HEAVY_TRIPLE_DASHED);
150
151/// Border Set with light quadruple-dashed border lines
152///
153/// ```text
154/// ┌┈┈┈┈┈┐
155/// ┊xxxxx┊
156/// ┊xxxxx┊
157/// └┈┈┈┈┈┘
158/// ```
159pub const LIGHT_QUADRUPLE_DASHED: Set = from_line_set(line::LIGHT_QUADRUPLE_DASHED);
160
161/// Border Set with thick quadruple-dashed border lines
162///
163/// ```text
164/// ┏┉┉┉┉┉┓
165/// ┋xxxxx┋
166/// ┋xxxxx┋
167/// ┗┉┉┉┉┉┛
168/// ```
169pub const HEAVY_QUADRUPLE_DASHED: Set = from_line_set(line::HEAVY_QUADRUPLE_DASHED);
170
171pub const QUADRANT_TOP_LEFT: &str = "▘";
172pub const QUADRANT_TOP_RIGHT: &str = "▝";
173pub const QUADRANT_BOTTOM_LEFT: &str = "▖";
174pub const QUADRANT_BOTTOM_RIGHT: &str = "▗";
175pub const QUADRANT_TOP_HALF: &str = "▀";
176pub const QUADRANT_BOTTOM_HALF: &str = "▄";
177pub const QUADRANT_LEFT_HALF: &str = "▌";
178pub const QUADRANT_RIGHT_HALF: &str = "▐";
179pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▙";
180pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "▛";
181pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "▜";
182pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▟";
183pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "▚";
184pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "▞";
185pub const QUADRANT_BLOCK: &str = "█";
186
187/// Quadrant used for setting a border outside a block by one half cell "pixel".
188///
189/// ```text
190/// ▛▀▀▀▀▀▜
191/// ▌xxxxx▐
192/// ▌xxxxx▐
193/// ▙▄▄▄▄▄▟
194/// ```
195pub const QUADRANT_OUTSIDE: Set = Set {
196    top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
197    top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
198    bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
199    bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
200    vertical_left: QUADRANT_LEFT_HALF,
201    vertical_right: QUADRANT_RIGHT_HALF,
202    horizontal_top: QUADRANT_TOP_HALF,
203    horizontal_bottom: QUADRANT_BOTTOM_HALF,
204};
205
206/// Quadrant used for setting a border inside a block by one half cell "pixel".
207///
208/// ```text
209/// ▗▄▄▄▄▄▖
210/// ▐xxxxx▌
211/// ▐xxxxx▌
212/// ▝▀▀▀▀▀▘
213/// ```
214pub const QUADRANT_INSIDE: Set = Set {
215    top_right: QUADRANT_BOTTOM_LEFT,
216    top_left: QUADRANT_BOTTOM_RIGHT,
217    bottom_right: QUADRANT_TOP_LEFT,
218    bottom_left: QUADRANT_TOP_RIGHT,
219    vertical_left: QUADRANT_RIGHT_HALF,
220    vertical_right: QUADRANT_LEFT_HALF,
221    horizontal_top: QUADRANT_BOTTOM_HALF,
222    horizontal_bottom: QUADRANT_TOP_HALF,
223};
224
225pub const ONE_EIGHTH_TOP_EIGHT: &str = "▔";
226pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "▁";
227pub const ONE_EIGHTH_LEFT_EIGHT: &str = "▏";
228pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "▕";
229
230/// Wide border set based on McGugan box technique
231///
232/// ```text
233/// ▁▁▁▁▁▁▁
234/// ▏xxxxx▕
235/// ▏xxxxx▕
236/// ▔▔▔▔▔▔▔
237/// ```
238#[expect(clippy::doc_markdown)]
239pub const ONE_EIGHTH_WIDE: Set = Set {
240    top_right: ONE_EIGHTH_BOTTOM_EIGHT,
241    top_left: ONE_EIGHTH_BOTTOM_EIGHT,
242    bottom_right: ONE_EIGHTH_TOP_EIGHT,
243    bottom_left: ONE_EIGHTH_TOP_EIGHT,
244    vertical_left: ONE_EIGHTH_LEFT_EIGHT,
245    vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
246    horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
247    horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
248};
249
250/// Tall border set based on McGugan box technique
251///
252/// ```text
253/// ▕▔▔▏
254/// ▕xx▏
255/// ▕xx▏
256/// ▕▁▁▏
257/// ```
258#[expect(clippy::doc_markdown)]
259pub const ONE_EIGHTH_TALL: Set = Set {
260    top_right: ONE_EIGHTH_LEFT_EIGHT,
261    top_left: ONE_EIGHTH_RIGHT_EIGHT,
262    bottom_right: ONE_EIGHTH_LEFT_EIGHT,
263    bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
264    vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
265    vertical_right: ONE_EIGHTH_LEFT_EIGHT,
266    horizontal_top: ONE_EIGHTH_TOP_EIGHT,
267    horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
268};
269
270/// Wide proportional (visually equal width and height) border with using set of quadrants.
271///
272/// The border is created by using half blocks for top and bottom, and full
273/// blocks for right and left sides to make horizontal and vertical borders seem equal.
274///
275/// ```text
276/// ▄▄▄▄
277/// █xx█
278/// █xx█
279/// ▀▀▀▀
280/// ```
281pub const PROPORTIONAL_WIDE: Set = Set {
282    top_right: QUADRANT_BOTTOM_HALF,
283    top_left: QUADRANT_BOTTOM_HALF,
284    bottom_right: QUADRANT_TOP_HALF,
285    bottom_left: QUADRANT_TOP_HALF,
286    vertical_left: QUADRANT_BLOCK,
287    vertical_right: QUADRANT_BLOCK,
288    horizontal_top: QUADRANT_BOTTOM_HALF,
289    horizontal_bottom: QUADRANT_TOP_HALF,
290};
291
292/// Tall proportional (visually equal width and height) border with using set of quadrants.
293///
294/// The border is created by using full blocks for all sides, except for the top and bottom,
295/// which use half blocks to make horizontal and vertical borders seem equal.
296///
297/// ```text
298/// ▕█▀▀█
299/// ▕█xx█
300/// ▕█xx█
301/// ▕█▄▄█
302/// ```
303pub const PROPORTIONAL_TALL: Set = Set {
304    top_right: QUADRANT_BLOCK,
305    top_left: QUADRANT_BLOCK,
306    bottom_right: QUADRANT_BLOCK,
307    bottom_left: QUADRANT_BLOCK,
308    vertical_left: QUADRANT_BLOCK,
309    vertical_right: QUADRANT_BLOCK,
310    horizontal_top: QUADRANT_TOP_HALF,
311    horizontal_bottom: QUADRANT_BOTTOM_HALF,
312};
313
314/// Solid border set
315///
316/// The border is created by using full blocks for all sides.
317///
318/// ```text
319/// ████
320/// █xx█
321/// █xx█
322/// ████
323/// ```
324pub const FULL: Set = Set {
325    top_left: block::FULL,
326    top_right: block::FULL,
327    bottom_left: block::FULL,
328    bottom_right: block::FULL,
329    vertical_left: block::FULL,
330    vertical_right: block::FULL,
331    horizontal_top: block::FULL,
332    horizontal_bottom: block::FULL,
333};
334
335/// Empty border set
336///
337/// The border is created by using empty strings for all sides.
338///
339/// This is useful for ensuring that the border style is applied to a border on a block with a title
340/// without actually drawing a border.
341///
342/// ░ Example
343///
344/// `░` represents the content in the area not covered by the border to make it easier to see the
345/// blank symbols.
346///
347/// ```text
348/// ░░░░░░░░
349/// ░░    ░░
350/// ░░ ░░ ░░
351/// ░░ ░░ ░░
352/// ░░    ░░
353/// ░░░░░░░░
354/// ```
355pub const EMPTY: Set = Set {
356    top_left: " ",
357    top_right: " ",
358    bottom_left: " ",
359    bottom_right: " ",
360    vertical_left: " ",
361    vertical_right: " ",
362    horizontal_top: " ",
363    horizontal_bottom: " ",
364};
365
366#[cfg(test)]
367mod tests {
368    use alloc::format;
369    use alloc::string::String;
370
371    use indoc::{formatdoc, indoc};
372
373    use super::*;
374
375    #[test]
376    fn default() {
377        assert_eq!(Set::default(), PLAIN);
378    }
379
380    /// A helper function to render a border set to a string.
381    ///
382    /// '░' (U+2591 Light Shade) is used as a placeholder for empty space to make it easier to see
383    /// the size of the border symbols.
384    fn render(set: Set) -> String {
385        formatdoc!(
386            "░░░░░░
387             ░{}{}{}{}░
388             ░{}░░{}░
389             ░{}░░{}░
390             ░{}{}{}{}░
391             ░░░░░░",
392            set.top_left,
393            set.horizontal_top,
394            set.horizontal_top,
395            set.top_right,
396            set.vertical_left,
397            set.vertical_right,
398            set.vertical_left,
399            set.vertical_right,
400            set.bottom_left,
401            set.horizontal_bottom,
402            set.horizontal_bottom,
403            set.bottom_right
404        )
405    }
406
407    #[test]
408    fn border_set_from_line_set() {
409        let custom_line_set = line::Set {
410            top_left: "a",
411            top_right: "b",
412            bottom_left: "c",
413            bottom_right: "d",
414            vertical: "e",
415            horizontal: "f",
416            vertical_left: "g",
417            vertical_right: "h",
418            horizontal_down: "i",
419            horizontal_up: "j",
420            cross: "k",
421        };
422
423        let border_set = from_line_set(custom_line_set);
424
425        assert_eq!(
426            border_set,
427            Set {
428                top_left: "a",
429                top_right: "b",
430                bottom_left: "c",
431                bottom_right: "d",
432                vertical_left: "e",
433                vertical_right: "e",
434                horizontal_bottom: "f",
435                horizontal_top: "f",
436            }
437        );
438    }
439
440    #[test]
441    fn plain() {
442        assert_eq!(
443            render(PLAIN),
444            indoc!(
445                "░░░░░░
446                 ░┌──┐░
447                 ░│░░│░
448                 ░│░░│░
449                 ░└──┘░
450                 ░░░░░░"
451            )
452        );
453    }
454
455    #[test]
456    fn rounded() {
457        assert_eq!(
458            render(ROUNDED),
459            indoc!(
460                "░░░░░░
461                 ░╭──╮░
462                 ░│░░│░
463                 ░│░░│░
464                 ░╰──╯░
465                 ░░░░░░"
466            )
467        );
468    }
469
470    #[test]
471    fn double() {
472        assert_eq!(
473            render(DOUBLE),
474            indoc!(
475                "░░░░░░
476                 ░╔══╗░
477                 ░║░░║░
478                 ░║░░║░
479                 ░╚══╝░
480                 ░░░░░░"
481            )
482        );
483    }
484
485    #[test]
486    fn thick() {
487        assert_eq!(
488            render(THICK),
489            indoc!(
490                "░░░░░░
491                 ░┏━━┓░
492                 ░┃░░┃░
493                 ░┃░░┃░
494                 ░┗━━┛░
495                 ░░░░░░"
496            )
497        );
498    }
499
500    #[test]
501    fn light_double_dashed() {
502        assert_eq!(
503            render(LIGHT_DOUBLE_DASHED),
504            indoc!(
505                "░░░░░░
506                 ░┌╌╌┐░
507                 ░╎░░╎░
508                 ░╎░░╎░
509                 ░└╌╌┘░
510                 ░░░░░░"
511            )
512        );
513    }
514
515    #[test]
516    fn heavy_double_dashed() {
517        assert_eq!(
518            render(HEAVY_DOUBLE_DASHED),
519            indoc!(
520                "░░░░░░
521                 ░┏╍╍┓░
522                 ░╏░░╏░
523                 ░╏░░╏░
524                 ░┗╍╍┛░
525                 ░░░░░░"
526            )
527        );
528    }
529
530    #[test]
531    fn light_triple_dashed() {
532        assert_eq!(
533            render(LIGHT_TRIPLE_DASHED),
534            indoc!(
535                "░░░░░░
536                 ░┌┄┄┐░
537                 ░┆░░┆░
538                 ░┆░░┆░
539                 ░└┄┄┘░
540                 ░░░░░░"
541            )
542        );
543    }
544
545    #[test]
546    fn heavy_triple_dashed() {
547        assert_eq!(
548            render(HEAVY_TRIPLE_DASHED),
549            indoc!(
550                "░░░░░░
551                 ░┏┅┅┓░
552                 ░┇░░┇░
553                 ░┇░░┇░
554                 ░┗┅┅┛░
555                 ░░░░░░"
556            )
557        );
558    }
559
560    #[test]
561    fn light_quadruple_dashed() {
562        assert_eq!(
563            render(LIGHT_QUADRUPLE_DASHED),
564            indoc!(
565                "░░░░░░
566                 ░┌┈┈┐░
567                 ░┊░░┊░
568                 ░┊░░┊░
569                 ░└┈┈┘░
570                 ░░░░░░"
571            )
572        );
573    }
574
575    #[test]
576    fn heavy_quadruple_dashed() {
577        assert_eq!(
578            render(HEAVY_QUADRUPLE_DASHED),
579            indoc!(
580                "░░░░░░
581                 ░┏┉┉┓░
582                 ░┋░░┋░
583                 ░┋░░┋░
584                 ░┗┉┉┛░
585                 ░░░░░░"
586            )
587        );
588    }
589
590    #[test]
591    fn quadrant_outside() {
592        assert_eq!(
593            render(QUADRANT_OUTSIDE),
594            indoc!(
595                "░░░░░░
596                 ░▛▀▀▜░
597                 ░▌░░▐░
598                 ░▌░░▐░
599                 ░▙▄▄▟░
600                 ░░░░░░"
601            )
602        );
603    }
604
605    #[test]
606    fn quadrant_inside() {
607        assert_eq!(
608            render(QUADRANT_INSIDE),
609            indoc!(
610                "░░░░░░
611                 ░▗▄▄▖░
612                 ░▐░░▌░
613                 ░▐░░▌░
614                 ░▝▀▀▘░
615                 ░░░░░░"
616            )
617        );
618    }
619
620    #[test]
621    fn one_eighth_wide() {
622        assert_eq!(
623            render(ONE_EIGHTH_WIDE),
624            indoc!(
625                "░░░░░░
626                 ░▁▁▁▁░
627                 ░▏░░▕░
628                 ░▏░░▕░
629                 ░▔▔▔▔░
630                 ░░░░░░"
631            )
632        );
633    }
634
635    #[test]
636    fn one_eighth_tall() {
637        assert_eq!(
638            render(ONE_EIGHTH_TALL),
639            indoc!(
640                "░░░░░░
641                 ░▕▔▔▏░
642                 ░▕░░▏░
643                 ░▕░░▏░
644                 ░▕▁▁▏░
645                 ░░░░░░"
646            )
647        );
648    }
649
650    #[test]
651    fn proportional_wide() {
652        assert_eq!(
653            render(PROPORTIONAL_WIDE),
654            indoc!(
655                "░░░░░░
656                 ░▄▄▄▄░
657                 ░█░░█░
658                 ░█░░█░
659                 ░▀▀▀▀░
660                 ░░░░░░"
661            )
662        );
663    }
664
665    #[test]
666    fn proportional_tall() {
667        assert_eq!(
668            render(PROPORTIONAL_TALL),
669            indoc!(
670                "░░░░░░
671                 ░█▀▀█░
672                 ░█░░█░
673                 ░█░░█░
674                 ░█▄▄█░
675                 ░░░░░░"
676            )
677        );
678    }
679
680    #[test]
681    fn full() {
682        assert_eq!(
683            render(FULL),
684            indoc!(
685                "░░░░░░
686                 ░████░
687                 ░█░░█░
688                 ░█░░█░
689                 ░████░
690                 ░░░░░░"
691            )
692        );
693    }
694
695    #[test]
696    fn empty() {
697        assert_eq!(
698            render(EMPTY),
699            indoc!(
700                "░░░░░░
701                 ░    ░
702                 ░ ░░ ░
703                 ░ ░░ ░
704                 ░    ░
705                 ░░░░░░"
706            )
707        );
708    }
709}