fatui 0.1.0

a silly little framework for terminal user interfaces
Documentation
use std::ops::Range;

use crate::pos::{Pos, Rect, Size, X, Y};

/// maps between `Grid<StyledChar>` "absolute" positions and [`Frame`][super::Frame] "virtual" ones
///
/// frames *actually* occupy a subsection of a `Grid<StyledChar>`.
/// but they can *think* they're larger, for scrollable areas.
/// the area actually occupied is called the "absolute" area,
/// and the area they represent is the "virtual" area.
/// most interactions with frames are in virtual space --
/// but obviously that then needs to be translated to absolute space
/// to actually, you know, render anything.
///
/// "graphically":
///
/// ```text
/// +----------------+
/// |    virtual     |
/// |  +----------+  |
/// |  | absolute |  |
/// +--+----------+--+
/// ```
///
/// this type encapsulates all the messiness of that conversion!
#[derive(Debug)]
pub struct FrameArea {
	/// the size and position of the absolute area
	#[cfg(not(test))]
	bounds: Rect,
	#[cfg(test)]
	pub bounds: Rect,
	/// how far up and left the virtual top left is from the real top left
	offset: Size,
	/// the size of the virtual frame
	vsize: Size,
}

impl FrameArea {
	pub fn full(size: Size) -> Self {
		Self { bounds: Rect::tlsz(Pos::rnew(0, 0), size), offset: Size::rnew(0, 0), vsize: size }
	}
	pub fn vgrow(&mut self, tl: Size, br: Size) {
		self.offset += tl;
		self.vsize += tl + br;
	}

	pub fn size(&self) -> Size {
		self.vsize
	}
	pub fn rowinfo(&self, y: Y) -> Option<RowInfo> {
		if y < self.offset.height {
			return None;
		}
		let y = y - self.offset.height;
		if y < self.bounds.size.height {
			let y = y + self.bounds.tl.y;
			let slice_start = self.bounds.tl.x.0;
			let x_start = slice_start + self.offset.width.0;
			let len = self.bounds.size.width.0;
			Some(RowInfo {
				real_y: y,
				slice: slice_start..(slice_start + len),
				xs: x_start..(x_start + len),
			})
		} else {
			None
		}
	}
}

pub struct RowInfo {
	pub real_y: Y,
	pub slice: Range<usize>,
	pub xs: Range<usize>,
}

macro_rules! split_impl {
	($split_v:ident($y:ident: $Y:ident), $height:ident) => {
		pub fn $split_v(self, $y: $Y) -> (Self, Self) {
			// precalculate the new vsizes since those will be the same
			let tvs = Size { $height: $y, ..self.vsize };
			let bvs = self.vsize - $y;
			// three cases:
			if $y <= self.offset.$height {
				// - shaving off some (or all) of the top virtual area
				return (
					Self {
						bounds: Rect::EMPTY,
						offset: Size::ZERO, // no offset to no area on the screen!
						vsize: tvs,
					},
					Self { bounds: self.bounds, offset: self.offset - $y, vsize: bvs },
				);
			}
			let $y = $y - self.offset.$height;
			if $y < self.bounds.size.$height {
				// - cutting the actual area in two
				let (t, b) = self.bounds.$split_v($y);
				return (
					Self { bounds: t, offset: self.offset, vsize: tvs },
					Self { bounds: b, offset: Size { $height: $Y(0), ..self.offset }, vsize: bvs },
				);
			}
			// - shaving some (or all) of the bottom virtual area
			(
				Self { bounds: self.bounds, offset: self.offset, vsize: tvs },
				Self { bounds: Rect::EMPTY, offset: Size::ZERO, vsize: bvs },
			)
		}
	};
}

