use crate::textarea::TextArea;
use crate::util::num_digits;
use crate::wrap::WrapMode;
#[cfg(feature = "portable-atomic")]
use portable_atomic::{AtomicU64, Ordering};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::text::{Line, Span, Text};
use ratatui_core::widgets::Widget;
use ratatui_widgets::paragraph::Paragraph;
use std::cmp;
#[cfg(not(feature = "portable-atomic"))]
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Default, Debug)]
pub struct Viewport(AtomicU64);
impl Clone for Viewport {
fn clone(&self) -> Self {
let u = self.0.load(Ordering::Relaxed);
Viewport(AtomicU64::new(u))
}
}
impl Viewport {
pub fn scroll_top(&self) -> (u16, u16) {
let u = self.0.load(Ordering::Relaxed);
((u >> 16) as u16, u as u16)
}
pub fn rect(&self) -> (u16, u16, u16, u16) {
let u = self.0.load(Ordering::Relaxed);
let width = (u >> 48) as u16;
let height = (u >> 32) as u16;
let row = (u >> 16) as u16;
let col = u as u16;
(row, col, width, height)
}
pub fn position(&self) -> (u16, u16, u16, u16) {
let (row_top, col_top, width, height) = self.rect();
let row_bottom = row_top.saturating_add(height).saturating_sub(1);
let col_bottom = col_top.saturating_add(width).saturating_sub(1);
(
row_top,
col_top,
cmp::max(row_top, row_bottom),
cmp::max(col_top, col_bottom),
)
}
fn store(&self, row: u16, col: u16, width: u16, height: u16) {
let u =
((width as u64) << 48) | ((height as u64) << 32) | ((row as u64) << 16) | col as u64;
self.0.store(u, Ordering::Relaxed);
}
pub fn scroll(&mut self, rows: i16, cols: i16) {
fn apply_scroll(pos: u16, delta: i16) -> u16 {
if delta >= 0 {
pos.saturating_add(delta as u16)
} else {
pos.saturating_sub(-delta as u16)
}
}
let u = self.0.get_mut();
let row = apply_scroll((*u >> 16) as u16, rows);
let col = apply_scroll(*u as u16, cols);
*u = (*u & 0xffff_ffff_0000_0000) | ((row as u64) << 16) | (col as u64);
}
}
#[inline]
fn next_scroll_top(prev_top: u16, cursor: u16, len: u16) -> u16 {
if cursor < prev_top {
cursor
} else if prev_top + len <= cursor {
cursor + 1 - len
} else {
prev_top
}
}
impl<'a> TextArea<'a> {
fn text_widget(&'a self, top_row: usize, height: usize) -> Text<'a> {
let lnum_len = num_digits(self.lines().len());
let screen_lines = self.screen_lines.borrow();
let bottom_row = cmp::min(top_row + height, screen_lines.len());
let mut lines = Vec::with_capacity(bottom_row - top_row);
for row in &screen_lines[top_row..bottom_row] {
let line = &self.lines()[row.wrapped.row];
lines.push(self.line_spans_segment(line, &row.wrapped, lnum_len));
}
Text::from(lines)
}
fn scroll_top_row(&self, prev_top: u16, height: u16) -> u16 {
next_scroll_top(prev_top, self.screen_cursor().row as u16, height)
}
fn scroll_top_col(&self, prev_top: u16, width: u16) -> u16 {
let mut cursor = self.screen_cursor().col as u16;
if self.line_number_style().is_some() {
let lnum = num_digits(self.lines().len()) as u16 + 2; if cursor <= lnum {
cursor *= 2; } else {
cursor += lnum; };
}
next_scroll_top(prev_top, cursor, width)
}
}
impl Widget for &TextArea<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let inner_area = if let Some(b) = self.block() {
b.inner(area)
} else {
area
};
let Rect { width, height, .. } = inner_area;
if self.area.get() != inner_area {
self.area.set(inner_area);
self.screen_map_load();
}
let (prev_top_row, prev_top_col) = self.viewport.scroll_top();
let (text, top_row, top_col) = if self.is_empty() && !self.placeholder.lines.is_empty() {
let mut placeholder = self.placeholder.clone();
let cursor = Span::styled(" ", self.cursor_style);
if let Some(first_line) = placeholder.lines.first_mut() {
first_line.spans.insert(0, cursor);
} else {
placeholder.lines.push(Line::from(vec![cursor]));
}
(placeholder, 0u16, 0u16)
} else {
let top_row = self.scroll_top_row(prev_top_row, height);
let top_col = if self.wrap_mode() == WrapMode::None {
self.scroll_top_col(prev_top_col, width)
} else {
0
};
(
self.text_widget(top_row as _, height as _)
.style(self.style()),
top_row,
top_col,
)
};
let mut text_area = area;
let mut inner = Paragraph::new(text).alignment(self.alignment());
if let Some(b) = self.block() {
text_area = b.inner(area);
b.render(area, buf)
}
if top_col != 0 {
inner = inner.scroll((0, top_col));
}
self.viewport.store(top_row, top_col, width, height);
inner.render(text_area, buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_store_and_load() {
let vp = Viewport::default();
vp.store(3, 7, 80, 24);
assert_eq!(vp.scroll_top(), (3, 7));
let (row, col, width, height) = vp.rect();
assert_eq!((row, col, width, height), (3, 7, 80, 24));
}
#[test]
fn viewport_clone() {
let vp = Viewport::default();
vp.store(5, 2, 40, 10);
let vp2 = vp.clone();
assert_eq!(vp2.scroll_top(), (5, 2));
}
}