use ratatui::{
style::{Color, Style},
text::Span,
};
use unicode_width::UnicodeWidthChar;
use super::{LineCaret, TextCursor};
use crate::{style, theme::Palette};
#[must_use]
pub fn intersect(
s: usize,
e: usize,
lo: usize,
hi: usize,
) -> Option<(usize, usize)> {
let start = s.max(lo);
let end = e.min(hi);
(start < end).then_some((start, end))
}
pub(super) const SCROLL_MARKER: &str = "\u{2026}";
#[derive(Debug, Clone, Copy)]
pub(super) struct LineView<'a> {
pub(super) width: usize,
pub(super) base: Style,
pub(super) content: Option<&'a [Style]>,
pub(super) caret: bool,
pub(super) head_marker: bool,
pub(super) tail_marker: bool,
}
impl<'a> LineView<'a> {
pub(super) fn field(width: usize, base: Style, focused: bool) -> Self {
Self {
width,
base,
content: None,
caret: focused,
head_marker: false,
tail_marker: false,
}
}
pub(super) fn flat(width: usize) -> Self {
Self {
width,
base: Style::default(),
content: None,
caret: true,
head_marker: true,
tail_marker: false,
}
}
pub(super) fn embedded(
width: usize,
base: Style,
content: Option<&'a [Style]>,
) -> Self {
Self {
width,
base,
content,
caret: true,
head_marker: true,
tail_marker: true,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LinePaint<'a> {
pub caret: LineCaret,
pub base: Style,
pub content: Option<&'a [Style]>,
}
#[must_use]
pub fn line_spans(
visible: &str,
paint: LinePaint<'_>,
palette: &Palette,
) -> Vec<Span<'static>> {
let chars: Vec<char> = visible.chars().collect();
let cursor_style = style::cursor(palette).fg(Color::Black);
let selection_style = style::bg(palette.selection);
let mut spans: Vec<Span<'static>> = Vec::new();
let mut run = String::new();
let mut run_style = paint.base;
for (index, ch) in chars.iter().enumerate() {
let cell_base = match paint.content.and_then(|over| over.get(index)) {
Some(over) => paint.base.patch(*over),
None => paint.base,
};
let style = cell_style(
index,
paint.caret,
cell_base,
cursor_style,
selection_style,
);
if !run.is_empty() && style != run_style {
spans.push(Span::styled(std::mem::take(&mut run), run_style));
}
if run.is_empty() {
run_style = style;
}
run.push(*ch);
}
if !run.is_empty() {
spans.push(Span::styled(run, run_style));
}
if paint.caret.cursor == Some(chars.len()) {
spans.push(Span::styled(" ".to_string(), cursor_style));
}
spans
}
pub(super) fn cell_style(
index: usize,
caret: LineCaret,
base: Style,
cursor_style: Style,
selection_style: Style,
) -> Style {
if caret.cursor == Some(index) {
return base.patch(cursor_style);
}
if let Some((start, end)) = caret.selection
&& index >= start
&& index < end
{
return base.patch(selection_style);
}
base
}
#[derive(Debug, Clone, Copy)]
pub struct ScrollPaint<'a> {
pub cursor: TextCursor,
pub width: usize,
pub base: Style,
pub content: Option<&'a [Style]>,
}
#[must_use]
pub fn scrolled_line_spans(
value: &str,
paint: ScrollPaint<'_>,
palette: &Palette,
) -> Vec<Span<'static>> {
let view = LineView::embedded(paint.width, paint.base, paint.content);
caret_spans(value, &paint.cursor, palette, view).0
}
pub fn query_spans(
text: &str,
palette: &Palette,
width: usize,
) -> Vec<Span<'static>> {
query_spans_at(text, TextCursor::at_end(text), palette, width)
}
#[must_use]
pub fn query_spans_at(
text: &str,
cursor: TextCursor,
palette: &Palette,
width: usize,
) -> Vec<Span<'static>> {
caret_spans(text, &cursor, palette, LineView::flat(width)).0
}
pub(super) fn caret_spans(
text: &str,
cursor: &TextCursor,
palette: &Palette,
view: LineView<'_>,
) -> (Vec<Span<'static>>, usize) {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let pos = cursor.pos.min(len);
let width = view.width.max(1);
let widths: Vec<usize> =
chars.iter().map(|ch| ch.width().unwrap_or(0)).collect();
let mut column_at = vec![0usize; len + 1];
for index in 0..len {
column_at[index + 1] = column_at[index] + widths[index];
}
let (start, end, head, tail) = window(&column_at, pos, width, view);
let marker_style = style::secondary(palette);
let mut spans: Vec<Span<'static>> = Vec::new();
let mut used = 0usize;
if head {
spans.push(Span::styled(SCROLL_MARKER, marker_style));
used += 1;
}
let visible: String = chars[start..end].iter().collect();
let caret_col = (view.caret && pos >= start && pos <= end)
.then(|| pos.saturating_sub(start));
let selection = cursor
.selection()
.and_then(|(from, to)| intersect(from, to, start, end))
.map(|(from, to)| (from - start, to - start));
let paint = LinePaint {
caret: LineCaret {
cursor: caret_col,
selection,
},
base: view.base,
content: view.content.map(|over| &over[start..end.min(over.len())]),
};
spans.extend(line_spans(&visible, paint, palette));
used += column_at[end] - column_at[start];
if caret_col == Some(end - start) {
used += 1; }
if tail {
spans.push(Span::styled(SCROLL_MARKER, marker_style));
used += 1;
}
(spans, used)
}
pub(super) fn window(
column_at: &[usize],
pos: usize,
width: usize,
view: LineView<'_>,
) -> (usize, usize, bool, bool) {
let len = column_at.len() - 1;
let caret_column = column_at[pos];
let (mut head, mut tail) = (false, false);
loop {
let avail = width - usize::from(head) - usize::from(tail);
let start = scroll_start(column_at, caret_column, avail);
let mut end = start;
while end < len && column_at[end + 1] - column_at[start] <= avail {
end += 1;
}
let next = afford(
width,
view.head_marker && start > 0,
view.tail_marker && end < len,
);
if next == (head, tail) {
return (start, end, head, tail);
}
(head, tail) = next;
}
}
pub(super) fn afford(width: usize, head: bool, tail: bool) -> (bool, bool) {
let (mut head, mut tail) = (head, tail);
while usize::from(head) + usize::from(tail) + 1 > width {
if tail {
tail = false;
} else if head {
head = false;
} else {
break;
}
}
(head, tail)
}
pub(super) fn scroll_start(
column_at: &[usize],
caret_column: usize,
width: usize,
) -> usize {
let last = column_at.len() - 1;
let window = caret_column.saturating_sub(width.max(1) - 1);
(0..=last)
.find(|&index| column_at[index] >= window)
.unwrap_or(last)
}