1#![forbid(unsafe_code)]
2
3use crate::{StatefulWidget, Widget};
6use ftui_core::geometry::Rect;
7use ftui_render::buffer::Buffer;
8use ftui_render::cell::Cell;
9use ftui_render::frame::Frame;
10use std::collections::hash_map::DefaultHasher;
11use std::hash::{Hash, Hasher};
12use std::mem::size_of;
13
14#[cfg(feature = "tracing")]
15use tracing::{debug, trace};
16
17pub trait CacheKey<W> {
19 fn cache_key(&self, widget: &W) -> Option<u64>;
21}
22
23#[derive(Debug, Clone, Copy, Default)]
25pub struct NoCacheKey;
26
27impl<W> CacheKey<W> for NoCacheKey {
28 fn cache_key(&self, _widget: &W) -> Option<u64> {
29 None
30 }
31}
32
33#[derive(Debug, Clone, Copy, Default)]
35pub struct HashKey;
36
37impl<W: Hash> CacheKey<W> for HashKey {
38 fn cache_key(&self, widget: &W) -> Option<u64> {
39 Some(hash_value(widget))
40 }
41}
42
43#[derive(Debug, Clone, Copy)]
45pub struct FnKey<F>(pub F);
46
47impl<W, F: Fn(&W) -> u64> CacheKey<W> for FnKey<F> {
48 fn cache_key(&self, widget: &W) -> Option<u64> {
49 Some((self.0)(widget))
50 }
51}
52
53pub struct CachedWidget<W, K = NoCacheKey> {
57 inner: W,
58 key: K,
59}
60
61#[derive(Debug, Clone)]
63struct CachedBuffer {
64 buffer: Buffer,
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct CachedWidgetState {
70 cache: Option<CachedBuffer>,
71 last_area: Option<Rect>,
72 dirty: bool,
73 last_key: Option<u64>,
74}
75
76#[cfg(feature = "tracing")]
77#[derive(Debug, Clone, Copy)]
78enum CacheMissReason {
79 Empty,
80 Dirty,
81 AreaChanged,
82 KeyChanged,
83}
84
85impl<W> CachedWidget<W, NoCacheKey> {
86 pub fn new(widget: W) -> Self {
88 Self {
89 inner: widget,
90 key: NoCacheKey,
91 }
92 }
93}
94
95impl<W: Hash> CachedWidget<W, HashKey> {
96 pub fn with_hash(widget: W) -> Self {
98 Self {
99 inner: widget,
100 key: HashKey,
101 }
102 }
103}
104
105impl<W, F: Fn(&W) -> u64> CachedWidget<W, FnKey<F>> {
106 pub fn with_key(widget: W, key_fn: F) -> Self {
108 Self {
109 inner: widget,
110 key: FnKey(key_fn),
111 }
112 }
113}
114
115impl<W, K> CachedWidget<W, K> {
116 pub fn inner(&self) -> &W {
118 &self.inner
119 }
120
121 pub fn inner_mut(&mut self) -> &mut W {
123 &mut self.inner
124 }
125
126 pub fn into_inner(self) -> W {
128 self.inner
129 }
130
131 pub fn mark_dirty(&self, state: &mut CachedWidgetState) {
133 state.mark_dirty();
134 #[cfg(feature = "tracing")]
135 debug!(
136 widget = std::any::type_name::<W>(),
137 "Cache invalidated via mark_dirty()"
138 );
139 }
140}
141
142impl CachedWidgetState {
143 pub fn new() -> Self {
145 Self::default()
146 }
147
148 pub fn mark_dirty(&mut self) {
150 self.dirty = true;
151 }
152
153 pub fn clear_cache(&mut self) {
155 self.cache = None;
156 }
157
158 pub fn cache_size_bytes(&self) -> usize {
160 self.cache
161 .as_ref()
162 .map(|cache| cache.buffer.len() * size_of::<Cell>())
163 .unwrap_or(0)
164 }
165}
166
167impl<W: Widget, K: CacheKey<W>> StatefulWidget for CachedWidget<W, K> {
168 type State = CachedWidgetState;
169
170 fn render(&self, area: Rect, frame: &mut Frame, state: &mut CachedWidgetState) {
171 #[cfg(feature = "tracing")]
172 let _span = tracing::debug_span!(
173 "widget_render",
174 widget = "CachedWidget",
175 x = area.x,
176 y = area.y,
177 w = area.width,
178 h = area.height
179 )
180 .entered();
181
182 if area.is_empty() {
183 state.clear_cache();
184 state.last_area = Some(area);
185 return;
186 }
187
188 let key = self.key.cache_key(&self.inner);
189 let area_changed = state.last_area != Some(area);
190 let key_changed = key != state.last_key;
191
192 let needs_render = state.cache.is_none() || state.dirty || area_changed || key_changed;
193
194 #[cfg(feature = "tracing")]
195 let reason = if state.cache.is_none() {
196 CacheMissReason::Empty
197 } else if state.dirty {
198 CacheMissReason::Dirty
199 } else if area_changed {
200 CacheMissReason::AreaChanged
201 } else {
202 CacheMissReason::KeyChanged
203 };
204
205 if needs_render {
206 let local_area = Rect::from_size(area.width, area.height);
207 let mut cache_frame = Frame::new(area.width, area.height, frame.pool);
209 self.inner.render(local_area, &mut cache_frame);
210 state.cache = Some(CachedBuffer {
212 buffer: cache_frame.buffer,
213 });
214 state.last_area = Some(area);
215 state.dirty = false;
216 state.last_key = key;
217
218 #[cfg(feature = "tracing")]
219 debug!(
220 widget = std::any::type_name::<W>(),
221 reason = ?reason,
222 "Cache miss, re-rendering"
223 );
224 } else {
225 #[cfg(feature = "tracing")]
226 trace!(
227 widget = std::any::type_name::<W>(),
228 "Cache hit, using cached buffer"
229 );
230 }
231
232 if let Some(cache) = &state.cache {
233 let src_rect = Rect::from_size(area.width, area.height);
234 frame
235 .buffer
236 .copy_from(&cache.buffer, src_rect, area.x, area.y);
237 }
238 }
239}
240
241fn hash_value<T: Hash>(value: &T) -> u64 {
242 let mut hasher = DefaultHasher::new();
243 value.hash(&mut hasher);
244 hasher.finish()
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use ftui_render::grapheme_pool::GraphemePool;
251 use std::cell::Cell as CounterCell;
252 use std::rc::Rc;
253
254 #[derive(Debug, Clone)]
255 struct CountWidget {
256 count: Rc<CounterCell<u32>>,
257 }
258
259 impl Widget for CountWidget {
260 fn render(&self, area: Rect, frame: &mut Frame) {
261 self.count.set(self.count.get() + 1);
262 if !area.is_empty() {
263 frame.buffer.set(area.x, area.y, Cell::from_char('x'));
264 }
265 }
266 }
267
268 #[derive(Debug, Clone)]
269 struct KeyWidget {
270 count: Rc<CounterCell<u32>>,
271 key: Rc<CounterCell<u64>>,
272 }
273
274 impl Widget for KeyWidget {
275 fn render(&self, area: Rect, frame: &mut Frame) {
276 self.count.set(self.count.get() + 1);
277 if !area.is_empty() {
278 frame.buffer.set(area.x, area.y, Cell::from_char('k'));
279 }
280 }
281 }
282
283 #[test]
284 fn cache_hit_skips_rerender() {
285 let count = Rc::new(CounterCell::new(0));
286 let widget = CountWidget {
287 count: count.clone(),
288 };
289 let cached = CachedWidget::new(widget);
290 let mut state = CachedWidgetState::default();
291 let mut pool = GraphemePool::new();
292 let mut frame = Frame::new(5, 5, &mut pool);
293 let area = Rect::new(1, 1, 3, 3);
294
295 cached.render(area, &mut frame, &mut state);
296 cached.render(area, &mut frame, &mut state);
297
298 assert_eq!(count.get(), 1);
299 }
300
301 #[test]
302 fn area_change_forces_rerender() {
303 let count = Rc::new(CounterCell::new(0));
304 let widget = CountWidget {
305 count: count.clone(),
306 };
307 let cached = CachedWidget::new(widget);
308 let mut state = CachedWidgetState::default();
309 let mut pool = GraphemePool::new();
310 let mut frame = Frame::new(6, 6, &mut pool);
311
312 cached.render(Rect::new(0, 0, 3, 3), &mut frame, &mut state);
313 cached.render(Rect::new(1, 1, 3, 3), &mut frame, &mut state);
314
315 assert_eq!(count.get(), 2);
316 }
317
318 #[test]
319 fn mark_dirty_forces_rerender() {
320 let count = Rc::new(CounterCell::new(0));
321 let widget = CountWidget {
322 count: count.clone(),
323 };
324 let cached = CachedWidget::new(widget);
325 let mut state = CachedWidgetState::default();
326 let mut pool = GraphemePool::new();
327 let mut frame = Frame::new(5, 5, &mut pool);
328 let area = Rect::new(0, 0, 3, 3);
329
330 cached.render(area, &mut frame, &mut state);
331 cached.mark_dirty(&mut state);
332 cached.render(area, &mut frame, &mut state);
333
334 assert_eq!(count.get(), 2);
335 }
336
337 #[test]
338 fn key_change_forces_rerender() {
339 let count = Rc::new(CounterCell::new(0));
340 let key = Rc::new(CounterCell::new(1));
341 let widget = KeyWidget {
342 count: count.clone(),
343 key: key.clone(),
344 };
345 let cached = CachedWidget::with_key(widget, |w| w.key.get());
346 let mut state = CachedWidgetState::default();
347 let mut pool = GraphemePool::new();
348 let mut frame = Frame::new(5, 5, &mut pool);
349 let area = Rect::new(0, 0, 3, 3);
350
351 cached.render(area, &mut frame, &mut state);
352 key.set(2);
353 cached.render(area, &mut frame, &mut state);
354
355 assert_eq!(count.get(), 2);
356 }
357
358 #[test]
359 fn empty_area_clears_cache() {
360 let count = Rc::new(CounterCell::new(0));
361 let widget = CountWidget {
362 count: count.clone(),
363 };
364 let cached = CachedWidget::new(widget);
365 let mut state = CachedWidgetState::default();
366 let mut pool = GraphemePool::new();
367 let mut frame = Frame::new(5, 5, &mut pool);
368
369 cached.render(Rect::new(0, 0, 3, 3), &mut frame, &mut state);
371 assert!(state.cache.is_some());
372
373 cached.render(Rect::new(0, 0, 0, 0), &mut frame, &mut state);
375 assert!(state.cache.is_none());
376 assert_eq!(count.get(), 1);
377 }
378
379 #[test]
380 fn cache_size_bytes_empty() {
381 let state = CachedWidgetState::new();
382 assert_eq!(state.cache_size_bytes(), 0);
383 }
384
385 #[test]
386 fn cache_size_bytes_after_render() {
387 let count = Rc::new(CounterCell::new(0));
388 let widget = CountWidget {
389 count: count.clone(),
390 };
391 let cached = CachedWidget::new(widget);
392 let mut state = CachedWidgetState::new();
393 let mut pool = GraphemePool::new();
394 let mut frame = Frame::new(5, 5, &mut pool);
395
396 cached.render(Rect::new(0, 0, 3, 3), &mut frame, &mut state);
397 assert!(state.cache_size_bytes() > 0);
398 assert_eq!(state.cache_size_bytes(), 9 * std::mem::size_of::<Cell>());
399 }
400
401 #[test]
402 fn clear_cache_drops_buffer() {
403 let count = Rc::new(CounterCell::new(0));
404 let widget = CountWidget {
405 count: count.clone(),
406 };
407 let cached = CachedWidget::new(widget);
408 let mut state = CachedWidgetState::new();
409 let mut pool = GraphemePool::new();
410 let mut frame = Frame::new(5, 5, &mut pool);
411
412 cached.render(Rect::new(0, 0, 3, 3), &mut frame, &mut state);
413 assert!(state.cache_size_bytes() > 0);
414
415 state.clear_cache();
416 assert_eq!(state.cache_size_bytes(), 0);
417 }
418
419 #[test]
420 fn mark_dirty_then_clear_on_render() {
421 let count = Rc::new(CounterCell::new(0));
422 let widget = CountWidget {
423 count: count.clone(),
424 };
425 let cached = CachedWidget::new(widget);
426 let mut state = CachedWidgetState::new();
427 let mut pool = GraphemePool::new();
428 let mut frame = Frame::new(5, 5, &mut pool);
429 let area = Rect::new(0, 0, 3, 3);
430
431 cached.render(area, &mut frame, &mut state);
432 assert_eq!(count.get(), 1);
433
434 state.mark_dirty();
435 assert!(state.dirty);
436
437 cached.render(area, &mut frame, &mut state);
438 assert_eq!(count.get(), 2);
439 assert!(!state.dirty);
440 }
441
442 #[test]
443 fn no_cache_key_returns_none() {
444 let key = NoCacheKey;
445 assert_eq!(CacheKey::<u32>::cache_key(&key, &42), None);
446 }
447
448 #[test]
449 fn hash_key_returns_some() {
450 let key = HashKey;
451 let result = CacheKey::<String>::cache_key(&key, &"hello".to_string());
452 assert!(result.is_some());
453 }
454
455 #[test]
456 fn hash_key_same_value_same_key() {
457 let key = HashKey;
458 let a = CacheKey::<u64>::cache_key(&key, &42);
459 let b = CacheKey::<u64>::cache_key(&key, &42);
460 assert_eq!(a, b);
461 }
462
463 #[test]
464 fn hash_key_different_value_different_key() {
465 let key = HashKey;
466 let a = CacheKey::<u64>::cache_key(&key, &1);
467 let b = CacheKey::<u64>::cache_key(&key, &2);
468 assert_ne!(a, b);
469 }
470
471 #[test]
472 fn fn_key_custom_function() {
473 let key = FnKey(|x: &u32| (*x as u64) * 100);
474 assert_eq!(CacheKey::<u32>::cache_key(&key, &5), Some(500));
475 assert_eq!(CacheKey::<u32>::cache_key(&key, &0), Some(0));
476 }
477
478 #[test]
479 fn inner_accessors() {
480 let count = Rc::new(CounterCell::new(0));
481 let widget = CountWidget {
482 count: count.clone(),
483 };
484 let mut cached = CachedWidget::new(widget);
485
486 assert_eq!(cached.inner().count.get(), 0);
487
488 cached.inner_mut().count.set(5);
489 assert_eq!(count.get(), 5);
490
491 let inner = cached.into_inner();
492 assert_eq!(inner.count.get(), 5);
493 }
494
495 #[test]
496 fn cached_content_matches_uncached() {
497 let count = Rc::new(CounterCell::new(0));
498 let widget = CountWidget {
499 count: count.clone(),
500 };
501 let cached = CachedWidget::new(widget.clone());
502 let mut state = CachedWidgetState::new();
503 let area = Rect::new(0, 0, 3, 3);
504
505 let mut pool_cached = GraphemePool::new();
506 let mut frame_cached = Frame::new(3, 3, &mut pool_cached);
507 cached.render(area, &mut frame_cached, &mut state);
508
509 let mut pool_direct = GraphemePool::new();
510 let mut frame_direct = Frame::new(3, 3, &mut pool_direct);
511 widget.render(area, &mut frame_direct);
512
513 assert_eq!(
514 frame_cached.buffer.get(0, 0).unwrap().content.as_char(),
515 frame_direct.buffer.get(0, 0).unwrap().content.as_char()
516 );
517 }
518
519 #[test]
520 fn multiple_cache_hits_never_rerender() {
521 let count = Rc::new(CounterCell::new(0));
522 let widget = CountWidget {
523 count: count.clone(),
524 };
525 let cached = CachedWidget::new(widget);
526 let mut state = CachedWidgetState::new();
527 let mut pool = GraphemePool::new();
528 let mut frame = Frame::new(5, 5, &mut pool);
529 let area = Rect::new(0, 0, 3, 3);
530
531 for _ in 0..10 {
532 cached.render(area, &mut frame, &mut state);
533 }
534 assert_eq!(count.get(), 1);
535 }
536
537 #[test]
538 fn with_hash_uses_hash_key() {
539 #[derive(Debug, Clone, Hash)]
541 struct HashableLabel(String);
542
543 impl Widget for HashableLabel {
544 fn render(&self, area: Rect, frame: &mut Frame) {
545 if !area.is_empty() {
546 frame.buffer.set(area.x, area.y, Cell::from_char('h'));
547 }
548 }
549 }
550
551 let widget = HashableLabel("hello".to_string());
552 let cached = CachedWidget::with_hash(widget);
553 let mut state = CachedWidgetState::new();
554 let mut pool = GraphemePool::new();
555 let mut frame = Frame::new(5, 5, &mut pool);
556 let area = Rect::new(0, 0, 3, 3);
557
558 cached.render(area, &mut frame, &mut state);
560 assert!(state.cache.is_some());
561
562 cached.render(area, &mut frame, &mut state);
564 assert!(state.last_key.is_some());
566 }
567
568 #[test]
569 fn no_cache_key_default() {
570 let key = NoCacheKey;
571 assert_eq!(CacheKey::<u32>::cache_key(&key, &100), None);
572 }
573
574 #[test]
575 fn hash_key_default() {
576 let key = HashKey;
577 let result = CacheKey::<u32>::cache_key(&key, &42);
578 assert!(result.is_some());
579 }
580
581 #[test]
582 fn cached_widget_state_new_equals_default() {
583 let a = CachedWidgetState::new();
584 let b = CachedWidgetState::default();
585 assert_eq!(a.cache_size_bytes(), b.cache_size_bytes());
586 assert!(!a.dirty);
587 assert!(!b.dirty);
588 }
589
590 #[test]
591 fn same_key_no_rerender() {
592 let count = Rc::new(CounterCell::new(0));
593 let key = Rc::new(CounterCell::new(42));
594 let widget = KeyWidget {
595 count: count.clone(),
596 key: key.clone(),
597 };
598 let cached = CachedWidget::with_key(widget, |w| w.key.get());
599 let mut state = CachedWidgetState::new();
600 let mut pool = GraphemePool::new();
601 let mut frame = Frame::new(5, 5, &mut pool);
602 let area = Rect::new(0, 0, 3, 3);
603
604 cached.render(area, &mut frame, &mut state);
605 cached.render(area, &mut frame, &mut state);
606 cached.render(area, &mut frame, &mut state);
607
608 assert_eq!(count.get(), 1);
609 }
610}