mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-26 20:40:44 +00:00
feat: support merging the borders of blocks (#1874)
When two borders overlap, they will automatically merge into a single, clean border instead of overlapping. This improves visual clarity and reduces rendering glitches around corners. For example: ``` assert_eq!(Cell::new("┘").merge_symbol("┏", MergeStrategy::Exact).symbol(), "╆"); ``` Co-authored-by: pauladam94 <poladam2002@gmail.com> Co-authored-by: Paul Adam <65903440+pauladam94@users.noreply.github.com> Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com> Co-authored-by: Orhun Parmaksız <orhun@archlinux.org> Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
parent
12cb5a28fe
commit
671c2b4fd4
@ -1,6 +1,7 @@
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
use crate::symbols::merge::MergeStrategy;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@ -61,6 +62,46 @@ impl Cell {
|
||||
self.symbol.as_str()
|
||||
}
|
||||
|
||||
/// Merges the symbol of the cell with the one already on the cell, using the provided
|
||||
/// [`MergeStrategy`].
|
||||
///
|
||||
/// Merges [Box Drawing Unicode block] characters to create a single character representing
|
||||
/// their combination, useful for [border collapsing]. Currently limited to box drawing
|
||||
/// characters, with potential future support for others.
|
||||
///
|
||||
/// Merging may not be perfect due to Unicode limitations; some symbol combinations might not
|
||||
/// produce a valid character. [`MergeStrategy`] defines how to handle such cases, e.g.,
|
||||
/// `Exact` for valid merges only, or `Fuzzy` for close matches.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui_core::buffer::Cell;
|
||||
/// use ratatui_core::symbols::merge::MergeStrategy;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Cell::new("┘")
|
||||
/// .merge_symbol("┏", MergeStrategy::Exact)
|
||||
/// .symbol(),
|
||||
/// "╆",
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Cell::new("╭")
|
||||
/// .merge_symbol("┘", MergeStrategy::Fuzzy)
|
||||
/// .symbol(),
|
||||
/// "┼",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [border collapsing]: https://ratatui.rs/recipes/layout/collapse-borders/
|
||||
/// [Box Drawing Unicode block]: https://en.wikipedia.org/wiki/Box_Drawing
|
||||
pub fn merge_symbol(&mut self, symbol: &str, strategy: MergeStrategy) -> &mut Self {
|
||||
let merged = strategy.merge(self.symbol(), symbol);
|
||||
self.symbol = CompactString::new(merged);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
|
||||
self.symbol = CompactString::new(symbol);
|
||||
|
@ -9,5 +9,6 @@ pub mod braille;
|
||||
pub mod half_block;
|
||||
pub mod line;
|
||||
pub mod marker;
|
||||
pub mod merge;
|
||||
pub mod scrollbar;
|
||||
pub mod shade;
|
||||
|
738
ratatui-core/src/symbols/merge.rs
Normal file
738
ratatui-core/src/symbols/merge.rs
Normal file
@ -0,0 +1,738 @@
|
||||
//! This module provides strategies for merging symbols in a layout.
|
||||
//!
|
||||
//! It defines the [`MergeStrategy`] enum, which allows for different behaviors when combining
|
||||
//! symbols, such as replacing the previous symbol, merging them if an exact match exists, or using
|
||||
//! a fuzzy match to find the closest representation.
|
||||
//!
|
||||
//! The merging strategies are useful for [collapsing borders] in layouts, where multiple symbols
|
||||
//! may need to be combined to create a single, coherent border representation.
|
||||
//!
|
||||
//! [collapsing borders]: https://ratatui.rs/recipes/layout/collapse-borders
|
||||
use core::str::FromStr;
|
||||
|
||||
/// A strategy for merging two symbols into one.
|
||||
///
|
||||
/// This enum defines how two symbols should be merged together, allowing for different behaviors
|
||||
/// when combining symbols, such as replacing the previous symbol, merging them if an exact match
|
||||
/// exists, or using a fuzzy match to find the closest representation.
|
||||
///
|
||||
/// This is useful for [collapsing borders] in layouts, where multiple symbols may need to be
|
||||
/// combined to create a single, coherent border representation.
|
||||
///
|
||||
/// Not all combinations of box drawing symbols can be represented as a single unicode character, as
|
||||
/// many of them are not defined in the [Box Drawing Unicode block]. This means that some merging
|
||||
/// strategies will not yield a valid unicode character. The [`MergeStrategy::Replace`] strategy
|
||||
/// will be used as a fallback in such cases, replacing the previous symbol with the next one.
|
||||
///
|
||||
/// Specifically, the following combinations of box drawing symbols are not defined in the [Box
|
||||
/// Drawing Unicode block]:
|
||||
///
|
||||
/// - Combining any dashed segments with any non dashed segments (e.g. `╎` with `─` or `━`).
|
||||
/// - Combining any rounded segments with any other segments (e.g. `╯` with `─` or `━`).
|
||||
/// - Combining any double segments with any thick segments (e.g. `═` with `┃` or `━`).
|
||||
/// - Combining some double segments with some plain segments (e.g. `┐` with `╔`).
|
||||
///
|
||||
/// The merging strategies include:
|
||||
///
|
||||
/// - [`Self::Replace`]: Replaces the previous symbol with the next one.
|
||||
/// - [`Self::Exact`]: Merges symbols only if an exact composite unicode character exists, falling
|
||||
/// back to [`Self::Replace`] if not.
|
||||
/// - [`Self::Fuzzy`]: Merges symbols even if an exact composite unicode character doesn't exist,
|
||||
/// using the closest match, and falling back to [`Self::Exact`] if necessary.
|
||||
///
|
||||
/// See [`Cell::merge_symbol`] for how to use this strategy in practice, and
|
||||
/// [`Block::merge_borders`] for a more concrete example of merging borders in a layout.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui_core::symbols::merge::MergeStrategy;
|
||||
///
|
||||
/// assert_eq!(MergeStrategy::Replace.merge("│", "━"), "━");
|
||||
/// assert_eq!(MergeStrategy::Exact.merge("│", "─"), "┼");
|
||||
/// assert_eq!(MergeStrategy::Fuzzy.merge("┘", "╔"), "╬");
|
||||
/// ```
|
||||
///
|
||||
/// [Box Drawing Unicode block]: https://en.wikipedia.org/wiki/Box_Drawing
|
||||
/// [collapsing borders]: https://ratatui.rs/recipes/layout/collapse-borders
|
||||
/// [`Block::merge_borders`]:
|
||||
/// https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html#method.merge_borders
|
||||
/// [`Cell::merge_symbol`]: crate::buffer::Cell::merge_symbol
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum MergeStrategy {
|
||||
/// Replaces the previous symbol with the next one.
|
||||
///
|
||||
/// This strategy simply replaces the previous symbol with the next one, without attempting to
|
||||
/// merge them. This is useful when you want to ensure that the last rendered symbol takes
|
||||
/// precedence over the previous one, regardless of their compatibility.
|
||||
///
|
||||
/// The following diagram illustrates how this would apply to several overlapping blocks where
|
||||
/// the thick bordered blocks are rendered last, replacing the previous symbols:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───┐ ┌───┐ ┌───┏━━━┓┌───┐
|
||||
/// │ │ │ │ │ ┃ ┃│ │
|
||||
/// │ │ │ ┏━━━┓│ ┃ ┃│ │
|
||||
/// │ │ │ ┃ │ ┃│ ┃ ┃│ │
|
||||
/// └───┏━━━┓└─┃─┘ ┃└───┗━━━┛┏━━━┓
|
||||
/// ┃ ┃ ┃ ┃ ┃ ┃
|
||||
/// ┃ ┃ ┗━━━┛ ┃ ┃
|
||||
/// ┃ ┃ ┃ ┃
|
||||
/// ┗━━━┛ ┗━━━┛
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui_core::symbols::merge::MergeStrategy;
|
||||
/// let strategy = MergeStrategy::Replace;
|
||||
/// assert_eq!(strategy.merge("│", "━"), "━");
|
||||
/// ```
|
||||
#[default]
|
||||
Replace,
|
||||
|
||||
/// Merges symbols only if an exact composite unicode character exists.
|
||||
///
|
||||
/// This strategy attempts to merge two symbols into a single composite unicode character if the
|
||||
/// exact representation exists. If the required unicode symbol does not exist, it falls back to
|
||||
/// [`MergeStrategy::Replace`], replacing the previous symbol with the next one.
|
||||
///
|
||||
/// The following diagram illustrates how this would apply to several overlapping blocks where
|
||||
/// the thick bordered blocks are rendered last, merging the previous symbols into a single
|
||||
/// composite character. All combindations of the plain and thick segments exist, so these
|
||||
/// symbols can be merged into a single character:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───┐ ┌───┐ ┌───┲━━━┓┌───┐
|
||||
/// │ │ │ │ │ ┃ ┃│ │
|
||||
/// │ │ │ ┏━┿━┓│ ┃ ┃│ │
|
||||
/// │ │ │ ┃ │ ┃│ ┃ ┃│ │
|
||||
/// └───╆━━━┓└─╂─┘ ┃└───┺━━━┛┢━━━┪
|
||||
/// ┃ ┃ ┃ ┃ ┃ ┃
|
||||
/// ┃ ┃ ┗━━━┛ ┃ ┃
|
||||
/// ┃ ┃ ┃ ┃
|
||||
/// ┗━━━┛ ┗━━━┛
|
||||
/// ```
|
||||
///
|
||||
/// The following diagram illustrates how this would apply to several overlapping blocks where
|
||||
/// the characters don't have a composite unicode character, so the previous symbols are
|
||||
/// replaced by the next one:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───┐ ┌───┐ ┌───╔═══╗┌───┐
|
||||
/// │ │ │ │ │ ║ ║│ │
|
||||
/// │ │ │ ╔═╪═╗│ ║ ║│ │
|
||||
/// │ │ │ ║ │ ║│ ║ ║│ │
|
||||
/// └───╔═══╗└─╫─┘ ║└───╚═══╝╔═══╗
|
||||
/// ║ ║ ║ ║ ║ ║
|
||||
/// ║ ║ ╚═══╝ ║ ║
|
||||
/// ║ ║ ║ ║
|
||||
/// ╚═══╝ ╚═══╝
|
||||
/// ┌───┐ ┌───┐ ┌───╭───╮┌───┐
|
||||
/// │ │ │ │ │ │ ││ │
|
||||
/// │ │ │ ╭─┼─╮│ │ ││ │
|
||||
/// │ │ │ │ │ ││ │ ││ │
|
||||
/// └───╭───╮└─┼─┘ │└───╰───╯╭───╮
|
||||
/// │ │ │ │ │ │
|
||||
/// │ │ ╰───╯ │ │
|
||||
/// │ │ │ │
|
||||
/// ╰───╯ ╰───╯
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui_core::symbols::merge::MergeStrategy;
|
||||
/// let strategy = MergeStrategy::Exact;
|
||||
/// assert_eq!(strategy.merge("│", "━"), "┿"); // exact match exists
|
||||
/// assert_eq!(strategy.merge("┘", "╔"), "╔"); // no exact match, falls back to Replace
|
||||
/// ```
|
||||
Exact,
|
||||
|
||||
/// Merges symbols even if an exact composite unicode character doesn't exist, using the closest
|
||||
/// match.
|
||||
///
|
||||
/// If required unicode symbol exists, acts exactly like [`MergeStrategy::Exact`], if not, the
|
||||
/// following rules are applied:
|
||||
///
|
||||
/// 1. There are no characters that combine dashed with plain / thick segments, so we replace
|
||||
/// dashed segments with plain and thick dashed segments with thick. The following diagram
|
||||
/// shows how this would apply to merging a block with thick dashed borders over a block with
|
||||
/// plain dashed borders:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌╌╌╌┐ ┌╌╌╌┐ ┌╌╌╌┲╍╍╍┓┌╌╌╌┐
|
||||
/// ╎ ╎ ╎ ╎ ╎ ╏ ╏╎ ╎
|
||||
/// ╎ ╎ ╎ ┏╍┿╍┓╎ ╏ ╏╎ ╎
|
||||
/// ╎ ╎ ╎ ╏ ╎ ╏╎ ╏ ╏╎ ╎
|
||||
/// └╌╌╌╆╍╍╍┓└╌╂╌┘ ╏└╌╌╌┺╍╍╍┛┢╍╍╍┪
|
||||
/// ╏ ╏ ╏ ╏ ╏ ╏
|
||||
/// ╏ ╏ ┗╍╍╍┛ ╏ ╏
|
||||
/// ╏ ╏ ╏ ╏
|
||||
/// ┗╍╍╍┛ ┗╍╍╍┛
|
||||
/// ```
|
||||
///
|
||||
/// 2. There are no characters that combine rounded segments with other segments, so we replace
|
||||
/// rounded segments with plain. The following diagram shows how this would apply to merging
|
||||
/// a block with rounded corners over a block with plain corners:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───┐ ┌───┐ ┌───┬───╮┌───┐
|
||||
/// │ │ │ │ │ │ ││ │
|
||||
/// │ │ │ ╭─┼─╮│ │ ││ │
|
||||
/// │ │ │ │ │ ││ │ ││ │
|
||||
/// └───┼───╮└─┼─┘ │└───┴───╯├───┤
|
||||
/// │ │ │ │ │ │
|
||||
/// │ │ ╰───╯ │ │
|
||||
/// │ │ │ │
|
||||
/// ╰───╯ ╰───╯
|
||||
/// ```
|
||||
///
|
||||
/// 3. There are no symbols that combine thick and double borders, so we replace all double
|
||||
/// segments with thick or all thick with double. The second symbol parameter takes
|
||||
/// precedence in choosing whether to use double or thick. The following diagram shows how
|
||||
/// this would apply to merging a block with double borders over a block with thick borders
|
||||
/// and then the reverse (merging a block with thick borders over a block with double
|
||||
/// borders):
|
||||
///
|
||||
/// ```text
|
||||
/// ┏━━━┓ ┏━━━┓ ┏━━━╦═══╗┏━━━┓
|
||||
/// ┃ ┃ ┃ ┃ ┃ ║ ║┃ ┃
|
||||
/// ┃ ┃ ┃ ╔═╬═╗┃ ║ ║┃ ┃
|
||||
/// ┃ ┃ ┃ ║ ┃ ║┃ ║ ║┃ ┃
|
||||
/// ┗━━━╬═══╗┗━╬━┛ ║┗━━━╩═══╝╠═══╣
|
||||
/// ║ ║ ║ ║ ║ ║
|
||||
/// ║ ║ ╚═══╝ ║ ║
|
||||
/// ║ ║ ║ ║
|
||||
/// ╚═══╝ ╚═══╝
|
||||
///
|
||||
/// ╔═══╗ ╔═══╗ ╔═══┳━━━┓╔═══╗
|
||||
/// ║ ║ ║ ║ ║ ┃ ┃║ ║
|
||||
/// ║ ║ ║ ┏━╋━┓║ ┃ ┃║ ║
|
||||
/// ║ ║ ║ ┃ ║ ┃║ ┃ ┃║ ║
|
||||
/// ╚═══╋━━━┓╚═╋═╝ ┃╚═══┻━━━┛┣━━━┫
|
||||
/// ┃ ┃ ┃ ┃ ┃ ┃
|
||||
/// ┃ ┃ ┗━━━┛ ┃ ┃
|
||||
/// ┃ ┃ ┃ ┃
|
||||
/// ┗━━━┛ ┗━━━┛
|
||||
/// ```
|
||||
///
|
||||
/// 4. Some combinations of double and plain don't exist, so if the symbol is still
|
||||
/// unrepresentable, change all plain segments with double or all double with plain. The
|
||||
/// second symbol parameter takes precedence in choosing whether to use double or plain. The
|
||||
/// following diagram shows how this would apply to merging a block with double borders over
|
||||
/// a block with plain borders and then the reverse (merging a block with plain borders over
|
||||
/// a block with double borders):
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───┐ ┌───┐ ┌───╦═══╗┌───┐
|
||||
/// │ │ │ │ │ ║ ║│ │
|
||||
/// │ │ │ ╔═╪═╗│ ║ ║│ │
|
||||
/// │ │ │ ║ │ ║│ ║ ║│ │
|
||||
/// └───╬═══╗└─╫─┘ ║└───╩═══╝╠═══╣
|
||||
/// ║ ║ ║ ║ ║ ║
|
||||
/// ║ ║ ╚═══╝ ║ ║
|
||||
/// ║ ║ ║ ║
|
||||
/// ╚═══╝ ╚═══╝
|
||||
/// ╔═══╗ ╔═══╗ ╔═══┬───┐╔═══╗
|
||||
/// ║ ║ ║ ║ ║ │ │║ ║
|
||||
/// ║ ║ ║ ┌─╫─┐║ │ │║ ║
|
||||
/// ║ ║ ║ │ ║ │║ │ │║ ║
|
||||
/// ╚═══┼───┐╚═╪═╝ │╚═══┴───┘├───┤
|
||||
/// │ │ │ │ │ │
|
||||
/// │ │ └───┘ │ │
|
||||
/// │ │ │ │
|
||||
/// └───┘ └───┘
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui_core::symbols::merge::MergeStrategy;
|
||||
/// let strategy = MergeStrategy::Fuzzy;
|
||||
///
|
||||
/// // exact matches are merged normally
|
||||
/// assert_eq!(strategy.merge("┌", "┐"), "┬");
|
||||
///
|
||||
/// // dashed segments are replaced with plain
|
||||
/// assert_eq!(strategy.merge("╎", "╍"), "┿");
|
||||
///
|
||||
/// // rounded segments are replaced with plain
|
||||
/// assert_eq!(strategy.merge("┘", "╭"), "┼");
|
||||
///
|
||||
/// // double and thick segments are merged based on the second symbol
|
||||
/// assert_eq!(strategy.merge("┃", "═"), "╬");
|
||||
/// assert_eq!(strategy.merge("═", "┃"), "╋");
|
||||
///
|
||||
/// // combindations of double with plain that don't exist are merged based on the second symbol
|
||||
/// assert_eq!(strategy.merge("┐", "╔"), "╦");
|
||||
/// assert_eq!(strategy.merge("╔", "┐"), "┬");
|
||||
/// ```
|
||||
Fuzzy,
|
||||
}
|
||||
|
||||
impl MergeStrategy {
|
||||
/// Merges two symbols using this merge strategy.
|
||||
///
|
||||
/// This method takes two string slices representing the previous and next symbols, and
|
||||
/// returns a string slice representing the merged symbol based on the merge strategy.
|
||||
///
|
||||
/// If either of the symbols are not in the [Box Drawing Unicode block], the `next` symbol is
|
||||
/// returned as is. If both symbols are valid, they are merged according to the rules defined
|
||||
/// in the [`MergeStrategy`].
|
||||
///
|
||||
/// Most code using this method will use the [`Cell::merge_symbol`] method, which uses this
|
||||
/// method internally to merge the symbols of a cell.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui_core::symbols::merge::MergeStrategy;
|
||||
///
|
||||
/// let strategy = MergeStrategy::Fuzzy;
|
||||
/// assert_eq!(strategy.merge("┌", "┐"), "┬"); // merges to a single character
|
||||
/// assert_eq!(strategy.merge("┘", "╭"), "┼"); // replaces rounded with plain
|
||||
/// assert_eq!(strategy.merge("╎", "╍"), "┿"); // replaces dashed with plain
|
||||
/// assert_eq!(strategy.merge("┐", "╔"), "╦"); // merges double with plain
|
||||
/// assert_eq!(strategy.merge("╔", "┐"), "┬"); // merges plain with double
|
||||
/// ```
|
||||
///
|
||||
/// [Box Drawing Unicode block]: https://en.wikipedia.org/wiki/Box_Drawing
|
||||
/// [`Cell::merge_symbol`]: crate::buffer::Cell::merge_symbol
|
||||
pub fn merge<'a>(self, prev: &'a str, next: &'a str) -> &'a str {
|
||||
let (Ok(prev_symbol), Ok(next_symbol)) =
|
||||
(BorderSymbol::from_str(prev), BorderSymbol::from_str(next))
|
||||
else {
|
||||
return next;
|
||||
};
|
||||
if let Ok(merged) = prev_symbol.merge(next_symbol, self).try_into() {
|
||||
return merged;
|
||||
}
|
||||
next
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a composite border symbol using individual line components.
|
||||
///
|
||||
/// This is an internal type for now specifically used to make the merge logic easier to implement.
|
||||
/// At some point in the future, we might make a similar type public to represent the
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
struct BorderSymbol {
|
||||
right: LineStyle,
|
||||
up: LineStyle,
|
||||
left: LineStyle,
|
||||
down: LineStyle,
|
||||
}
|
||||
|
||||
impl BorderSymbol {
|
||||
/// Creates a new [`BorderSymbol`], based on individual line styles.
|
||||
#[must_use]
|
||||
const fn new(right: LineStyle, up: LineStyle, left: LineStyle, down: LineStyle) -> Self {
|
||||
Self {
|
||||
right,
|
||||
up,
|
||||
left,
|
||||
down,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the closest representation of the [`BorderSymbol`], that has a corresponding unicode
|
||||
/// character.
|
||||
#[must_use]
|
||||
fn fuzzy(mut self, other: Self) -> Self {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use LineStyle::*;
|
||||
|
||||
// Dashes only include vertical and horizontal lines.
|
||||
if !self.is_straight() {
|
||||
self = self
|
||||
.replace(DoubleDash, Plain)
|
||||
.replace(TripleDash, Plain)
|
||||
.replace(QuadrupleDash, Plain)
|
||||
.replace(DoubleDashThick, Thick)
|
||||
.replace(TripleDashThick, Thick)
|
||||
.replace(QuadrupleDashThick, Thick);
|
||||
}
|
||||
|
||||
// Rounded has only corner variants.
|
||||
if !self.is_corner() {
|
||||
self = self.replace(Rounded, Plain);
|
||||
}
|
||||
|
||||
// There are no Double + Thick variants.
|
||||
if self.contains(Double) && self.contains(Thick) {
|
||||
// Decide whether to use Double or Thick, based on the last merged-in symbol.
|
||||
if other.contains(Double) {
|
||||
self = self.replace(Thick, Double);
|
||||
} else {
|
||||
self = self.replace(Double, Thick);
|
||||
}
|
||||
}
|
||||
|
||||
// Some Plain + Double variants don't exist.
|
||||
if <&str>::try_from(self).is_err() {
|
||||
// Decide whether to use Double or Plain, based on the last merged-in symbol.
|
||||
if other.contains(Double) {
|
||||
self = self.replace(Plain, Double);
|
||||
} else {
|
||||
self = self.replace(Double, Plain);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Return true only if the symbol is a line and both parts have the same [`LineStyle`].
|
||||
fn is_straight(self) -> bool {
|
||||
use LineStyle::Nothing;
|
||||
(self.up == self.down && self.left == self.right)
|
||||
&& (self.up == Nothing || self.left == Nothing)
|
||||
}
|
||||
|
||||
/// Return true only if the symbol is a corner and both parts have the same [`LineStyle`].
|
||||
fn is_corner(self) -> bool {
|
||||
use LineStyle::Nothing;
|
||||
match (self.up, self.right, self.down, self.left) {
|
||||
(up, right, Nothing, Nothing) => up == right,
|
||||
(Nothing, right, down, Nothing) => right == down,
|
||||
(Nothing, Nothing, down, left) => down == left,
|
||||
(up, Nothing, Nothing, left) => up == left,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if any of the line components making the [`BorderSymbol`] matches the `style`.
|
||||
fn contains(self, style: LineStyle) -> bool {
|
||||
self.up == style || self.right == style || self.down == style || self.left == style
|
||||
}
|
||||
|
||||
/// Replaces all line styles matching `from` by `to`.
|
||||
#[must_use]
|
||||
fn replace(mut self, from: LineStyle, to: LineStyle) -> Self {
|
||||
self.up = if self.up == from { to } else { self.up };
|
||||
self.right = if self.right == from { to } else { self.right };
|
||||
self.down = if self.down == from { to } else { self.down };
|
||||
self.left = if self.left == from { to } else { self.left };
|
||||
self
|
||||
}
|
||||
|
||||
/// Merges two border symbols into one.
|
||||
fn merge(self, other: Self, strategy: MergeStrategy) -> Self {
|
||||
let exact_result = Self::new(
|
||||
self.right.merge(other.right),
|
||||
self.up.merge(other.up),
|
||||
self.left.merge(other.left),
|
||||
self.down.merge(other.down),
|
||||
);
|
||||
match strategy {
|
||||
MergeStrategy::Replace => other,
|
||||
MergeStrategy::Fuzzy => exact_result.fuzzy(other),
|
||||
MergeStrategy::Exact => exact_result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
enum BorderSymbolError {
|
||||
#[error("cannot parse &str `{0}` to BorderSymbol")]
|
||||
CannotParse(alloc::string::String),
|
||||
#[error("cannot convert BorderSymbol `{0:#?}` to &str: no such symbol exists")]
|
||||
Unrepresentable(BorderSymbol),
|
||||
}
|
||||
|
||||
/// A visual style defining the appearance of a single line making up a block border.
|
||||
///
|
||||
/// This is an internal type used to represent the different styles of lines that can be used in
|
||||
/// border symbols.
|
||||
///
|
||||
/// At some point in the future, we might make this type (or a similar one) public to allow users to
|
||||
/// work with line styles directly, but for now, it is used internally only to simplify the merge
|
||||
/// logic of border symbols.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LineStyle {
|
||||
/// Represents the absence of a line.
|
||||
Nothing,
|
||||
|
||||
/// A single line (e.g. `─`, `│`).
|
||||
Plain,
|
||||
|
||||
/// A rounded line style, only applicable in corner symbols (e.g. `╭`, `╯`).
|
||||
Rounded,
|
||||
|
||||
/// A double line (e.g. `═`, `║`).
|
||||
Double,
|
||||
|
||||
/// A thickened line (e.g. `━`, `┃`).
|
||||
Thick,
|
||||
|
||||
/// A dashed line with a double dash pattern (e.g. `╌`, `╎`).
|
||||
DoubleDash,
|
||||
|
||||
/// A thicker variant of the double dash (e.g. `╍`, `╏`)
|
||||
DoubleDashThick,
|
||||
|
||||
/// A dashed line with a triple dash pattern (e.g. `┄`, `┆`).
|
||||
TripleDash,
|
||||
|
||||
/// A thicker variant of the triple dash (e.g. `┅`, `┇`).
|
||||
TripleDashThick,
|
||||
|
||||
/// A dashed line with four dashes (e.g. `┈`, `┊`).
|
||||
QuadrupleDash,
|
||||
|
||||
/// A thicker variant of the quadruple dash (e.g. `┉`, `┋`).
|
||||
QuadrupleDashThick,
|
||||
}
|
||||
|
||||
impl LineStyle {
|
||||
/// Merges line styles.
|
||||
#[must_use]
|
||||
pub fn merge(self, other: Self) -> Self {
|
||||
if other == Self::Nothing { self } else { other }
|
||||
}
|
||||
}
|
||||
|
||||
// Defines a translation between `BorderSymbol` and the corresponding character.
|
||||
macro_rules! define_symbols {
|
||||
(
|
||||
$( $symbol:expr => ($right:ident, $up:ident, $left:ident, $down:ident) ),* $(,)?
|
||||
) => {
|
||||
|
||||
impl FromStr for BorderSymbol {
|
||||
type Err = BorderSymbolError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use LineStyle::*;
|
||||
use alloc::string::ToString;
|
||||
match s {
|
||||
$( $symbol => Ok(Self::new($right, $up, $left, $down)) ),* ,
|
||||
_ => Err(BorderSymbolError::CannotParse(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<BorderSymbol> for &'static str {
|
||||
type Error = BorderSymbolError;
|
||||
fn try_from(value: BorderSymbol) -> Result<Self, Self::Error> {
|
||||
use LineStyle::*;
|
||||
match (value.right, value.up, value.left, value.down) {
|
||||
$( ($right, $up, $left, $down) => Ok($symbol) ),* ,
|
||||
_ => Err(BorderSymbolError::Unrepresentable(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_symbols!(
|
||||
" " => (Nothing, Nothing, Nothing, Nothing),
|
||||
"─" => (Plain, Nothing, Plain, Nothing),
|
||||
"━" => (Thick, Nothing, Thick, Nothing),
|
||||
"│" => (Nothing, Plain, Nothing, Plain),
|
||||
"┃" => (Nothing, Thick, Nothing, Thick),
|
||||
"┄" => (TripleDash, Nothing, TripleDash, Nothing),
|
||||
"┅" => (TripleDashThick, Nothing, TripleDashThick, Nothing),
|
||||
"┆" => (Nothing, TripleDash, Nothing, TripleDash),
|
||||
"┇" => (Nothing, TripleDashThick, Nothing, TripleDashThick),
|
||||
"┈" => (QuadrupleDash, Nothing, QuadrupleDash, Nothing),
|
||||
"┉" => (QuadrupleDashThick, Nothing, QuadrupleDashThick, Nothing),
|
||||
"┊" => (Nothing, QuadrupleDash, Nothing, QuadrupleDash),
|
||||
"┋" => (Nothing, QuadrupleDashThick, Nothing, QuadrupleDashThick),
|
||||
"┌" => (Plain, Nothing, Nothing, Plain),
|
||||
"┍" => (Thick, Nothing, Nothing, Plain),
|
||||
"┎" => (Plain, Nothing, Nothing, Thick),
|
||||
"┏" => (Thick, Nothing, Nothing, Thick),
|
||||
"┐" => (Nothing, Nothing, Plain, Plain),
|
||||
"┑" => (Nothing, Nothing, Thick, Plain),
|
||||
"┒" => (Nothing, Nothing, Plain, Thick),
|
||||
"┓" => (Nothing, Nothing, Thick, Thick),
|
||||
"└" => (Plain, Plain, Nothing, Nothing),
|
||||
"┕" => (Thick, Plain, Nothing, Nothing),
|
||||
"┖" => (Plain, Thick, Nothing, Nothing),
|
||||
"┗" => (Thick, Thick, Nothing, Nothing),
|
||||
"┘" => (Nothing, Plain, Plain, Nothing),
|
||||
"┙" => (Nothing, Plain, Thick, Nothing),
|
||||
"┚" => (Nothing, Thick, Plain, Nothing),
|
||||
"┛" => (Nothing, Thick, Thick, Nothing),
|
||||
"├" => (Plain, Plain, Nothing, Plain),
|
||||
"┝" => (Thick, Plain, Nothing, Plain),
|
||||
"┞" => (Plain, Thick, Nothing, Plain),
|
||||
"┟" => (Plain, Plain, Nothing, Thick),
|
||||
"┠" => (Plain, Thick, Nothing, Thick),
|
||||
"┡" => (Thick, Thick, Nothing, Plain),
|
||||
"┢" => (Thick, Plain, Nothing, Thick),
|
||||
"┣" => (Thick, Thick, Nothing, Thick),
|
||||
"┤" => (Nothing, Plain, Plain, Plain),
|
||||
"┥" => (Nothing, Plain, Thick, Plain),
|
||||
"┦" => (Nothing, Thick, Plain, Plain),
|
||||
"┧" => (Nothing, Plain, Plain, Thick),
|
||||
"┨" => (Nothing, Thick, Plain, Thick),
|
||||
"┩" => (Nothing, Thick, Thick, Plain),
|
||||
"┪" => (Nothing, Plain, Thick, Thick),
|
||||
"┫" => (Nothing, Thick, Thick, Thick),
|
||||
"┬" => (Plain, Nothing, Plain, Plain),
|
||||
"┭" => (Plain, Nothing, Thick, Plain),
|
||||
"┮" => (Thick, Nothing, Plain, Plain),
|
||||
"┯" => (Thick, Nothing, Thick, Plain),
|
||||
"┰" => (Plain, Nothing, Plain, Thick),
|
||||
"┱" => (Plain, Nothing, Thick, Thick),
|
||||
"┲" => (Thick, Nothing, Plain, Thick),
|
||||
"┳" => (Thick, Nothing, Thick, Thick),
|
||||
"┴" => (Plain, Plain, Plain, Nothing),
|
||||
"┵" => (Plain, Plain, Thick, Nothing),
|
||||
"┶" => (Thick, Plain, Plain, Nothing),
|
||||
"┷" => (Thick, Plain, Thick, Nothing),
|
||||
"┸" => (Plain, Thick, Plain, Nothing),
|
||||
"┹" => (Plain, Thick, Thick, Nothing),
|
||||
"┺" => (Thick, Thick, Plain, Nothing),
|
||||
"┻" => (Thick, Thick, Thick, Nothing),
|
||||
"┼" => (Plain, Plain, Plain, Plain),
|
||||
"┽" => (Plain, Plain, Thick, Plain),
|
||||
"┾" => (Thick, Plain, Plain, Plain),
|
||||
"┿" => (Thick, Plain, Thick, Plain),
|
||||
"╀" => (Plain, Thick, Plain, Plain),
|
||||
"╁" => (Plain, Plain, Plain, Thick),
|
||||
"╂" => (Plain, Thick, Plain, Thick),
|
||||
"╃" => (Plain, Thick, Thick, Plain),
|
||||
"╄" => (Thick, Thick, Plain, Plain),
|
||||
"╅" => (Plain, Plain, Thick, Thick),
|
||||
"╆" => (Thick, Plain, Plain, Thick),
|
||||
"╇" => (Thick, Thick, Thick, Plain),
|
||||
"╈" => (Thick, Plain, Thick, Thick),
|
||||
"╉" => (Plain, Thick, Thick, Thick),
|
||||
"╊" => (Thick, Thick, Plain, Thick),
|
||||
"╋" => (Thick, Thick, Thick, Thick),
|
||||
"╌" => (DoubleDash, Nothing, DoubleDash, Nothing),
|
||||
"╍" => (DoubleDashThick, Nothing, DoubleDashThick, Nothing),
|
||||
"╎" => (Nothing, DoubleDash, Nothing, DoubleDash),
|
||||
"╏" => (Nothing, DoubleDashThick, Nothing, DoubleDashThick),
|
||||
"═" => (Double, Nothing, Double, Nothing),
|
||||
"║" => (Nothing, Double, Nothing, Double),
|
||||
"╒" => (Double, Nothing, Nothing, Plain),
|
||||
"╓" => (Plain, Nothing, Nothing, Double),
|
||||
"╔" => (Double, Nothing, Nothing, Double),
|
||||
"╕" => (Nothing, Nothing, Double, Plain),
|
||||
"╖" => (Nothing, Nothing, Plain, Double),
|
||||
"╗" => (Nothing, Nothing, Double, Double),
|
||||
"╘" => (Double, Plain, Nothing, Nothing),
|
||||
"╙" => (Plain, Double, Nothing, Nothing),
|
||||
"╚" => (Double, Double, Nothing, Nothing),
|
||||
"╛" => (Nothing, Plain, Double, Nothing),
|
||||
"╜" => (Nothing, Double, Plain, Nothing),
|
||||
"╝" => (Nothing, Double, Double, Nothing),
|
||||
"╞" => (Double, Plain, Nothing, Plain),
|
||||
"╟" => (Plain, Double, Nothing, Double),
|
||||
"╠" => (Double, Double, Nothing, Double),
|
||||
"╡" => (Nothing, Plain, Double, Plain),
|
||||
"╢" => (Nothing, Double, Plain, Double),
|
||||
"╣" => (Nothing, Double, Double, Double),
|
||||
"╤" => (Double, Nothing, Double, Plain),
|
||||
"╥" => (Plain, Nothing, Plain, Double),
|
||||
"╦" => (Double, Nothing, Double, Double),
|
||||
"╧" => (Double, Plain, Double, Nothing),
|
||||
"╨" => (Plain, Double, Plain, Nothing),
|
||||
"╩" => (Double, Double, Double, Nothing),
|
||||
"╪" => (Double, Plain, Double, Plain),
|
||||
"╫" => (Plain, Double, Plain, Double),
|
||||
"╬" => (Double, Double, Double, Double),
|
||||
"╭" => (Rounded, Nothing, Nothing, Rounded),
|
||||
"╮" => (Nothing, Nothing, Rounded, Rounded),
|
||||
"╯" => (Nothing, Rounded, Rounded, Nothing),
|
||||
"╰" => (Rounded, Rounded, Nothing, Nothing),
|
||||
"╴" => (Nothing, Nothing, Plain, Nothing),
|
||||
"╵" => (Nothing, Plain, Nothing, Nothing),
|
||||
"╶" => (Plain, Nothing, Nothing, Nothing),
|
||||
"╷" => (Nothing, Nothing, Nothing, Plain),
|
||||
"╸" => (Nothing, Nothing, Thick, Nothing),
|
||||
"╹" => (Nothing, Thick, Nothing, Nothing),
|
||||
"╺" => (Thick, Nothing, Nothing, Nothing),
|
||||
"╻" => (Nothing, Nothing, Nothing, Thick),
|
||||
"╼" => (Thick, Nothing, Plain, Nothing),
|
||||
"╽" => (Nothing, Plain, Nothing, Thick),
|
||||
"╾" => (Plain, Nothing, Thick, Nothing),
|
||||
"╿" => (Nothing, Thick, Nothing, Plain),
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn replace_merge_strategy() {
|
||||
let strategy = MergeStrategy::Replace;
|
||||
let symbols = [
|
||||
"─", "━", "│", "┃", "┄", "┅", "┆", "┇", "┈", "┉", "┊", "┋", "┌", "┍", "┎", "┏", "┐",
|
||||
"┑", "┒", "┓", "└", "┕", "┖", "┗", "┘", "┙", "┚", "┛", "├", "┝", "┞", "┟", "┠", "┡",
|
||||
"┢", "┣", "┤", "┥", "┦", "┧", "┨", "┩", "┪", "┫", "┬", "┭", "┮", "┯", "┰", "┱", "┲",
|
||||
"┳", "┴", "┵", "┶", "┷", "┸", "┹", "┺", "┻", "┼", "┽", "┾", "┿", "╀", "╁", "╂", "╃",
|
||||
"╄", "╅", "╆", "╇", "╈", "╉", "╊", "╋", "╌", "╍", "╎", "╏", "═", "║", "╒", "╓", "╔",
|
||||
"╕", "╖", "╗", "╘", "╙", "╚", "╛", "╜", "╝", "╞", "╟", "╠", "╡", "╢", "╣", "╤", "╥",
|
||||
"╦", "╧", "╨", "╩", "╪", "╫", "╬", "╭", "╮", "╯", "╰", "╴", "╵", "╶", "╷", "╸", "╹",
|
||||
"╺", "╻", "╼", "╽", "╾", "╿", " ",
|
||||
];
|
||||
|
||||
for a in symbols {
|
||||
for b in symbols {
|
||||
assert_eq!(strategy.merge(a, b), b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_merge_strategy() {
|
||||
let strategy = MergeStrategy::Exact;
|
||||
assert_eq!(strategy.merge("┆", "─"), "─");
|
||||
assert_eq!(strategy.merge("┏", "┆"), "┆");
|
||||
assert_eq!(strategy.merge("╎", "┉"), "┉");
|
||||
assert_eq!(strategy.merge("╎", "┉"), "┉");
|
||||
assert_eq!(strategy.merge("┋", "┋"), "┋");
|
||||
assert_eq!(strategy.merge("╷", "╶"), "┌");
|
||||
assert_eq!(strategy.merge("╭", "┌"), "┌");
|
||||
assert_eq!(strategy.merge("│", "┕"), "┝");
|
||||
assert_eq!(strategy.merge("┏", "│"), "┝");
|
||||
assert_eq!(strategy.merge("│", "┏"), "┢");
|
||||
assert_eq!(strategy.merge("╽", "┕"), "┢");
|
||||
assert_eq!(strategy.merge("│", "─"), "┼");
|
||||
assert_eq!(strategy.merge("┘", "┌"), "┼");
|
||||
assert_eq!(strategy.merge("┵", "┝"), "┿");
|
||||
assert_eq!(strategy.merge("│", "━"), "┿");
|
||||
assert_eq!(strategy.merge("┵", "╞"), "╞");
|
||||
assert_eq!(strategy.merge(" ", "╠"), "╠");
|
||||
assert_eq!(strategy.merge("╠", " "), "╠");
|
||||
assert_eq!(strategy.merge("╎", "╧"), "╧");
|
||||
assert_eq!(strategy.merge("╛", "╒"), "╪");
|
||||
assert_eq!(strategy.merge("│", "═"), "╪");
|
||||
assert_eq!(strategy.merge("╤", "╧"), "╪");
|
||||
assert_eq!(strategy.merge("╡", "╞"), "╪");
|
||||
assert_eq!(strategy.merge("┌", "╭"), "╭");
|
||||
assert_eq!(strategy.merge("┘", "╭"), "╭");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzzy_merge_strategy() {
|
||||
let strategy = MergeStrategy::Fuzzy;
|
||||
assert_eq!(strategy.merge("┄", "╴"), "─");
|
||||
assert_eq!(strategy.merge("│", "┆"), "┆");
|
||||
assert_eq!(strategy.merge(" ", "┉"), "┉");
|
||||
assert_eq!(strategy.merge("┋", "┋"), "┋");
|
||||
assert_eq!(strategy.merge("╷", "╶"), "┌");
|
||||
assert_eq!(strategy.merge("╭", "┌"), "┌");
|
||||
assert_eq!(strategy.merge("│", "┕"), "┝");
|
||||
assert_eq!(strategy.merge("┏", "│"), "┝");
|
||||
assert_eq!(strategy.merge("┏", "┆"), "┝");
|
||||
assert_eq!(strategy.merge("│", "┏"), "┢");
|
||||
assert_eq!(strategy.merge("╽", "┕"), "┢");
|
||||
assert_eq!(strategy.merge("│", "─"), "┼");
|
||||
assert_eq!(strategy.merge("┆", "─"), "┼");
|
||||
assert_eq!(strategy.merge("┘", "┌"), "┼");
|
||||
assert_eq!(strategy.merge("┘", "╭"), "┼");
|
||||
assert_eq!(strategy.merge("╎", "┉"), "┿");
|
||||
assert_eq!(strategy.merge(" ", "╠"), "╠");
|
||||
assert_eq!(strategy.merge("╠", " "), "╠");
|
||||
assert_eq!(strategy.merge("┵", "╞"), "╪");
|
||||
assert_eq!(strategy.merge("╛", "╒"), "╪");
|
||||
assert_eq!(strategy.merge("│", "═"), "╪");
|
||||
assert_eq!(strategy.merge("╤", "╧"), "╪");
|
||||
assert_eq!(strategy.merge("╡", "╞"), "╪");
|
||||
assert_eq!(strategy.merge("╎", "╧"), "╪");
|
||||
assert_eq!(strategy.merge("┌", "╭"), "╭");
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ use ratatui_core::buffer::Buffer;
|
||||
use ratatui_core::layout::{Alignment, Rect};
|
||||
use ratatui_core::style::{Style, Styled};
|
||||
use ratatui_core::symbols::border;
|
||||
use ratatui_core::symbols::merge::MergeStrategy;
|
||||
use ratatui_core::text::Line;
|
||||
use ratatui_core::widgets::Widget;
|
||||
|
||||
@ -55,6 +56,8 @@ pub mod title;
|
||||
/// - [`Block::border_style`] Defines the style of the borders.
|
||||
/// - [`Block::border_type`] Sets the symbols used to display the border (e.g. single line, double
|
||||
/// line, thick or rounded borders).
|
||||
/// - [`Block::border_set`] Sets the symbols used to display the border as a [`border::Set`].
|
||||
/// - [`Block::merge_borders`] Sets the block's [`MergeStrategy`] for overlapping characters.
|
||||
/// - [`Block::padding`] Defines the padding inside a [`Block`].
|
||||
/// - [`Block::style`] Sets the base style of the widget.
|
||||
/// - [`Block::title`] Adds a title to the block.
|
||||
@ -126,6 +129,8 @@ pub struct Block<'a> {
|
||||
style: Style,
|
||||
/// Block padding
|
||||
padding: Padding,
|
||||
/// Border merging strategy
|
||||
merge_borders: MergeStrategy,
|
||||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
@ -141,6 +146,7 @@ impl<'a> Block<'a> {
|
||||
border_set: BorderType::Plain.to_border_set(),
|
||||
style: Style::new(),
|
||||
padding: Padding::ZERO,
|
||||
merge_borders: MergeStrategy::Replace,
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,6 +536,54 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the block's [`MergeStrategy`] for overlapping characters.
|
||||
///
|
||||
/// Defaults to [`Replace`], which completely replaces the previously rendered character.
|
||||
/// Changing the strategy to [`Exact`] or [`Fuzzy`] collapses border characters that intersect
|
||||
/// with any previously rendered borders.
|
||||
///
|
||||
/// For more information and examples, see the [collapse borders recipe] and [`MergeStrategy`]
|
||||
/// docs.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::symbols::merge::MergeStrategy;
|
||||
/// # use ratatui::widgets::{Block, BorderType};
|
||||
///
|
||||
/// // Given several blocks with plain borders (1)
|
||||
/// Block::bordered();
|
||||
/// // and other blocks with thick borders (2) which are rendered on top of the first
|
||||
/// Block::bordered()
|
||||
/// .border_type(BorderType::Thick)
|
||||
/// .merge_borders(MergeStrategy::Exact);
|
||||
/// ```
|
||||
///
|
||||
/// Rendering these blocks with `MergeStrategy::Exact` or `MergeStrategy::Fuzzy` will collapse
|
||||
/// the borders, resulting in a clean layout without connected borders.
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───┐ ┌───┐ ┌───┲━━━┓┌───┐
|
||||
/// │ │ │ 1 │ │ ┃ ┃│ │
|
||||
/// │ 1 │ │ ┏━┿━┓│ 1 ┃ 2 ┃│ 1 │
|
||||
/// │ │ │ ┃ │ ┃│ ┃ ┃│ │
|
||||
/// └───╆━━━┓└─╂─┘ ┃└───┺━━━┛┢━━━┪
|
||||
/// ┃ ┃ ┃ 2 ┃ ┃ ┃
|
||||
/// ┃ 2 ┃ ┗━━━┛ ┃ 2 ┃
|
||||
/// ┃ ┃ ┃ ┃
|
||||
/// ┗━━━┛ ┗━━━┛
|
||||
/// ```
|
||||
///
|
||||
/// [collapse borders recipe]: https://ratatui.rs/recipes/layout/collapse-borders/
|
||||
/// [`Replace`]: MergeStrategy::Replace
|
||||
/// [`Exact`]: MergeStrategy::Exact
|
||||
/// [`Fuzzy`]: MergeStrategy::Fuzzy
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn merge_borders(mut self, strategy: MergeStrategy) -> Self {
|
||||
self.merge_borders = strategy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the inner area of a block based on its border visibility rules.
|
||||
///
|
||||
/// # Examples
|
||||
@ -614,17 +668,102 @@ impl Widget for &Block<'_> {
|
||||
|
||||
impl Block<'_> {
|
||||
fn render_borders(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_left_side(area, buf);
|
||||
self.render_top_side(area, buf);
|
||||
self.render_right_side(area, buf);
|
||||
self.render_bottom_side(area, buf);
|
||||
|
||||
self.render_bottom_right_corner(buf, area);
|
||||
self.render_top_right_corner(buf, area);
|
||||
self.render_bottom_left_corner(buf, area);
|
||||
self.render_top_left_corner(buf, area);
|
||||
self.render_sides(area, buf);
|
||||
self.render_corners(area, buf);
|
||||
}
|
||||
|
||||
fn render_sides(&self, area: Rect, buf: &mut Buffer) {
|
||||
let left = area.left();
|
||||
let top = area.top();
|
||||
// area.right() and area.bottom() are outside the rect, subtract 1 to get the last row/col
|
||||
let right = area.right() - 1;
|
||||
let bottom = area.bottom() - 1;
|
||||
|
||||
// The first and last element of each line are not drawn when there is an adjacent line as
|
||||
// this would cause the corner to initially be merged with a side character and then a
|
||||
// corner character to be drawn on top of it. Some merge strategies would not produce a
|
||||
// correct character in that case.
|
||||
let is_replace = self.merge_borders != MergeStrategy::Replace;
|
||||
let left_inset = left + u16::from(is_replace && self.borders.contains(Borders::LEFT));
|
||||
let top_inset = top + u16::from(is_replace && self.borders.contains(Borders::TOP));
|
||||
let right_inset = right - u16::from(is_replace && self.borders.contains(Borders::RIGHT));
|
||||
let bottom_inset = bottom - u16::from(is_replace && self.borders.contains(Borders::BOTTOM));
|
||||
|
||||
let sides = [
|
||||
(
|
||||
Borders::LEFT,
|
||||
left..=left,
|
||||
top_inset..=bottom_inset,
|
||||
self.border_set.vertical_left,
|
||||
),
|
||||
(
|
||||
Borders::TOP,
|
||||
left_inset..=right_inset,
|
||||
top..=top,
|
||||
self.border_set.horizontal_top,
|
||||
),
|
||||
(
|
||||
Borders::RIGHT,
|
||||
right..=right,
|
||||
top_inset..=bottom_inset,
|
||||
self.border_set.vertical_right,
|
||||
),
|
||||
(
|
||||
Borders::BOTTOM,
|
||||
left_inset..=right_inset,
|
||||
bottom..=bottom,
|
||||
self.border_set.horizontal_bottom,
|
||||
),
|
||||
];
|
||||
for (border, x_range, y_range, symbol) in sides {
|
||||
if self.borders.contains(border) {
|
||||
for x in x_range {
|
||||
for y in y_range.clone() {
|
||||
buf[(x, y)]
|
||||
.merge_symbol(symbol, self.merge_borders)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_corners(&self, area: Rect, buf: &mut Buffer) {
|
||||
let corners = [
|
||||
(
|
||||
Borders::RIGHT | Borders::BOTTOM,
|
||||
area.right() - 1,
|
||||
area.bottom() - 1,
|
||||
self.border_set.bottom_right,
|
||||
),
|
||||
(
|
||||
Borders::RIGHT | Borders::TOP,
|
||||
area.right() - 1,
|
||||
area.top(),
|
||||
self.border_set.top_right,
|
||||
),
|
||||
(
|
||||
Borders::LEFT | Borders::BOTTOM,
|
||||
area.left(),
|
||||
area.bottom() - 1,
|
||||
self.border_set.bottom_left,
|
||||
),
|
||||
(
|
||||
Borders::LEFT | Borders::TOP,
|
||||
area.left(),
|
||||
area.top(),
|
||||
self.border_set.top_left,
|
||||
),
|
||||
];
|
||||
|
||||
for (border, x, y, symbol) in corners {
|
||||
if self.borders.contains(border) {
|
||||
buf[(x, y)]
|
||||
.merge_symbol(symbol, self.merge_borders)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn render_titles(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_title_position(Position::Top, area, buf);
|
||||
self.render_title_position(Position::Bottom, area, buf);
|
||||
@ -637,80 +776,6 @@ impl Block<'_> {
|
||||
self.render_left_titles(position, area, buf);
|
||||
}
|
||||
|
||||
fn render_left_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf[(area.left(), y)]
|
||||
.set_symbol(self.border_set.vertical_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::TOP) {
|
||||
for x in area.left()..area.right() {
|
||||
buf[(x, area.top())]
|
||||
.set_symbol(self.border_set.horizontal_top)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_right_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::RIGHT) {
|
||||
let x = area.right() - 1;
|
||||
for y in area.top()..area.bottom() {
|
||||
buf[(x, y)]
|
||||
.set_symbol(self.border_set.vertical_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bottom_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::BOTTOM) {
|
||||
let y = area.bottom() - 1;
|
||||
for x in area.left()..area.right() {
|
||||
buf[(x, y)]
|
||||
.set_symbol(self.border_set.horizontal_bottom)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bottom_right_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf[(area.right() - 1, area.bottom() - 1)]
|
||||
.set_symbol(self.border_set.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_right_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
buf[(area.right() - 1, area.top())]
|
||||
.set_symbol(self.border_set.top_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bottom_left_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
buf[(area.left(), area.bottom() - 1)]
|
||||
.set_symbol(self.border_set.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_left_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf[(area.left(), area.top())]
|
||||
.set_symbol(self.border_set.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render titles aligned to the right of the block
|
||||
///
|
||||
/// Currently (due to the way lines are truncated), the right side of the leftmost title will
|
||||
@ -904,7 +969,8 @@ impl Styled for Block<'_> {
|
||||
mod tests {
|
||||
use alloc::{format, vec};
|
||||
|
||||
use ratatui_core::layout::HorizontalAlignment;
|
||||
use itertools::iproduct;
|
||||
use ratatui_core::layout::{HorizontalAlignment, Offset};
|
||||
use ratatui_core::style::{Color, Modifier, Stylize};
|
||||
use rstest::rstest;
|
||||
use strum::ParseError;
|
||||
@ -1150,6 +1216,7 @@ mod tests {
|
||||
border_set: BorderType::Plain.to_border_set(),
|
||||
style: Style::new(),
|
||||
padding: Padding::ZERO,
|
||||
merge_borders: MergeStrategy::Replace,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -1647,4 +1714,158 @@ mod tests {
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::replace(MergeStrategy::Replace)]
|
||||
#[case::exact(MergeStrategy::Exact)]
|
||||
#[case::fuzzy(MergeStrategy::Fuzzy)]
|
||||
fn render_partial_borders(#[case] strategy: MergeStrategy) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
Block::new()
|
||||
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
|
||||
.merge_borders(strategy)
|
||||
.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"┌────────┐",
|
||||
"│ │",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
Block::new()
|
||||
.borders(Borders::TOP | Borders::LEFT)
|
||||
.merge_borders(strategy)
|
||||
.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"┌─────────",
|
||||
"│ ",
|
||||
"│ ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
Block::new()
|
||||
.borders(Borders::TOP | Borders::RIGHT)
|
||||
.merge_borders(strategy)
|
||||
.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"─────────┐",
|
||||
" │",
|
||||
" │",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
Block::new()
|
||||
.borders(Borders::BOTTOM | Borders::LEFT)
|
||||
.merge_borders(strategy)
|
||||
.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"│ ",
|
||||
"│ ",
|
||||
"└─────────",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
Block::new()
|
||||
.borders(Borders::BOTTOM | Borders::RIGHT)
|
||||
.merge_borders(strategy)
|
||||
.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" │",
|
||||
" │",
|
||||
"─────────┘",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
Block::new()
|
||||
.borders(Borders::TOP | Borders::BOTTOM)
|
||||
.merge_borders(strategy)
|
||||
.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"──────────",
|
||||
" ",
|
||||
"──────────",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.merge_borders(strategy)
|
||||
.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
/// Renders a series of blocks with all the possible border types and merges them according to
|
||||
/// the specified strategy. The resulting buffer is compared against the expected output for
|
||||
/// each merge strategy.
|
||||
///
|
||||
/// At some point, it might be convenient to replace the manual `include_str!` calls with
|
||||
/// [insta](https://crates.io/crates/insta)
|
||||
#[rstest]
|
||||
#[case::replace(MergeStrategy::Replace, include_str!("../tests/block/merge_replace.txt"))]
|
||||
#[case::exact(MergeStrategy::Exact, include_str!("../tests/block/merge_exact.txt"))]
|
||||
#[case::fuzzy(MergeStrategy::Fuzzy, include_str!("../tests/block/merge_fuzzy.txt"))]
|
||||
fn render_merged_borders(#[case] strategy: MergeStrategy, #[case] expected: &'static str) {
|
||||
let border_types = [
|
||||
BorderType::Plain,
|
||||
BorderType::Rounded,
|
||||
BorderType::Thick,
|
||||
BorderType::Double,
|
||||
BorderType::LightDoubleDashed,
|
||||
BorderType::HeavyDoubleDashed,
|
||||
BorderType::LightTripleDashed,
|
||||
BorderType::HeavyTripleDashed,
|
||||
BorderType::LightQuadrupleDashed,
|
||||
BorderType::HeavyQuadrupleDashed,
|
||||
];
|
||||
let rects = [
|
||||
// touching at corners
|
||||
(Rect::new(0, 0, 5, 5), Rect::new(4, 4, 5, 5)),
|
||||
// overlapping
|
||||
(Rect::new(10, 0, 5, 5), Rect::new(12, 2, 5, 5)),
|
||||
// touching vertical edges
|
||||
(Rect::new(18, 0, 5, 5), Rect::new(22, 0, 5, 5)),
|
||||
// touching horizontal edges
|
||||
(Rect::new(28, 0, 5, 5), Rect::new(28, 4, 5, 5)),
|
||||
];
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 43, 1000));
|
||||
|
||||
let mut offset = Offset::ZERO;
|
||||
for (border_type_1, border_type_2) in iproduct!(border_types, border_types) {
|
||||
let title = format!("{border_type_1} + {border_type_2}");
|
||||
let title_area = Rect::new(0, 0, 43, 1).offset(offset);
|
||||
title.render(title_area, &mut buffer);
|
||||
offset.y += 1;
|
||||
for (rect_1, rect_2) in rects {
|
||||
Block::bordered()
|
||||
.border_type(border_type_1)
|
||||
.merge_borders(strategy)
|
||||
.render(rect_1.offset(offset), &mut buffer);
|
||||
Block::bordered()
|
||||
.border_type(border_type_2)
|
||||
.merge_borders(strategy)
|
||||
.render(rect_2.offset(offset), &mut buffer);
|
||||
}
|
||||
offset.y += 9;
|
||||
}
|
||||
pretty_assertions::assert_eq!(Buffer::with_lines(expected.lines()), buffer);
|
||||
}
|
||||
}
|
||||
|
1000
ratatui-widgets/tests/block/merge_exact.txt
Normal file
1000
ratatui-widgets/tests/block/merge_exact.txt
Normal file
File diff suppressed because it is too large
Load Diff
1000
ratatui-widgets/tests/block/merge_fuzzy.txt
Normal file
1000
ratatui-widgets/tests/block/merge_fuzzy.txt
Normal file
File diff suppressed because it is too large
Load Diff
1000
ratatui-widgets/tests/block/merge_replace.txt
Normal file
1000
ratatui-widgets/tests/block/merge_replace.txt
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user