use std::time::Duration;
use ratatui::{layout::Rect, symbols::border, widgets::Block};
use crate::config::document::Glyphs;
macro_rules! count_idents {
() => { 0usize };
($head:ident $(, $tail:ident)* $(,)?) => {
1usize + $crate::glyphs::count_idents!($($tail),*)
};
}
pub(crate) use count_idents;
macro_rules! border {
($($field:ident),* $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Border {
$( pub $field: char, )*
}
impl Border {
pub fn chars(&self) -> [char; count_idents!($($field),*)] {
[ $( self.$field ),* ]
}
}
};
}
border!(
top_left,
top_right,
bottom_left,
bottom_right,
horizontal,
vertical
);
macro_rules! glyph_set {
(
gutter: { $($g_variant:ident : $g_field:ident),* $(,)? },
value: { $($v_variant:ident : $v_field:ident),* $(,)? } $(,)?
) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GlyphSet {
$( pub $g_field: char, )*
$( pub $v_field: char, )*
pub loading: &'static [char],
pub sort_ascending: char,
pub sort_descending: char,
pub scrollbar_track: char,
pub scrollbar_thumb: char,
pub border: Border,
pub capture_elision: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Meaning {
$( $g_variant, )*
$( $v_variant, )*
Loading,
}
impl Meaning {
pub const ALL: [Meaning; count_idents!($($g_variant),*) + count_idents!($($v_variant),*) + 1] = [
$( Meaning::$g_variant, )*
$( Meaning::$v_variant, )*
Meaning::Loading,
];
}
impl GlyphSet {
pub fn row_interior(&self) -> Vec<(Meaning, char)> {
let mut glyphs = vec![
$( (Meaning::$g_variant, self.$g_field), )*
$( (Meaning::$v_variant, self.$v_field), )*
];
glyphs.extend(self.loading.iter().map(|&frame| (Meaning::Loading, frame)));
glyphs
}
#[allow(dead_code)]
pub fn all_glyphs(&self) -> Vec<char> {
let mut glyphs: Vec<char> =
self.row_interior().into_iter().map(|(_, c)| c).collect();
glyphs.extend(self.border.chars());
glyphs.extend([self.sort_ascending, self.sort_descending]);
glyphs.extend([self.scrollbar_track, self.scrollbar_thumb]);
glyphs.extend(self.capture_elision.chars());
glyphs
}
const fn gutter_core(&self) -> [char; count_idents!($($g_variant),*)] {
[ $( self.$g_field ),* ]
}
const fn value_core(&self) -> [char; count_idents!($($v_variant),*)] {
[ $( self.$v_field ),* ]
}
pub fn for_config(glyphs: Glyphs) -> &'static GlyphSet {
match glyphs {
Glyphs::Full => &FULL,
Glyphs::Ascii => &ASCII,
}
}
}
};
}
glyph_set! {
gutter: {
Fresh: fresh,
Stale: stale,
Unknown: unknown,
Failed: failed,
},
value: {
InSync: in_sync,
Clean: clean,
NoUpstream: no_upstream,
NoRemote: no_remote,
Ahead: ahead,
Behind: behind,
Changed: changed,
ChildRow: child_row,
OrphanChildRow: orphan_child_row,
Checked: checked,
Ignored: ignored,
Truncated: truncated,
},
}
const FULL_SPINNER: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
pub const FULL_SPINNER_INTERVAL: Duration = Duration::from_millis(80);
pub const FULL: GlyphSet = GlyphSet {
fresh: ' ',
stale: '~',
unknown: '?',
failed: '!',
loading: &FULL_SPINNER,
in_sync: '≡',
clean: '·',
no_upstream: '-',
no_remote: '∅',
ahead: '↑',
behind: '↓',
changed: '●',
child_row: '└',
orphan_child_row: '┆',
checked: '✓',
ignored: '⊘',
truncated: '$',
sort_ascending: '\u{2191}',
sort_descending: '\u{2193}',
scrollbar_track: '│',
scrollbar_thumb: '█',
border: Border {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
},
capture_elision: "···",
};
const ASCII_SPINNER: [char; 3] = ['\\', '|', '/'];
pub const ASCII: GlyphSet = GlyphSet {
fresh: ' ',
stale: '~',
unknown: '?',
failed: '!',
loading: &ASCII_SPINNER,
in_sync: '=',
clean: '.',
no_upstream: '-',
no_remote: 'x',
ahead: '>',
behind: '<',
changed: '*',
child_row: '`',
orphan_child_row: ':',
checked: '+',
ignored: '#',
truncated: '$',
sort_ascending: '^',
sort_descending: 'v',
scrollbar_track: '|',
scrollbar_thumb: '#',
border: Border {
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
horizontal: '-',
vertical: '|',
},
capture_elision: "...",
};
#[derive(Debug, Default)]
pub struct BorderScratch {
slots: [[u8; 4]; 8],
}
impl BorderScratch {
pub fn new() -> Self {
Self::default()
}
}
impl GlyphSet {
pub fn bordered_block<'a>(&self, scratch: &'a mut BorderScratch) -> Block<'a> {
let frame = self.border;
let [tl, tr, bl, br, vl, vr, ht, hb] = &mut scratch.slots;
Block::bordered().border_set(border::Set {
top_left: frame.top_left.encode_utf8(tl),
top_right: frame.top_right.encode_utf8(tr),
bottom_left: frame.bottom_left.encode_utf8(bl),
bottom_right: frame.bottom_right.encode_utf8(br),
vertical_left: frame.vertical.encode_utf8(vl),
vertical_right: frame.vertical.encode_utf8(vr),
horizontal_top: frame.horizontal.encode_utf8(ht),
horizontal_bottom: frame.horizontal.encode_utf8(hb),
})
}
}
pub(crate) fn bordered_interior(area: Rect) -> Rect {
FULL.bordered_block(&mut BorderScratch::new()).inner(area)
}
const fn disjoint(a: &[char], b: &[char]) -> bool {
let mut i = 0;
while i < a.len() {
let mut j = 0;
while j < b.len() {
if a[i] == b[j] {
return false;
}
j += 1;
}
i += 1;
}
true
}
const _: () = {
let gutter = FULL.gutter_core();
let value = FULL.value_core();
assert!(
disjoint(&gutter, &value),
"the full glyph table's gutter marks and value marks intersect"
);
assert!(
disjoint(FULL.loading, &value),
"the full spinner's loading frames intersect the full table's value marks"
);
};
const _: () = {
let gutter = ASCII.gutter_core();
let value = ASCII.value_core();
assert!(
disjoint(&gutter, &value),
"the ascii glyph table's gutter marks and value marks intersect"
);
assert!(
disjoint(ASCII.loading, &value),
"the ascii spinner's loading frames intersect the ascii table's value marks"
);
};
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use unicode_width::UnicodeWidthStr;
use super::*;
fn assert_row_interior_is_injective(label: &str, set: &GlyphSet) {
let glyphs = set.row_interior();
for i in 0..glyphs.len() {
for j in (i + 1)..glyphs.len() {
let (meaning_a, glyph_a) = glyphs[i];
let (meaning_b, glyph_b) = glyphs[j];
if meaning_a == meaning_b {
continue;
}
assert_ne!(
glyph_a, glyph_b,
"{label} glyph table: {meaning_a:?} and {meaning_b:?} both render as {glyph_a:?}"
);
}
}
}
#[test]
fn the_full_table_never_lets_two_meanings_share_a_glyph() {
assert_row_interior_is_injective("full", &FULL);
}
#[test]
fn the_ascii_table_never_lets_two_meanings_share_a_glyph() {
assert_row_interior_is_injective("ascii", &ASCII);
}
#[test]
fn a_character_present_in_both_tables_carries_the_same_meaning_in_each() {
let full: std::collections::HashMap<char, Meaning> = FULL
.row_interior()
.into_iter()
.map(|(m, c)| (c, m))
.collect();
for (ascii_meaning, glyph) in ASCII.row_interior() {
if let Some(&full_meaning) = full.get(&glyph) {
assert_eq!(
full_meaning, ascii_meaning,
"'{glyph}' means {full_meaning:?} in the full table and {ascii_meaning:?} in the ascii table"
);
}
}
}
#[test]
fn both_tables_define_glyphs_for_the_same_set_of_meanings() {
let full: HashSet<Meaning> = FULL.row_interior().into_iter().map(|(m, _)| m).collect();
let ascii: HashSet<Meaning> = ASCII.row_interior().into_iter().map(|(m, _)| m).collect();
assert_eq!(
full, ascii,
"the full and ascii tables define glyphs for a different set of meanings"
);
}
#[test]
fn the_full_spinner_is_the_canonical_ten_frame_dots_set_with_no_blank_frame() {
assert_eq!(FULL.loading.len(), 10);
assert_eq!(
FULL.loading,
&['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
);
assert!(
!FULL.loading.contains(&'\u{2800}'),
"the full spinner must not contain U+2800, the blank braille frame, which would \
render as Fresh's space"
);
}
#[test]
fn the_dropped_submodule_marker_appears_in_neither_table() {
let dropped = '\u{2219}';
assert!(
!FULL.all_glyphs().contains(&dropped),
"'{dropped}' reappeared in the full table"
);
assert!(
!ASCII.all_glyphs().contains(&dropped),
"'{dropped}' reappeared in the ascii table"
);
}
#[test]
fn every_glyph_in_both_tables_measures_one_column_under_the_renderers_width_function() {
for (label, set) in [("full", &FULL), ("ascii", &ASCII)] {
for glyph in set.all_glyphs() {
let rendered = glyph.to_string();
assert_eq!(
UnicodeWidthStr::width(rendered.as_str()),
1,
"{label} glyph {glyph:?} does not measure one column under \
UnicodeWidthStr::width, the function ratatui budgets with"
);
}
}
}
#[test]
fn running_the_row_interior_disjointness_check_over_the_ascii_frame_table_fails() {
let frame_glyphs = [
ASCII.border.top_left,
ASCII.border.top_right,
ASCII.border.bottom_left,
ASCII.border.bottom_right,
ASCII.border.horizontal,
ASCII.border.vertical,
];
assert!(
!disjoint(&ASCII.value_core(), &frame_glyphs),
"expected the ascii frame to collide with the row interior's value glyphs, which \
is the collision the frame's exemption exists to permit"
);
}
#[test]
fn the_ascii_border_shares_its_horizontal_rule_with_the_no_upstream_value_mark_and_that_is_permitted()
{
assert_eq!(
ASCII.border.horizontal, ASCII.no_upstream,
"the specific known collision the frame's exemption from row-interior \
disjointness covers"
);
}
#[test]
fn the_ascii_border_shares_its_corner_glyph_with_the_checked_value_mark_and_that_is_permitted()
{
assert_eq!(
ASCII.border.top_left, ASCII.checked,
"the specific known collision the frame's exemption from row-interior \
disjointness covers"
);
}
#[test]
fn the_truncated_value_mark_is_the_same_dollar_character_in_both_tables_and_collides_with_neither_frame()
{
assert_eq!(FULL.truncated, '$');
assert_eq!(ASCII.truncated, '$');
assert_ne!(
ASCII.border.top_left, ASCII.truncated,
"the ascii border's corner must not collide with the truncation mark the way it \
does, permitted, with `Checked`"
);
assert_ne!(
ASCII.border.horizontal, ASCII.truncated,
"the ascii border's horizontal rule must not collide with the truncation mark"
);
}
#[test]
fn the_ascii_frames_four_corners_collapse_onto_one_character() {
let corners = [
ASCII.border.top_left,
ASCII.border.top_right,
ASCII.border.bottom_left,
ASCII.border.bottom_right,
];
assert!(
corners.iter().all(|&corner| corner == '+'),
"expected every ascii corner to collapse onto '+', got {corners:?}"
);
}
#[test]
fn the_ascii_spinners_middle_frame_shares_the_border_verticals_glyph_and_that_is_the_accepted_one_beat_in_three_artefact()
{
assert_eq!(
ASCII.loading[1], '|',
"expected the ascii spinner's middle frame to be '|', the frame ADR 0020 names as \
the one that collides with the border"
);
assert_eq!(
ASCII.border.vertical, ASCII.loading[1],
"the specific accepted collision: one beat in three, only while a row holds no \
values at all"
);
}
#[test]
fn for_config_selects_the_table_the_glyphs_key_names() {
assert_eq!(GlyphSet::for_config(Glyphs::Full), &FULL);
assert_eq!(GlyphSet::for_config(Glyphs::Ascii), &ASCII);
}
fn extract_value_glyph_table_rows(spec: &str) -> Vec<String> {
const HEADING: &str = "In-cell glyphs for real values:";
let after_heading = &spec[spec
.find(HEADING)
.expect("layout-and-provenance.md must contain the in-cell glyphs heading")
+ HEADING.len()..];
let table_lines: Vec<&str> = after_heading
.lines()
.skip_while(|line| !line.trim_start().starts_with('|'))
.take_while(|line| line.trim_start().starts_with('|'))
.map(str::trim)
.collect();
assert!(
table_lines.len() > 2,
"layout-and-provenance.md's in-cell value glyph table has no data rows"
);
table_lines[2..]
.iter()
.map(|line| line.to_string())
.collect()
}
fn value_glyph_meaning(phrase: &str) -> Meaning {
let phrase = phrase.to_lowercase();
if phrase == "in sync" {
Meaning::InSync
} else if phrase == "clean" {
Meaning::Clean
} else if phrase == "no upstream" {
Meaning::NoUpstream
} else if phrase.contains("no remote") {
Meaning::NoRemote
} else if phrase.starts_with("ahead") {
Meaning::Ahead
} else if phrase.starts_with("behind") {
Meaning::Behind
} else if phrase.contains("changed") {
Meaning::Changed
} else {
panic!(
"layout-and-provenance.md's in-cell value glyph table names a meaning this \
test does not recognise: {phrase:?}"
)
}
}
#[test]
fn the_ignored_mark_is_the_pair_theming_mds_own_two_sets_table_names() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let spec = std::fs::read_to_string(manifest_dir.join("../../docs/spec/theming.md"))
.expect("read docs/spec/theming.md");
let row = spec
.lines()
.map(str::trim)
.find(|line| line.starts_with("| ignored |"))
.expect("theming.md's \"The two sets\" table must name `ignored`");
let cells: Vec<&str> = row.trim_matches('|').split('|').map(str::trim).collect();
let [_, full, ascii] = cells.as_slice() else {
panic!("the ignored row does not have exactly three cells: {row:?}");
};
let one = |cell: &str| {
let mut chars = cell.trim_matches('`').chars();
let glyph = chars.next().expect("a glyph");
assert!(
chars.next().is_none(),
"expected one character, got {cell:?}"
);
glyph
};
assert_eq!(FULL.ignored, one(full));
assert_eq!(ASCII.ignored, one(ascii));
}
#[test]
fn every_in_cell_value_glyph_matches_layout_and_provenance_mds_own_table_in_both_directions() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let spec =
std::fs::read_to_string(manifest_dir.join("../../docs/spec/layout-and-provenance.md"))
.expect("read the layout and provenance specification");
let mut spec_glyphs: HashMap<Meaning, char> = HashMap::new();
for row in extract_value_glyph_table_rows(&spec) {
let cells: Vec<&str> = row.trim_matches('|').split('|').map(str::trim).collect();
let [glyph_cell, meaning_cell] = cells.as_slice() else {
panic!(
"layout-and-provenance.md's value glyph row does not have exactly two \
cells: {row:?}"
);
};
let meaning = value_glyph_meaning(meaning_cell);
let glyph = glyph_cell
.trim_matches('`')
.chars()
.next()
.unwrap_or_else(|| panic!("empty glyph cell in row: {row:?}"));
assert!(
spec_glyphs.insert(meaning, glyph).is_none(),
"layout-and-provenance.md's value glyph table names {meaning:?} more than once"
);
}
let code_meanings: HashMap<Meaning, char> = FULL
.row_interior()
.into_iter()
.filter(|(meaning, _)| {
!matches!(
meaning,
Meaning::Fresh
| Meaning::Stale
| Meaning::Unknown
| Meaning::Failed
| Meaning::Loading
| Meaning::ChildRow
| Meaning::OrphanChildRow
| Meaning::Checked
| Meaning::Ignored
| Meaning::Truncated
)
})
.collect();
for (meaning, glyph) in &spec_glyphs {
match code_meanings.get(meaning) {
Some(code_glyph) => assert_eq!(
code_glyph, glyph,
"{meaning:?}'s glyph disagrees between the full glyph table \
({code_glyph:?}) and layout-and-provenance.md ({glyph:?})"
),
None => panic!(
"layout-and-provenance.md's value glyph table names {meaning:?}, which \
the full glyph table does not implement"
),
}
}
for meaning in code_meanings.keys() {
assert!(
spec_glyphs.contains_key(meaning),
"the full glyph table implements {meaning:?}, which \
layout-and-provenance.md's value glyph table does not name"
);
}
}
const BORDER_REGION: &str = "the one bordered block";
const BORDER_CONSTRUCTION_NEEDLES: [&str; 5] = [
"Block::bordered",
"Borders::",
"border::",
"BorderType",
"border_set",
];
fn block<'a>() -> Block<'a> {
Block::new()
}
fn house_set<'a>() -> border::Set<'a> {
border::PLAIN
}
macro_rules! border_construction_cases {
($($needle:literal => $construction:expr),+ $(,)?) => {
[$(($needle, {
#[allow(dead_code)]
fn compiled<'a>() -> Block<'a> {
$construction
}
stringify!($construction)
})),+]
};
}
const BORDER_CONSTRUCTION_CASES: [(&str, &str); 5] = border_construction_cases![
"Block::bordered" => ratatui::widgets::Block::bordered(),
"Borders::" => block().borders(ratatui::widgets::Borders::ALL),
"border::" => block().border_set(ratatui::symbols::border::PLAIN),
"BorderType" => block().border_type(ratatui::widgets::BorderType::Plain),
"border_set" => block().border_set(house_set()),
];
fn sanctioned_border_region() -> (PathBuf, std::ops::Range<usize>) {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/glyphs.rs");
let source = crate::test_support::production_source_at(&path);
assert!(
crate::test_support::source_region(&source, BORDER_REGION).is_some(),
"the `// scan: {BORDER_REGION}` marker pair is gone from {}, so the scan below \
would have nothing to sanction and would fail on the real constructor instead \
of on a second one",
path.display()
);
let marker = |suffix: &str| {
let marker = format!("// scan: {BORDER_REGION} {suffix}");
source
.lines()
.position(|line| line.trim() == marker)
.unwrap_or_else(|| panic!("the {marker:?} line is gone from {}", path.display()))
};
let (begin, end) = (marker("begin"), marker("end"));
(path, (begin + 2)..(end + 1))
}
#[test]
fn only_the_one_marked_region_in_this_file_builds_a_bordered_block() {
let dirs = crate::test_support::workspace_crate_src_dirs();
let files_scanned: usize = dirs
.iter()
.map(|dir| crate::test_support::rust_source_files(dir).len())
.sum();
assert!(
files_scanned > 0,
"scanned zero source files; workspace_crate_src_dirs points somewhere that no \
longer exists, and this scan would otherwise pass on having inspected nothing"
);
let (sanctioned_path, sanctioned_lines) = sanctioned_border_region();
let mut sanctioned_hits = 0;
for needle in BORDER_CONSTRUCTION_NEEDLES {
for hit in crate::test_support::production_lines_containing(needle) {
let (path, line) = hit
.rsplit_once(':')
.unwrap_or_else(|| panic!("expected a `path:line` hit, got {hit:?}"));
let line: usize = line
.parse()
.unwrap_or_else(|_| panic!("expected a line number in {hit:?}"));
assert!(
Path::new(path) == sanctioned_path && sanctioned_lines.contains(&line),
"{hit} reaches for a border outside `GlyphSet::bordered_block`; every \
framed surface takes its frame characters from the glyph table, so \
ratatui's own default set can never reach the screen"
);
sanctioned_hits += 1;
}
}
assert!(
sanctioned_hits > 0,
"the scan found no border construction at all, not even the sanctioned one: \
{BORDER_CONSTRUCTION_NEEDLES:?} no longer name how a border is built, and this \
test has stopped checking anything"
);
}
#[test]
fn the_border_scan_would_catch_a_surface_that_built_its_own_bordered_block() {
for needle in BORDER_CONSTRUCTION_NEEDLES {
assert!(
BORDER_CONSTRUCTION_CASES
.iter()
.any(|(covered, _)| *covered == needle),
"{needle:?} is scanned for but never planted, so nothing proves it still names \
a way a border is built"
);
}
for (needle, construction) in BORDER_CONSTRUCTION_CASES {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(
dir.path().join("offender.rs"),
format!("fn frame() -> ratatui::widgets::Block<'static> {{\n{construction}\n}}\n"),
)
.expect("write fixture file");
let offending = crate::test_support::production_lines_under_containing(
&[dir.path().to_path_buf()],
needle,
);
assert_eq!(
offending.len(),
1,
"expected the scan for {needle:?} to catch exactly the one planted \
construction, got: {offending:?}"
);
assert!(
offending[0].contains("offender.rs:2"),
"expected {needle:?} caught on the construction's own line, got {offending:?}"
);
}
}
#[test]
fn both_tables_frame_characters_match_theming_mds_own_panel_border_row() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let spec = std::fs::read_to_string(manifest_dir.join("../../docs/spec/theming.md"))
.expect("read the theming specification");
let row = spec
.lines()
.find(|line| line.trim_start().starts_with("| panel border |"))
.expect("theming.md's \"The two sets\" table names a `panel border` row");
let cells: Vec<String> = row
.split('`')
.skip(1)
.step_by(2)
.map(|cell| cell.replace(' ', ""))
.collect();
let [full_cell, ascii_cell] = cells.as_slice() else {
panic!("expected exactly two code spans in theming.md's panel border row: {row:?}");
};
for (label, cell, table) in [("full", full_cell, &FULL), ("ascii", ascii_cell, &ASCII)] {
let drawn: String = table.border.chars().into_iter().collect();
assert_eq!(
&drawn, cell,
"the {label} table's frame disagrees with theming.md's panel border row"
);
}
}
#[test]
fn both_tables_sort_arrows_match_theming_mds_own_sort_arrow_row() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let spec = std::fs::read_to_string(manifest_dir.join("../../docs/spec/theming.md"))
.expect("read the theming specification");
let row = spec
.lines()
.find(|line| line.trim_start().starts_with("| sort arrow"))
.expect("theming.md's \"The two sets\" table names a `sort arrow` row");
let cells: Vec<String> = row
.split('`')
.skip(1)
.step_by(2)
.map(|cell| cell.replace(' ', ""))
.collect();
let [full_cell, ascii_cell] = cells.as_slice() else {
panic!("expected exactly two code spans in theming.md's sort arrow row: {row:?}");
};
for (label, cell, table) in [("full", full_cell, &FULL), ("ascii", ascii_cell, &ASCII)] {
let drawn: String = [table.sort_ascending, table.sort_descending]
.into_iter()
.collect();
assert_eq!(
&drawn, cell,
"the {label} table's sort arrows disagree with theming.md's sort arrow row"
);
}
}
#[test]
fn both_tables_scrollbar_characters_match_theming_mds_own_scrollbar_row() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let spec = std::fs::read_to_string(manifest_dir.join("../../docs/spec/theming.md"))
.expect("read the theming specification");
let row = spec
.lines()
.find(|line| line.trim_start().starts_with("| scrollbar"))
.expect("theming.md's \"The two sets\" table names a `scrollbar` row");
let cells: Vec<String> = row
.split('`')
.skip(1)
.step_by(2)
.map(|cell| cell.replace(' ', ""))
.collect();
let [full_cell, ascii_cell] = cells.as_slice() else {
panic!("expected exactly two code spans in theming.md's scrollbar row: {row:?}");
};
for (label, cell, table) in [("full", full_cell, &FULL), ("ascii", ascii_cell, &ASCII)] {
let drawn: String = [table.scrollbar_track, table.scrollbar_thumb]
.into_iter()
.collect();
assert_eq!(
&drawn, cell,
"the {label} table's scrollbar characters disagree with theming.md's \
scrollbar row"
);
}
}
#[test]
fn the_scrollbar_track_repeats_the_frames_vertical_rule_in_both_tables() {
for (label, table) in [("full", &FULL), ("ascii", &ASCII)] {
assert_eq!(
table.scrollbar_track, table.border.vertical,
"the {label} table's scrollbar track must be the vertical rule it is drawn over"
);
assert_ne!(
table.scrollbar_thumb, table.scrollbar_track,
"the {label} table's scrollbar thumb is indistinguishable from its track"
);
}
}
#[test]
fn the_full_tables_sort_arrows_repeat_its_ahead_and_behind_marks_and_that_is_permitted() {
assert_eq!(FULL.sort_ascending, FULL.ahead);
assert_eq!(FULL.sort_descending, FULL.behind);
assert!(
!ASCII.value_core().contains(&ASCII.sort_ascending)
&& !ASCII.value_core().contains(&ASCII.sort_descending),
"the ascii table's own arrows introduce no such overlap"
);
}
#[test]
fn the_bordered_block_draws_every_cell_of_the_frame_from_this_tables_own_characters() {
use ratatui::{Terminal, backend::TestBackend};
for (label, table) in [("full", &FULL), ("ascii", &ASCII)] {
let area = Rect::new(0, 0, 12, 5);
let backend = TestBackend::new(area.width, area.height);
let mut terminal = Terminal::new(backend).expect("create test terminal");
terminal
.draw(|frame| {
let mut scratch = BorderScratch::new();
frame.render_widget(table.bordered_block(&mut scratch), area);
})
.expect("draw the frame");
crate::test_support::assert_frame_drawn_with(
terminal.backend().buffer(),
area,
table.border,
"",
&format!("the {label} table's own bordered block"),
);
}
}
#[test]
fn the_two_tables_frame_a_panel_with_different_characters() {
for (label, full, ascii) in [
("top left", FULL.border.top_left, ASCII.border.top_left),
("top right", FULL.border.top_right, ASCII.border.top_right),
(
"bottom left",
FULL.border.bottom_left,
ASCII.border.bottom_left,
),
(
"bottom right",
FULL.border.bottom_right,
ASCII.border.bottom_right,
),
(
"horizontal",
FULL.border.horizontal,
ASCII.border.horizontal,
),
("vertical", FULL.border.vertical, ASCII.border.vertical),
] {
assert_ne!(
full, ascii,
"the two tables draw the same {label}, so nothing on screen degrades when \
`glyphs = \"ascii\"` is set"
);
}
}
#[test]
fn the_frame_inset_is_the_same_under_either_table() {
for area in [
Rect::new(0, 0, 40, 10),
Rect::new(3, 7, 88, 24),
Rect::new(0, 0, 2, 2),
] {
assert_eq!(
FULL.bordered_block(&mut BorderScratch::new()).inner(area),
ASCII.bordered_block(&mut BorderScratch::new()).inner(area),
"the two tables disagree about the interior of {area:?}"
);
assert_eq!(
bordered_interior(area),
FULL.bordered_block(&mut BorderScratch::new()).inner(area)
);
}
}
}