fatui 0.1.0

a silly little framework for terminal user interfaces
Documentation
//! a frame of input and output

mod area;
mod cgptr;

use std::{
	fmt::{self, Debug},
	ops::{Index, IndexMut, Range},
};

use area::FrameArea;
use cgptr::CgPtr;

use crate::{
	Event, InputState, StateEvent,
	grid::Grid,
	input::{FrameInput, Nop},
	output::StyledChar,
	pos::{Pos, Rect, Size, X, Y},
};

#[cfg(test)]
mod test_draw;
#[cfg(test)]
mod test_split;

/// a single frame of input/output
///
/// one [`Frame`] encompasses:
/// - a single user input event
/// - a character grid you can render the ui to
///
/// you get it from `Backend::step`, update state and render accordingly,
/// and then step to the next frame.
///
/// the frame you get by default has an [`Event`][crate::Event],
/// to avoid the overhead of keeping an [`Input`][crate::Input] up to date unnecessarily.
/// if you want to, though, use [`Self::with`].
///
/// # virtual cells
///
/// there's one bit of weirdness:
/// much like x11, where a "window" doesn't necessarily have a full pixel buffer,
/// frames don't necessarily have *actual character grid cells*
/// underlying the entire extent of the frame.
/// instead they know which bits of the grid they do own,
/// and can draw to those,
/// and the rest of the ("virtual") cells simply have writes discarded.
///
/// you always start with a [`Row`].
/// for simple implementation, access via indexing always provides a `&mut Styled<char>`,
/// which is just sometimes an internal discard buffer.
/// if you'd like to optimize, you can use `Row::extents`,
/// which
pub struct Frame<'b, Input> {
	/// the character grid buffer this is writing to
	///
	/// can't have multiple &mut to the same memory --
	/// so we have a *mut, and tie the lifetime with a PhantomData
	/// then slice out specific bits, so our &muts never overlap!
	///
	/// this can be `None` if the frame is fully shadowed,
	/// to let writes short-circuit.
	buf: Option<CgPtr<'b>>,
	/// the input associated with this frame
	input: Input,
	/// the portion of the [`Grid<StyledChar>`] this frame owns
	area: FrameArea,
	// TODO: extra extents, maybe in FrameArea, for scrollability
}

impl<Input: Debug> fmt::Debug for Frame<'_, Input> {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		f.debug_struct("Frame<'_>")
			.field("buf", &..)
			.field("input", &self.input)
			.field("area", &self.area)
			.finish()
	}
}

impl Frame<'static, Nop> {
	/// create a new [`Frame::null`] without associated input
	///
	/// mostly useful for tests.
	pub fn nop(size: Size) -> Self {
		Self::null(Nop, size)
	}
}

impl<Input> Frame<'static, Input> {
	/// create a new frame with no backing memory
	///
	/// this effectively makes the entire frame "virtual cells",
	/// which can be useful to allow components to skip rendering entirely.
	pub fn null(input: Input, size: Size) -> Self {
		Self { buf: None, input, area: FrameArea::full(size) }
	}
}

impl<'b, Input: FrameInput> Frame<'b, Input> {
	/// create a new [`Frame`] containing an entire frame buffer
	pub fn new(input: Input, chars: &'b mut Grid<StyledChar>) -> Self {
		let size = chars.size();
		if size.is_empty() {
			return Frame::null(input, chars.size());
		}
		Self { buf: Some(CgPtr::new(chars)), input, area: FrameArea::full(size) }
	}

	/// split this framebuffer in two vertically adjacent pieces, at the row
	///
	/// **you probably want [`Self::split`]!**
	/// this is meant to be used to implement splitters,
	/// not directly by end users.
	/// (it does have some niche utility though.)
	///
	/// the first is rows `[0, y)` and the second is `[y, height)`,
	/// but if `y` is out of bounds, this returns `None`.
	pub fn split_v(self, y: Y) -> Option<(Self, Self)> {
		if y > self.size().height {
			return None;
		}
		let (ta, ba) = self.area.split_v(y);
		// TODO: handle virtual areas properly -- maybe `area` can inform that?
		let (ti, bi) = self.input.split_v(y);
		Some((
			Self { buf: self.buf.clone(), input: ti, area: ta },
			Self { buf: self.buf.clone(), input: bi, area: ba },
		))
	}

	/// split this framebuffer in two horizontally adjacent pieces, at the column.
	///
	/// the first is rows `[0, x)` and the second is `[x, width)`,
	/// but if `x` is out of bounds, this returns `None`.
	pub fn split_h(self, x: X) -> Option<(Self, Self)> {
		if x > self.size().width {
			return None;
		}
		let (la, ra) = self.area.split_h(x);
		// TODO: handle virtual areas properly -- maybe `area` can inform that?
		let (li, ri) = self.input.split_h(x);
		Some((
			Self { buf: self.buf.clone(), input: li, area: la },
			Self { buf: self.buf.clone(), input: ri, area: ra },
		))
	}

	/// get the input associated with this frame
	pub fn input(&self) -> &Input {
		&self.input
	}
	/// get the size of this frame
	pub fn size(&self) -> Size {
		self.area.size()
	}
	/// get whether this frame contains any cells
	pub fn is_empty(&self) -> bool {
		self.size().is_empty()
	}

	/// expand the frame to occupy more virtual cells
	///
	/// `tl` is how many virtual cells up and to the left should be additionally covered,
	/// and `br` is how many down and to the right.
	pub fn vgrow(&mut self, tl: Size, br: Size) {
		self.area.vgrow(tl, br);
	}

