use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::io::{BufWriter, Stdout};
use std::num::NonZero;
use arrayvec::ArrayString;
use bitflags::bitflags;
use crossterm::QueueableCommand;
use crossterm::style::{self, Attribute};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use vector2d::Vector2D;
pub use arrayvec_const as arrayvec;
pub use as_any;
pub use crossterm;
pub use vector2d;
pub use crossterm::event::Event;
pub use crossterm::style::Color;
pub type TSize = u16;
pub type TPoint = Vector2D<TSize>;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CharDirection {
LeftRight,
RightLeft,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Alignment {
LowerBound,
Center,
HigherBound,
}
impl From<Alignment> for HorizontalAlignment {
fn from(value: Alignment) -> Self {
match value {
Alignment::LowerBound => Self::Left,
Alignment::Center => Self::Center,
Alignment::HigherBound => Self::Right,
}
}
}
impl From<Alignment> for VerticalAlignment {
fn from(value: Alignment) -> Self {
match value {
Alignment::LowerBound => Self::Top,
Alignment::Center => Self::Center,
Alignment::HigherBound => Self::Bottom,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum HorizontalAlignment {
Left,
#[default]
Center,
Right,
}
impl From<HorizontalAlignment> for Alignment {
fn from(value: HorizontalAlignment) -> Self {
match value {
HorizontalAlignment::Left => Self::HigherBound,
HorizontalAlignment::Center => Self::Center,
HorizontalAlignment::Right => Self::HigherBound,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum VerticalAlignment {
Top,
#[default]
Center,
Bottom,
}
impl From<VerticalAlignment> for Alignment {
fn from(value: VerticalAlignment) -> Self {
match value {
VerticalAlignment::Top => Self::HigherBound,
VerticalAlignment::Center => Self::Center,
VerticalAlignment::Bottom => Self::HigherBound,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[repr(usize)]
pub enum Orientation {
Horizontal = 0,
Vertical,
}
impl Orientation {
const INV_MAP: [Orientation; 2] = [Self::Vertical, Self::Horizontal];
pub fn invert(&self) -> Self {
Self::INV_MAP[*self as usize]
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub struct Line2D<T> {
pub a: Vector2D<T>,
pub b: Vector2D<T>,
}
impl Line2D<TSize> {
pub const fn new(x1: TSize, y1: TSize, x2: TSize, y2: TSize) -> Self {
Self {
a: Vector2D::new(x1, y1),
b: Vector2D::new(x2, y2),
}
}
pub const fn from(p1: TPoint, p2: TPoint) -> Self {
Self { a: p1, b: p2 }
}
pub const fn is_vertical(&self) -> bool {
self.a.x == self.b.x
}
pub const fn is_horizontal(&self) -> bool {
self.a.y == self.b.y
}
pub const fn is_ascending(&self) -> bool {
self.a.y < self.b.y
}
pub const fn is_descending(&self) -> bool {
self.a.y > self.b.y
}
pub const fn is_constant(&self) -> bool {
self.a.y == self.b.y
}
pub const fn iter_points(&self) -> PointIterator<Line2D<TSize>> {
PointIterator::<Line2D<TSize>>::new(*self)
}
}
pub struct PointIterator<T: Sized> {
line: T,
cursor: TSize,
}
impl Iterator for PointIterator<Line2D<TSize>> {
type Item = TPoint;
fn next(&mut self) -> Option<Self::Item> {
let mut ret = None;
if self.line.is_vertical() {
if self.cursor < self.line.b.y {
ret = Some(Vector2D::new(self.line.a.y, self.cursor));
self.cursor += 1;
}
}
else if self.cursor < self.line.b.x {
let div = self.line.b.x - self.line.a.x;
let mut m = TSize::MIN;
if div != TSize::MIN {
m = (self.line.b.y - self.line.a.y) / div;
}
let b = self.line.a.y - self.line.a.x * m;
ret = Some(Vector2D::new(self.cursor, m * self.cursor + b));
self.cursor += 1;
}
ret
}
}
impl PointIterator<Line2D<TSize>> {
const fn new(line: Line2D<TSize>) -> Self {
if line.is_vertical() {
Self {
line,
cursor: line.a.y,
}
}
else {
Self {
line,
cursor: line.a.x,
}
}
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Percent(NonZero<u8>);
impl Default for Percent {
fn default() -> Self {
Self::DEFAULT
}
}
impl Percent {
pub const MIN: Self = Self(NonZero::<u8>::MIN);
pub const MAX: Self = Self(NonZero::new(100).unwrap());
pub const DEFAULT: Self = Self::MAX;
pub const fn from_int(u: u8) -> Self {
if u == 0 {
Self::MIN
}
else if u >= 100 {
Self::MAX
}
else {
Self(NonZero::new(u).unwrap())
}
}
pub const fn from_float(f: f32) -> Self {
if !f.is_normal() {
return Self::DEFAULT;
}
let u = (f * 100.) as u8;
if u == 0 {
Self::MIN
}
else if u >= 100 {
Self::MAX
}
else {
Self(NonZero::new(u).unwrap())
}
}
pub const fn multiply(self, u: TSize) -> TSize {
(u * self.0.get() as TSize) / 100
}
pub const fn value(self) -> u8 {
self.0.get()
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[repr(usize)]
pub enum GlyphWidth {
Half = 1,
Full = 2,
}
impl TryFrom<usize> for GlyphWidth {
type Error = GraphemeError;
fn try_from(value: usize) -> Result<Self, GraphemeError> {
match value {
1 => Ok(GlyphWidth::Half),
2 => Ok(GlyphWidth::Full),
_ => Err(GraphemeError::ConversionError),
}
}
}
impl Default for GlyphWidth {
fn default() -> Self {
Self::Half
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Grapheme {
inner: ArrayString<{ Self::MAX_SIZE }>,
width: GlyphWidth,
}
impl Default for Grapheme {
fn default() -> Self {
Self::PLACEHOLDER
}
}
impl Grapheme {
pub const MAX_SIZE: usize = 16;
pub const PLACEHOLDER: Self = Self::new_unchecked(" ", GlyphWidth::Half);
pub const REPLACEMENT: Self = Self::new_unchecked("\u{FFFD}", GlyphWidth::Half);
pub(crate) const fn new_unchecked(grapheme: &'static str, width: GlyphWidth) -> Self {
if grapheme.len() > Self::MAX_SIZE {
panic!(stringify!(GraphemeError::GraphemeTooBig));
}
if grapheme.as_bytes()[0] < 32 {
panic!(stringify!(GraphemeError::InvalidGlyphWidth));
}
match ArrayString::from(grapheme) {
Ok(v) => Self { width, inner: v },
Err(_) => panic!("Error creating ArrayString"),
}
}
pub fn from(grapheme: &str) -> Result<Self, GraphemeError> {
if grapheme.len() > Self::MAX_SIZE {
return Err(GraphemeError::GraphemeTooBig);
}
let mut graphemes = grapheme.graphemes(true);
let _ = graphemes.next();
let g2 = graphemes.next();
if g2.is_some() {
return Err(GraphemeError::TooManyGraphemes);
}
let width = grapheme.width();
if grapheme.as_bytes()[0] < 32 || width == 0 {
return Err(GraphemeError::InvalidGlyphWidth);
}
Ok(Self {
inner: ArrayString::from(grapheme).unwrap(),
width: GlyphWidth::try_from(width).unwrap(),
})
}
#[inline(always)]
pub(crate) fn get_string(&self) -> ArrayString<{ Self::MAX_SIZE }> {
self.inner
}
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.inner
}
#[inline(always)]
pub fn width(&self) -> GlyphWidth {
self.width
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum GraphemeError {
GraphemeTooBig,
TooManyGraphemes,
InvalidGlyphWidth,
ConversionError,
}
impl Display for GraphemeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{self:?}"))
}
}
impl Error for GraphemeError {}
bitflags! {
#[derive(Debug, Copy, Clone, PartialEq, Default, PartialOrd, Hash)]
pub struct Style: u8{
const None = 0b0000_0000;
const ResetBefore = 0b0000_0001;
const ResetAfter = 0b0000_0010;
const Bold = 0b0000_0100;
const NoBold = 0b0000_1000;
const Underline = 0b0001_0000;
const NoUnderline = 0b0010_0000;
const Reverse = 0b0100_0000;
const NoReverse = 0b1000_0000;
}
}
impl Style {
const ATTRIBUTES: [Attribute; 8] = [
Attribute::Reset,
Attribute::Reset,
Attribute::Bold,
Attribute::NoBold,
Attribute::Underlined,
Attribute::NoUnderline,
Attribute::Reverse,
Attribute::NoReverse,
];
#[inline(always)]
pub(crate) fn apply_pre_styles(
stdout: &mut BufWriter<Stdout>,
flags: Style,
) -> Result<(), std::io::Error> {
for idx in [0, 2, 3, 4, 5, 6, 7] {
if flags.bits() & (1 << idx) != 0 {
stdout.queue(style::SetAttribute(Self::ATTRIBUTES[idx]))?;
}
}
Ok(())
}
#[inline(always)]
pub(crate) fn apply_post_styles(
stdout: &mut BufWriter<Stdout>,
flags: Style,
) -> Result<(), std::io::Error> {
const INDEX: u8 = Style::ResetAfter.flag_index();
if flags.bits() & (1 << INDEX) != 0 {
stdout.queue(style::SetAttribute(Self::ATTRIBUTES[INDEX as usize]))?;
}
Ok(())
}
#[inline(always)]
const fn flag_index(self) -> u8 {
let mut n = u8::MIN;
while (self.bits() >> (n + 1)) != 0 {
n += 1;
}
n
}
#[inline(always)]
pub fn when(self, when: bool) -> Self {
Self::from_bits_retain(self.bits() * when as u8)
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) struct Glyph {
pub style: Style,
pub bg: Option<Color>,
pub fg: Option<Color>,
pub grapheme: ArrayString<{ Grapheme::MAX_SIZE }>,
}
impl Default for Glyph {
fn default() -> Self {
Self {
style: Style::default(),
bg: None,
fg: None,
grapheme: ArrayString::from(Grapheme::PLACEHOLDER.as_str()).unwrap(),
}
}
}
impl Glyph {
pub const NULL: &'static str = "\0";
pub fn nullify(&mut self) {
self.grapheme.clear();
self.grapheme.push_str(Self::NULL);
}
pub fn is_null(&self) -> bool {
self.grapheme.as_str() == Self::NULL
}
}
pub enum RectError {
HorizontalBorderExceeds(Rect, Rect),
VerticalBorderExceeds(Rect, Rect),
}
impl Debug for RectError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::HorizontalBorderExceeds(rect, base) => write!(
f,
"Rect ({rect:?}) exceeds the base rect's ({base:?}) right border."
),
Self::VerticalBorderExceeds(rect, base) => write!(
f,
"Rect ({rect:?}) exceeds the base rect's ({base:?}) right border."
),
}
}
}
impl Display for RectError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Rect {
pub(crate) start: TPoint,
pub(crate) size: TPoint,
}
impl Rect {
pub const fn subrect(
&self,
x: TSize,
y: TSize,
width: TSize,
height: TSize,
) -> Result<Rect, RectError> {
let abs = Rect {
start: Vector2D::new(self.start.x + x, self.start.y + y),
size: Vector2D::new(width, height),
};
let base_end = self.end();
let end = abs.end();
if end.x > base_end.x {
return Err(RectError::HorizontalBorderExceeds(*self, abs));
}
if end.y > base_end.y {
return Err(RectError::VerticalBorderExceeds(*self, abs));
}
Ok(abs)
}
pub fn subrect2(&self, offset: TPoint, size: TPoint) -> Result<Rect, RectError> {
let abs = Rect {
start: self.start + offset,
size,
};
let base_end = self.end();
let end = abs.end();
if end.x > base_end.x {
return Err(RectError::HorizontalBorderExceeds(*self, abs));
}
if end.y > base_end.y {
return Err(RectError::VerticalBorderExceeds(*self, abs));
}
Ok(abs)
}
pub(crate) fn from(width: TSize, height: TSize) -> Self {
Self {
start: Vector2D::new(TSize::MIN, TSize::MIN),
size: Vector2D::new(width, height),
}
}
pub const fn start(&self) -> TPoint {
self.start
}
pub const fn size(&self) -> TPoint {
self.size
}
pub const fn end(&self) -> TPoint {
Vector2D::new(self.start.x + self.size.x, self.start.y + self.size.y)
}
pub fn overlaps(&self, other: &Self) -> bool {
let end1 = self.end();
let end2 = other.end();
let x_overlap = self.start.x < end2.x
&& end1.x > other.start.x
&& self.start.y < end2.y
&& end1.y > other.start.y;
let y_overlap = self.start.x < end2.x
&& end1.x > other.start.x
&& self.start.y > end2.y
&& end1.y < other.start.y;
x_overlap || y_overlap
}
pub fn split(
&self,
orientation: Orientation,
ratio: Percent,
mut padding: TSize,
) -> Result<SplitRect, RectError> {
let min_base_base = self.size[orientation.invert() as usize].saturating_sub(2);
if padding >= min_base_base {
padding = min_base_base;
}
eprintln!("padding: {padding}");
match orientation {
Orientation::Horizontal => {
let y = self.size().y - padding;
let subsize = (ratio.value() as TSize * y).div_ceil(100);
let padding_area = match padding {
0 => None,
v => Some(self.subrect(0, subsize, self.size().x, v)?),
};
Ok(SplitRect {
rects: (
self.subrect(0, 0, self.size().x, subsize)?,
self.subrect(0, subsize + padding, self.size().x, y - subsize)?,
),
padding_area,
})
}
Orientation::Vertical => {
let x = self.size().x - padding;
let subsize = (ratio.value() as TSize * x).div_ceil(100);
let padding_area = match padding {
0 => None,
v => Some(self.subrect(subsize, 0, v, self.size().y)?),
};
Ok(SplitRect {
rects: (
self.subrect(0, 0, subsize, self.size().y)?,
self.subrect(subsize + padding, 0, x - subsize, self.size().y)?,
),
padding_area,
})
}
}
}
pub const fn area(&self) -> TSize {
self.size.x * self.size.y
}
}
#[derive(Debug)]
pub struct SplitRect {
pub rects: (Rect, Rect),
pub padding_area: Option<Rect>,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn grapheme_invalid_chars() {
for c in 0..32 {
assert_eq!(
Grapheme::from(&char::from_u32(c).unwrap().to_string()),
Err(GraphemeError::InvalidGlyphWidth)
);
}
}
#[test]
fn grapheme_check_count() {
assert_eq!(Grapheme::from("abc"), Err(GraphemeError::TooManyGraphemes));
assert_eq!(Grapheme::from("ʤĵ"), Err(GraphemeError::TooManyGraphemes));
}
#[test]
fn grapheme_check_size() {
assert_eq!(Grapheme::from("🤦🏻♂️"), Err(GraphemeError::GraphemeTooBig));
}
#[test]
fn percent_int() {
assert_eq!(Percent::from_int(0), Percent::from_int(1));
assert_eq!(Percent::from_int(101), Percent::from_int(100));
for v in 1..=100 {
assert_eq!(Percent::from_int(v).value(), v);
}
}
#[test]
fn percent_float() {
assert_eq!(Percent::from_float(0.001), Percent::from_int(1));
assert_eq!(Percent::from_float(1.1), Percent::from_int(100));
assert_eq!(Percent::from_float(f32::INFINITY), Percent::from_int(100));
let mut v = 0.01;
while v <= 1. {
assert_eq!(Percent::from_float(v).value(), (v * 100.) as u8);
v += 0.01;
}
}
mod line {
use super::*;
#[test]
fn check_ctor() {
assert_eq!(
Line2D::from(Vector2D::new(0, 4), Vector2D::new(2, 1)),
Line2D::new(0, 4, 2, 1)
);
}
#[test]
fn check_iter() {
let f1 = |x: TSize| 2 * x + 4;
let l1 = Line2D::new(0, 4, 16, 4);
let l2 = Line2D::new(2, 4, 2, 20);
let l3 = Line2D::new(0, f1(0), 12, f1(12));
for (v, exp) in l1.iter_points().zip(0..16) {
assert_eq!(v.x, exp);
}
for (v, exp) in l2.iter_points().zip(4..20) {
assert_eq!(v.y, exp);
}
for v in l3.iter_points() {
assert_eq!(v.y, f1(v.x));
}
}
#[test]
fn check_linear_properties() {
let l1 = Line2D::new(0, 4, 2, 1);
let l2 = Line2D::new(2, 1, 4, 5);
assert!(l1.is_descending());
assert!(l2.is_ascending());
assert!(!l1.is_vertical());
assert!(!l2.is_horizontal());
let l3 = Line2D::new(2, 4, 2, 12);
let l4 = Line2D::new(1, 2, 12, 2);
assert!(l3.is_vertical());
assert!(l4.is_horizontal());
assert!(l4.is_constant());
}
}
#[test]
fn rect_check() {
let rect = Rect::from(100, 100);
let subrect = rect.subrect(20, 20, 50, 50).unwrap();
assert_ne!(rect.start(), subrect.start());
assert_ne!(rect.end(), subrect.end());
assert_ne!(rect.area(), subrect.area());
assert!(subrect.overlaps(&rect));
assert!(!subrect.overlaps(&Rect::from(20, 20)));
assert!(subrect.overlaps(&Rect::from(21, 21)));
assert!(!subrect.overlaps(&Rect::from(20, 21)));
assert!(!subrect.overlaps(&Rect::from(21, 20)));
assert!(!subrect.overlaps(&rect.subrect(70, 70, 20, 20).unwrap()));
assert!(subrect.overlaps(&rect.subrect(69, 69, 20, 20).unwrap()));
assert!(!subrect.overlaps(&rect.subrect(70, 69, 20, 20).unwrap()));
assert!(!subrect.overlaps(&rect.subrect(69, 70, 20, 20).unwrap()));
let subsubrect = subrect.subrect(10, 10, 20, 20).unwrap();
assert_eq!(subsubrect.start(), subrect.start() + Vector2D::new(10, 10));
assert!(
subrect
.subrect(0, 0, subrect.size().x + 1, subrect.size().y + 1)
.is_err()
);
}
#[test]
fn rect_split_check() {
let rect = Rect::from(64, 64);
let p = Percent::from_int(20);
let split = rect.split(Orientation::Horizontal, p, 2).unwrap();
let expected_rect_0 = Rect {
start: Vector2D::new(0, 0),
size: Vector2D::new(64, 13),
};
let expected_rect_1 = Rect {
start: Vector2D::new(0, 15),
size: Vector2D::new(64, 49),
};
assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
let p = Percent::from_int(40);
let split = rect.split(Orientation::Vertical, p, 3).unwrap();
let expected_rect_0 = Rect {
start: Vector2D::new(0, 0),
size: Vector2D::new(25, 64),
};
let expected_rect_1 = Rect {
start: Vector2D::new(28, 0),
size: Vector2D::new(36, 64),
};
assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
let small_rect = Rect::from(4, 4);
let p = Percent::from_int(20);
let split = small_rect.split(Orientation::Horizontal, p, 2).unwrap();
let expected_rect_0 = Rect {
start: Vector2D::new(0, 0),
size: Vector2D::new(4, 1),
};
let expected_rect_1 = Rect {
start: Vector2D::new(0, 3),
size: Vector2D::new(4, 1),
};
assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
let split = small_rect.split(Orientation::Horizontal, p, 4).unwrap();
assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
}
}