impl FrameArea {
	split_impl!(split_v(y: Y), height);
	split_impl!(split_h(x: X), width);
}

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

	// apologies for the macro hell; it's the only way to keep the tests even remotely compact.

	macro_rules! area {
		(
			$sx:literal, $sy:literal
			$(~ $gbx:literal, $gby:literal; $gax:literal, $gay:literal)?
		) => {{
			#[allow(unused_mut)]
			let mut f = FrameArea::full(Size::rnew($sx, $sy));
			$(
				f.vgrow(Size::rnew($gbx, $gby), Size::rnew($gax, $gay));
			)?
			f
		}}
	}
	macro_rules! assert_area {
		(
			$area:expr,
			bounds: ($bx:expr, $by:expr; $bw:expr, $bh:expr),
			offset: ($ox:expr, $oy:expr),
			vsize: ($vx:expr, $vy:expr) $(,)?
		) => {
			let area = &$area;
			assert_eq!(
				area.bounds,
				Rect::tlsz(Pos::rnew($bx, $by), Size::rnew($bw, $bh)),
				concat!(stringify!($area), "bounds was wrong"),
			);
			assert_eq!(area.offset, Size::rnew($ox, $oy), concat!(stringify!($area), "offset was wrong"),);
			assert_eq!(area.vsize, Size::rnew($vx, $vy), concat!(stringify!($area), "vsize was wrong"),);
		};
	}

	macro_rules! area_test {
		(
			$name:ident:
			(
				$sx:literal, $sy:literal
				$(~ $gbx:literal, $gby:literal; $gax:literal, $gay:literal)?
			) / $split:literal =>
			top: (
				bounds: ($tbx:expr, $tby:expr; $tbw:expr, $tbh:expr),
				offset: ($tox:expr, $toy:expr),
				vsize: ($tvx:expr, $tvy:expr) $(,)?
			),
			bottom: (
				bounds: ($bbx:expr, $bby:expr; $bbw:expr, $bbh:expr),
				offset: ($box:expr, $boy:expr),
				vsize: ($bvx:expr, $bvy:expr) $(,)?
			) $(,)?
		) => {
			paste::paste! { area_test! {
				@raw [<$name _v>]:
				($sx, $sy $(~ $gbx, $gby; $gax, $gay)?) / split_v($split) =>
				top: (
					bounds: ($tbx, $tby; $tbw, $tbh),
					offset: ($tox, $toy),
					vsize: ($tvx, $tvy)
				),
				bottom: (
					bounds: ($bbx, $bby; $bbw, $bbh),
					offset: ($box, $boy),
					vsize: ($bvx, $bvy)
				),
			} }
			paste::paste! { area_test! {
				@raw [<$name _h>]:
				($sy, $sx $(~ $gby, $gbx; $gay, $gax)?) / split_h($split) =>
				top: (
					bounds: ($tby, $tbx; $tbh, $tbw),
					offset: ($toy, $tox),
					vsize: ($tvy, $tvx)
				),
				bottom: (
					bounds: ($bby, $bbx; $bbh, $bbw),
					offset: ($boy, $box),
					vsize: ($bvy, $bvx)
				),
			} }
		};
		(@raw
			$name:ident: ($($area:tt)*) / $mthd:ident($split:literal) =>
			top: ($($top:tt)*), bottom: ($($bottom:tt)*) $(,)?
		) => {
			#[test]
			fn $name() {
				let area = area!($($area)*);
				let (tl, br) = area.$mthd($split.into());
				assert_area!(tl, $($top)*);
				assert_area!(br, $($bottom)*);
			}
		}
	}

	// TODO: can this be DRY'd further?
	area_test!(
		matching_split_start: (4, 4) / 0 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 0)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 4)),
	);
	area_test!(
		matching_split_on_absolute: (4, 4) / 1 =>
		top: (bounds: (0, 0; 4, 1), offset: (0, 0), vsize: (4, 1)),
		bottom: (bounds: (0, 1; 4, 3), offset: (0, 0), vsize: (4, 3)),
	);
	area_test!(
		matching_split_end: (4, 4) / 4 =>
		top: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 4)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 0)),
	);
	area_test!(
		wider_split_start: (4, 4 ~ 2, 0; 2, 0) / 0 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (8, 0)),
		bottom: (bounds: (0, 0; 4, 4), offset: (2, 0), vsize: (8, 4)),
	);
	area_test!(
		wider_split_on_absolute: (4, 4 ~ 2, 0; 2, 0) / 1 =>
		top: (bounds: (0, 0; 4, 1), offset: (2, 0), vsize: (8, 1)),
		bottom: (bounds: (0, 1; 4, 3), offset: (2, 0), vsize: (8, 3)),
	);
	area_test!(
		wider_split_end: (4, 4 ~ 2, 0; 2, 0) / 4 =>
		top: (bounds: (0, 0; 4, 4), offset: (2, 0), vsize: (8, 4)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (8, 0)),
	);
	area_test!(
		prelonger_split_start: (4, 4 ~ 0, 4; 0, 0) / 0 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 0)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 4), vsize: (4, 8)),
	);
	area_test!(
		prelonger_split_previrt: (4, 4 ~ 0, 4; 0, 0) / 2 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 2)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 2), vsize: (4, 6)),
	);
	area_test!(
		prelonger_split_barely_previrt: (4, 4 ~ 0, 4; 0, 0) / 4 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 4)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 4)),
	);
	area_test!(
		prelonger_split_on_absolute: (4, 4 ~ 0, 4; 0, 0) / 6 =>
		top: (bounds: (0, 0; 4, 2), offset: (0, 4), vsize: (4, 6)),
		bottom: (bounds: (0, 2; 4, 2), offset: (0, 0), vsize: (4, 2)),
	);
	area_test!(
		prelonger_split_end: (4, 4 ~ 0, 4; 0, 0) / 8 =>
		top: (bounds: (0, 0; 4, 4), offset: (0, 4), vsize: (4, 8)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 0)),
	);
	area_test!(
		postlonger_split_start: (4, 4 ~ 0, 0; 0, 4) / 0 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 0)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 8)),
	);
	area_test!(
		postlonger_split_on_absolute: (4, 4 ~ 0, 0; 0, 4) / 2 =>
		top: (bounds: (0, 0; 4, 2), offset: (0, 0), vsize: (4, 2)),
		bottom: (bounds: (0, 2; 4, 2), offset: (0, 0), vsize: (4, 6)),
	);
	area_test!(
		postlonger_split_barely_postvirt: (4, 4 ~ 0, 0; 0, 4) / 4 =>
		top: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 4)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 4)),
	);
	area_test!(
		postlonger_split_postvirt: (4, 4 ~ 0, 0; 0, 4) / 6 =>
		top: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 6)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 2)),
	);
	area_test!(
		postlonger_split_end: (4, 4 ~ 0, 0; 0, 4) / 8 =>
		top: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 8)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 0)),
	);
	area_test!(
		longer_split_start: (4, 4 ~ 0, 3; 0, 2) / 0 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 0)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 3), vsize: (4, 9)),
	);
	area_test!(
		longer_split_previrt: (4, 4 ~ 0, 3; 0, 2) / 1 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 1)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 2), vsize: (4, 8)),
	);
	area_test!(
		longer_split_barely_previrt: (4, 4 ~ 0, 3; 0, 2) / 3 =>
		top: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 3)),
		bottom: (bounds: (0, 0; 4, 4), offset: (0, 0), vsize: (4, 6)),
	);
	area_test!(
		longer_split_absolute: (4, 4 ~ 0, 3; 0, 2) / 4 =>
		top: (bounds: (0, 0; 4, 1), offset: (0, 3), vsize: (4, 4)),
		bottom: (bounds: (0, 1; 4, 3), offset: (0, 0), vsize: (4, 5)),
	);
	area_test!(
		longer_split_barely_postvirt: (4, 4 ~ 0, 3; 0, 2) / 7 =>
		top: (bounds: (0, 0; 4, 4), offset: (0, 3), vsize: (4, 7)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 2)),
	);
	area_test!(
		longer_split_postvirt: (4, 4 ~ 0, 3; 0, 2) / 8 =>
		top: (bounds: (0, 0; 4, 4), offset: (0, 3), vsize: (4, 8)),
		bottom: (bounds: (0, 0; 0, 0), offset: (0, 0), vsize: (4, 1)),
	);
}