	/// fill every cell in this frame with a specific character
	pub fn fill(&mut self, ch: StyledChar) {
		for ry in self.rows() {
			let mut row = self.row(ry);
			row.fill(ch);
		}
	}

	/// iterate over all the row positions of this frame
	///
	/// note that you still need to get each row with `row`,
	/// but you're guaranteed to get a `Some` back.
	/// (for `&mut` safety reasons there's no way to do an iterator of rows directly.)
	pub fn rows(&self) -> impl Iterator<Item = Y> + use<Input> {
		(0..self.size().height.0).map(Y)
	}

	/// get the row of characters at the given y-coordinate,
	/// panicking if it's out of bounds.
	///
	/// this allows you to directly futz with the contents of a [`Grid<StyledChar>`],
	/// but be cautious doing so with a frame you've already rendered to --
	/// it won't cause ub but it's a recipe for things to look bad!
	///
	/// on the other hand, this is literally exactly how you're meant to render to a blank frame.
	/// go for it, buddy!
	///
	/// please note that you can't have more than one row "checked out" at a time,
	/// so you need to make sure not to let the lifetimes overlap
	/// if you don't have a convenient scope already lying around (e.g. a loop body):
	/// ```compile_fail(E0499)
	/// # use fatui::{frame::Frame, input::Nop, pos::{Y, Size}};
	/// let mut frame = // backend.step(), etc.
	/// # Frame::nop(Size::ZERO);
	/// let row1 = frame.row(Y(1));
	/// // that counts as a mutable reference, so trying to take another fails!
	/// let row2 = frame.row(Y(2));
	/// // (and then some attempt to use both at once,
	/// // because otherwise rustc is smart enough to drop `row1` for you!)
	/// println!("{row1:?}, {row2:?}");
	/// ```
	///
	/// if you're using this in a loop body, you likely won't even notice,
	/// since each row naturally dies at the end of the body before you get the next.
	/// but if you're not, you might need to do something like:
	/// ```
	/// # use fatui::{frame::Frame, input::Nop, pos::{Y, Size}};
	/// let mut frame = // backend.step(), etc.
	/// # Frame::nop(Size::rnew(2, 2));
	/// let row1 = frame.row(Y(1));
	/// std::mem::drop(row1);
	/// let row2 = frame.row(Y(2));
	/// ```
	pub fn row<'s>(&'s mut self, y: Y) -> Row<'s> {
		let vwidth = self.size().width;
		let Some(ref mut cg) = self.buf else {
			return Row::vir(vwidth);
		};
		let Some(ri) = self.area.rowinfo(y) else {
			return Row::vir(vwidth);
		};
		// SAFETY:
		// - `area`'s bit is only derived by splitting, which is mutually exclusive
		// - `compile_fail` doctest above proves `row` returns are mutually exclusive
		// - (`slice` also does bound checks, but these should all be in-bounds too.)
		let slice = unsafe { cg.slice(ri.slice, ri.real_y) };
		Row::abs(vwidth, slice, ri.xs)
	}
}

impl<'g> Frame<'g, Event> {
	/// update an [`InputState`] and return this frame with it attached
	pub fn with<'i>(self, state: &'i mut InputState) -> Frame<'g, StateEvent<'i>> {
		state.update(&self.input);
		Frame {
			buf: self.buf.clone(),
			input: StateEvent {
				event: self.input,
				state: &*state,
				// TODO: handle virtual sizes correctly
				rect: Rect::tlsz(Pos::ZERO, self.area.size()),
			},
			area: self.area,
		}
	}
}

/// One row of a [`Frame`].
pub struct Row<'b> {
	vwidth: X,
	absolute: Option<(&'b mut [StyledChar], Range<usize>)>,
	dummy: StyledChar,
}
impl fmt::Debug for Row<'_> {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		f.debug_struct("Row")
			.field("vwidth", &self.vwidth.0)
			.field("absolute", &self.absolute.as_ref().map(|(_, r)| ([..], r)))
			.finish()
	}
}

impl<'b> Row<'b> {
	fn vir(vwidth: X) -> Self {
		Self { vwidth, absolute: None, dummy: StyledChar::BLANK }
	}
	fn abs(vwidth: X, slice: &'b mut [StyledChar], range: Range<usize>) -> Self {
		Self { vwidth, absolute: Some((slice, range)), dummy: StyledChar::BLANK }
	}

	/// fill the row with a specific character
	pub fn fill(&mut self, ch: StyledChar) {
		if let Some((s, _)) = &mut self.absolute {
			s.fill(ch);
		}
	}

	/// iterate over all the real characters in this row
	///
	/// **pay close attention to the index!**
	/// it very well might not start at `0` or end at `.len`!
	pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut StyledChar> {
		[].into_iter()
	}
}

impl<'b> Index<X> for Row<'b> {
	type Output = StyledChar;
	fn index(&self, x: X) -> &Self::Output {
		assert!(x < self.vwidth, "x={x} is out of bounds ({})", self.vwidth);
		let Some((s, r)) = &self.absolute else {
			return &self.dummy;
		};
		if !r.contains(&x.0) { &self.dummy } else { &s[x.0 - r.start] }
	}
}
impl<'b> IndexMut<X> for Row<'b> {
	fn index_mut(&mut self, x: X) -> &mut Self::Output {
		assert!(x < self.vwidth, "x={x} is out of bounds ({})", self.vwidth);
		let Some((s, r)) = &mut self.absolute else {
			return &mut self.dummy;
		};
		if !r.contains(&x.0) { &mut self.dummy } else { &mut s[x.0 - r.start] }
	}
}