reratui_core/
component.rs1use ratatui::Frame;
2use ratatui::buffer::Buffer;
3use ratatui::layout::Rect;
4use std::collections::HashMap;
5
6thread_local! {
7 pub(crate) static MOUNT_STATE: std::cell::RefCell<MountState> = Default::default();
9}
10
11type CleanupFn = Box<dyn Fn() + 'static>;
13
14#[derive(Default)]
15pub(crate) struct MountState {
16 mounted: std::collections::HashSet<usize>,
18 current_render: std::collections::HashSet<usize>,
20 cleanup_fns: HashMap<usize, CleanupFn>,
22}
23
24impl MountState {
25 pub(crate) fn track_mount<F>(&mut self, id_hash: usize, cleanup_fn: F) -> bool
26 where
27 F: Fn() + 'static,
28 {
29 self.current_render.insert(id_hash);
30
31 let is_new = self.mounted.insert(id_hash);
33
34 if is_new {
35 self.cleanup_fns.insert(id_hash, Box::new(cleanup_fn));
37 }
38
39 is_new
40 }
41
42 fn cleanup_unmounted(&mut self) {
43 let unmounted: Vec<_> = self
45 .mounted
46 .difference(&self.current_render)
47 .cloned()
48 .collect();
49
50 for &id_hash in &unmounted {
52 if let Some(cleanup_fn) = self.cleanup_fns.remove(&id_hash) {
53 cleanup_fn(); }
55 self.mounted.remove(&id_hash);
56 }
57
58 self.current_render.clear();
60 }
61}
62
63pub trait Component: 'static {
64 fn on_mount(&self) {}
66
67 fn on_unmount(&self) {}
69
70 fn render(&self, area: Rect, buffer: &mut Buffer);
72
73 fn component_id(&self) -> String {
75 std::any::type_name::<Self>().to_string()
77 }
78
79 fn clone_box(&self) -> Box<dyn Component>
82 where
83 Self: Clone,
84 {
85 Box::new(self.clone())
86 }
87
88 fn render_with_mount(&self, area: Rect, frame: &mut Frame)
90 where
91 Self: Clone,
92 {
93 let self_clone = self.clone();
94 let cleanup_fn = move || {
95 self_clone.on_unmount();
96 };
97
98 track_and_call_lifecycle(self, cleanup_fn);
99 self.render(area, frame.buffer_mut());
100 }
101}
102
103fn track_and_call_lifecycle<F>(component: &dyn Component, cleanup_fn: F)
105where
106 F: Fn() + 'static,
107{
108 let component_id = component.component_id();
109 let id_hash = {
110 use std::collections::hash_map::DefaultHasher;
111 use std::hash::{Hash, Hasher};
112 let mut hasher = DefaultHasher::new();
113 component_id.hash(&mut hasher);
114 hasher.finish() as usize
115 };
116
117 let is_first_render = MOUNT_STATE.with(|state| {
119 let mut state = state.borrow_mut();
120 state.track_mount(id_hash, cleanup_fn)
121 });
122
123 if is_first_render {
125 component.on_mount();
126 }
127}
128
129pub(crate) fn render_component_with_lifecycle(
132 component: &std::rc::Rc<dyn Component>,
133 area: Rect,
134 buffer: &mut Buffer,
135) {
136 let component_clone = std::rc::Rc::clone(component);
138 let cleanup_fn = move || {
139 component_clone.on_unmount();
140 };
141
142 track_and_call_lifecycle(component.as_ref(), cleanup_fn);
143 component.render(area, buffer);
144}
145
146pub fn cleanup_unmounted() {
149 MOUNT_STATE.with(|state| {
150 let mut state = state.borrow_mut();
151 state.cleanup_unmounted();
152 });
153}