use std::cell::RefCell;
use crate::core::buffer::Buffer;
use crate::core::rect::Rect;
use crate::scroll_state::ScrollState;
use crate::style::Style;
use crate::theme::ThemeTokens;
use crate::widgets::paragraph::{render_line, Alignment, Line, Paragraph, Text, WrapMode};
use crate::widgets::Widget;
#[derive(Debug, Clone, PartialEq)]
struct WrappedTextCache {
width: u16,
text: Text,
style: Style,
wrap: WrapMode,
alignment: Alignment,
rows: Vec<Line>,
}
#[derive(Debug, Clone)]
pub struct ScrollableText {
pub text: Text,
pub style: Style,
pub wrap: WrapMode,
pub scroll: ScrollState,
pub alignment: Alignment,
cache: RefCell<Option<WrappedTextCache>>,
}
impl ScrollableText {
pub fn new(text: Text) -> Self {
Self {
text,
style: Style::new(),
wrap: WrapMode::Word { trim: true },
scroll: ScrollState::new(),
alignment: Alignment::Left,
cache: RefCell::new(None),
}
}
pub fn raw(content: &str) -> Self {
Self::new(Text::raw(content))
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self.invalidate();
self
}
pub fn with_theme_tokens(self, tokens: ThemeTokens) -> Self {
self.with_style(tokens.text_style())
}
pub fn wrap(mut self, mode: WrapMode) -> Self {
self.wrap = mode;
self.invalidate();
self
}
pub fn with_scroll(mut self, scroll: ScrollState) -> Self {
self.scroll = scroll;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self.invalidate();
self
}
pub fn set_text(&mut self, text: Text) {
self.text = text;
self.invalidate();
}
pub fn push_line(&mut self, line: Line) {
self.text.push_line(line);
self.invalidate();
}
pub fn invalidate(&self) {
*self.cache.borrow_mut() = None;
}
pub fn rows_for_width(&self, width: u16) -> Vec<Line> {
let mut cache = self.cache.borrow_mut();
if let Some(cached) = cache.as_ref() {
if cached.width == width
&& cached.text == self.text
&& cached.style == self.style
&& cached.wrap == self.wrap
&& cached.alignment == self.alignment
{
return cached.rows.clone();
}
}
let rows = Paragraph::from_text(self.text.clone())
.with_style(self.style)
.wrap(self.wrap)
.alignment(self.alignment)
.wrapped_rows(width);
*cache = Some(WrappedTextCache {
width,
text: self.text.clone(),
style: self.style,
wrap: self.wrap,
alignment: self.alignment,
rows: rows.clone(),
});
rows
}
pub fn rendered_height(&self, width: u16) -> usize {
self.rows_for_width(width).len()
}
}
impl From<Text> for ScrollableText {
fn from(value: Text) -> Self {
Self::new(value)
}
}
impl From<&str> for ScrollableText {
fn from(value: &str) -> Self {
Self::raw(value)
}
}
impl Widget for ScrollableText {
fn render(&self, buffer: &mut Buffer, area: Rect) {
if area.is_empty() {
return;
}
let rows = self.rows_for_width(area.width);
let mut scroll = self.scroll;
scroll.set_bounds(rows.len(), area.height as usize);
for (screen_row, row_idx) in scroll.visible_range().enumerate() {
let y = area.y as usize + screen_row;
if y >= area.bottom() as usize {
break;
}
render_line(buffer, &rows[row_idx], area, y, 0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::color::Color;
#[test]
fn scrollable_text_caches_wrapped_rows_by_width() {
let text = ScrollableText::raw("alpha beta gamma").wrap(WrapMode::Word { trim: true });
assert_eq!(text.rendered_height(8), 3);
assert_eq!(text.rendered_height(80), 1);
}
#[test]
fn scrollable_text_renders_styled_lines() {
let text = Text::new(vec![Line::styled("hello", Style::new().fg(Color::CYAN))]);
let widget = ScrollableText::new(text);
let mut buffer = Buffer::new(8, 1);
widget.render(&mut buffer, Rect::new(0, 0, 8, 1));
assert_eq!(buffer.get(0, 0).unwrap().ch, 'h');
assert_eq!(buffer.get(0, 0).unwrap().fg, Color::CYAN);
}
}