1use std::sync::Mutex;
4
5use std::any::Any;
6
7use super::component::Component;
8
9pub struct Text {
11 content: Mutex<String>,
12 padding_x: usize,
13 padding_y: usize,
14 cache: Mutex<Option<TextCache>>,
18}
19
20#[derive(Clone)]
21struct TextCache {
22 content: String,
23 width: usize,
24 theme_revision: u64,
25 lines: Vec<String>,
26}
27
28impl Text {
29 pub fn new(content: impl Into<String>, padding_x: usize, padding_y: usize) -> Self {
31 Self {
32 content: Mutex::new(content.into()),
33 padding_x,
34 padding_y,
35 cache: Mutex::new(None),
36 }
37 }
38
39 pub fn set_text(&self, text: impl Into<String>) {
41 if let Ok(mut content) = self.content.lock() {
42 *content = text.into();
43 }
44 if let Ok(mut cache) = self.cache.lock() {
45 *cache = None;
46 }
47 }
48
49 pub fn get_text(&self) -> String {
51 self.content.lock().map(|c| c.clone()).unwrap_or_default()
52 }
53}
54
55impl Component for Text {
56 fn render(&self, width: usize) -> Vec<String> {
57 let content = self.content.lock().map(|c| c.clone()).unwrap_or_default();
58 let theme_revision = crate::theme::theme_revision();
59
60 if let Ok(cache) = self.cache.lock() {
61 if let Some(cached) = cache.as_ref() {
62 if cached.width == width
63 && cached.content == content
64 && cached.theme_revision == theme_revision
65 {
66 return cached.lines.clone();
67 }
68 }
69 }
70
71 let mut lines = Vec::new();
72
73 for _ in 0..self.padding_y {
75 lines.push(String::new());
76 }
77
78 let pad = " ".repeat(self.padding_x);
84 let content_width = width.saturating_sub(self.padding_x).max(1);
85 for line in content.lines() {
86 let wrapped = crate::utils::wrap_text_with_ansi(line, content_width);
87 for wl in wrapped {
88 if self.padding_x > 0 {
89 lines.push(format!("{pad}{wl}"));
90 } else {
91 lines.push(wl);
92 }
93 }
94 }
95
96 for _ in 0..self.padding_y {
98 lines.push(String::new());
99 }
100
101 if lines.is_empty() {
102 lines.push(String::new());
103 }
104
105 if let Ok(mut cache) = self.cache.lock() {
106 *cache = Some(TextCache {
107 content,
108 width,
109 theme_revision,
110 lines: lines.clone(),
111 });
112 }
113 lines
114 }
115
116 fn invalidate(&self) {
117 if let Ok(mut cache) = self.cache.lock() {
118 *cache = None;
119 }
120 }
121
122 fn as_any(&self) -> &dyn Any {
123 self
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::component::Component;
131
132 #[test]
138 fn render_truncates_multibyte_at_char_boundary() {
139 let t = Text::new("x⏎y", 0, 0);
140 let out = t.render(1);
142 assert_eq!(out[0], "x");
143 let out = t.render(2);
145 assert_eq!(out[0], "x⏎");
146 let out = t.render(3);
148 assert_eq!(out[0], "x⏎y");
149 }
150
151 #[test]
152 fn render_keeps_ansi_and_truncates() {
153 let styled = "\x1b[31mred\x1b[0m text";
154 let t = Text::new(styled, 0, 0);
155 let out = t.render(3);
156 assert!(crate::ansi::visible_width(&out[0]) <= 3);
158 }
159}