//! Types relating to indexing into a terminal grid. use std::cmp::Ordering; use std::fmt; use std::ops::{Add, AddAssign, Range, Sub, SubAssign}; use serde::{Deserialize, Serialize}; use galaxyui::units::Lines; use super::grid::Dimensions; /// Behavior for handling grid boundaries. pub enum Boundary { /// Clamp to grid boundaries. /// /// When an operation exceeds the grid boundaries, the last point will be returned no matter /// how far the boundaries were exceeded. Clamp, /// Wrap around grid bondaries. /// /// When an operation exceeds the grid boundaries, the point will wrap around the entire grid /// history and continue at the other side. Wrap, } /// An integral index representing a row or column in a grid. /// /// This exists to encapsulate logic needed to account for floating point error /// when converting from a floating-point grid position to an integral one. /// Accumulation of small errors over time can cause a simple truncation from an /// f32 to a usize to produce an off-by-one error, so when constructing an /// `Index` from `Lines`, we adjust the value upwards by a small amount before /// truncating. pub struct Index(usize); impl Index { /// The amount by which we are willing to round up when converting from the /// floating-point `Lines` to an integral grid row/column index. This helps /// account for small errors that accumulate as arithmetic operations are /// performed on floating-point values. const FLOATING_POINT_ERROR_ADJUSTMENT: f64 = 0.0001; } impl From for Index { fn from(value: usize) -> Self { Self(value) } } impl From for Index { fn from(value: Lines) -> Self { // Adjust the value upwards slightly before truncating, to round up // when the value is sufficiently close to the next integer boundary. let error_adjusted = value.as_f64() + Self::FLOATING_POINT_ERROR_ADJUSTMENT; Self(error_adjusted as usize) } } /// Index in the grid using row, column notation. #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, Eq, PartialEq, Hash)] pub struct Point { pub row: usize, pub col: usize, } impl Point { pub fn new(line: impl Into, col: impl Into) -> Point { Point { row: line.into().0, col: col.into().0, } } pub const fn zero() -> Self { Self { row: 0, col: 0 } } #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] /// Increments the `Point` by `distance` cells, wrapping around if the column value exceeds /// `num_cols`. pub fn wrapping_add(mut self, num_cols: usize, distance: usize) -> Point { self.row += (distance + self.col) / num_cols; self.col = (self.col + distance) % num_cols; self } #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] /// Decrements the `Point` by `distance` cells, wrapping around if the column value would drop /// below zero. /// /// Note: This will also saturate at (0, 0) as a minimum value. pub fn wrapping_sub(mut self, num_cols: usize, distance: usize) -> Point { let line_changes = (distance + num_cols - 1).saturating_sub(self.col) / num_cols; if self.row >= line_changes { self.row -= line_changes; self.col = (num_cols + self.col - distance % num_cols) % num_cols; self } else { Point::new(0, 0) } } /// Returns 1D representation of point, as an index into a 1D array of cells (assumes left-to-right, top-to-bottom order). fn as_one_dimensional_index(&self, num_cols: usize) -> usize { self.row * num_cols + self.col } /// Compares Point against another Point, returning the one that is maximal, given the number of /// columns in the grid being considered. pub fn max_point<'a>(&'a self, other: &'a Point, num_cols: usize) -> &'a Point { if self.as_one_dimensional_index(num_cols) >= other.as_one_dimensional_index(num_cols) { self } else { other } } /// Computes left-to-right distance between two points. The result is an absolute value. pub fn distance(&self, num_cols: usize, other: &Point) -> usize { let this_index = self.as_one_dimensional_index(num_cols); let other_index = other.as_one_dimensional_index(num_cols); this_index.abs_diff(other_index) } pub fn to_visible_point(&self, history_size: usize) -> VisiblePoint { VisiblePoint { row: VisibleRow(self.row.saturating_sub(history_size)), col: self.col, } } } impl Point { #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] pub fn sub_absolute(mut self, dimensions: &D, boundary: Boundary, rhs: usize) -> Point where D: Dimensions, { let total_lines = dimensions.total_rows(); let num_cols = dimensions.columns(); self.row += (rhs + num_cols - 1).saturating_sub(self.col) / num_cols; self.col = (num_cols + self.col - rhs % num_cols) % num_cols; if self.row >= total_lines { match boundary { Boundary::Clamp => Point::new(total_lines - 1, 0), Boundary::Wrap => Point::new(self.row - total_lines, self.col), } } else { self } } #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] pub fn add_absolute(mut self, dimensions: &D, boundary: Boundary, rhs: usize) -> Point where D: Dimensions, { let num_cols = dimensions.columns(); let line_delta = (rhs + self.col) / num_cols; if self.row >= line_delta { self.row -= line_delta; self.col = (self.col + rhs) % num_cols; self } else { match boundary { Boundary::Clamp => Point::new(0, num_cols - 1), Boundary::Wrap => { let col = (self.col + rhs) % num_cols; let line = dimensions.total_rows() + self.row - line_delta; Point::new(line, col) } } } } } impl PartialOrd for Point { fn partial_cmp(&self, other: &Point) -> Option { Some(self.cmp(other)) } } impl Ord for Point { fn cmp(&self, other: &Point) -> Ordering { match (self.row.cmp(&other.row), self.col.cmp(&other.col)) { (Ordering::Equal, ord) | (ord, _) => ord, } } } #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct VisibleRow(pub usize); impl Sub for VisibleRow { type Output = VisibleRow; fn sub(self, rhs: usize) -> Self::Output { Self(self.0 - rhs) } } impl SubAssign for VisibleRow { fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0; } } impl Sub for VisibleRow { type Output = usize; fn sub(self, rhs: Self) -> Self::Output { self.0 - rhs.0 } } impl Add for VisibleRow { type Output = VisibleRow; fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) } } impl Add for VisibleRow { type Output = VisibleRow; fn add(self, rhs: usize) -> Self::Output { Self(self.0 + rhs) } } impl AddAssign for VisibleRow { fn add_assign(&mut self, rhs: usize) { self.0 += rhs; } } impl VisibleRow { pub fn saturating_sub(&self, rhs: usize) -> VisibleRow { VisibleRow(self.0.saturating_sub(rhs)) } #[inline] pub fn steps_between(start: VisibleRow, end: VisibleRow, by: VisibleRow) -> Option { if by == VisibleRow(0) { return None; } if start < end { // Note: We assume $t <= usize here. let diff = end - start; let by = by.0; if !diff.is_multiple_of(by) { Some(diff / by + 1) } else { Some(diff / by) } } else { Some(0) } } #[inline] pub fn steps_between_by_one(start: VisibleRow, end: VisibleRow) -> Option { Self::steps_between(start, end, VisibleRow(1)) } } impl fmt::Display for VisibleRow { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] pub struct VisiblePoint { pub row: VisibleRow, pub col: usize, } impl VisiblePoint { pub fn zero() -> Self { Self { row: VisibleRow(0), col: 0, } } #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] /// Increments the `VisiblePoint` by `distance` cells, wrapping around if the column value /// exceeds `num_cols`. pub fn wrapping_add(mut self, num_cols: usize, distance: usize) -> VisiblePoint { self.row += (distance + self.col) / num_cols; self.col = (self.col + distance) % num_cols; self } #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] /// Decrements the `VisiblePoint` by `distance` cells, wrapping around if the column value /// would drop below zero. /// /// Note: This will also saturate at (0, 0) as a minimum value. pub fn wrapping_sub(mut self, num_cols: usize, distance: usize) -> VisiblePoint { let line_changes = (distance + num_cols - 1).saturating_sub(self.col) / num_cols; if self.row >= VisibleRow(line_changes) { self.row = self.row - line_changes; self.col = (num_cols + self.col - distance % num_cols) % num_cols; self } else { VisiblePoint { row: VisibleRow(0), col: 0, } } } /// Wrap this point to a maximum width of `num_cols`, incrementing the row if it is beyond /// that value pub fn wrap(self, num_cols: usize) -> VisiblePoint { self.wrapping_add(num_cols, 0) } } impl PartialOrd for VisiblePoint { fn partial_cmp(&self, other: &VisiblePoint) -> Option { Some(self.cmp(other)) } } impl Ord for VisiblePoint { fn cmp(&self, other: &VisiblePoint) -> Ordering { match (self.row.cmp(&other.row), self.col.cmp(&other.col)) { (Ordering::Equal, ord) | (ord, _) => ord, } } } /// This exists because we can't implement Iterator on Range /// and the existing impl needs the unstable Step trait /// This should be removed and replaced with a Step impl /// in the ops macro when `step_by` is stabilized. pub struct IndexRange(pub Range); impl From> for IndexRange { fn from(from: Range) -> Self { IndexRange(from) } } impl Iterator for IndexRange { type Item = usize; #[inline] fn next(&mut self) -> Option { if self.0.start < self.0.end { let old = self.0.start; self.0.start = old + 1; Some(old) } else { None } } } impl DoubleEndedIterator for IndexRange { #[inline] fn next_back(&mut self) -> Option { if self.0.start < self.0.end { let new = self.0.end - 1; self.0.end = new; Some(new) } else { None } } } impl Iterator for IndexRange { type Item = VisibleRow; #[inline] fn next(&mut self) -> Option { if self.0.start < self.0.end { let old = self.0.start; self.0.start = old + 1; Some(old) } else { None } } #[inline] fn size_hint(&self) -> (usize, Option) { match Self::Item::steps_between_by_one(self.0.start, self.0.end) { Some(hint) => (hint, Some(hint)), None => (0, None), } } } impl DoubleEndedIterator for IndexRange { #[inline] fn next_back(&mut self) -> Option { if self.0.start < self.0.end { let new = self.0.end - 1; self.0.end = new; Some(new) } else { None } } } #[cfg(test)] #[path = "indexing_tests.rs"] mod tests;