425 lines
12 KiB
Rust
425 lines
12 KiB
Rust
//! 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<usize> for Index {
|
|
fn from(value: usize) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
impl From<Lines> 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<Index>, col: impl Into<Index>) -> 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<D>(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<D>(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<Ordering> {
|
|
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<usize> for VisibleRow {
|
|
type Output = VisibleRow;
|
|
|
|
fn sub(self, rhs: usize) -> Self::Output {
|
|
Self(self.0 - rhs)
|
|
}
|
|
}
|
|
|
|
impl SubAssign<Self> for VisibleRow {
|
|
fn sub_assign(&mut self, rhs: Self) {
|
|
self.0 -= rhs.0;
|
|
}
|
|
}
|
|
|
|
impl Sub<Self> for VisibleRow {
|
|
type Output = usize;
|
|
|
|
fn sub(self, rhs: Self) -> Self::Output {
|
|
self.0 - rhs.0
|
|
}
|
|
}
|
|
|
|
impl Add<Self> for VisibleRow {
|
|
type Output = VisibleRow;
|
|
|
|
fn add(self, rhs: Self) -> Self::Output {
|
|
Self(self.0 + rhs.0)
|
|
}
|
|
}
|
|
|
|
impl Add<usize> for VisibleRow {
|
|
type Output = VisibleRow;
|
|
|
|
fn add(self, rhs: usize) -> Self::Output {
|
|
Self(self.0 + rhs)
|
|
}
|
|
}
|
|
|
|
impl AddAssign<usize> 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<usize> {
|
|
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<usize> {
|
|
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<Ordering> {
|
|
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<T>(pub Range<T>);
|
|
|
|
impl<T> From<Range<T>> for IndexRange<T> {
|
|
fn from(from: Range<T>) -> Self {
|
|
IndexRange(from)
|
|
}
|
|
}
|
|
|
|
impl Iterator for IndexRange<usize> {
|
|
type Item = usize;
|
|
#[inline]
|
|
fn next(&mut self) -> Option<usize> {
|
|
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<usize> {
|
|
#[inline]
|
|
fn next_back(&mut self) -> Option<usize> {
|
|
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<VisibleRow> {
|
|
type Item = VisibleRow;
|
|
#[inline]
|
|
fn next(&mut self) -> Option<VisibleRow> {
|
|
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<usize>) {
|
|
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<VisibleRow> {
|
|
#[inline]
|
|
fn next_back(&mut self) -> Option<VisibleRow> {
|
|
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;
|