dotmax/animation/
frame_buffer.rs

1//! Double-buffered frame management for flicker-free animation.
2//!
3//! This module provides [`FrameBuffer`], a double-buffering implementation that
4//! enables smooth terminal animations without tearing or flickering.
5
6use crate::error::DotmaxError;
7use crate::grid::BrailleGrid;
8use crate::render::TerminalRenderer;
9
10/// Double-buffered frame management for flicker-free animation.
11///
12/// `FrameBuffer` maintains two [`BrailleGrid`] buffers:
13/// - **Front buffer**: The currently displayed frame (rendered to terminal)
14/// - **Back buffer**: The next frame being prepared (where you draw)
15///
16/// This separation ensures users never see partially drawn frames, eliminating
17/// visual tearing and flickering common in terminal graphics.
18///
19/// # Performance
20///
21/// Buffer swapping is an O(1) pointer swap operation (<1ms), not a data copy.
22/// This makes it suitable for high frame-rate animations (60+ fps).
23///
24/// # Examples
25///
26/// Basic double-buffering workflow:
27///
28/// ```
29/// use dotmax::animation::FrameBuffer;
30///
31/// // Create a double-buffered frame system (80x24 terminal cells)
32/// let mut buffer = FrameBuffer::new(80, 24);
33///
34/// // Draw to the back buffer
35/// {
36///     let back = buffer.get_back_buffer();
37///     back.clear();
38///     back.set_dot(10, 10).unwrap();
39///     back.set_dot(11, 10).unwrap();
40/// }
41///
42/// // Swap buffers - the back buffer becomes the new front
43/// buffer.swap_buffers();
44///
45/// // Now the front buffer contains what we drew
46/// assert!(buffer.get_front_buffer().get_dot(10 / 2, 10 / 4, 0).is_ok());
47/// ```
48///
49/// Animation loop pattern:
50///
51/// ```no_run
52/// use dotmax::animation::FrameBuffer;
53/// use dotmax::TerminalRenderer;
54///
55/// let mut buffer = FrameBuffer::new(80, 24);
56/// let mut renderer = TerminalRenderer::new().unwrap();
57///
58/// loop {
59///     // 1. Clear back buffer
60///     buffer.get_back_buffer().clear();
61///
62///     // 2. Draw next frame
63///     buffer.get_back_buffer().set_dot(10, 10).unwrap();
64///
65///     // 3. Swap buffers (instant)
66///     buffer.swap_buffers();
67///
68///     // 4. Render to terminal
69///     buffer.render(&mut renderer).unwrap();
70///
71///     // 5. Wait for next frame timing
72///     std::thread::sleep(std::time::Duration::from_millis(16)); // ~60fps
73/// }
74/// ```
75pub struct FrameBuffer {
76    /// The currently displayed buffer (front)
77    front: BrailleGrid,
78    /// The buffer being prepared (back)
79    back: BrailleGrid,
80}
81
82impl FrameBuffer {
83    /// Creates a new double-buffered frame system.
84    ///
85    /// Allocates two [`BrailleGrid`] buffers of the specified dimensions.
86    /// Both buffers are initialized empty (all dots cleared).
87    ///
88    /// # Arguments
89    ///
90    /// * `width` - Width in terminal cells (characters)
91    /// * `height` - Height in terminal cells (lines)
92    ///
93    /// # Panics
94    ///
95    /// Panics if `BrailleGrid::new()` fails (e.g., zero dimensions).
96    /// For fallible construction, use the underlying `BrailleGrid::new()` directly.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use dotmax::animation::FrameBuffer;
102    ///
103    /// // Standard terminal size
104    /// let buffer = FrameBuffer::new(80, 24);
105    /// assert_eq!(buffer.width(), 80);
106    /// assert_eq!(buffer.height(), 24);
107    ///
108    /// // Larger buffer for detailed graphics
109    /// let large = FrameBuffer::new(200, 50);
110    /// assert_eq!(large.width(), 200);
111    /// ```
112    #[must_use]
113    pub fn new(width: usize, height: usize) -> Self {
114        Self {
115            front: BrailleGrid::new(width, height)
116                .expect("FrameBuffer: invalid grid dimensions"),
117            back: BrailleGrid::new(width, height)
118                .expect("FrameBuffer: invalid grid dimensions"),
119        }
120    }
121
122    /// Returns a mutable reference to the back buffer for drawing.
123    ///
124    /// Use this to prepare the next frame. Draw operations on the back buffer
125    /// do not affect the currently displayed front buffer.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use dotmax::animation::FrameBuffer;
131    ///
132    /// let mut buffer = FrameBuffer::new(80, 24);
133    ///
134    /// // Get the back buffer and draw to it
135    /// let back = buffer.get_back_buffer();
136    /// back.clear();
137    /// back.set_dot(0, 0).unwrap();  // Top-left dot
138    /// back.set_dot(1, 0).unwrap();  // Adjacent dot
139    ///
140    /// // Front buffer is unchanged until swap_buffers() is called
141    /// ```
142    #[must_use]
143    pub fn get_back_buffer(&mut self) -> &mut BrailleGrid {
144        &mut self.back
145    }
146
147    /// Returns an immutable reference to the front buffer.
148    ///
149    /// The front buffer contains the currently displayed frame. This is
150    /// read-only access; to modify a buffer, use [`get_back_buffer()`](Self::get_back_buffer).
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use dotmax::animation::FrameBuffer;
156    ///
157    /// let buffer = FrameBuffer::new(80, 24);
158    ///
159    /// // Inspect the front buffer (read-only)
160    /// let front = buffer.get_front_buffer();
161    /// let (width, height) = front.dimensions();
162    /// assert_eq!(width, 80);
163    /// assert_eq!(height, 24);
164    /// ```
165    #[must_use]
166    pub const fn get_front_buffer(&self) -> &BrailleGrid {
167        &self.front
168    }
169
170    /// Atomically swaps the front and back buffers.
171    ///
172    /// After this call:
173    /// - The previous back buffer becomes the new front buffer
174    /// - The previous front buffer becomes the new back buffer
175    ///
176    /// This is an O(1) pointer swap operation, not a data copy.
177    /// Typical execution time is <1μs (well under the 1ms target).
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use dotmax::animation::FrameBuffer;
183    ///
184    /// let mut buffer = FrameBuffer::new(80, 24);
185    ///
186    /// // Draw a dot in the back buffer
187    /// buffer.get_back_buffer().set_dot(0, 0).unwrap();
188    ///
189    /// // Swap - now the front buffer has the dot
190    /// buffer.swap_buffers();
191    ///
192    /// // The old front (now back) is available for the next frame
193    /// buffer.get_back_buffer().clear();  // Prepare for next frame
194    /// ```
195    pub fn swap_buffers(&mut self) {
196        std::mem::swap(&mut self.front, &mut self.back);
197    }
198
199    /// Renders the front buffer to the terminal.
200    ///
201    /// Delegates to [`TerminalRenderer::render()`] to display the current
202    /// front buffer contents. Supports both colored and non-colored grids.
203    ///
204    /// # Arguments
205    ///
206    /// * `renderer` - The terminal renderer to output to
207    ///
208    /// # Errors
209    ///
210    /// Returns [`DotmaxError::Terminal`] if terminal I/O fails.
211    ///
212    /// # Examples
213    ///
214    /// ```no_run
215    /// use dotmax::animation::FrameBuffer;
216    /// use dotmax::TerminalRenderer;
217    ///
218    /// let mut buffer = FrameBuffer::new(80, 24);
219    /// let mut renderer = TerminalRenderer::new().unwrap();
220    ///
221    /// // Draw something
222    /// buffer.get_back_buffer().set_dot(10, 10).unwrap();
223    /// buffer.swap_buffers();
224    ///
225    /// // Render to terminal
226    /// buffer.render(&mut renderer).expect("Failed to render");
227    ///
228    /// // Clean up
229    /// renderer.cleanup().unwrap();
230    /// ```
231    pub fn render(&self, renderer: &mut TerminalRenderer) -> Result<(), DotmaxError> {
232        renderer.render(&self.front)
233    }
234
235    /// Returns the width of the buffers in terminal cells.
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// use dotmax::animation::FrameBuffer;
241    ///
242    /// let buffer = FrameBuffer::new(80, 24);
243    /// assert_eq!(buffer.width(), 80);
244    /// ```
245    #[must_use]
246    pub const fn width(&self) -> usize {
247        self.front.width()
248    }
249
250    /// Returns the height of the buffers in terminal cells.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use dotmax::animation::FrameBuffer;
256    ///
257    /// let buffer = FrameBuffer::new(80, 24);
258    /// assert_eq!(buffer.height(), 24);
259    /// ```
260    #[must_use]
261    pub const fn height(&self) -> usize {
262        self.front.height()
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    // ========================================================================
271    // AC #1: FrameBuffer::new() Creates Two BrailleGrid Buffers
272    // ========================================================================
273
274    #[test]
275    fn test_new_creates_buffers_with_correct_dimensions() {
276        let buffer = FrameBuffer::new(80, 24);
277
278        // Verify both buffers have correct dimensions
279        assert_eq!(buffer.front.width(), 80);
280        assert_eq!(buffer.front.height(), 24);
281        assert_eq!(buffer.back.width(), 80);
282        assert_eq!(buffer.back.height(), 24);
283    }
284
285    #[test]
286    fn test_new_dimensions_1x1() {
287        // Edge case: minimum dimensions
288        let buffer = FrameBuffer::new(1, 1);
289        assert_eq!(buffer.width(), 1);
290        assert_eq!(buffer.height(), 1);
291    }
292
293    #[test]
294    fn test_new_dimensions_80x24() {
295        // Standard terminal size
296        let buffer = FrameBuffer::new(80, 24);
297        assert_eq!(buffer.width(), 80);
298        assert_eq!(buffer.height(), 24);
299    }
300
301    #[test]
302    fn test_new_dimensions_200x50() {
303        // Large buffer stress test
304        let buffer = FrameBuffer::new(200, 50);
305        assert_eq!(buffer.width(), 200);
306        assert_eq!(buffer.height(), 50);
307    }
308
309    // ========================================================================
310    // AC #2: get_back_buffer() Returns Mutable Reference
311    // ========================================================================
312
313    #[test]
314    fn test_get_back_buffer_returns_mutable_reference() {
315        let mut buffer = FrameBuffer::new(80, 24);
316
317        // Should be able to draw to back buffer
318        let back = buffer.get_back_buffer();
319        let result = back.set_dot(0, 0);
320        assert!(result.is_ok());
321    }
322
323    #[test]
324    fn test_back_buffer_modifications_dont_affect_front() {
325        let mut buffer = FrameBuffer::new(10, 10);
326
327        // Draw to back buffer
328        buffer.get_back_buffer().set_dot(0, 0).unwrap();
329
330        // Front buffer should still be empty
331        // Cell (0,0) should have pattern 0 (empty)
332        let front = buffer.get_front_buffer();
333        let pattern = front.cell_to_braille_char(0, 0).unwrap();
334        assert_eq!(pattern, '⠀', "Front buffer should be empty before swap");
335    }
336
337    // ========================================================================
338    // AC #3: swap_buffers() Exchanges Front/Back
339    // ========================================================================
340
341    #[test]
342    fn test_swap_buffers_exchanges_buffers() {
343        let mut buffer = FrameBuffer::new(10, 10);
344
345        // Draw pattern to back buffer
346        buffer.get_back_buffer().set_dot(0, 0).unwrap();
347
348        // Verify front is empty before swap
349        let front_before = buffer.get_front_buffer().cell_to_braille_char(0, 0).unwrap();
350        assert_eq!(front_before, '⠀');
351
352        // Swap buffers
353        buffer.swap_buffers();
354
355        // Now front should have the pattern
356        let front_after = buffer.get_front_buffer().cell_to_braille_char(0, 0).unwrap();
357        assert_ne!(front_after, '⠀', "Front should have content after swap");
358    }
359
360    #[test]
361    fn test_swap_buffers_double_swap_restores_original() {
362        let mut buffer = FrameBuffer::new(10, 10);
363
364        // Draw to back buffer
365        buffer.get_back_buffer().set_dot(0, 0).unwrap();
366
367        // Swap twice
368        buffer.swap_buffers();
369        buffer.swap_buffers();
370
371        // Back buffer should have the content again
372        let back_char = buffer.get_back_buffer().cell_to_braille_char(0, 0).unwrap();
373        assert_ne!(back_char, '⠀', "Double swap should restore back buffer content");
374    }
375
376    #[test]
377    fn test_multiple_sequential_swaps() {
378        let mut buffer = FrameBuffer::new(10, 10);
379
380        // Draw different patterns to track which buffer is which
381        buffer.get_back_buffer().set_dot(0, 0).unwrap();  // Back has dot at (0,0)
382
383        // Swap 1: back->front
384        buffer.swap_buffers();
385        assert_ne!(buffer.get_front_buffer().cell_to_braille_char(0, 0).unwrap(), '⠀');
386
387        // Add another dot to the new back buffer
388        buffer.get_back_buffer().set_dot(2, 0).unwrap();
389
390        // Swap 2: back->front (now front has dot at (2,0))
391        buffer.swap_buffers();
392        assert_ne!(buffer.get_front_buffer().cell_to_braille_char(1, 0).unwrap(), '⠀');
393
394        // Swap 3: back->front (back to original with dot at (0,0))
395        buffer.swap_buffers();
396        assert_ne!(buffer.get_front_buffer().cell_to_braille_char(0, 0).unwrap(), '⠀');
397    }
398
399    // ========================================================================
400    // AC #6: Unit Tests Verify Buffer Swap Correctness
401    // ========================================================================
402
403    #[test]
404    fn test_width_height_accessors() {
405        let buffer = FrameBuffer::new(100, 50);
406        assert_eq!(buffer.width(), 100);
407        assert_eq!(buffer.height(), 50);
408    }
409
410    #[test]
411    fn test_get_back_buffer_after_swap() {
412        let mut buffer = FrameBuffer::new(10, 10);
413
414        // Draw to back, swap, then get back buffer again
415        buffer.get_back_buffer().set_dot(0, 0).unwrap();
416        buffer.swap_buffers();
417
418        // The new back buffer (old front) should be empty
419        let new_back_char = buffer.get_back_buffer().cell_to_braille_char(0, 0).unwrap();
420        assert_eq!(new_back_char, '⠀', "New back buffer should be empty after swap");
421    }
422
423    #[test]
424    fn test_content_preservation_through_swap() {
425        let mut buffer = FrameBuffer::new(20, 20);
426
427        // Draw a complex pattern to back buffer
428        for i in 0..10 {
429            buffer.get_back_buffer().set_dot(i * 2, i * 4).unwrap();
430        }
431
432        // Swap and verify front has all the content
433        buffer.swap_buffers();
434
435        for i in 0..10 {
436            let cell_x = i;
437            let cell_y = i;
438            let char_at_cell = buffer.get_front_buffer().cell_to_braille_char(cell_x, cell_y).unwrap();
439            assert_ne!(char_at_cell, '⠀', "Pattern should be preserved at ({}, {})", cell_x, cell_y);
440        }
441    }
442}