From 1ee52ed7fcb467982ad403d3d88c10b672dff26d Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Sat, 23 Sep 2023 22:08:32 -0700 Subject: [PATCH] feat(block)!: allow custom symbols for borders (#529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `Block::border_set` method that allows the user to specify the symbols used for the border. Added two new border types: `BorderType::QuadrantOutside` and `BorderType::QuadrantInside`. These are used to draw borders using the unicode quadrant characters (which look like half block "pixels"). QuadrantOutside: ``` ▛▀▀▜ ▌ ▐ ▙▄▄▟ ``` QuadrantInside: ``` ▗▄▄▖ ▐ ▌ ▝▀▀▘ ``` Fixes: https://github.com/ratatui-org/ratatui/issues/528 BREAKING CHANGES: - BorderType::to_line_set is renamed to to_border_set - BorderType::line_symbols is renamed to border_symbols --- src/symbols.rs | 150 +++++++++++++++++++++++++++++++++++++++- src/widgets/block.rs | 160 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 289 insertions(+), 21 deletions(-) diff --git a/src/symbols.rs b/src/symbols.rs index 1b99fbfc29..815cdbb618 100644 --- a/src/symbols.rs +++ b/src/symbols.rs @@ -157,7 +157,7 @@ pub mod line { pub const DOUBLE_CROSS: &str = "╬"; pub const THICK_CROSS: &str = "╋"; - #[derive(Debug, Clone, Eq, PartialEq, Hash)] + #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub struct Set { pub vertical: &'static str, pub horizontal: &'static str, @@ -229,6 +229,154 @@ pub mod line { }; } +pub mod border { + use super::line; + + #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] + pub struct Set { + pub top_left: &'static str, + pub top_right: &'static str, + pub bottom_left: &'static str, + pub bottom_right: &'static str, + pub vertical_left: &'static str, + pub vertical_right: &'static str, + pub horizontal_top: &'static str, + pub horizontal_bottom: &'static str, + } + + impl Default for Set { + fn default() -> Self { + PLAIN + } + } + + /// Border Set with a single line width + /// + /// ```text + /// ┌─────┐ + /// │xxxxx│ + /// │xxxxx│ + /// └─────┘ + pub const PLAIN: Set = Set { + top_left: line::NORMAL.top_left, + top_right: line::NORMAL.top_right, + bottom_left: line::NORMAL.bottom_left, + bottom_right: line::NORMAL.bottom_right, + vertical_left: line::NORMAL.vertical, + vertical_right: line::NORMAL.vertical, + horizontal_top: line::NORMAL.horizontal, + horizontal_bottom: line::NORMAL.horizontal, + }; + + /// Border Set with a single line width and rounded corners + /// + /// ```text + /// ╭─────╮ + /// │xxxxx│ + /// │xxxxx│ + /// ╰─────╯ + pub const ROUNDED: Set = Set { + top_left: line::ROUNDED.top_left, + top_right: line::ROUNDED.top_right, + bottom_left: line::ROUNDED.bottom_left, + bottom_right: line::ROUNDED.bottom_right, + vertical_left: line::ROUNDED.vertical, + vertical_right: line::ROUNDED.vertical, + horizontal_top: line::ROUNDED.horizontal, + horizontal_bottom: line::ROUNDED.horizontal, + }; + + /// Border Set with a double line width + /// + /// ```text + /// ╔═════╗ + /// ║xxxxx║ + /// ║xxxxx║ + /// ╚═════╝ + pub const DOUBLE: Set = Set { + top_left: line::DOUBLE.top_left, + top_right: line::DOUBLE.top_right, + bottom_left: line::DOUBLE.bottom_left, + bottom_right: line::DOUBLE.bottom_right, + vertical_left: line::DOUBLE.vertical, + vertical_right: line::DOUBLE.vertical, + horizontal_top: line::DOUBLE.horizontal, + horizontal_bottom: line::DOUBLE.horizontal, + }; + + /// Border Set with a thick line width + /// + /// ```text + /// ┏━━━━━┓ + /// ┃xxxxx┃ + /// ┃xxxxx┃ + /// ┗━━━━━┛ + pub const THICK: Set = Set { + top_left: line::THICK.top_left, + top_right: line::THICK.top_right, + bottom_left: line::THICK.bottom_left, + bottom_right: line::THICK.bottom_right, + vertical_left: line::THICK.vertical, + vertical_right: line::THICK.vertical, + horizontal_top: line::THICK.horizontal, + horizontal_bottom: line::THICK.horizontal, + }; + + pub const QUADRANT_TOP_LEFT: &str = "▘"; + pub const QUADRANT_TOP_RIGHT: &str = "▝"; + pub const QUADRANT_BOTTOM_LEFT: &str = "▖"; + pub const QUADRANT_BOTTOM_RIGHT: &str = "▗"; + pub const QUADRANT_TOP_HALF: &str = "▀"; + pub const QUADRANT_BOTTOM_HALF: &str = "▄"; + pub const QUADRANT_LEFT_HALF: &str = "▌"; + pub const QUADRANT_RIGHT_HALF: &str = "▐"; + pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▙"; + pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "▛"; + pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "▜"; + pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▟"; + pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "▚"; + pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "▞"; + pub const QUADRANT_BLOCK: &str = "█"; + + /// Quadrant used for setting a border outside a block by one half cell "pixel". + /// + /// ```text + /// ▛▀▀▀▀▀▜ + /// ▌xxxxx▐ + /// ▌xxxxx▐ + /// ▙▄▄▄▄▄▟ + /// ``` + pub const QUADRANT_OUTSIDE: Set = Set { + top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT, + top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT, + bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT, + bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT, + vertical_left: QUADRANT_LEFT_HALF, + vertical_right: QUADRANT_RIGHT_HALF, + horizontal_top: QUADRANT_TOP_HALF, + horizontal_bottom: QUADRANT_BOTTOM_HALF, + }; + + /// Quadrant used for setting a border inside a block by one half cell "pixel". + /// + /// ```text + /// ▗▄▄▄▄▄▖ + /// ▐xxxxx▌ + /// ▐xxxxx▌ + /// ▝▀▀▀▀▀▘ + /// ``` + pub const QUADRANT_INSIDE: Set = Set { + top_right: QUADRANT_BOTTOM_LEFT, + top_left: QUADRANT_BOTTOM_RIGHT, + bottom_right: QUADRANT_TOP_LEFT, + bottom_left: QUADRANT_TOP_RIGHT, + vertical_left: QUADRANT_RIGHT_HALF, + vertical_right: QUADRANT_LEFT_HALF, + horizontal_top: QUADRANT_BOTTOM_HALF, + horizontal_bottom: QUADRANT_TOP_HALF, + }; +} + pub const DOT: &str = "•"; pub mod braille { diff --git a/src/widgets/block.rs b/src/widgets/block.rs index b2cb83e7e2..d8f3a9a400 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -16,7 +16,7 @@ use crate::{ buffer::Buffer, layout::{Alignment, Rect}, style::{Style, Styled}, - symbols::line, + symbols::border, widgets::{Borders, Widget}, }; @@ -70,18 +70,46 @@ pub enum BorderType { /// ┗━━━━━━━┛ /// ``` Thick, + /// A border with a single line on the inside of a half block. + /// + /// # Example + /// + /// ```plain + /// ▗▄▄▄▄▄▄▄▖ + /// ▐ ▌ + /// ▐ ▌ + /// ▝▀▀▀▀▀▀▀▘ + QuadrantInside, + + /// A border with a single line on the outside of a half block. + /// + /// # Example + /// + /// ```plain + /// ▛▀▀▀▀▀▀▀▜ + /// ▌ ▐ + /// ▌ ▐ + /// ▙▄▄▄▄▄▄▄▟ + QuadrantOutside, } impl BorderType { - /// Convert this `BorderType` into the corresponding [`Set`](line::Set) of lines. - pub const fn line_symbols(border_type: BorderType) -> line::Set { + /// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols. + pub const fn border_symbols(border_type: BorderType) -> border::Set { match border_type { - BorderType::Plain => line::NORMAL, - BorderType::Rounded => line::ROUNDED, - BorderType::Double => line::DOUBLE, - BorderType::Thick => line::THICK, + BorderType::Plain => border::PLAIN, + BorderType::Rounded => border::ROUNDED, + BorderType::Double => border::DOUBLE, + BorderType::Thick => border::THICK, + BorderType::QuadrantInside => border::QUADRANT_INSIDE, + BorderType::QuadrantOutside => border::QUADRANT_OUTSIDE, } } + + /// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols. + pub const fn to_border_set(self) -> border::Set { + Self::border_symbols(self) + } } /// Defines the padding of a [`Block`]. @@ -213,10 +241,9 @@ pub struct Block<'a> { borders: Borders, /// Border style border_style: Style, - /// Type of the border. The default is plain lines but one can choose to have rounded or - /// doubled lines instead. - border_type: BorderType, - + /// The symbols used to render the border. The default is plain lines but one can choose to + /// have rounded or doubled lines instead or a custom set of symbols + border_set: border::Set, /// Widget style style: Style, /// Block padding @@ -233,7 +260,7 @@ impl<'a> Block<'a> { titles_position: Position::Top, borders: Borders::NONE, border_style: Style::new(), - border_type: BorderType::Plain, + border_set: BorderType::Plain.to_border_set(), style: Style::new(), padding: Padding::zero(), } @@ -414,9 +441,40 @@ impl<'a> Block<'a> { /// Sets the symbols used to display the border (e.g. single line, double line, thick or /// rounded borders). /// + /// Setting this overwrites any custom [`border_set`](Block::border_set) that was set. + /// /// See [`BorderType`] for the full list of available symbols. + /// + /// # Examples + /// + /// ``` + /// # use ratatui::{prelude::*, widgets::*}; + /// Block::default().title("Block").borders(Borders::ALL).border_type(BorderType::Rounded); + /// // Renders + /// // ╭Block╮ + /// // │ │ + /// // ╰─────╯ + /// ``` pub const fn border_type(mut self, border_type: BorderType) -> Block<'a> { - self.border_type = border_type; + self.border_set = border_type.to_border_set(); + self + } + + /// Sets the symbols used to display the border as a [`crate::symbols::border::Set`]. + /// + /// Setting this overwrites any [`border_type`](Block::border_type) that was set. + /// + /// # Examples + /// + /// ``` + /// # use ratatui::{prelude::*, widgets::*}; + /// Block::default().title("Block").borders(Borders::ALL).border_set(symbols::border::DOUBLE); + /// // Renders + /// // ╔Block╗ + /// // ║ ║ + /// // ╚═════╝ + pub const fn border_set(mut self, border_set: border::Set) -> Block<'a> { + self.border_set = border_set; self } @@ -511,20 +569,20 @@ impl<'a> Block<'a> { fn render_borders(&self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); - let symbols = BorderType::line_symbols(self.border_type); + let symbols = self.border_set; // Sides if self.borders.intersects(Borders::LEFT) { for y in area.top()..area.bottom() { buf.get_mut(area.left(), y) - .set_symbol(symbols.vertical) + .set_symbol(symbols.vertical_left) .set_style(self.border_style); } } if self.borders.intersects(Borders::TOP) { for x in area.left()..area.right() { buf.get_mut(x, area.top()) - .set_symbol(symbols.horizontal) + .set_symbol(symbols.horizontal_top) .set_style(self.border_style); } } @@ -532,7 +590,7 @@ impl<'a> Block<'a> { let x = area.right() - 1; for y in area.top()..area.bottom() { buf.get_mut(x, y) - .set_symbol(symbols.vertical) + .set_symbol(symbols.vertical_right) .set_style(self.border_style); } } @@ -540,7 +598,7 @@ impl<'a> Block<'a> { let y = area.bottom() - 1; for x in area.left()..area.right() { buf.get_mut(x, y) - .set_symbol(symbols.horizontal) + .set_symbol(symbols.horizontal_bottom) .set_style(self.border_style); } } @@ -883,7 +941,7 @@ mod tests { #[test] fn border_type_can_be_const() { - const _PLAIN: line::Set = BorderType::line_symbols(BorderType::Plain); + const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain); } #[test] @@ -927,7 +985,7 @@ mod tests { titles_position: Position::Top, borders: Borders::NONE, border_style: Style::new(), - border_type: BorderType::Plain, + border_set: BorderType::Plain.to_border_set(), style: Style::new(), padding: Padding::zero(), } @@ -1119,6 +1177,7 @@ mod tests { ]) ); } + #[test] fn render_rounded_border() { let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); @@ -1135,6 +1194,7 @@ mod tests { ]) ); } + #[test] fn render_double_border() { let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); @@ -1152,6 +1212,40 @@ mod tests { ); } + #[test] + fn render_quadrant_inside() { + let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::QuadrantInside) + .render(buffer.area, &mut buffer); + assert_buffer_eq!( + buffer, + Buffer::with_lines(vec![ + "▗▄▄▄▄▄▄▄▄▄▄▄▄▄▖", + "▐ ▌", + "▝▀▀▀▀▀▀▀▀▀▀▀▀▀▘", + ]) + ); + } + + #[test] + fn render_border_quadrant_outside() { + let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::QuadrantOutside) + .render(buffer.area, &mut buffer); + assert_buffer_eq!( + buffer, + Buffer::with_lines(vec![ + "▛▀▀▀▀▀▀▀▀▀▀▀▀▀▜", + "▌ ▐", + "▙▄▄▄▄▄▄▄▄▄▄▄▄▄▟", + ]) + ); + } + #[test] fn render_solid_border() { let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); @@ -1168,4 +1262,30 @@ mod tests { ]) ); } + + #[test] + fn render_custom_border_set() { + let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); + Block::default() + .borders(Borders::ALL) + .border_set(border::Set { + top_left: "1", + top_right: "2", + bottom_left: "3", + bottom_right: "4", + vertical_left: "L", + vertical_right: "R", + horizontal_top: "T", + horizontal_bottom: "B", + }) + .render(buffer.area, &mut buffer); + assert_buffer_eq!( + buffer, + Buffer::with_lines(vec![ + "1TTTTTTTTTTTTT2", + "L R", + "3BBBBBBBBBBBBB4", + ]) + ); + } }