fatui 0.1.0

a silly little framework for terminal user interfaces
Documentation
//! utilities and types for tracking and calculating with locations

use std::{fmt, ops::*};

/// the type used to represent coordinates
///
/// **avoid using this directly!**
/// instead, prefer to use one of the more typesafe wrappers,
/// e.g. [`Row`] or [`Rect`].
pub type Coord = usize;

macro_rules! assign {
	( $for:ident: $(
		$trait:ident$(<$rhs:ty>)?::$method:ident via $normal:ident
	);* $(;)? ) => { $(
		impl $trait$(<$rhs>)? for $for {
			fn $method(&mut self, rhs: assign!(@first $($rhs,)? Self,)) {
				*self = self.$normal(rhs);
			}
		}
	)* };

	( @first $res:ty, $($other:tt)* ) => { $res }
}

macro_rules! peq_from_into {
	( $type:ty, $rhs:ty ) => {
		impl PartialEq<$rhs> for $type {
			fn eq(&self, other: &$rhs) -> bool {
				// bit of a hack here: only implemented for `Copy` types
				// so we can get away without cloning or w/e, lol
				self == &Into::<$type>::into(*other)
			}
		}
		impl PartialEq<$rhs> for &$type {
			fn eq(&self, other: &$rhs) -> bool {
				// bit of a hack here: only implemented for `Copy` types
				// so we can get away without cloning or w/e, lol
				*self == &Into::<$type>::into(*other)
			}
		}
	};
}

macro_rules! int_wrap {
	( $(
		$(#[$attr:meta])*
		$pub:vis struct $name:ident($fpub:vis $wrapping:ty)
	);* $(;)? ) => { $(
		$(#[$attr])*
		#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
		$pub struct $name($fpub $wrapping);
		impl From<usize> for $name {
			fn from(v: usize) -> Self {
				Self(v)
			}
		}
		impl fmt::Display for $name {
			fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
				write!(f, "{}", self.0)
			}
		}
		impl Add for $name {
			type Output = Self;
			fn add(self, rhs: Self) -> Self::Output {
				Self(self.0 + rhs.0)
			}
		}
		impl Sub for $name {
			type Output = Self;
			fn sub(self, rhs: Self) -> Self::Output {
				Self(self.0 - rhs.0)
			}
		}
		assign! {
			$name:
			AddAssign::add_assign via add;
			SubAssign::sub_assign via sub;
		}
		peq_from_into!($name, usize);
		impl PartialOrd<usize> for $name {
			fn partial_cmp(&self, other: &usize) -> Option<std::cmp::Ordering> {
				self.0.partial_cmp(other)
			}
		}
	)* }
}

int_wrap! {
	/// index of the column of characters onscreen,
	/// or the width in columns of something onscreen
	///
	/// columns start at 0 and go from left to right,
	/// so the leftmost row is 0 and the rightmost has the largest coordinate.
	/// this is the opposite of standard arabic reading order for boring historical reasons.
	pub struct X(pub Coord);
	/// index of the row of characters onscreen,
	/// or the height in rows of something onscreen
	///
	/// rows start at 0 and go from top to bottom,
	/// so the topmost row is 0 and the bottommost has the largest coordinate.
	/// this is the opposite of graphs in math because graphics are typically drawn top to bottom,
	/// i.e. in "standard reading order",
	/// which places the earliest memory index at the highest part of the screen.
	pub struct Y(pub Coord);
}

macro_rules! pair_wrap {
	( $(
		$(#[$attr:meta])*
		$pub:vis struct $name:ident($fpub:vis $x:ident, $y:ident)
	);* $(;)? ) => { $(
		$(#[$attr])*
		#[derive(Clone, Copy, Default, PartialEq, Eq)]
		$pub struct $name {
			#[allow(missing_docs)]
			$fpub $x: X,
			#[allow(missing_docs)]
			$fpub $y: Y,
		}
		impl $name {
			#[doc = concat!("create a new ", stringify!($name), " with the given X/Y")]
			pub const fn new($x: X, $y: Y) -> Self {
				Self { $x, $y }
			}
			#[doc = concat!("create a new ", stringify!($name), " with the given X/Y usizes")]
			pub const fn rnew($x: usize, $y: usize) -> Self {
				Self { $x: X($x), $y: Y($y) }
			}
			#[doc = concat!("the zero ", stringify!($name))]
			pub const ZERO: Self = Self { $x: X(0), $y: Y(0) };
		}
		impl From<(usize, usize)> for $name {
			fn from((x, y): (usize, usize)) -> Self {
				Self::rnew(x, y)
			}
		}
		impl From<(X, Y)> for $name {
			fn from((x, y): (X, Y)) -> Self {
				Self::new(x, y)
			}
		}
		impl fmt::Debug for $name {
			fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
				write!(f, concat!(stringify!($name), "({}, {})"), self.$x, self.$y)
			}
		}
		impl fmt::Display for $name {
			fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
				fmt::Debug::fmt(&self, f)
			}
		}
		impl Add<Size> for $name {
			type Output = Self;
			fn add(self, rhs: Size) -> Self {
				Self {
					$x: self.$x + rhs.width,
					$y: self.$y + rhs.height,
				}
			}
		}
		impl Sub<Size> for $name {
			type Output = Self;
			fn sub(self, rhs: Size) -> Self {
				Self {
					$x: self.$x - rhs.width,
					$y: self.$y - rhs.height,
				}
			}
		}
		impl Add<X> for $name {
			type Output = Self;
			fn add(self, rhs: X) -> Self {
				Self {
					$x: self.$x + rhs,
					$y: self.$y,
				}
			}
		}
		impl Sub<X> for $name {
			type Output = Self;
			fn sub(self, rhs: X) -> Self {
				Self {
					$x: self.$x - rhs,
					$y: self.$y,
				}
			}
		}
		impl Add<Y> for $name {
			type Output = Self;
			fn add(self, rhs: Y) -> Self {
				Self {
					$x: self.$x,
					$y: self.$y + rhs,
				}
			}
		}
		impl Sub<Y> for $name {
			type Output = Self;
			fn sub(self, rhs: Y) -> Self {
				Self {
					$x: self.$x,
					$y: self.$y - rhs,
				}
			}
		}
		assign! {
			$name:
			AddAssign<Size>::add_assign via add;
			AddAssign<X>::add_assign via add;
			AddAssign<Y>::add_assign via add;
			SubAssign<Size>::sub_assign via sub;
			SubAssign<X>::sub_assign via sub;
			SubAssign<Y>::sub_assign via sub;
		}
		peq_from_into!($name, (usize, usize));
		peq_from_into!($name, (X, Y));
	)* };
}

pair_wrap! {
	/// location of a single cell onscreen
	pub struct Pos(pub x, y);
	/// extents in cells of an onscreen region
	pub struct Size(pub width, height);
}

impl Sub<Pos> for Pos {
	type Output = Size;
	fn sub(self, rhs: Pos) -> Size {
		Size { width: self.x - rhs.x, height: self.y - rhs.y }
	}
}

impl Size {
	/// whether or not this area has any actual cells in it
	pub fn is_empty(&self) -> bool {
		self.width.0 == 0 || self.height.0 == 0
	}

	/// the total area covered
	pub fn area(&self) -> usize {
		self.width.0 * self.height.0
	}
}

/// location of a contiguous block of cells onscreen
#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub struct Rect {
	/// the location of the top-left corner
	pub tl: Pos,
	/// the size of the rectangle
	pub size: Size,
}

