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:
Jagoda Estera Ślązak 2025-06-04 11:06:39 +02:00 committed by Orhun Parmaksız
parent 12cb5a28fe
commit 671c2b4fd4
No known key found for this signature in database
GPG Key ID: F83424824B3E4B90
7 changed files with 4085 additions and 84 deletions

View File

@ -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);

View File

@ -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;

View 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("", ""), "");
}
}

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff