Skip to main content

flywheel/actor/
engine.rs

1//! Engine: Main coordinator that ties actors together.
2//!
3//! The Engine is the entry point for applications using Flywheel.
4//! It manages the terminal, spawns actors, and provides the main
5//! event loop.
6
7use super::messages::{InputEvent, RenderCommand};
8use super::{InputActor, RendererActor};
9use crate::buffer::{Buffer, Cell, Rgb};
10use crate::layout::Rect;
11use crossbeam_channel::{bounded, Receiver, Sender, TryRecvError};
12use crossterm::{
13    cursor,
14    event::EnableMouseCapture,
15    execute,
16    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
17};
18use std::io::{self};
19use std::time::{Duration, Instant};
20
21/// Configuration for the Engine.
22#[derive(Debug, Clone)]
23pub struct EngineConfig {
24    /// Target frames per second.
25    pub target_fps: u32,
26    /// Input poll timeout.
27    pub input_poll_timeout: Duration,
28    /// Whether to enable mouse capture.
29    pub enable_mouse: bool,
30    /// Whether to use alternate screen buffer.
31    pub alternate_screen: bool,
32}
33
34impl Default for EngineConfig {
35    fn default() -> Self {
36        Self {
37            target_fps: 60,
38            input_poll_timeout: Duration::from_millis(10),
39            enable_mouse: false,
40            alternate_screen: true,
41        }
42    }
43}
44
45/// The main Flywheel engine.
46///
47/// This coordinates between the input and render actors, providing
48/// a simple interface for applications.
49pub struct Engine {
50    /// Configuration.
51    config: EngineConfig,
52    /// Input event receiver.
53    input_rx: Receiver<InputEvent>,
54    /// Render command sender.
55    render_tx: Sender<RenderCommand>,
56    /// Input actor handle.
57    input_actor: Option<InputActor>,
58    /// Renderer actor handle.
59    #[allow(dead_code)]
60    renderer_actor: Option<RendererActor>,
61    /// Application buffer (for modifications).
62    buffer: Buffer,
63    /// Terminal width.
64    width: u16,
65    /// Terminal height.
66    height: u16,
67    /// Frame timing.
68    frame_start: Instant,
69    frame_duration: Duration,
70    frame_count: u64,
71    /// Whether the engine is running.
72    running: bool,
73}
74
75impl Engine {
76    /// Create a new engine with default configuration.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if terminal setup fails (raw mode, alternate screen, etc.).
81    pub fn new() -> io::Result<Self> {
82        Self::with_config(EngineConfig::default())
83    }
84
85    /// Create a new engine with custom configuration.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if terminal setup fails.
90    pub fn with_config(config: EngineConfig) -> io::Result<Self> {
91        // Get terminal size
92        let (width, height) = terminal::size()?;
93
94        // Enter raw mode and alternate screen
95        terminal::enable_raw_mode()?;
96
97        let mut stdout = io::stdout();
98        if config.alternate_screen {
99            execute!(stdout, EnterAlternateScreen)?;
100        }
101        if config.enable_mouse {
102            execute!(stdout, EnableMouseCapture)?;
103        }
104        execute!(stdout, cursor::Hide)?;
105
106        // Create channels
107        let (input_tx, input_rx) = bounded::<InputEvent>(64);
108        let (render_tx, render_rx) = bounded::<RenderCommand>(16);
109
110        // Spawn actors
111        let input_actor = InputActor::spawn(input_tx, config.input_poll_timeout);
112        let renderer_actor = RendererActor::spawn(render_rx, width, height);
113
114        let frame_duration = Duration::from_secs(1) / config.target_fps;
115
116        Ok(Self {
117            config,
118            input_rx,
119            render_tx,
120            input_actor: Some(input_actor),
121            renderer_actor: Some(renderer_actor),
122            buffer: Buffer::new(width, height),
123            width,
124            height,
125            frame_start: Instant::now(),
126            frame_duration,
127            frame_count: 0,
128            running: true,
129        })
130    }
131
132    /// Get the terminal width.
133    pub const fn width(&self) -> u16 {
134        self.width
135    }
136
137    /// Get the terminal height.
138    pub const fn height(&self) -> u16 {
139        self.height
140    }
141
142    /// Get a reference to the buffer.
143    pub const fn buffer(&self) -> &Buffer {
144        &self.buffer
145    }
146
147    /// Get a mutable reference to the buffer.
148    pub const fn buffer_mut(&mut self) -> &mut Buffer {
149        &mut self.buffer
150    }
151
152    /// Get the input receiver for event-driven loops.
153    pub const fn input_receiver(&self) -> &Receiver<InputEvent> {
154        &self.input_rx
155    }
156
157    /// Check if the engine is still running.
158    pub const fn is_running(&self) -> bool {
159        self.running
160    }
161
162    /// Stop the engine.
163    pub const fn stop(&mut self) {
164        self.running = false;
165    }
166
167    /// Poll for the next input event (non-blocking).
168    ///
169    /// Returns `None` if no event is available.
170    pub fn poll_input(&self) -> Option<InputEvent> {
171        match self.input_rx.try_recv() {
172            Ok(event) => Some(event),
173            Err(TryRecvError::Empty) => None,
174            Err(TryRecvError::Disconnected) => {
175                Some(InputEvent::Error("Input channel disconnected".to_string()))
176            }
177        }
178    }
179
180    /// Wait for the next input event (blocking with timeout).
181    pub fn wait_input(&self, timeout: Duration) -> Option<InputEvent> {
182        self.input_rx.recv_timeout(timeout).ok()
183    }
184
185    /// Drain all pending input events.
186    pub fn drain_input(&self) -> Vec<InputEvent> {
187        let mut events = Vec::new();
188        while let Ok(event) = self.input_rx.try_recv() {
189            events.push(event);
190        }
191        events
192    }
193
194    /// Request a full redraw.
195    pub fn request_redraw(&self) {
196        let _ = self.render_tx.send(RenderCommand::FullRedraw(Box::new(self.buffer.clone())));
197    }
198
199    /// Request a diff-based update.
200    pub fn request_update(&self) {
201        let _ = self.render_tx.send(RenderCommand::Update(Box::new(self.buffer.clone())));
202    }
203
204    /// Set the cursor position (or hide it).
205    pub fn set_cursor(&self, x: Option<u16>, y: u16) {
206        let _ = self.render_tx.send(RenderCommand::SetCursor { x, y });
207    }
208
209    /// Write raw bytes to the output (Fast Path).
210    pub fn write_raw(&self, bytes: Vec<u8>) {
211        let _ = self.render_tx.send(RenderCommand::RawOutput { bytes });
212    }
213
214    /// Handle a resize event.
215    pub fn handle_resize(&mut self, width: u16, height: u16) {
216        self.width = width;
217        self.height = height;
218        self.buffer.resize(width, height);
219        let _ = self.render_tx.send(RenderCommand::Resize { width, height });
220    }
221
222    /// Begin a new frame.
223    ///
224    /// Call this at the start of your render loop.
225    pub fn begin_frame(&mut self) {
226        self.frame_start = Instant::now();
227    }
228
229    /// End a frame and request update.
230    ///
231    /// This will sleep if necessary to maintain the target FPS.
232    pub fn end_frame(&mut self) {
233        self.frame_count += 1;
234
235        // Request render
236        self.request_update();
237
238        // Frame rate limiting
239        let elapsed = self.frame_start.elapsed();
240        if elapsed < self.frame_duration {
241            std::thread::sleep(self.frame_duration - elapsed);
242        }
243    }
244
245    /// Get the current frame count.
246    pub const fn frame_count(&self) -> u64 {
247        self.frame_count
248    }
249
250    /// Convenience: Set a cell in the buffer.
251    pub fn set_cell(&mut self, x: u16, y: u16, cell: Cell) {
252        self.buffer.set(x, y, cell);
253    }
254
255    /// Convenience: Set a grapheme in the buffer.
256    pub fn set_grapheme(&mut self, x: u16, y: u16, grapheme: &str, fg: Rgb, bg: Rgb) -> u8 {
257        self.buffer.set_grapheme(x, y, grapheme, fg, bg)
258    }
259
260    /// Convenience: Clear the buffer.
261    pub fn clear(&mut self) {
262        self.buffer.clear();
263    }
264
265    /// Convenience: Fill a rectangle.
266    pub fn fill_rect(&mut self, rect: Rect, cell: Cell) {
267        self.buffer.fill_rect(rect.x, rect.y, rect.width, rect.height, cell);
268    }
269
270    /// Draw text at a position.
271    ///
272    /// Returns the number of columns used.
273    pub fn draw_text(&mut self, x: u16, y: u16, text: &str, fg: Rgb, bg: Rgb) -> u16 {
274        let mut col = x;
275        for grapheme in unicode_segmentation::UnicodeSegmentation::graphemes(text, true) {
276            if col >= self.width {
277                break;
278            }
279            let width = self.buffer.set_grapheme(col, y, grapheme, fg, bg);
280            col += u16::from(width);
281        }
282        col - x
283    }
284}
285
286impl Drop for Engine {
287    fn drop(&mut self) {
288        // Stop actors
289        if let Some(actor) = self.input_actor.take() {
290            actor.join();
291        }
292
293        let _ = self.render_tx.send(RenderCommand::Shutdown);
294
295        // Restore terminal state
296        let mut stdout = io::stdout();
297        let _ = execute!(stdout, cursor::Show);
298        if self.config.enable_mouse {
299            let _ = execute!(stdout, crossterm::event::DisableMouseCapture);
300        }
301        if self.config.alternate_screen {
302            let _ = execute!(stdout, LeaveAlternateScreen);
303        }
304        let _ = terminal::disable_raw_mode();
305    }
306}