sql_cli/ui/rendering/
render_state.rs

1//! Render State Manager - Tracks when UI needs re-rendering
2//!
3//! This module provides centralized tracking of UI state changes
4//! and determines when the table (and other components) need to be re-rendered.
5
6use std::time::{Duration, Instant};
7use tracing::{debug, trace};
8
9/// Reasons why a re-render might be needed
10#[derive(Debug, Clone, PartialEq)]
11pub enum RenderReason {
12    /// Initial render
13    Initial,
14    /// User input/key press
15    UserInput,
16    /// Search results updated
17    SearchUpdate,
18    /// Navigation/cursor moved
19    NavigationChange,
20    /// Data changed (filter, sort, etc.)
21    DataChange,
22    /// Window resized
23    WindowResize,
24    /// Periodic refresh
25    PeriodicRefresh,
26    /// Debounced action completed
27    DebouncedAction,
28}
29
30/// Manages rendering state and dirty flags
31pub struct RenderState {
32    /// Whether the UI needs re-rendering
33    dirty: bool,
34    /// Reason for the dirty state
35    dirty_reason: Option<RenderReason>,
36    /// Last render time
37    last_render: Instant,
38    /// Minimum time between renders (to prevent excessive redraws)
39    min_render_interval: Duration,
40    /// Force render on next check
41    force_render: bool,
42    /// Track if we're in a search/input mode that needs frequent updates
43    high_frequency_mode: bool,
44}
45
46impl Default for RenderState {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl RenderState {
53    /// Create a new render state manager
54    #[must_use]
55    pub fn new() -> Self {
56        Self {
57            dirty: true, // Start dirty to trigger initial render
58            dirty_reason: Some(RenderReason::Initial),
59            last_render: Instant::now(),
60            min_render_interval: Duration::from_millis(16), // ~60 FPS max
61            force_render: false,
62            high_frequency_mode: false,
63        }
64    }
65
66    /// Mark the UI as needing re-render
67    pub fn mark_dirty(&mut self, reason: RenderReason) {
68        if !self.dirty {
69            debug!("Marking render state dirty: {:?}", reason);
70        }
71        self.dirty = true;
72        self.dirty_reason = Some(reason);
73    }
74
75    /// Check if re-render is needed
76    pub fn needs_render(&self) -> bool {
77        if self.force_render {
78            return true;
79        }
80
81        if !self.dirty {
82            return false;
83        }
84
85        // Check if enough time has passed since last render
86        let elapsed = self.last_render.elapsed();
87        if elapsed < self.min_render_interval && !self.high_frequency_mode {
88            trace!("Skipping render, only {:?} elapsed", elapsed);
89            return false;
90        }
91
92        true
93    }
94
95    /// Mark that a render has occurred
96    pub fn rendered(&mut self) {
97        trace!("Render completed, reason was: {:?}", self.dirty_reason);
98        self.dirty = false;
99        self.dirty_reason = None;
100        self.last_render = Instant::now();
101        self.force_render = false;
102    }
103
104    /// Force a render on the next check
105    pub fn force_render(&mut self) {
106        debug!("Forcing render on next check");
107        self.force_render = true;
108        self.dirty = true;
109    }
110
111    /// Set high-frequency mode (for search/input)
112    pub fn set_high_frequency_mode(&mut self, enabled: bool) {
113        if self.high_frequency_mode != enabled {
114            debug!("High-frequency render mode: {}", enabled);
115            self.high_frequency_mode = enabled;
116            if enabled {
117                // Reduce minimum interval for more responsive updates
118                self.min_render_interval = Duration::from_millis(8); // ~120 FPS max
119            } else {
120                self.min_render_interval = Duration::from_millis(16); // ~60 FPS max
121            }
122        }
123    }
124
125    /// Get the current dirty reason
126    #[must_use]
127    pub fn dirty_reason(&self) -> Option<&RenderReason> {
128        self.dirty_reason.as_ref()
129    }
130
131    /// Check if currently dirty
132    #[must_use]
133    pub fn is_dirty(&self) -> bool {
134        self.dirty
135    }
136}
137
138/// Helper methods for common state changes
139impl RenderState {
140    /// Navigation changed (cursor moved)
141    pub fn on_navigation_change(&mut self) {
142        self.mark_dirty(RenderReason::NavigationChange);
143    }
144
145    /// Search results updated
146    pub fn on_search_update(&mut self) {
147        self.mark_dirty(RenderReason::SearchUpdate);
148        // Search updates should render immediately
149        self.force_render = true;
150    }
151
152    /// Data changed (filter, sort, etc.)
153    pub fn on_data_change(&mut self) {
154        self.mark_dirty(RenderReason::DataChange);
155    }
156
157    /// User input received
158    pub fn on_user_input(&mut self) {
159        self.mark_dirty(RenderReason::UserInput);
160    }
161
162    /// Window resized
163    pub fn on_window_resize(&mut self) {
164        self.mark_dirty(RenderReason::WindowResize);
165        self.force_render = true;
166    }
167
168    /// Debounced action completed
169    pub fn on_debounced_action(&mut self) {
170        self.mark_dirty(RenderReason::DebouncedAction);
171        // Debounced actions should render immediately to show results
172        self.force_render = true;
173    }
174}