use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::widgets::{Paragraph, Widget};
use super::block::{Text, TextSpan};
use super::controller::settings::theme::groups::Groups;
use super::wrap::{WordWrap, WrapPolicy};
pub type SpanLine = Vec<TextSpan>;
pub fn wrap(line: &[TextSpan], width: u16, policy: &dyn WrapPolicy) -> Vec<SpanLine> {
let mut text = String::new();
let bounds = line
.iter()
.map(|span| {
let start = text.len();
text.push_str(&span.content);
(start..text.len(), span)
})
.collect::<Vec<_>>();
policy
.wrap(&text, width)
.into_iter()
.map(|visual| {
bounds
.iter()
.filter_map(|(range, span)| {
let start = range.start.max(visual.range.start);
let end = range.end.min(visual.range.end);
(start < end).then(|| span.with_content(&text[start..end]))
})
.collect()
})
.collect()
}
pub struct Presentation {
lines: Vec<SpanLine>,
policy: Box<dyn WrapPolicy + Send>,
cells: Buffer,
}
impl Presentation {
pub fn new(lines: Vec<SpanLine>, policy: impl WrapPolicy + Send + 'static) -> Self {
Self {
lines,
policy: Box::new(policy),
cells: Buffer::empty(Rect::ZERO),
}
}
pub fn prose(lines: Vec<SpanLine>) -> Self {
Self::new(lines, WordWrap)
}
pub fn resolve(&mut self, width: u16, groups: &Groups, base: Style) {
let rows = self
.lines
.iter()
.flat_map(|line| wrap(line, width, self.policy.as_ref()))
.map(|row| Line::from(row.spans(groups)))
.collect::<Vec<_>>();
let height = u16::try_from(rows.len()).unwrap_or(u16::MAX);
let area = Rect::new(0, 0, width, height);
self.cells = Buffer::empty(area);
Paragraph::new(rows)
.style(base)
.render(area, &mut self.cells);
}
pub fn cells(&self) -> &Buffer {
&self.cells
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::controller::settings::theme::groups::HLGroup;
fn line() -> SpanLine {
vec![
TextSpan::new("hello ", HLGroup::Keyword),
TextSpan::new("world", HLGroup::Variable),
]
}
#[test]
fn wrapping_divides_a_straddling_span_and_keeps_its_group() {
let rows = wrap(&line(), 8, &WordWrap);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][0].content, "hello");
assert_eq!(rows[0][0].kind, HLGroup::Keyword);
assert_eq!(rows[1][0].content, "world");
assert_eq!(rows[1][0].kind, HLGroup::Variable);
}
#[test]
fn a_row_narrower_than_a_span_splits_it_without_losing_its_group() {
let rows = wrap(
&[TextSpan::new("abcdefgh", HLGroup::Variable)],
4,
&WordWrap,
);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][0].content, "abcd");
assert_eq!(rows[1][0].content, "efgh");
assert!(rows.iter().all(|row| row[0].kind == HLGroup::Variable));
}
#[test]
fn resolving_paints_each_group_through_the_scheme() {
let groups = Groups::default();
let mut presentation = Presentation::prose(vec![line()]);
presentation.resolve(20, &groups, Style::new());
let cells = presentation.cells();
assert_eq!(cells.area().width, 20);
assert_eq!(cells[(0, 0)].symbol(), "h");
assert_eq!(cells[(0, 0)].fg, groups.style(HLGroup::Keyword).fg.unwrap());
assert_eq!(
cells[(6, 0)].fg,
groups.style(HLGroup::Variable).fg.unwrap()
);
}
#[test]
fn rescheming_repaints_the_same_wrapped_content() {
let mut presentation = Presentation::prose(vec![line()]);
presentation.resolve(8, &Groups::default(), Style::new());
let wrapped = presentation.cells().area().height;
presentation.resolve(8, &Groups::default(), Style::new());
assert_eq!(presentation.cells().area().height, wrapped);
}
}