r3bl_tui/tui/terminal_lib_backends/
offscreen_buffer.rs1use std::{fmt::{self, Debug},
3 ops::{Deref, DerefMut}};
4
5use diff_chunks::PixelCharDiffChunks;
6use smallvec::smallvec;
7
8use super::{FlushKind, RenderOps};
9use crate::{CachedMemorySize, ColWidth, GetMemSize, InlineVec, List, LockedOutputDevice,
10 MemoizedMemorySize, MemorySize, Pos, Size, TinyInlineString, TuiColor,
11 TuiStyle, col, dim_underline, fg_green, fg_magenta, get_mem_size,
12 inline_string, ok, row, tiny_inline_string};
13
14#[derive(Clone, PartialEq)]
28pub struct OffscreenBuffer {
29 pub buffer: PixelCharLines,
30 pub window_size: Size,
31 pub my_pos: Pos,
32 pub my_fg_color: Option<TuiColor>,
33 pub my_bg_color: Option<TuiColor>,
34 memory_size_calc_cache: MemoizedMemorySize,
39}
40
41impl GetMemSize for OffscreenBuffer {
42 fn get_mem_size(&self) -> usize {
45 self.buffer.get_mem_size()
46 + std::mem::size_of::<Size>()
47 + std::mem::size_of::<Pos>()
48 + std::mem::size_of::<Option<TuiColor>>()
49 + std::mem::size_of::<Option<TuiColor>>()
50 }
51}
52
53impl CachedMemorySize for OffscreenBuffer {
54 fn memory_size_cache(&self) -> &MemoizedMemorySize { &self.memory_size_calc_cache }
55
56 fn memory_size_cache_mut(&mut self) -> &mut MemoizedMemorySize {
57 &mut self.memory_size_calc_cache
58 }
59}
60
61pub mod diff_chunks {
62 #[allow(clippy::wildcard_imports)]
63 use super::*;
64
65 #[derive(Clone, Default, PartialEq)]
68 pub struct PixelCharDiffChunks {
69 pub inner: List<DiffChunk>,
70 }
71
72 pub type DiffChunk = (Pos, PixelChar);
73
74 impl Deref for PixelCharDiffChunks {
75 type Target = List<DiffChunk>;
76
77 fn deref(&self) -> &Self::Target { &self.inner }
78 }
79
80 impl From<List<DiffChunk>> for PixelCharDiffChunks {
81 fn from(list: List<DiffChunk>) -> Self { Self { inner: list } }
82 }
83}
84
85mod offscreen_buffer_impl {
86 #[allow(clippy::wildcard_imports)]
87 use super::*;
88
89 impl Debug for PixelCharDiffChunks {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 for (pos, pixel_char) in self.iter() {
92 writeln!(f, "\t{pos:?}: {pixel_char:?}")?;
93 }
94 ok!()
95 }
96 }
97
98 impl Deref for OffscreenBuffer {
99 type Target = PixelCharLines;
100
101 fn deref(&self) -> &Self::Target { &self.buffer }
102 }
103
104 impl DerefMut for OffscreenBuffer {
105 fn deref_mut(&mut self) -> &mut Self::Target {
111 self.invalidate_memory_size_calc_cache();
113 &mut self.buffer
114 }
115 }
116
117 impl Debug for OffscreenBuffer {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 writeln!(f, "window_size: {:?}, ", self.window_size)?;
120
121 let height = self.window_size.row_height.as_usize();
122 for row_index in 0..height {
123 if let Some(row) = self.buffer.get(row_index) {
124 if row_index > 0 {
126 writeln!(f)?;
127 }
128
129 writeln!(
131 f,
132 "{}",
133 fg_green(&inline_string!("row_index: {}", row_index))
134 )?;
135
136 write!(f, "{row:?}")?;
138 }
139 }
140
141 writeln!(f)
142 }
143 }
144
145 impl OffscreenBuffer {
146 #[must_use]
152 pub fn get_mem_size_cached(&mut self) -> MemorySize {
153 self.get_cached_memory_size()
154 }
155
156 fn invalidate_memory_size_calc_cache(&mut self) {
159 self.invalidate_memory_size_cache();
160 self.update_memory_size_cache(); }
162
163 #[must_use]
166 pub fn diff(&self, other: &Self) -> Option<PixelCharDiffChunks> {
167 if self.window_size != other.window_size {
168 return None;
169 }
170
171 let mut acc = List::default();
172
173 for (row_idx, (self_row, other_row)) in
174 self.buffer.iter().zip(other.buffer.iter()).enumerate()
175 {
176 for (col_idx, (self_pixel_char, other_pixel_char)) in
177 self_row.iter().zip(other_row.iter()).enumerate()
178 {
179 if self_pixel_char != other_pixel_char {
180 let pos = col(col_idx) + row(row_idx);
181 acc.push((pos, *other_pixel_char));
182 }
183 }
184 }
185 Some(PixelCharDiffChunks::from(acc))
186 }
187
188 #[must_use]
190 pub fn new_with_capacity_initialized(window_size: Size) -> Self {
191 let mut buffer = Self {
192 buffer: PixelCharLines::new_with_capacity_initialized(window_size),
193 window_size,
194 my_pos: Pos::default(),
195 my_fg_color: None,
196 my_bg_color: None,
197 memory_size_calc_cache: MemoizedMemorySize::default(),
198 };
199 let size = buffer.get_mem_size();
202 buffer
203 .memory_size_calc_cache
204 .upsert(|| MemorySize::new(size));
205 buffer
206 }
207
208 pub fn clear(&mut self) {
210 for line in self.buffer.iter_mut() {
211 for pixel_char in line.iter_mut() {
212 if pixel_char != &PixelChar::Spacer {
213 *pixel_char = PixelChar::Spacer;
214 }
215 }
216 }
217 self.invalidate_memory_size_calc_cache();
219 }
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Hash)]
224pub struct PixelCharLines {
225 pub lines: InlineVec<PixelCharLine>,
226}
227
228mod pixel_char_lines_impl {
229 #[allow(clippy::wildcard_imports)]
230 use super::*;
231
232 impl GetMemSize for PixelCharLines {
233 fn get_mem_size(&self) -> usize { get_mem_size::slice_size(self.lines.as_ref()) }
234 }
235
236 impl Deref for PixelCharLines {
237 type Target = InlineVec<PixelCharLine>;
238 fn deref(&self) -> &Self::Target { &self.lines }
239 }
240
241 impl DerefMut for PixelCharLines {
242 fn deref_mut(&mut self) -> &mut Self::Target { &mut self.lines }
243 }
244
245 impl PixelCharLines {
246 #[must_use]
247 pub fn new_with_capacity_initialized(window_size: Size) -> Self {
248 let window_height = window_size.row_height;
249 let window_width = window_size.col_width;
250 Self {
251 lines: smallvec![
252 PixelCharLine::new_with_capacity_initialized(window_width);
253 window_height.as_usize()
254 ],
255 }
256 }
257 }
258}
259
260#[derive(Clone, PartialEq, Eq, Hash)]
261pub struct PixelCharLine {
262 pub pixel_chars: Vec<PixelChar>,
263}
264
265impl GetMemSize for PixelCharLine {
266 fn get_mem_size(&self) -> usize {
267 get_mem_size::slice_size(self.pixel_chars.as_ref())
268 }
269}
270
271mod pixel_char_line_impl {
272 #[allow(clippy::wildcard_imports)]
273 use super::*;
274
275 impl Debug for PixelCharLine {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 const MAX_PIXEL_CHARS_PER_LINE: usize = 6;
280
281 let mut void_indices: InlineVec<usize> = smallvec![];
282 let mut spacer_indices: InlineVec<usize> = smallvec![];
283 let mut void_count: InlineVec<TinyInlineString> = smallvec![];
284 let mut spacer_count: InlineVec<TinyInlineString> = smallvec![];
285
286 let mut char_count = 0;
287
288 for (col_index, pixel_char) in self.iter().enumerate() {
290 match pixel_char {
291 PixelChar::Void => {
292 void_count.push(TinyInlineString::from(col_index.to_string()));
293 void_indices.push(col_index);
294 }
295 PixelChar::Spacer => {
296 spacer_count.push(TinyInlineString::from(col_index.to_string()));
297 spacer_indices.push(col_index);
298 }
299 PixelChar::PlainText { .. } => {}
300 }
301
302 write!(
304 f,
305 "{}{:?}",
306 dim_underline(&tiny_inline_string!("{col_index:03}")),
307 pixel_char
308 )?;
309
310 char_count += 1;
312 if char_count >= MAX_PIXEL_CHARS_PER_LINE {
313 char_count = 0;
314 writeln!(f)?;
315 }
316 }
317
318 {
321 if !void_count.is_empty() {
322 write!(f, "void [ ")?;
323 fmt_impl_index_values(&void_indices, f)?;
324 write!(f, " ]")?;
325
326 if !spacer_count.is_empty() {
328 write!(f, " | ")?;
329 }
330 }
331
332 if !spacer_count.is_empty() {
333 if !void_count.is_empty() {
335 write!(f, ", ")?;
336 }
337 write!(f, "spacer [ ")?;
338 fmt_impl_index_values(&spacer_indices, f)?;
339 write!(f, " ]")?;
340 }
341 }
342
343 ok!()
344 }
345 }
346
347 fn fmt_impl_index_values(
348 values: &[usize],
349 f: &mut fmt::Formatter<'_>,
350 ) -> std::fmt::Result {
351 mod helpers {
352 pub enum Peek {
353 NextItemContinuesRange,
354 NextItemDoesNotContinueRange,
355 }
356
357 pub fn peek_does_next_item_continues_range(
358 values: &[usize],
359 index: usize,
360 ) -> Peek {
361 if values.get(index + 1).is_none() {
362 return Peek::NextItemDoesNotContinueRange;
363 }
364 if values[index + 1] == values[index] + 1 {
365 Peek::NextItemContinuesRange
366 } else {
367 Peek::NextItemDoesNotContinueRange
368 }
369 }
370
371 pub enum CurrentRange {
372 DoesNotExist,
373 Exists,
374 }
375
376 pub fn does_current_range_exist(current_range: &[usize]) -> CurrentRange {
377 if current_range.is_empty() {
378 CurrentRange::DoesNotExist
379 } else {
380 CurrentRange::Exists
381 }
382 }
383 }
384
385 let mut acc_current_range: InlineVec<usize> = smallvec![];
387
388 for (index, value) in values.iter().enumerate() {
390 match (
391 helpers::peek_does_next_item_continues_range(values, index),
392 helpers::does_current_range_exist(&acc_current_range),
393 ) {
394 (
396 helpers::Peek::NextItemContinuesRange,
397 helpers::CurrentRange::DoesNotExist | helpers::CurrentRange::Exists,
398 ) => {
399 acc_current_range.push(*value);
400 }
401 (
404 helpers::Peek::NextItemDoesNotContinueRange,
405 helpers::CurrentRange::DoesNotExist,
406 ) => {
407 if index > 0 {
408 write!(f, ", ")?;
409 }
410 write!(f, "{value}")?;
411 }
412 (
415 helpers::Peek::NextItemDoesNotContinueRange,
416 helpers::CurrentRange::Exists,
417 ) => {
418 if index > 0 {
419 write!(f, ", ")?;
420 }
421 acc_current_range.push(*value);
422 write!(
423 f,
424 "{}-{}",
425 acc_current_range[0],
426 acc_current_range[acc_current_range.len() - 1]
427 )?;
428 acc_current_range.clear();
429 }
430 }
431 }
432
433 ok!()
434 }
435
436 impl PixelCharLine {
438 #[must_use]
440 pub fn new_with_capacity_initialized(window_width: ColWidth) -> Self {
441 Self {
442 pixel_chars: vec![PixelChar::Spacer; window_width.as_usize()],
443 }
444 }
445 }
446
447 impl Deref for PixelCharLine {
448 type Target = Vec<PixelChar>;
449 fn deref(&self) -> &Self::Target { &self.pixel_chars }
450 }
451
452 impl DerefMut for PixelCharLine {
453 fn deref_mut(&mut self) -> &mut Self::Target { &mut self.pixel_chars }
454 }
455}
456
457#[derive(Clone, Copy, PartialEq, Eq, Hash)]
458pub enum PixelChar {
459 Void,
460 Spacer,
461 PlainText {
462 display_char: char,
463 maybe_style: Option<TuiStyle>,
464 },
465}
466
467impl GetMemSize for PixelChar {
468 fn get_mem_size(&self) -> usize {
469 std::mem::size_of::<PixelChar>()
471 }
472}
473
474const EMPTY_CHAR: char = '╳';
475const VOID_CHAR: char = '❯';
476
477mod pixel_char_impl {
478 #[allow(clippy::wildcard_imports)]
479 use super::*;
480
481 impl Default for PixelChar {
482 fn default() -> Self { Self::Spacer }
483 }
484
485 impl Debug for PixelChar {
486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487 const WIDTH: usize = 16;
488
489 match self {
490 PixelChar::Void => {
491 write!(f, " V {VOID_CHAR:░^WIDTH$}")?;
492 }
493 PixelChar::Spacer => {
494 write!(f, " S {EMPTY_CHAR:░^WIDTH$}")?;
495 }
496 PixelChar::PlainText {
497 display_char,
498 maybe_style,
499 } => {
500 match maybe_style {
501 Some(style) => {
503 write!(
504 f,
505 " {} '{display_char}'→{style: ^WIDTH$}",
506 fg_magenta("P")
507 )?;
508 }
509 _ => {
511 write!(f, " {} '{display_char}': ^WIDTH$", fg_magenta("P"))?;
512 }
513 }
514 }
515 }
516
517 ok!()
518 }
519 }
520}
521
522pub trait OffscreenBufferPaint {
523 fn render(&mut self, offscreen_buffer: &OffscreenBuffer) -> RenderOps;
524
525 fn render_diff(&mut self, diff_chunks: &PixelCharDiffChunks) -> RenderOps;
526
527 fn paint(
528 &mut self,
529 render_ops: RenderOps,
530 flush_kind: FlushKind,
531 window_size: Size,
532 locked_output_device: LockedOutputDevice<'_>,
533 is_mock: bool,
534 );
535
536 fn paint_diff(
537 &mut self,
538 render_ops: RenderOps,
539 window_size: Size,
540 locked_output_device: LockedOutputDevice<'_>,
541 is_mock: bool,
542 );
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use crate::{assert_eq2, height, new_style, tui_color, width};
549
550 #[test]
551 fn test_offscreen_buffer_construction() {
552 let window_size = width(10) + height(2);
553 let my_offscreen_buffer =
554 OffscreenBuffer::new_with_capacity_initialized(window_size);
555 assert_eq2!(my_offscreen_buffer.buffer.len(), 2);
556 assert_eq2!(my_offscreen_buffer.buffer[0].len(), 10);
557 assert_eq2!(my_offscreen_buffer.buffer[1].len(), 10);
558 for line in my_offscreen_buffer.buffer.iter() {
559 for pixel_char in line.iter() {
560 assert_eq2!(pixel_char, &PixelChar::Spacer);
561 }
562 }
563 }
565
566 #[test]
567 fn test_offscreen_buffer_re_init() {
568 let window_size = width(10) + height(2);
569 let mut my_offscreen_buffer =
570 OffscreenBuffer::new_with_capacity_initialized(window_size);
571
572 my_offscreen_buffer.buffer[0][0] = PixelChar::PlainText {
573 display_char: 'a',
574 maybe_style: Some(new_style!(color_bg: {tui_color!(green)})),
575 };
576
577 my_offscreen_buffer.buffer[1][9] = PixelChar::PlainText {
578 display_char: 'z',
579 maybe_style: Some(new_style!(color_bg: {tui_color!(red)})),
580 };
581
582 my_offscreen_buffer.clear();
584 for line in my_offscreen_buffer.buffer.iter() {
585 for pixel_char in line.iter() {
586 assert_eq2!(pixel_char, &PixelChar::Spacer);
587 }
588 }
589 }
591
592 #[test]
593 fn test_memory_size_caching() {
594 let window_size = width(10) + height(2);
595 let mut my_offscreen_buffer =
596 OffscreenBuffer::new_with_capacity_initialized(window_size);
597
598 let size1 = my_offscreen_buffer.get_mem_size_cached();
600 assert_ne!(format!("{size1}"), "?");
601
602 let size2 = my_offscreen_buffer.get_mem_size_cached();
604 assert_eq!(format!("{size1}"), format!("{}", size2));
605
606 my_offscreen_buffer.buffer[0][0] = PixelChar::PlainText {
608 display_char: 'x',
609 maybe_style: None,
610 };
611
612 let size3 = my_offscreen_buffer.get_mem_size_cached();
614 assert_ne!(format!("{size3}"), "?");
615
616 my_offscreen_buffer.clear();
618 let size4 = my_offscreen_buffer.get_mem_size_cached();
619 assert_ne!(format!("{size4}"), "?");
620 }
621}