1use std::collections::HashMap;
30use std::hash::{Hash, Hasher};
31use std::sync::Mutex;
32
33#[derive(Clone, Debug, PartialEq, Eq, Hash)]
35struct LayoutKey {
36 text_hash: u64,
37 max_width_bits: u32, size_bits: u32, }
40
41impl LayoutKey {
42 fn new(text: &str, max_width: f32, size: f32) -> Self {
43 let mut hasher = std::collections::hash_map::DefaultHasher::new();
45 text.hash(&mut hasher);
46 let text_hash = hasher.finish();
47
48 Self {
49 text_hash,
50 max_width_bits: max_width.to_bits(),
51 size_bits: size.to_bits(),
52 }
53 }
54}
55
56#[derive(Clone, Debug)]
58pub struct WrappedText {
59 pub lines: Vec<String>,
61 pub line_height: f32,
63 pub total_height: f32,
65}
66
67pub struct TextLayoutCache {
69 cache: Mutex<HashMap<LayoutKey, WrappedText>>,
70 max_entries: usize,
71}
72
73impl TextLayoutCache {
74 pub fn new(max_entries: usize) -> Self {
76 Self {
77 cache: Mutex::new(HashMap::new()),
78 max_entries,
79 }
80 }
81
82 pub fn get_or_wrap(
84 &self,
85 text: &str,
86 max_width: f32,
87 size: f32,
88 line_height_factor: f32,
89 ) -> WrappedText {
90 let key = LayoutKey::new(text, max_width, size);
91
92 {
94 let cache = self.cache.lock().unwrap();
95 if let Some(wrapped) = cache.get(&key) {
96 return wrapped.clone();
97 }
98 }
99
100 let wrapped = wrap_text_fast(text, max_width, size, line_height_factor);
102
103 {
105 let mut cache = self.cache.lock().unwrap();
106 if cache.len() >= self.max_entries * 2 {
108 let target_size = self.max_entries;
110 let keys_to_remove: Vec<_> = cache
111 .keys()
112 .take(cache.len() - target_size)
113 .cloned()
114 .collect();
115 for k in keys_to_remove {
116 cache.remove(&k);
117 }
118 }
119 cache.insert(key, wrapped.clone());
120 }
121
122 wrapped
123 }
124
125 pub fn clear(&self) {
127 self.cache.lock().unwrap().clear();
128 }
129}
130
131impl Default for TextLayoutCache {
132 fn default() -> Self {
133 Self::new(100)
134 }
135}
136
137pub fn wrap_text_fast(
142 text: &str,
143 max_width: f32,
144 size: f32,
145 line_height_factor: f32,
146) -> WrappedText {
147 let line_height = size * line_height_factor;
148
149 let avg_char_width = size * 0.65;
153 let max_chars = (max_width / avg_char_width).floor() as usize;
154
155 if max_chars == 0 {
156 return WrappedText {
157 lines: vec![],
158 line_height,
159 total_height: 0.0,
160 };
161 }
162
163 let words: Vec<&str> = text.split_whitespace().collect();
165 let mut lines: Vec<String> = Vec::new();
166 let mut current_line = String::new();
167
168 for word in words {
169 let test = if current_line.is_empty() {
170 word.to_string()
171 } else {
172 format!("{} {}", current_line, word)
173 };
174
175 if test.len() <= max_chars {
176 current_line = test;
177 } else {
178 if !current_line.is_empty() {
179 lines.push(current_line);
180 }
181 if word.len() > max_chars {
183 let mut remaining = word;
184 while remaining.len() > max_chars {
185 let (chunk, rest) = remaining.split_at(max_chars);
186 lines.push(chunk.to_string());
187 remaining = rest;
188 }
189 current_line = remaining.to_string();
190 } else {
191 current_line = word.to_string();
192 }
193 }
194 }
195 if !current_line.is_empty() {
196 lines.push(current_line);
197 }
198
199 let total_height = lines.len() as f32 * line_height;
200
201 WrappedText {
202 lines,
203 line_height,
204 total_height,
205 }
206}
207
208pub fn render_wrapped_text<F>(wrapped: &WrappedText, pos: [f32; 2], mut render_line: F)
212where
213 F: FnMut(&str, [f32; 2]),
214{
215 for (i, line) in wrapped.lines.iter().enumerate() {
216 let y = pos[1] + (i as f32) * wrapped.line_height;
217 render_line(line, [pos[0], y]);
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_wrap_text_fast() {
227 let text = "This is a test of the text wrapping system.";
228 let wrapped = wrap_text_fast(text, 100.0, 16.0, 1.2);
229 assert!(!wrapped.lines.is_empty());
230 assert!(wrapped.total_height > 0.0);
231 }
232
233 #[test]
234 fn test_cache() {
235 let cache = TextLayoutCache::new(10);
236 let text = "Hello world";
237
238 let w1 = cache.get_or_wrap(text, 100.0, 16.0, 1.2);
239 let w2 = cache.get_or_wrap(text, 100.0, 16.0, 1.2);
240
241 assert_eq!(w1.lines.len(), w2.lines.len());
243 }
244}