use ratatui::backend::TestBackend;
use ratatui::buffer::{Buffer, Cell, CellDiffOption};
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::widgets::Widget;
use ratatui::{Frame, Terminal};
use crate::view::fitted::words_of;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Run {
pub(crate) said: String,
pub(crate) style: Style,
}
impl Run {
pub(crate) fn new(said: &str, style: Style) -> Self {
Self {
said: said.to_string(),
style,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Painted(Vec<Vec<Run>>);
impl Painted {
pub(crate) fn of<W: Widget>(widget: W, width: u16, height: u16) -> Self {
Self::drawn_by(width, height, |frame| {
frame.render_widget(widget, frame.area());
})
}
pub(crate) fn drawn_by(width: u16, height: u16, draw: impl FnOnce(&mut Frame)) -> Self {
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("a test backend");
terminal.draw(draw).expect("a draw into memory");
Self::read(terminal.backend().buffer())
}
pub(crate) fn read(buffer: &Buffer) -> Self {
Self(
(buffer.area.top()..buffer.area.bottom())
.map(|y| {
let mut runs: Vec<Run> = Vec::new();
let mut x = buffer.area.left();
while x < buffer.area.right() {
let cell = &buffer[(x, y)];
x += covered(cell);
let said = words_of(cell.symbol());
match runs.last_mut() {
Some(run) if run.style == cell.style() => run.said.push_str(&said),
_ => runs.push(Run::new(&said, cell.style())),
}
}
runs
})
.collect(),
)
}
pub(crate) fn rows(&self) -> Vec<String> {
self.0
.iter()
.map(|runs| runs.iter().map(|run| run.said.as_str()).collect())
.collect()
}
pub(crate) fn row(&self, y: usize) -> Vec<Run> {
self.0[y].clone()
}
pub(crate) fn margins(&self, window: Rect) -> String {
let rows = self.rows();
(window.top() + 1..window.bottom().saturating_sub(1))
.flat_map(|y| {
let row: Vec<char> = rows[usize::from(y)].chars().collect();
[
row[usize::from(window.left()) + 1],
row[usize::from(window.right()) - 2],
]
})
.collect()
}
}
fn covered(cell: &Cell) -> u16 {
match cell.diff_option {
CellDiffOption::ForcedWidth(width) => width.get(),
_ => 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::num::NonZeroU16;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier};
use ratatui::text::{Line, Span};
#[test]
fn a_run_ends_wherever_any_part_of_the_style_changes() {
let plain = Style::new().fg(Color::Red);
let painted = Painted::of(
Line::from(vec![
Span::styled("a", plain),
Span::styled("b", plain),
Span::styled("cd", plain.add_modifier(Modifier::BOLD)),
]),
4,
1,
);
let runs = painted.row(0);
assert_eq!(runs.len(), 2, "{runs:?}");
assert_eq!(runs[0].said, "ab", "one style, one run: {runs:?}");
assert_eq!(runs[1].said, "cd", "{runs:?}");
assert!(
!runs[0].style.add_modifier.contains(Modifier::BOLD),
"{runs:?}"
);
assert!(
runs[1].style.add_modifier.contains(Modifier::BOLD),
"{runs:?}"
);
}
#[test]
fn a_hyperlink_is_not_among_the_words() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
buffer
.cell_mut((0, 0))
.expect("a cell to write")
.set_symbol("\x1b]8;;https://forge.invalid/dunwich/arkham\x1b\\⇢ #12\x1b]8;;\x1b\\")
.set_diff_option(CellDiffOption::ForcedWidth(
NonZeroU16::new(5).expect("five columns"),
));
assert_eq!(Painted::read(&buffer).rows(), vec!["⇢ #12"]);
}
#[test]
fn the_columns_a_forced_width_covers_are_not_read() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Line::from("kadath").render(Rect::new(0, 0, 4, 1), &mut buffer);
buffer
.cell_mut((0, 0))
.expect("a cell to write")
.set_diff_option(CellDiffOption::ForcedWidth(
NonZeroU16::new(3).expect("three columns"),
));
assert_eq!(Painted::read(&buffer).rows(), vec!["ka"]);
}
}