Skip to main content

reratui_core/
component.rs

1use ratatui::Frame;
2use ratatui::buffer::Buffer;
3use ratatui::layout::Rect;
4use std::collections::HashMap;
5
6thread_local! {
7    // Track mounted component instances and their mount states
8    pub(crate) static MOUNT_STATE: std::cell::RefCell<MountState> = Default::default();
9}
10
11// Store cleanup callbacks for unmounting
12type CleanupFn = Box<dyn Fn() + 'static>;
13
14#[derive(Default)]
15pub(crate) struct MountState {
16    // Tracks all currently mounted components by their ID hash
17    mounted: std::collections::HashSet<usize>,
18    // Components that were mounted in the last render
19    current_render: std::collections::HashSet<usize>,
20    // Cleanup functions for each mounted component
21    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        // Returns true if this is the first time mounting (newly inserted)
32        let is_new = self.mounted.insert(id_hash);
33
34        if is_new {
35            // Store cleanup function
36            self.cleanup_fns.insert(id_hash, Box::new(cleanup_fn));
37        }
38
39        is_new
40    }
41
42    fn cleanup_unmounted(&mut self) {
43        // Find components that were mounted before but not in current render
44        let unmounted: Vec<_> = self
45            .mounted
46            .difference(&self.current_render)
47            .cloned()
48            .collect();
49
50        // Call cleanup functions and remove unmounted components
51        for &id_hash in &unmounted {
52            if let Some(cleanup_fn) = self.cleanup_fns.remove(&id_hash) {
53                cleanup_fn(); // Call on_unmount
54            }
55            self.mounted.remove(&id_hash);
56        }
57
58        // Prepare for next render
59        self.current_render.clear();
60    }
61}
62
63pub trait Component: 'static {
64    /// Called once when the component is first mounted
65    fn on_mount(&self) {}
66
67    /// Called when the component is about to be unmounted
68    fn on_unmount(&self) {}
69
70    /// Called on every render
71    fn render(&self, area: Rect, buffer: &mut Buffer);
72
73    /// Gets a unique identifier for this component instance
74    fn component_id(&self) -> String {
75        // Default implementation uses the type name
76        std::any::type_name::<Self>().to_string()
77    }
78
79    /// Clone the component into a Box
80    /// This method makes the trait object-safe while still allowing cloning
81    fn clone_box(&self) -> Box<dyn Component>
82    where
83        Self: Clone,
84    {
85        Box::new(self.clone())
86    }
87
88    /// Renders the component with mount/unmount lifecycle tracking
89    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
103/// Helper function to track component lifecycle and call on_mount if needed
104fn 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    // Track this component in the current render
118    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    // Call on_mount on first render
124    if is_first_render {
125        component.on_mount();
126    }
127}
128
129/// Renders a component with lifecycle tracking (on_mount/on_unmount)
130/// This function should be called when rendering components from Elements
131pub(crate) fn render_component_with_lifecycle(
132    component: &std::rc::Rc<dyn Component>,
133    area: Rect,
134    buffer: &mut Buffer,
135) {
136    // Clone the Rc for the cleanup function
137    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
146/// Cleans up any components that were unmounted in the last render cycle
147/// This should be called after each render cycle
148pub fn cleanup_unmounted() {
149    MOUNT_STATE.with(|state| {
150        let mut state = state.borrow_mut();
151        state.cleanup_unmounted();
152    });
153}