use ratatui::text::{Line, Span};
use super::super::super::app::App;
use super::super::super::pane_bleats::{BleatsPane, Filters};
use super::super::super::tail::{Stream, TailLine};
use super::super::feed_ink;
use crate::output::width::char_columns;
use crate::vocabulary::Role;
pub(super) const TAG_PREFIX_WIDTH: u16 = 5;
pub(super) fn row_height(text: &str, text_width: u16, wrap: bool) -> usize {
if !wrap || text_width == 0 {
return 1;
}
let width = usize::from(text_width);
let mut rows = 1usize;
let mut col = 0usize;
for c in text.chars() {
let w = char_columns(c);
if col > 0 && col + w > width {
rows += 1;
col = 0;
}
col += w;
}
rows
}
pub(super) fn window_range(
len: usize,
scroll_offset: usize,
body_rows: usize,
row_height: impl Fn(usize) -> usize,
) -> std::ops::Range<usize> {
if len == 0 || body_rows == 0 {
return 0..0;
}
let base = tail_window_start(len, body_rows, &row_height);
let skip = base.saturating_sub(scroll_offset);
let mut end = skip;
let mut used = 0usize;
while end < len {
let cost = row_height(end);
if used > 0 && used + cost > body_rows {
break;
}
end += 1;
used += cost;
}
skip..end
}
fn tail_window_start(len: usize, body_rows: usize, cost: impl Fn(usize) -> usize) -> usize {
let mut base = len;
let mut used = 0usize;
while base > 0 {
let row = cost(base - 1);
if used > 0 && used + row > body_rows {
break;
}
base -= 1;
used += row;
}
base
}
#[must_use]
pub(crate) fn max_scroll_offset(app: &App, pane: &BleatsPane) -> usize {
let survivors = pane.visible(&app.feed().lines, &app.feed_classifier());
let body_rows = pane.body_rows();
if body_rows == 0 || survivors.is_empty() {
return 0;
}
let text_width = pane.width().saturating_sub(TAG_PREFIX_WIDTH);
let wrap = pane.wrapped() && pane.width() > 0;
let cost = |index: usize| row_height(&survivors[index].text, text_width, wrap);
tail_window_start(survivors.len(), body_rows, cost)
}
fn page_lines_back(start: usize, body_rows: usize, cost: impl Fn(usize) -> usize) -> usize {
(start - tail_window_start(start, body_rows, &cost)).max(1)
}
fn page_lines_forward(shown: &std::ops::Range<usize>) -> usize {
shown.len().max(1)
}
#[must_use]
pub(crate) fn page_amount_up(app: &App, pane: &BleatsPane) -> usize {
let body_rows = pane.body_rows();
if !pane.wrapped() || pane.width() == 0 {
return body_rows.max(1);
}
let survivors = pane.visible(&app.feed().lines, &app.feed_classifier());
let text_width = pane.width().saturating_sub(TAG_PREFIX_WIDTH);
let cost = |index: usize| row_height(&survivors[index].text, text_width, true);
let shown = window_range(survivors.len(), pane.scroll_offset(), body_rows, cost);
page_lines_back(shown.start, body_rows, cost)
}
#[must_use]
pub(crate) fn page_amount_down(app: &App, pane: &BleatsPane) -> usize {
let body_rows = pane.body_rows();
if !pane.wrapped() || pane.width() == 0 {
return body_rows.max(1);
}
let survivors = pane.visible(&app.feed().lines, &app.feed_classifier());
let text_width = pane.width().saturating_sub(TAG_PREFIX_WIDTH);
let shown = window_range(survivors.len(), pane.scroll_offset(), body_rows, |index| {
row_height(&survivors[index].text, text_width, true)
});
page_lines_forward(&shown)
}
pub(super) fn feed_line_rows(
app: &App,
filters: &Filters,
line: &TailLine,
width: u16,
wrap: bool,
) -> Vec<Line<'static>> {
let palette = app.palette();
let tag = match line.stream {
Stream::Out => "out",
Stream::Err => "err",
};
let prefix = format!("{tag} ");
let text_width = width.saturating_sub(TAG_PREFIX_WIDTH);
let ranges = filters.match_ranges(&line.text);
let ink = feed_ink::Ink::filtered(app.data_palette(), &ranges, palette.band(Role::Butter));
if !wrap {
let mut spans = vec![Span::styled(prefix, palette.muted())];
spans.extend(feed_ink::fitted_spans(line, ink, text_width));
return vec![Line::from(spans)];
}
let spans = feed_ink::text_spans(line, ink);
let indent = " ".repeat(usize::from(TAG_PREFIX_WIDTH));
wrap_spans(spans, usize::from(text_width))
.into_iter()
.enumerate()
.map(|(i, row_spans)| {
let lead = if i == 0 { &prefix } else { &indent };
let mut all = vec![Span::styled(lead.clone(), palette.muted())];
all.extend(row_spans);
Line::from(all)
})
.collect()
}
fn wrap_spans(spans: Vec<Span<'static>>, width: usize) -> Vec<Vec<Span<'static>>> {
if width == 0 {
return vec![spans];
}
let mut rows: Vec<Vec<Span<'static>>> = vec![Vec::new()];
let mut col = 0usize;
for span in spans {
let style = span.style;
let mut buf = String::new();
for c in span.content.chars() {
let w = char_columns(c);
if col > 0 && col + w > width {
if !buf.is_empty() {
rows.last_mut()
.expect("rows always has a current row")
.push(Span::styled(std::mem::take(&mut buf), style));
}
rows.push(Vec::new());
col = 0;
}
buf.push(c);
col += w;
}
if !buf.is_empty() {
rows.last_mut()
.expect("rows always has a current row")
.push(Span::styled(buf, style));
}
}
rows
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn row_height_and_the_rendered_rows_agree_on_a_wide_line() {
let wide = "\u{5e83}".repeat(10);
assert_eq!(row_height(&wide, 8, true), 3, "counted by columns");
assert_eq!(
row_height(&wide, 8, true),
wrap_spans(vec![Span::raw(wide.clone())], 8).len(),
"the counter and the renderer agree"
);
assert_eq!(row_height(&wide, 8, false), 1);
}
#[test]
fn neither_page_direction_steps_over_a_line() {
let profile = |seed: usize, index: usize| -> usize {
1 + (seed.wrapping_mul(31).wrapping_add(index.wrapping_mul(17))) % 4
};
for seed in 0..200usize {
for len in [3usize, 7, 12, 25] {
for body_rows in [2usize, 3, 5, 8] {
let cost = |index: usize| profile(seed, index);
for offset in 0..len {
let shown = window_range(len, offset, body_rows, cost);
if shown.is_empty() {
continue;
}
let back = page_lines_back(shown.start, body_rows, cost);
let up = window_range(len, offset + back, body_rows, cost);
assert!(
up.end + 1 >= shown.start,
"seed {seed} len {len} rows {body_rows} offset {offset}: \
ctrl-u went from {shown:?} to {up:?}"
);
let forward = page_lines_forward(&shown);
let down =
window_range(len, offset.saturating_sub(forward), body_rows, cost);
assert!(
down.start <= shown.end,
"seed {seed} len {len} rows {body_rows} offset {offset}: \
ctrl-d went from {shown:?} to {down:?}"
);
}
}
}
}
}
#[test]
fn page_lines_back_steps_at_least_one_line() {
assert_eq!(page_lines_back(0, 4, |_| 1), 1);
assert_eq!(page_lines_back(3, 2, |_| 5), 1);
assert_eq!(page_lines_back(6, 4, |_| 2), 2);
assert_eq!(page_lines_back(5, 4, |i| if i == 4 { 3 } else { 1 }), 2);
}
}