use std::sync::{Arc, Mutex};
use std::any::Any;
use super::component::{Component, RenderedDocument};
pub struct Text {
content: Mutex<String>,
padding_x: usize,
padding_y: usize,
cache: Mutex<Option<TextCache>>,
}
#[derive(Clone)]
struct TextCache {
width: usize,
theme_revision: u64,
lines: Arc<Vec<String>>,
}
impl Text {
pub fn new(content: impl Into<String>, padding_x: usize, padding_y: usize) -> Self {
Self {
content: Mutex::new(content.into()),
padding_x,
padding_y,
cache: Mutex::new(None),
}
}
pub fn set_text(&self, text: impl Into<String>) {
if let Ok(mut content) = self.content.lock() {
*content = text.into();
if let Ok(mut cache) = self.cache.lock() {
*cache = None;
}
}
}
pub fn get_text(&self) -> String {
self.content.lock().map(|c| c.clone()).unwrap_or_default()
}
}
impl Text {
fn shared_lines(&self, width: usize) -> Arc<Vec<String>> {
let content = self.content.lock().unwrap_or_else(|e| e.into_inner());
let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
let theme_revision = crate::theme::theme_revision();
if let Some(cached) = cache.as_ref() {
if cached.width == width && cached.theme_revision == theme_revision {
return Arc::clone(&cached.lines);
}
}
let mut lines = Vec::new();
for _ in 0..self.padding_y {
lines.push(String::new());
}
let pad = " ".repeat(self.padding_x);
let content_width = width.saturating_sub(self.padding_x).max(1);
for line in content.lines() {
let wrapped = crate::utils::wrap_text_with_ansi(line, content_width);
for wl in wrapped {
if self.padding_x > 0 {
lines.push(format!("{pad}{wl}"));
} else {
lines.push(wl);
}
}
}
for _ in 0..self.padding_y {
lines.push(String::new());
}
if lines.is_empty() {
lines.push(String::new());
}
let lines = Arc::new(lines);
*cache = Some(TextCache {
width,
theme_revision,
lines: Arc::clone(&lines),
});
lines
}
}
impl Component for Text {
fn render(&self, width: usize) -> Vec<String> {
self.shared_lines(width).as_ref().clone()
}
fn render_document(&self, width: usize) -> RenderedDocument {
RenderedDocument::from_shared(self.shared_lines(width))
}
fn invalidate(&self) {
if let Ok(_content) = self.content.lock() {
if let Ok(mut cache) = self.cache.lock() {
*cache = None;
}
}
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::component::Component;
#[test]
fn shared_rows_reuse_and_regenerate_on_mutation_and_width() {
let text = Text::new("abcdefgh", 0, 0);
let first = text.shared_lines(80);
let _document = text.render_document(80);
assert!(Arc::ptr_eq(&first, &text.shared_lines(80)));
assert_eq!(first.as_ref(), &text.render(80));
let narrow = text.shared_lines(4);
assert!(!Arc::ptr_eq(&first, &narrow));
assert_eq!(narrow.as_ref(), &vec!["abcd", "efgh"]);
text.set_text("new text");
let changed = text.shared_lines(80);
assert!(!Arc::ptr_eq(&first, &changed));
assert_eq!(changed.as_ref(), &vec!["new text"]);
text.invalidate();
let refreshed = text.shared_lines(80);
assert!(!Arc::ptr_eq(&changed, &refreshed));
assert_eq!(changed.as_ref(), refreshed.as_ref());
}
#[test]
fn render_truncates_multibyte_at_char_boundary() {
let t = Text::new("x⏎y", 0, 0);
let out = t.render(1);
assert_eq!(out[0], "x");
let out = t.render(2);
assert_eq!(out[0], "x⏎");
let out = t.render(3);
assert_eq!(out[0], "x⏎y");
}
#[test]
fn render_keeps_ansi_and_truncates() {
let styled = "\x1b[31mred\x1b[0m text";
let t = Text::new(styled, 0, 0);
let out = t.render(3);
assert!(crate::ansi::visible_width(&out[0]) <= 3);
}
}