flux-tui 0.5.0

Fast and lightweight Terminal UI drawing library
Documentation
use std::{
	cell::{Ref, RefCell},
	ops::{Index, IndexMut},
	rc::Rc,
};
use vector2d::Vector2D;

use crate::common::*;

#[derive(Debug, PartialEq)]
pub(crate) struct Frame {
	buffer: Vec<Glyph>,
	size: TPoint,
}

impl Frame {
	fn new(size: TPoint) -> Rc<RefCell<Self>> {
		Rc::new(RefCell::new(Self {
			buffer: vec![Glyph::default(); (size.x * size.y) as usize],
			size: Vector2D::new(size.x, size.y),
		}))
	}

	#[cfg(test)]
	pub(crate) fn index(&self, x: TSize, y: TSize) -> usize {
		x as usize + y as usize * self.size.x as usize
	}

	fn resize(&mut self, size: TPoint) {
		self.buffer
			.resize((size.x * size.y) as usize, Glyph::default());
		self.size = Vector2D::new(size.x, size.y);
	}

	pub(crate) fn size(&self) -> TPoint {
		self.size
	}
}

impl Index<usize> for Frame {
	type Output = Glyph;

	fn index(&self, index: usize) -> &Self::Output {
		&self.buffer[index]
	}
}

impl IndexMut<usize> for Frame {
	fn index_mut(&mut self, index: usize) -> &mut Self::Output {
		&mut self.buffer[index]
	}
}

#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[repr(usize)]
enum BufferSide {
	#[default]
	Primary = 0,
	Secondary,
}

impl BufferSide {
	const SWAP_MAP: [BufferSide; Self::variant_count()] =
		[BufferSide::Secondary, BufferSide::Primary];

	//TODO: Replace by std::mem::variant_count
	pub const fn variant_count() -> usize {
		BufferSide::Secondary as usize + 1
	}

	pub const fn swap(&self) -> Self {
		Self::SWAP_MAP[*self as usize]
	}
}

/// Holds two buffers of the frame to be presenter in the terminal
/// One is the previous frame and the other is the current frame to be drawn
pub(crate) struct Framebuffer {
	buffers: [Rc<RefCell<Frame>>; BufferSide::variant_count()],
	active: BufferSide,
	force_redraw: bool,
}


impl Framebuffer {
	pub(crate) fn new(size: TPoint) -> Self {
		Self {
			buffers: [Frame::new(size), Frame::new(size)],
			active: BufferSide::default(),
			force_redraw: true,
		}
	}

	/// Resizes the inner buffers according to the new terminal size
	pub(crate) fn resize(&mut self, size: TPoint) {
		for idx in 0..BufferSide::variant_count() {
			self.reset_buffer(idx);
			self.buffers[idx].borrow_mut().resize(size);
		}

		self.force_redraw = true;
	}

	#[inline(always)]
	fn reset_buffer(&mut self, index: usize) {
		let glyph = Glyph::default();
		self.buffers[index].borrow_mut().buffer.fill(glyph);
	}

	/// Returns the size of the inner buffers
	pub(crate) fn size(&self) -> TPoint {
		self.buffers[BufferSide::Primary as usize].borrow().size()
	}

	/// Prepares the next buffer used for rendering and mutably returns it
	pub(crate) fn prepare_next_frame(&mut self) -> Rc<RefCell<Frame>> {
		let next_frame = self.active.swap() as usize;
		self.reset_buffer(next_frame);
		self.buffers[next_frame].clone()
	}

	/// Returns an iterator for all lines to be drawn to the terminal
	pub(crate) fn get_line_iterator(&mut self) -> GlyphLineIterator<'_> {
		self.active = self.active.swap();
		let iter = GlyphLineIterator {
			size: self.size(),
			active: self.active,
			line: TSize::MIN,
			force_redraw: self.force_redraw,
			buffers: [
				self.buffers[BufferSide::Primary as usize].borrow(),
				self.buffers[BufferSide::Secondary as usize].borrow(),
			],
		};

		self.force_redraw = false;

		iter
	}
}

#[derive(Debug, PartialEq)]
pub(crate) enum LineContent<'a> {
	Line { index: TSize, glyphs: &'a [Glyph] },
	Skip,
}

/// Iterator implementation which will return only the lines which are neccessary to be redrawn
/// This will compare each line with the previous buffer
/// If a line doesnt need to redrawn it will be empty
pub(crate) struct GlyphLineIterator<'a> {
	size: TPoint,
	active: BufferSide,
	line: TSize,
	force_redraw: bool,
	buffers: [Ref<'a, Frame>; BufferSide::variant_count()],
}

impl GlyphLineIterator<'_> {
	/// Custom lending iterator which returns a tuple of {index of the row, slice of glyphs}
	/// Use it with the while let syntax
	pub(crate) fn next(&mut self) -> Option<LineContent<'_>> {
		let cur_line = self.line;
		let prev_buffer = &self.buffers[self.active.swap() as usize].buffer;
		let cur_buffer = &self.buffers[self.active as usize].buffer;
		let range =
			(self.line * self.size.x) as usize..(self.size.x + self.line * self.size.x) as usize;
		self.line += 1;
		if cur_line < self.size.y {
			return match self.force_redraw
				|| cur_buffer[range.start..range.end] != prev_buffer[range.start..range.end]
			{
				true => Some(LineContent::Line {
					index: cur_line,
					glyphs: &cur_buffer[range],
				}),
				false => Some(LineContent::Skip),
			};
		}
		None
	}

	#[cfg(test)]
	fn count_redraws(mut self) -> usize {
		let mut n = 0;
		while let Some(v) = self.next() {
			if v != LineContent::Skip {
				n += 1;
			}
		}
		n
	}
}

#[cfg(test)]
mod test {
	use super::*;
	use arrayvec::ArrayString;

	#[test]
	fn check_frame() {
		let frame = Frame::new(Vector2D::new(16, 16));
		let default = Glyph::default();
		assert!(frame.borrow().buffer.iter().all(|x| *x == default));
		let dot = ArrayString::from(".").unwrap();
		frame
			.borrow_mut()
			.buffer
			.iter_mut()
			.for_each(|x| x.grapheme = dot);
		assert!(!frame.borrow().buffer.iter().all(|x| *x == default));
		frame.borrow_mut().resize(Vector2D::new(32, 32));
		assert_eq!(frame.borrow().size(), Vector2D::new(32, 32));
		assert!(frame.borrow().buffer.iter().any(|x| *x == default));
		assert!(frame.borrow().buffer.iter().any(|x| *x.grapheme == dot));
	}

	#[test]
	fn check_framebuffer() {
		let dot = ArrayString::from(".").unwrap();
		let mut fb = Framebuffer::new(Vector2D::new(16, 16));
		let mut iter = fb.get_line_iterator();
		assert_eq!(iter.count_redraws(), 16);
		iter = fb.get_line_iterator();
		assert_eq!(iter.count_redraws(), 0);
		assert_eq!(fb.buffers[0], fb.buffers[1]);

		let frame = fb.prepare_next_frame();
		let line4 = frame.borrow().index(4, 0);
		frame.borrow_mut().buffer[line4].grapheme = dot;
		iter = fb.get_line_iterator();
		assert_eq!(iter.count_redraws(), 1);
		assert_ne!(fb.buffers[0], fb.buffers[1]);

		let size = Vector2D::new(32, 32);
		fb.resize(size);
		assert_eq!(fb.size(), size);
	}
}