Skip to main content

scrive_core/
coords.rs

1//! Buffer-space coordinates and the primitives every position shares.
2//!
3//! Two ideas live here: the [`Point`] coordinate (row + byte column) and the
4//! [`Bias`] attachment side. Cell columns — the *display* space — are a
5//! separate type in the display map; the two spaces are distinct types, so
6//! mixing a byte column with a display cell is a compile error rather than a
7//! silent off-by-column.
8
9/// Attachment side for a position that sits *between* two characters.
10///
11/// `Left` means "stick to the character before me"; `Right` means "…after me".
12/// It is the single shared vocabulary for clipping to char boundaries (here),
13/// tracked-range stickiness (a named pair of biases), and hit-testing.
14#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
15pub enum Bias {
16    /// Bind to the character before the position.
17    Left,
18    /// Bind to the character after the position.
19    Right,
20}
21
22/// A buffer-space position: zero-based `row`, and a **byte** `col` within that
23/// row's text (the `\n` is not part of the row).
24///
25/// Columns are byte offsets, not cells and not grapheme clusters — cell columns
26/// are [`DisplayPoint`](crate) territory, and grapheme-cluster navigation is a
27/// deliberate non-goal. Ordering is row-major, so `Point`s compare in document
28/// order.
29#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
30pub struct Point {
31    /// Zero-based line index.
32    pub row: u32,
33    /// Byte offset within the row (excludes the trailing `\n`).
34    pub col: u32,
35}
36
37impl Point {
38    /// A `Point` at `(row, col)`.
39    #[must_use]
40    pub const fn new(row: u32, col: u32) -> Self {
41        Self { row, col }
42    }
43
44    /// The origin, `(0, 0)`.
45    pub const ZERO: Self = Self { row: 0, col: 0 };
46}
47
48/// Snap a byte offset to the nearest UTF-8 character boundary of `text`,
49/// choosing the direction from `bias`.
50///
51/// If `offset` already lands on a boundary (or on either end of `text`) it is
52/// returned unchanged. Otherwise `Bias::Left` moves to the boundary at or
53/// before `offset`, `Bias::Right` to the boundary at or after. Offsets past the
54/// end clamp to `text.len()`. This is the char-boundary half of the buffer's
55/// clip rules, which keep every stored offset on a valid boundary.
56#[must_use]
57pub(crate) fn snap_char_boundary(text: &str, offset: u32, bias: Bias) -> u32 {
58    let len = text.len() as u32;
59    if offset >= len {
60        return len;
61    }
62    let mut o = offset as usize;
63    if text.is_char_boundary(o) {
64        return offset;
65    }
66    match bias {
67        Bias::Left => {
68            while !text.is_char_boundary(o) {
69                o -= 1;
70            }
71        }
72        Bias::Right => {
73            while !text.is_char_boundary(o) {
74                o += 1;
75            }
76        }
77    }
78    o as u32
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn points_order_row_major() {
87        assert!(Point::new(0, 5) < Point::new(1, 0));
88        assert!(Point::new(1, 2) < Point::new(1, 3));
89        assert_eq!(Point::ZERO, Point::new(0, 0));
90    }
91
92    #[test]
93    fn snap_is_identity_on_ascii_and_ends() {
94        let s = "hello";
95        for o in 0..=s.len() as u32 {
96            assert_eq!(snap_char_boundary(s, o, Bias::Left), o);
97            assert_eq!(snap_char_boundary(s, o, Bias::Right), o);
98        }
99    }
100
101    #[test]
102    fn snap_moves_off_multibyte_interior() {
103        // "é" is 2 bytes (0xC3 0xA9): "aé" = bytes [a=0][é=1,2], len 3.
104        let s = "aé";
105        assert_eq!(s.len(), 3);
106        // Offset 2 is inside 'é'.
107        assert_eq!(snap_char_boundary(s, 2, Bias::Left), 1);
108        assert_eq!(snap_char_boundary(s, 2, Bias::Right), 3);
109        // Boundaries are untouched.
110        assert_eq!(snap_char_boundary(s, 1, Bias::Left), 1);
111        assert_eq!(snap_char_boundary(s, 3, Bias::Right), 3);
112    }
113}