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}