use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::Line;
use super::super::theme::Palette;
pub(super) const BOX_TOP_LEFT: char = '▛';
pub(super) const BOX_TOP_RIGHT: char = '▜';
const BOX_BOTTOM_LEFT: char = '▙';
const BOX_BOTTOM_RIGHT: char = '▟';
const BOX_LEFT: char = '▐';
pub(super) const BOX_TOP: char = '▀';
const BOX_BOTTOM: char = '▄';
const BOX_RIGHT: char = '▌';
pub(super) const fn floor_for(interior: u16) -> u16 {
interior.saturating_add(4)
}
pub(super) const fn is_boxed(width: u16, interior: u16) -> bool {
width >= floor_for(interior)
}
pub(super) fn boxed_height(lines: &[Line<'_>]) -> u16 {
u16::try_from(lines.len())
.unwrap_or(u16::MAX)
.saturating_add(2)
}
pub(super) fn draw_boxed(
lines: &[Line<'_>],
interior: u16,
palette: Palette,
ground: Style,
area: Rect,
buffer: &mut Buffer,
) {
let box_height = boxed_height(lines);
let rows = box_height.saturating_sub(2);
let margin = area.width.saturating_sub(interior.saturating_add(2)) / 2;
let box_x = area.x + margin;
let box_y = area.y + area.height.saturating_sub(box_height) / 2;
let line_style = palette.line();
let left = BOX_LEFT.to_string();
let right = BOX_RIGHT.to_string();
let blank = blank_of(interior);
buffer.set_string(
box_x,
box_y,
format!(
"{BOX_TOP_LEFT}{}{BOX_TOP_RIGHT}",
BOX_TOP.to_string().repeat(usize::from(interior))
),
line_style,
);
for (offset, line) in lines.iter().enumerate() {
let offset = u16::try_from(offset).expect("a boxed form is at most area.height lines");
let y = box_y + 1 + offset;
buffer.set_string(box_x, y, &left, line_style);
blank_row(buffer, box_x + 1, y, &blank, ground);
buffer.set_line(box_x + 1, y, line, interior);
buffer.set_string(box_x + 1 + interior, y, &right, line_style);
}
buffer.set_string(
box_x,
box_y + 1 + rows,
format!(
"{BOX_BOTTOM_LEFT}{}{BOX_BOTTOM_RIGHT}",
BOX_BOTTOM.to_string().repeat(usize::from(interior))
),
line_style,
);
}
pub(super) fn blank_row(buffer: &mut Buffer, x: u16, y: u16, blank: &str, style: Style) {
buffer.set_string(x, y, blank, style);
}
pub(super) fn blank_of(width: u16) -> String {
" ".repeat(usize::from(width))
}
pub(super) fn mute(buffer: &mut Buffer, area: Rect, palette: Palette) {
buffer.set_style(area, Style::reset());
buffer.set_style(area, palette.muted());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lookout::view::fixtures;
use crate::output::width::char_columns;
#[test]
fn mute_clears_the_ground_and_reverse_video_it_finds() {
let area = Rect::new(0, 0, 1, 1);
let mut buffer = Buffer::empty(area);
buffer.set_style(
area,
Style::default()
.bg(ratatui::style::Color::Red)
.add_modifier(ratatui::style::Modifier::REVERSED),
);
let palette = fixtures::coloured();
mute(&mut buffer, area, palette);
let style = buffer[(0, 0)].style();
assert!(
!style
.add_modifier
.contains(ratatui::style::Modifier::REVERSED),
"reverse video from underneath survived the mute: {style:?}"
);
assert_eq!(
style.bg,
Some(ratatui::style::Color::Reset),
"the prior background survived the mute: {style:?}"
);
assert_eq!(
style.fg,
palette.muted().fg,
"muted's own ink did not land: {style:?}"
);
}
#[test]
fn draw_boxed_centres_the_box_in_the_area() {
let area = Rect::new(0, 0, 20, 10);
let mut buffer = Buffer::empty(area);
let lines = [Line::default()];
draw_boxed(
&lines,
10,
fixtures::plain(),
Style::reset(),
area,
&mut buffer,
);
assert_eq!(
buffer[(4, 3)].symbol(),
BOX_TOP_LEFT.to_string(),
"top-left corner is not where the margin arithmetic puts it"
);
assert_eq!(
buffer[(4, 5)].symbol(),
BOX_BOTTOM_LEFT.to_string(),
"bottom-left corner is not where the margin arithmetic puts it"
);
}
#[test]
fn the_box_costs_two_rows_and_saturates_rather_than_wrapping() {
assert_eq!(boxed_height(&[]), 2);
assert_eq!(boxed_height(&vec![Line::default(); 17]), 19);
assert_eq!(
boxed_height(&vec![Line::default(); usize::from(u16::MAX) - 1]),
u16::MAX
);
assert_eq!(
boxed_height(&vec![Line::default(); usize::from(u16::MAX) + 5]),
u16::MAX,
"past u16 the try_from fallback takes over, and it saturates too"
);
}
#[test]
fn both_frames_floors_come_out_of_one_expression() {
assert_eq!(floor_for(86), 90, "1g");
assert_eq!(floor_for(126), 130, "1k");
}
#[test]
fn the_floor_is_the_narrowest_boxed_width() {
assert!(is_boxed(90, 86));
assert!(!is_boxed(89, 86));
assert!(is_boxed(130, 126));
assert!(!is_boxed(129, 126));
}
#[test]
fn the_border_vocabulary_is_the_one_that_was_checked() {
for glyph in [
BOX_TOP_LEFT,
BOX_TOP_RIGHT,
BOX_BOTTOM_LEFT,
BOX_BOTTOM_RIGHT,
BOX_LEFT,
BOX_TOP,
BOX_BOTTOM,
BOX_RIGHT,
'▘',
'⌫',
] {
assert_eq!(char_columns(glyph), 1, "{glyph}");
}
}
}