1use super::cell_buffer::{CellBuffer, Modifiers};
10use crate::color::ColorMode;
11use crossterm::cursor::MoveTo;
12use crossterm::style::{
13 Attribute, Color as CrosstermColor, Print, ResetColor, SetAttribute, SetBackgroundColor,
14 SetForegroundColor,
15};
16use crossterm::{queue, QueueableCommand};
17use presentar_core::Color;
18use std::io::{self, BufWriter, Write};
19
20#[derive(Clone, Copy, Debug, PartialEq)]
22struct StyleState {
23 fg: Color,
24 bg: Color,
25 modifiers: Modifiers,
26}
27
28impl Default for StyleState {
29 fn default() -> Self {
30 Self {
31 fg: Color::WHITE,
32 bg: Color::BLACK,
33 modifiers: Modifiers::NONE,
34 }
35 }
36}
37
38#[derive(Debug)]
43pub struct DiffRenderer {
44 color_mode: ColorMode,
46 cursor_x: u16,
48 cursor_y: u16,
49 last_style: StyleState,
51 cells_written: usize,
53 cursor_moves: usize,
55 style_changes: usize,
57}
58
59impl Default for DiffRenderer {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl DiffRenderer {
66 #[must_use]
68 pub fn new() -> Self {
69 Self {
70 color_mode: ColorMode::detect(),
71 cursor_x: u16::MAX,
72 cursor_y: u16::MAX,
73 last_style: StyleState::default(),
74 cells_written: 0,
75 cursor_moves: 0,
76 style_changes: 0,
77 }
78 }
79
80 #[must_use]
82 pub fn with_color_mode(color_mode: ColorMode) -> Self {
83 Self {
84 color_mode,
85 cursor_x: u16::MAX,
86 cursor_y: u16::MAX,
87 last_style: StyleState::default(),
88 cells_written: 0,
89 cursor_moves: 0,
90 style_changes: 0,
91 }
92 }
93
94 pub fn set_color_mode(&mut self, mode: ColorMode) {
96 self.color_mode = mode;
97 }
98
99 #[must_use]
101 pub const fn color_mode(&self) -> ColorMode {
102 self.color_mode
103 }
104
105 pub fn reset(&mut self) {
107 self.cursor_x = u16::MAX;
108 self.cursor_y = u16::MAX;
109 self.last_style = StyleState::default();
110 self.cells_written = 0;
111 self.cursor_moves = 0;
112 self.style_changes = 0;
113 }
114
115 #[must_use]
117 pub const fn cells_written(&self) -> usize {
118 self.cells_written
119 }
120
121 #[must_use]
123 pub const fn cursor_moves(&self) -> usize {
124 self.cursor_moves
125 }
126
127 #[must_use]
129 pub const fn style_changes(&self) -> usize {
130 self.style_changes
131 }
132
133 fn to_crossterm_color(&self, color: Color) -> CrosstermColor {
135 self.color_mode.to_crossterm(color)
136 }
137
138 pub fn flush<W: Write>(
146 &mut self,
147 buffer: &mut CellBuffer,
148 writer: &mut W,
149 ) -> io::Result<usize> {
150 debug_assert!(buffer.width() > 0, "buffer width must be positive");
151 debug_assert!(buffer.height() > 0, "buffer height must be positive");
152
153 self.cells_written = 0;
155 self.cursor_moves = 0;
156 self.style_changes = 0;
157
158 let mut buf_writer = BufWriter::with_capacity(8192, writer);
160
161 queue!(buf_writer, ResetColor)?;
163 self.last_style = StyleState::default();
164
165 let width = buffer.width();
166
167 for idx in buffer.iter_dirty() {
168 let (x, y) = buffer.coords(idx);
169 let cell = &buffer.cells()[idx];
170
171 if cell.is_continuation() {
173 continue;
174 }
175
176 if self.cursor_x != x || self.cursor_y != y {
178 queue!(buf_writer, MoveTo(x, y))?;
179 self.cursor_x = x;
180 self.cursor_y = y;
181 self.cursor_moves += 1;
182 }
183
184 let new_style = StyleState {
186 fg: cell.fg,
187 bg: cell.bg,
188 modifiers: cell.modifiers,
189 };
190
191 if new_style != self.last_style {
192 self.apply_style(&mut buf_writer, new_style)?;
193 self.last_style = new_style;
194 self.style_changes += 1;
195 }
196
197 queue!(buf_writer, Print(&cell.symbol))?;
199
200 self.cursor_x = self.cursor_x.saturating_add(cell.width() as u16);
202 if self.cursor_x >= width {
203 self.cursor_x = u16::MAX; }
205
206 self.cells_written += 1;
207 }
208
209 buffer.clear_dirty();
211
212 buf_writer.flush()?;
214
215 Ok(self.cells_written)
216 }
217
218 fn apply_style<W: Write>(&self, writer: &mut W, style: StyleState) -> io::Result<()> {
220 writer.queue(SetAttribute(Attribute::Reset))?;
222
223 let fg = self.to_crossterm_color(style.fg);
225 writer.queue(SetForegroundColor(fg))?;
226
227 let bg = self.to_crossterm_color(style.bg);
229 writer.queue(SetBackgroundColor(bg))?;
230
231 if style.modifiers.contains(Modifiers::BOLD) {
233 writer.queue(SetAttribute(Attribute::Bold))?;
234 }
235 if style.modifiers.contains(Modifiers::ITALIC) {
236 writer.queue(SetAttribute(Attribute::Italic))?;
237 }
238 if style.modifiers.contains(Modifiers::UNDERLINE) {
239 writer.queue(SetAttribute(Attribute::Underlined))?;
240 }
241 if style.modifiers.contains(Modifiers::STRIKETHROUGH) {
242 writer.queue(SetAttribute(Attribute::CrossedOut))?;
243 }
244 if style.modifiers.contains(Modifiers::DIM) {
245 writer.queue(SetAttribute(Attribute::Dim))?;
246 }
247 if style.modifiers.contains(Modifiers::BLINK) {
248 writer.queue(SetAttribute(Attribute::SlowBlink))?;
249 }
250 if style.modifiers.contains(Modifiers::REVERSE) {
251 writer.queue(SetAttribute(Attribute::Reverse))?;
252 }
253 if style.modifiers.contains(Modifiers::HIDDEN) {
254 writer.queue(SetAttribute(Attribute::Hidden))?;
255 }
256
257 Ok(())
258 }
259
260 pub fn render_full<W: Write>(
266 &mut self,
267 buffer: &mut CellBuffer,
268 writer: &mut W,
269 ) -> io::Result<usize> {
270 contract_pre_render!();
271 buffer.mark_all_dirty();
272 self.flush(buffer, writer)
273 }
274}
275
276#[cfg(test)]
277#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_renderer_creation() {
283 let renderer = DiffRenderer::new();
284 assert_eq!(renderer.cursor_x, u16::MAX);
285 assert_eq!(renderer.cursor_y, u16::MAX);
286 }
287
288 #[test]
289 fn test_renderer_with_color_mode() {
290 let renderer = DiffRenderer::with_color_mode(ColorMode::Color256);
291 assert_eq!(renderer.color_mode(), ColorMode::Color256);
292 }
293
294 #[test]
295 fn test_renderer_set_color_mode() {
296 let mut renderer = DiffRenderer::new();
297 renderer.set_color_mode(ColorMode::Color16);
298 assert_eq!(renderer.color_mode(), ColorMode::Color16);
299 }
300
301 #[test]
302 fn test_renderer_reset() {
303 let mut renderer = DiffRenderer::new();
304 renderer.cursor_x = 10;
305 renderer.cursor_y = 5;
306 renderer.cells_written = 100;
307
308 renderer.reset();
309
310 assert_eq!(renderer.cursor_x, u16::MAX);
311 assert_eq!(renderer.cursor_y, u16::MAX);
312 assert_eq!(renderer.cells_written(), 0);
313 }
314
315 #[test]
316 fn test_renderer_flush_empty() {
317 let mut renderer = DiffRenderer::new();
318 let mut buffer = CellBuffer::new(10, 5);
319 let mut output = Vec::new();
320
321 let count = renderer.flush(&mut buffer, &mut output).unwrap();
322 assert_eq!(count, 0);
323 }
324
325 #[test]
326 #[allow(clippy::len_zero)]
327 fn test_renderer_flush_dirty_cells() {
328 let mut renderer = DiffRenderer::new();
329 let mut buffer = CellBuffer::new(10, 5);
330 buffer.update(5, 2, "X", Color::RED, Color::BLACK, Modifiers::NONE);
331 let mut output = Vec::new();
332
333 let count = renderer.flush(&mut buffer, &mut output).unwrap();
334 assert_eq!(count, 1);
335 assert!(output.len() > 0);
336 }
337
338 #[test]
339 fn test_renderer_flush_multiple_dirty() {
340 let mut renderer = DiffRenderer::new();
341 let mut buffer = CellBuffer::new(10, 5);
342 buffer.update(0, 0, "A", Color::WHITE, Color::BLACK, Modifiers::NONE);
343 buffer.update(5, 2, "B", Color::WHITE, Color::BLACK, Modifiers::NONE);
344 buffer.update(9, 4, "C", Color::WHITE, Color::BLACK, Modifiers::NONE);
345 let mut output = Vec::new();
346
347 let count = renderer.flush(&mut buffer, &mut output).unwrap();
348 assert_eq!(count, 3);
349 assert_eq!(renderer.cursor_moves(), 3);
350 }
351
352 #[test]
353 fn test_renderer_flush_adjacent_cells() {
354 let mut renderer = DiffRenderer::new();
355 let mut buffer = CellBuffer::new(10, 5);
356 buffer.update(0, 0, "A", Color::WHITE, Color::BLACK, Modifiers::NONE);
358 buffer.update(1, 0, "B", Color::WHITE, Color::BLACK, Modifiers::NONE);
359 buffer.update(2, 0, "C", Color::WHITE, Color::BLACK, Modifiers::NONE);
360 let mut output = Vec::new();
361
362 let count = renderer.flush(&mut buffer, &mut output).unwrap();
363 assert_eq!(count, 3);
364 assert_eq!(renderer.cursor_moves(), 1);
366 }
367
368 #[test]
369 fn test_renderer_style_changes() {
370 let mut renderer = DiffRenderer::new();
371 let mut buffer = CellBuffer::new(10, 5);
372 buffer.update(0, 0, "A", Color::RED, Color::BLACK, Modifiers::NONE);
373 buffer.update(1, 0, "B", Color::BLUE, Color::BLACK, Modifiers::NONE);
374 let mut output = Vec::new();
375
376 renderer.flush(&mut buffer, &mut output).unwrap();
377 assert_eq!(renderer.style_changes(), 2);
378 }
379
380 #[test]
381 fn test_renderer_same_style_no_change() {
382 let mut renderer = DiffRenderer::new();
383 let mut buffer = CellBuffer::new(10, 5);
384 buffer.update(0, 0, "A", Color::RED, Color::BLUE, Modifiers::NONE);
386 buffer.update(1, 0, "B", Color::RED, Color::BLUE, Modifiers::NONE);
387 let mut output = Vec::new();
388
389 renderer.flush(&mut buffer, &mut output).unwrap();
390 assert_eq!(renderer.style_changes(), 1);
392 }
393
394 #[test]
395 fn test_renderer_with_modifiers() {
396 let mut renderer = DiffRenderer::new();
397 let mut buffer = CellBuffer::new(10, 5);
398 buffer.update(
399 0,
400 0,
401 "X",
402 Color::WHITE,
403 Color::BLACK,
404 Modifiers::BOLD | Modifiers::ITALIC,
405 );
406 let mut output = Vec::new();
407
408 let count = renderer.flush(&mut buffer, &mut output).unwrap();
409 assert_eq!(count, 1);
410 assert!(output.len() > 5);
412 }
413
414 #[test]
415 fn test_renderer_all_modifiers() {
416 let mut renderer = DiffRenderer::new();
417 let mut buffer = CellBuffer::new(10, 5);
418 let all_mods = Modifiers::BOLD
419 | Modifiers::ITALIC
420 | Modifiers::UNDERLINE
421 | Modifiers::STRIKETHROUGH
422 | Modifiers::DIM
423 | Modifiers::BLINK
424 | Modifiers::REVERSE
425 | Modifiers::HIDDEN;
426 buffer.update(0, 0, "X", Color::WHITE, Color::BLACK, all_mods);
427 let mut output = Vec::new();
428
429 renderer.flush(&mut buffer, &mut output).unwrap();
430 assert!(output.len() > 10);
431 }
432
433 #[test]
434 fn test_renderer_render_full() {
435 let mut renderer = DiffRenderer::new();
436 let mut buffer = CellBuffer::new(10, 5);
437 buffer.clear_dirty();
438
439 let mut output = Vec::new();
440 let count = renderer.render_full(&mut buffer, &mut output).unwrap();
441
442 assert_eq!(count, 50);
444 }
445
446 #[test]
447 fn test_renderer_skip_continuation() {
448 let mut renderer = DiffRenderer::new();
449 let mut buffer = CellBuffer::new(10, 5);
450
451 buffer.update(0, 0, "日", Color::WHITE, Color::BLACK, Modifiers::NONE);
453 if let Some(cell) = buffer.get_mut(1, 0) {
455 cell.make_continuation();
456 }
457 buffer.mark_dirty(1, 0);
458
459 let mut output = Vec::new();
460 let count = renderer.flush(&mut buffer, &mut output).unwrap();
461
462 assert_eq!(count, 1);
464 }
465
466 #[test]
467 fn test_renderer_cursor_wrap() {
468 let mut renderer = DiffRenderer::new();
469 let mut buffer = CellBuffer::new(5, 2);
470 buffer.update(4, 0, "X", Color::WHITE, Color::BLACK, Modifiers::NONE);
471 let mut output = Vec::new();
472
473 renderer.flush(&mut buffer, &mut output).unwrap();
474 assert_eq!(renderer.cursor_x, u16::MAX);
476 }
477
478 #[test]
479 fn test_renderer_statistics() {
480 let mut renderer = DiffRenderer::new();
481 let mut buffer = CellBuffer::new(10, 6);
482 buffer.update(0, 0, "A", Color::RED, Color::BLACK, Modifiers::NONE);
483 buffer.update(5, 5, "B", Color::BLUE, Color::WHITE, Modifiers::BOLD);
484 let mut output = Vec::new();
485
486 renderer.flush(&mut buffer, &mut output).unwrap();
487
488 assert_eq!(renderer.cells_written(), 2);
489 assert!(renderer.cursor_moves() >= 2);
490 assert!(renderer.style_changes() >= 2);
491 }
492
493 #[test]
494 fn test_renderer_default() {
495 let renderer = DiffRenderer::default();
496 assert_eq!(renderer.cursor_x, u16::MAX);
497 }
498
499 #[test]
500 fn test_style_state_default() {
501 let state = StyleState::default();
502 assert_eq!(state.fg, Color::WHITE);
503 assert_eq!(state.bg, Color::BLACK);
504 assert!(state.modifiers.is_empty());
505 }
506
507 #[test]
508 fn test_style_state_equality() {
509 let s1 = StyleState::default();
510 let s2 = StyleState::default();
511 assert_eq!(s1, s2);
512
513 let s3 = StyleState {
514 fg: Color::RED,
515 ..Default::default()
516 };
517 assert_ne!(s1, s3);
518 }
519}