impl Rect {
	/// make a [`Rect`] with its top left and bottom right corners
	pub const fn tlbr(tl: Pos, br: Pos) -> Self {
		let size = Size { width: X(br.x.0 - tl.x.0), height: Y(br.y.0 - tl.y.0) };
		Self { tl, size }
	}

	/// make a [`Rect`] with its top left corner and size
	pub const fn tlsz(tl: Pos, size: Size) -> Self {
		Self { tl, size }
	}

	/// An empty rectangle at 0, 0
	pub const EMPTY: Self = Self::tlsz(Pos::ZERO, Size::ZERO);

	/// get the bottom right corner of the rectangle
	///
	/// note this is not the same as `self.tl + self.size` --
	/// that points to one cell below/right of the bottom-right corner.
	#[inline]
	pub fn br(&self) -> Pos {
		self.tl + self.size - Size { width: X(1), height: Y(1) }
	}

	/// check whether this rectangle contains the point
	pub fn contains(&self, pos: Pos) -> bool {
		self.tl.x <= pos.x
			&& pos.x < self.tl.x + self.size.width
			&& self.tl.y <= pos.y
			&& pos.y < self.tl.y + self.size.height
	}

	/// get the position relative to this rectangle, or `None` if it's outside
	pub fn rel(&self, pos: Pos) -> Option<Pos> {
		let pos =
			Pos { x: X(pos.x.0.checked_sub(self.tl.x.0)?), y: Y(pos.y.0.checked_sub(self.tl.y.0)?) };
		if pos.x < self.size.width && pos.y < self.size.height { Some(pos) } else { None }
	}

	/// iterate over all the rows (Y coordinates) contained in this `Rect`
	pub fn rows(&self) -> impl Iterator<Item = Y> {
		(self.tl.y.0..self.br().y.0).map(Y)
	}

	/// whether this rectangle contains any cells
	pub fn is_empty(&self) -> bool {
		self.size.is_empty()
	}

	/// split this rect horizontally along the given column
	///
	/// this returns `(left, right)`.
	pub fn split_h(self, col: X) -> (Self, Self) {
		(
			Self { tl: self.tl, size: Size { width: col, height: self.size.height } },
			Self { tl: self.tl + col, size: self.size - col },
		)
	}

	/// split this rect vertically along the given row
	///
	/// this returns `(top, bottom)`.
	pub fn split_v(self, row: Y) -> (Self, Self) {
		(
			Self { tl: self.tl, size: Size { width: self.size.width, height: row } },
			Self { tl: self.tl + row, size: self.size - row },
		)
	}
}

impl fmt::Debug for Rect {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		write!(f, "Rect[{},{}+{},{}]", self.tl.x, self.tl.y, self.size.width, self.size.height)
	}
}