feat(canvas)!: Add quadrant, sextant and octant markers (#2235)

The octant marker is an alternative to the Braille marker with the same
resolution, but offering densely packed, regular pseudo-pixels, without
visible bands between rows and columns. 

Quadrant and Sextants are also added to support 2x2 and 2x3.

Sextant and Octant unicode characters that are less widely supported at
the moment, which is why `Braille` was left as the default.

BREAKING CHANGE: addition of new variants to `Marker` and removal of
no longer used constants in `ratatui:🔣:braille`.
This commit is contained in:
Serge Barral 2025-11-30 00:37:53 +01:00 committed by GitHub
parent ee673476d3
commit f18bcbf06b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 219 additions and 61 deletions

View File

@ -28,6 +28,8 @@ This is a quick summary of the sections below:
`Terminal`
- Support a broader range for `unicode-width` version
- `Marker` is now non-exhaustive
- `symbols::braille::BLANK` and `symbols::braille::DOTS` have been removed in favor of an ordered
array of all Braille characters
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer
const
@ -1106,7 +1108,7 @@ previously did not need to use type annotations to fail to compile. To fix this,
[#133]: https://github.com/ratatui/ratatui/issues/133
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
Code using the `Block` marker that previously rendered using a half block character (`'▀'`) now
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
the existing code.

View File

@ -122,7 +122,10 @@ impl App {
Marker::Dot => Marker::Braille,
Marker::Braille => Marker::Block,
Marker::Block => Marker::HalfBlock,
Marker::HalfBlock => Marker::Bar,
Marker::HalfBlock => Marker::Quadrant,
Marker::Quadrant => Marker::Sextant,
Marker::Sextant => Marker::Octant,
Marker::Octant => Marker::Bar,
Marker::Bar => Marker::Dot,
_ => unreachable!(),
};

View File

@ -10,5 +10,6 @@ pub mod half_block;
pub mod line;
pub mod marker;
pub mod merge;
pub mod pixel;
pub mod scrollbar;
pub mod shade;

View File

@ -1,7 +1,21 @@
pub const BLANK: u16 = 0x2800;
pub const DOTS: [[u16; 2]; 4] = [
[0x0001, 0x0008],
[0x0002, 0x0010],
[0x0004, 0x0020],
[0x0040, 0x0080],
//! Braille symbols.
//!
//! Note that the symbols are not listed according to their unicode codepoint but according to the
//! corresponding bit pattern in row-major order.
pub const BRAILLE: [char; 256] = [
'', '⠁', '⠈', '⠉', '⠂', '⠃', '⠊', '⠋', '⠐', '⠑', '⠘', '⠙', '⠒', '⠓', '⠚', '⠛', '⠄', '⠅', '⠌',
'⠍', '⠆', '⠇', '⠎', '⠏', '⠔', '⠕', '⠜', '⠝', '⠖', '⠗', '⠞', '⠟', '⠠', '⠡', '⠨', '⠩', '⠢', '⠣',
'⠪', '⠫', '⠰', '⠱', '⠸', '⠹', '⠲', '⠳', '⠺', '⠻', '⠤', '⠥', '⠬', '⠭', '⠦', '⠧', '⠮', '⠯', '⠴',
'⠵', '⠼', '⠽', '⠶', '⠷', '⠾', '⠿', '⡀', '⡁', '⡈', '⡉', '⡂', '⡃', '⡊', '⡋', '⡐', '⡑', '⡘', '⡙',
'⡒', '⡓', '⡚', '⡛', '⡄', '⡅', '⡌', '⡍', '⡆', '⡇', '⡎', '⡏', '⡔', '⡕', '⡜', '⡝', '⡖', '⡗', '⡞',
'⡟', '⡠', '⡡', '⡨', '⡩', '⡢', '⡣', '⡪', '⡫', '⡰', '⡱', '⡸', '⡹', '⡲', '⡳', '⡺', '⡻', '⡤', '⡥',
'⡬', '⡭', '⡦', '⡧', '⡮', '⡯', '⡴', '⡵', '⡼', '⡽', '⡶', '⡷', '⡾', '⡿', '⢀', '⢁', '⢈', '⢉', '⢂',
'⢃', '⢊', '⢋', '⢐', '⢑', '⢘', '⢙', '⢒', '⢓', '⢚', '⢛', '⢄', '⢅', '⢌', '⢍', '⢆', '⢇', '⢎', '⢏',
'⢔', '⢕', '⢜', '⢝', '⢖', '⢗', '⢞', '⢟', '⢠', '⢡', '⢨', '⢩', '⢢', '⢣', '⢪', '⢫', '⢰', '⢱', '⢸',
'⢹', '⢲', '⢳', '⢺', '⢻', '⢤', '⢥', '⢬', '⢭', '⢦', '⢧', '⢮', '⢯', '⢴', '⢵', '⢼', '⢽', '⢶', '⢷',
'⢾', '⢿', '⣀', '⣁', '⣈', '⣉', '⣂', '⣃', '⣊', '⣋', '⣐', '⣑', '⣘', '⣙', '⣒', '⣓', '⣚', '⣛', '⣄',
'⣅', '⣌', '⣍', '⣆', '⣇', '⣎', '⣏', '⣔', '⣕', '⣜', '⣝', '⣖', '⣗', '⣞', '⣟', '⣠', '⣡', '⣨', '⣩',
'⣢', '⣣', '⣪', '⣫', '⣰', '⣱', '⣸', '⣹', '⣲', '⣳', '⣺', '⣻', '⣤', '⣥', '⣬', '⣭', '⣦', '⣧', '⣮',
'⣯', '⣴', '⣵', '⣼', '⣽', '⣶', '⣷', '⣾', '⣿',
];

View File

@ -26,6 +26,33 @@ pub enum Marker {
/// a grid that is double the resolution of the terminal. Because each terminal cell is
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
HalfBlock,
/// Use quadrant characters to represent data points.
///
/// Quadrant characters display densely packed and regularly spaced pseudo-pixels with a 2x2
/// resolution per character, without visible bands between cells.
Quadrant,
/// Use sextant characters from the [Unicode Symbols for Legacy Computing
/// Supplement](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement) to
/// represent data points.
///
/// Sextant characters display densely packed and regularly spaced pseudo-pixels with a 2x3
/// resolution per character, without visible bands between cells.
///
/// Note: the Symbols for Legacy Computing Supplement block is a relatively recent addition to
/// unicode that is less broadly supported than Braille dots. If your terminal does not support
/// this, you will see unicode replacement characters (`<60>`) instead of sextants (`🬌`, `🬲`, `🬑`).
Sextant,
/// Use octant characters from the [Unicode Symbols for Legacy Computing
/// Supplement](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement) to
/// represent data points.
///
/// Octant characters have the same 2x4 resolution as Braille characters but display densely
/// packed and regularly spaced pseudo-pixels, without visible bands between cells.
///
/// Note: the Symbols for Legacy Computing Supplement block is a relatively recent addition to
/// unicode that is less broadly supported than Braille dots. If your terminal does not support
/// this, you will see unicode replacement characters (`<60>`) instead of octants (`𜴇`, `𜷀`, `𜴷`).
Octant,
}
#[cfg(test)]

View File

@ -0,0 +1,30 @@
//! Pseudo-pixel symbols: quadrant, sextant and octant characters.
//!
//! Note that the symbols are not listed according to their unicode codepoint but according to the
//! corresponding bit pattern in row-major order.
pub const QUADRANTS: [char; 16] = [
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
];
pub const SEXTANTS: [char; 64] = [
' ', '🬀', '🬁', '🬂', '🬃', '🬄', '🬅', '🬆', '🬇', '🬈', '🬉', '🬊', '🬋', '🬌', '🬍', '🬎', '🬏', '🬐', '🬑',
'🬒', '🬓', '▌', '🬔', '🬕', '🬖', '🬗', '🬘', '🬙', '🬚', '🬛', '🬜', '🬝', '🬞', '🬟', '🬠', '🬡', '🬢', '🬣',
'🬤', '🬥', '🬦', '🬧', '▐', '🬨', '🬩', '🬪', '🬫', '🬬', '🬭', '🬮', '🬯', '🬰', '🬱', '🬲', '🬳', '🬴', '🬵',
'🬶', '🬷', '🬸', '🬹', '🬺', '🬻', '█',
];
pub const OCTANTS: [char; 256] = [
' ', '𜺨', '𜺫', '🮂', '𜴀', '▘', '𜴁', '𜴂', '𜴃', '𜴄', '▝', '𜴅', '𜴆', '𜴇', '𜴈', '▀', '𜴉', '𜴊', '𜴋',
'𜴌', '🯦', '𜴍', '𜴎', '𜴏', '𜴐', '𜴑', '𜴒', '𜴓', '𜴔', '𜴕', '𜴖', '𜴗', '𜴘', '𜴙', '𜴚', '𜴛', '𜴜', '𜴝',
'𜴞', '𜴟', '🯧', '𜴠', '𜴡', '𜴢', '𜴣', '𜴤', '𜴥', '𜴦', '𜴧', '𜴨', '𜴩', '𜴪', '𜴫', '𜴬', '𜴭', '𜴮', '𜴯',
'𜴰', '𜴱', '𜴲', '𜴳', '𜴴', '𜴵', '🮅', '𜺣', '𜴶', '𜴷', '𜴸', '𜴹', '𜴺', '𜴻', '𜴼', '𜴽', '𜴾', '𜴿', '𜵀',
'𜵁', '𜵂', '𜵃', '𜵄', '▖', '𜵅', '𜵆', '𜵇', '𜵈', '▌', '𜵉', '𜵊', '𜵋', '𜵌', '▞', '𜵍', '𜵎', '𜵏', '𜵐',
'▛', '𜵑', '𜵒', '𜵓', '𜵔', '𜵕', '𜵖', '𜵗', '𜵘', '𜵙', '𜵚', '𜵛', '𜵜', '𜵝', '𜵞', '𜵟', '𜵠', '𜵡', '𜵢',
'𜵣', '𜵤', '𜵥', '𜵦', '𜵧', '𜵨', '𜵩', '𜵪', '𜵫', '𜵬', '𜵭', '𜵮', '𜵯', '𜵰', '𜺠', '𜵱', '𜵲', '𜵳', '𜵴',
'𜵵', '𜵶', '𜵷', '𜵸', '𜵹', '𜵺', '𜵻', '𜵼', '𜵽', '𜵾', '𜵿', '𜶀', '𜶁', '𜶂', '𜶃', '𜶄', '𜶅', '𜶆', '𜶇',
'𜶈', '𜶉', '𜶊', '𜶋', '𜶌', '𜶍', '𜶎', '𜶏', '▗', '𜶐', '𜶑', '𜶒', '𜶓', '▚', '𜶔', '𜶕', '𜶖', '𜶗', '▐',
'𜶘', '𜶙', '𜶚', '𜶛', '▜', '𜶜', '𜶝', '𜶞', '𜶟', '𜶠', '𜶡', '𜶢', '𜶣', '𜶤', '𜶥', '𜶦', '𜶧', '𜶨', '𜶩',
'𜶪', '𜶫', '▂', '𜶬', '𜶭', '𜶮', '𜶯', '𜶰', '𜶱', '𜶲', '𜶳', '𜶴', '𜶵', '𜶶', '𜶷', '𜶸', '𜶹', '𜶺', '𜶻',
'𜶼', '𜶽', '𜶾', '𜶿', '𜷀', '𜷁', '𜷂', '𜷃', '𜷄', '𜷅', '𜷆', '𜷇', '𜷈', '𜷉', '𜷊', '𜷋', '𜷌', '𜷍', '𜷎',
'𜷏', '𜷐', '𜷑', '𜷒', '𜷓', '𜷔', '𜷕', '𜷖', '𜷗', '𜷘', '𜷙', '𜷚', '▄', '𜷛', '𜷜', '𜷝', '𜷞', '▙', '𜷟',
'𜷠', '𜷡', '𜷢', '▟', '𜷣', '▆', '𜷤', '𜷥', '█',
];

View File

@ -23,6 +23,8 @@ use itertools::Itertools;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::{Color, Style};
use ratatui_core::symbols::braille::BRAILLE;
use ratatui_core::symbols::pixel::{OCTANTS, QUADRANTS, SEXTANTS};
use ratatui_core::symbols::{self, Marker};
use ratatui_core::text::Line as TextLine;
use ratatui_core::widgets::Widget;
@ -108,67 +110,89 @@ trait Grid: fmt::Debug {
fn reset(&mut self);
}
/// The `BrailleGrid` is a grid made up of cells each containing a Braille pattern.
/// The pattern and color of a `PatternGrid` cell.
#[derive(Copy, Clone, Debug, Default)]
struct PatternCell {
/// The pattern of a grid character.
///
/// The pattern is stored in the lower bits in a row-major order. For instance, for a 2x4
/// pattern marker, bits 0 to 7 of this field should represent the following pseudo-pixels:
///
/// | 0 1 |
/// | 2 3 |
/// | 4 5 |
/// | 6 7 |
pattern: u8,
/// The color of a cell only supports foreground colors for now as there's no way to
/// individually set the background color of each pseudo-pixel in a pattern character.
color: Option<Color>,
}
/// The `PatternGrid` is a grid made up of cells each containing a `W`x`H` pattern character.
///
/// This makes it possible to draw shapes with a resolution of 2x4 dots per cell. This is useful
/// when you want to draw shapes with a high resolution. Font support for Braille patterns is
/// required to see the dots. If your terminal or font does not support this unicode block, you
/// will see unicode replacement characters (<28>) instead of braille dots.
/// This makes it possible to draw shapes with a resolution of e.g. 2x4 (Braille or unicode octant)
/// per cell.
/// Font support for the relevant pattern character is required. If your terminal or font does not
/// support the relevant unicode block, you will see unicode replacement characters (<28>) instead.
///
/// This grid type only supports a single foreground color for each 2x4 dots cell. There is no way
/// to set the individual color of each dot in the braille pattern.
/// This grid type only supports a single foreground color for each `W`x`H` pattern character.
/// There is no way to set the individual color of each pseudo-pixel.
#[derive(Debug)]
struct BrailleGrid {
struct PatternGrid<const W: usize, const H: usize> {
/// Width of the grid in number of terminal columns
width: u16,
/// Height of the grid in number of terminal rows
height: u16,
/// Represents the unicode braille patterns. Will take a value between `0x2800` and `0x28FF`
/// this is converted to an utf16 string when converting to a layer. See
/// <https://en.wikipedia.org/wiki/Braille_Patterns> for more info.
utf16_code_points: Vec<u16>,
/// The color of each cell only supports foreground colors for now as there's no way to
/// individually set the background color of each dot in the braille pattern.
colors: Vec<Option<Color>>,
/// Pattern and color of the cells.
cells: Vec<PatternCell>,
/// Lookup table mapping patterns to characters.
char_table: &'static [char],
}
impl BrailleGrid {
/// Create a new `BrailleGrid` with the given width and height measured in terminal columns and
/// rows respectively.
fn new(width: u16, height: u16) -> Self {
impl<const W: usize, const H: usize> PatternGrid<W, H> {
/// Statically check that the dimension of the pattern is supported.
const _PATTERN_DIMENSION_CHECK: usize = u8::BITS as usize - W * H;
/// Create a new `PatternGrid` with the given width and height measured in terminal columns
/// and rows respectively.
fn new(width: u16, height: u16, char_table: &'static [char]) -> Self {
// Cause a static error if the pattern doesn't fit within 8 bits.
let _ = Self::_PATTERN_DIMENSION_CHECK;
let length = usize::from(width) * usize::from(height);
Self {
width,
height,
utf16_code_points: vec![symbols::braille::BLANK; length],
colors: vec![None; length],
cells: vec![PatternCell::default(); length],
char_table,
}
}
}
impl Grid for BrailleGrid {
impl<const W: usize, const H: usize> Grid for PatternGrid<W, H> {
fn resolution(&self) -> (f64, f64) {
(f64::from(self.width) * 2.0, f64::from(self.height) * 4.0)
(
f64::from(self.width) * W as f64,
f64::from(self.height) * H as f64,
)
}
fn save(&self) -> Layer {
let contents = self
.utf16_code_points
.cells
.iter()
.zip(&self.colors)
.map(|(&code_point, &color)| {
let symbol = match code_point {
// Skip rendering blank braille patterns to allow layers underneath
.map(|&cell| {
let symbol = match cell.pattern {
// Skip rendering blank patterns to allow layers underneath
// to show through.
symbols::braille::BLANK => None,
_ => Some(char::from_u32(code_point.into()).unwrap()),
0 => None,
idx => Some(self.char_table[idx as usize]),
};
LayerCell {
symbol,
fg: color,
// Braille patterns only affect foreground.
// This way we can have braille layered with block.
fg: cell.color,
// Patterns only affect foreground.
bg: None,
}
})
@ -178,22 +202,19 @@ impl Grid for BrailleGrid {
}
fn reset(&mut self) {
self.utf16_code_points.fill(symbols::braille::BLANK);
self.colors.fill(None);
self.cells.fill_with(Default::default);
}
fn paint(&mut self, x: usize, y: usize, color: Color) {
let index = y
.saturating_div(4)
.saturating_div(H)
.saturating_mul(self.width as usize)
.saturating_add(x.saturating_div(2));
.saturating_add(x.saturating_div(W));
// using get_mut here because we are indexing the vector with usize values
// and we want to make sure we don't panic if the index is out of bounds
if let Some(c) = self.utf16_code_points.get_mut(index) {
*c |= symbols::braille::DOTS[y % 4][x % 2];
}
if let Some(c) = self.colors.get_mut(index) {
*c = Some(color);
if let Some(cell) = self.cells.get_mut(index) {
cell.pattern |= 1u8 << ((x % W) + W * (y % H));
cell.color = Some(color);
}
}
}
@ -285,7 +306,7 @@ impl Grid for CharGrid {
/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per
/// cell.
///
/// This allows for more flexibility than the `BrailleGrid` which only supports a single
/// This allows for more flexibility than the `PatternGrid` which only supports a single
/// foreground color for each 2x4 dots cell, and the `CharGrid` which only supports a single
/// character for each cell.
#[derive(Debug)]
@ -561,8 +582,11 @@ impl<'a> Context<'a> {
match marker {
Marker::Block => Box::new(CharGrid::new(width, height, block).apply_color_to_bg()),
Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
Marker::Braille => Box::new(BrailleGrid::new(width, height)),
Marker::Braille => Box::new(PatternGrid::<2, 4>::new(width, height, &BRAILLE)),
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
Marker::Quadrant => Box::new(PatternGrid::<2, 2>::new(width, height, &QUADRANTS)),
Marker::Sextant => Box::new(PatternGrid::<2, 3>::new(width, height, &SEXTANTS)),
Marker::Octant => Box::new(PatternGrid::<2, 4>::new(width, height, &OCTANTS)),
Marker::Dot | _ => Box::new(CharGrid::new(width, height, dot)),
}
}
@ -627,16 +651,25 @@ impl<'a> Context<'a> {
///
/// By default the grid is made of Braille patterns but you may change the marker to use a different
/// set of symbols. If your terminal or font does not support this unicode block, you will see
/// unicode replacement characters (<28>) instead of braille dots. The Braille patterns provide a more
/// fine grained result (2x4 dots) but you might want to use a simple dot, block, or bar instead by
/// calling the [`marker`] method if your target environment does not support those symbols.
/// unicode replacement characters (<28>) instead of braille dots. The Braille patterns (as well the
/// octant character patterns) provide a more fine grained result with a 2x4 resolution per
/// character, but you might want to use a simple dot, block, or bar instead by calling the
/// [`marker`] method if your target environment does not support those symbols.
///
/// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info.
///
/// The `Octant` marker is similar to the `Braille` marker but, instead of sparse dots, displays
/// densely packed and regularly spaced pseudo-pixels, without visible bands between rows and
/// columns. However, it uses characters that are not yet as widely supported as the Braille
/// unicode block.
///
/// The `Quadrant` and `Sextant` markers are in turn akin to the `Octant` marker, but with a 2x2
/// and 2x3 resolution, respectively.
///
/// The `HalfBlock` marker is useful when you want to draw shapes with a higher resolution than a
/// `CharGrid` but lower than a `BrailleGrid`. This grid type supports a foreground and background
/// color for each terminal cell. This allows for more flexibility than the `BrailleGrid` which only
/// supports a single foreground color for each 2x4 dots cell.
/// `CharGrid` but lower than a `PatternGrid`. This grid type supports a foreground and background
/// color for each terminal cell. This allows for more flexibility than the `PatternGrid` which
/// only supports a single foreground color for each 2x4 dots cell.
///
/// The Canvas widget is used by calling the [`Canvas::paint`] method and passing a closure that
/// will be used to draw on the canvas. The closure will be passed a [`Context`] object that can be
@ -777,7 +810,7 @@ where
/// The [`HalfBlock`] marker is useful when you want to draw shapes with a higher resolution
/// than with a grid of characters (e.g. with [`Block`] or [`Dot`]) but lower than with
/// [`Braille`]. This grid type supports a foreground and background color for each terminal
/// cell. This allows for more flexibility than the `BrailleGrid` which only supports a single
/// cell. This allows for more flexibility than the `PatternGrid` which only supports a single
/// foreground color for each 2x4 dots cell.
///
/// [`Braille`]: ratatui_core::symbols::Marker::Braille
@ -940,6 +973,30 @@ mod tests {
xxxx
"
))]
#[case::quadrant(Marker::Quadrant, indoc!(
"
xxxx
xxxx
xxxx
xxxx
"
))]
#[case::sextant(Marker::Sextant, indoc!(
"
xxxx
xxxx
xxxx
xxxx
🬲🬭🬭🬭🬭"
))]
#[case::octant(Marker::Octant, indoc!(
"
xxxx
xxxx
xxxx
xxxx
𜷀"
))]
#[case::dot(Marker::Dot, indoc!(
"
xxxx
@ -1009,6 +1066,30 @@ mod tests {
xxx
xxx"
))]
#[case::quadrant(Marker::Quadrant, indoc!(
"
xxx
xxx
xxxx
xxx
xxx"
))]
#[case::sextant(Marker::Sextant, indoc!(
"
🬧xxx🬔
x🬧x🬔x
xxxx
x🬘x🬣x
🬘xxx🬣"
))]
#[case::octant(Marker::Octant, indoc!(
"
xxx
xxx
xxxx
xxx
xxx"
))]
#[case::dot(Marker::Dot, indoc!(
"
xxx
@ -1050,7 +1131,7 @@ mod tests {
// values to check if there are any integer overflows we just initialize the canvas painters
#[test]
fn check_canvas_paint_max() {
let mut b_grid = BrailleGrid::new(u16::MAX, 2);
let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 2, &OCTANTS);
let mut c_grid = CharGrid::new(u16::MAX, 2, 'd');
let max = u16::MAX as usize;
@ -1069,7 +1150,7 @@ mod tests {
// We delibately cause integer overflow to check if we don't panic and don't get weird behavior
#[test]
fn check_canvas_paint_overflow() {
let mut b_grid = BrailleGrid::new(u16::MAX, 3);
let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 3, &BRAILLE);
let mut c_grid = CharGrid::new(u16::MAX, 3, 'd');
let max = u16::MAX as usize + 10;