monospace_core/geometry.rs
1//! Positions and sizes, kept as separate types so the compiler refuses one where the other
2//! belongs: a position is signed and may be negative, a size never is, and one rectangle carrying
3//! both pairs would make the two interchangeable where the distinction matters.
4//!
5//! **The widths are the terminal's; [`docs/model.md`](../../../docs/model.md) §2 says what they are
6//! and what the range costs.**
7
8/// An absolute position in the plane a buffer occupies.
9///
10/// Coordinates may be negative: nothing in this crate treats the origin as a boundary.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Pos {
13 /// Grows to the right.
14 pub x: i16,
15 /// Grows downward.
16 pub y: i16,
17}
18
19/// A position **inside one window**, counted from its top-left corner.
20///
21/// Not the same thing as a [`Pos`], and kept apart for the same reason the two conventions a cell
22/// carries are: a `Pos` is a point in the plane and may be negative, an `Offset` is a column and a
23/// row within a rectangle and never is. Which one a method takes is the question it is asking —
24/// a figure is placed at a `Pos`, a click arrives as an `Offset` — so the two are separate types
25/// rather than one rectangle's worth of interchangeable numbers.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Offset {
28 /// Grows to the right, from the window's own leftmost column.
29 pub x: u16,
30 /// Grows downward, from the window's own topmost row.
31 pub y: u16,
32}
33
34/// The size of a window or a rendered area, in cells.
35///
36/// A width or height of zero is allowed: it describes an area with no positions, rather than
37/// being rejected.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct Size {
40 /// Never negative, by the type.
41 pub width: u16,
42 /// Never negative, by the type.
43 pub height: u16,
44}
45
46/// A rectangle in the plane: where it starts and how large it is.
47///
48/// **An origin and a [`Size`], and not a [`Footprint`]**, because the two answer different questions:
49/// a window is chosen before anything is drawn and may hold no position at all, while a footprint
50/// exists only once a stamp has reached somewhere. A [`Buffer`](crate::Buffer) is given one of
51/// these, and `render` is asked for one, so the same rectangle travels through both without being
52/// taken apart into two arguments.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Window {
55 /// The top-left corner, in the plane.
56 pub origin: Pos,
57 /// How far the window reaches from `origin`, right and down.
58 pub size: Size,
59}
60
61impl Window {
62 /// Whether `at` falls inside this window.
63 ///
64 /// The right and bottom edges are outside: a window of width `w` holds columns `origin.x` to
65 /// `origin.x + w - 1`, which is the rectangle a buffer stamps into.
66 #[must_use]
67 pub fn contains(self, at: Pos) -> bool {
68 let Some(dx) =
69 at.x.checked_sub(self.origin.x)
70 .and_then(|d| u16::try_from(d).ok())
71 else {
72 return false;
73 };
74 let Some(dy) =
75 at.y.checked_sub(self.origin.y)
76 .and_then(|d| u16::try_from(d).ok())
77 else {
78 return false;
79 };
80 dx < self.size.width && dy < self.size.height
81 }
82
83 /// The position `at` reaches, counted from this window's top-left corner, or `None` when the
84 /// origin and the offset cannot be added.
85 ///
86 /// The offset is the one a caller already holds — a click arrives as a column and a row of what
87 /// was rendered — so nothing is added before this translation, and the window's `origin` takes
88 /// no other part in the answer.
89 #[must_use]
90 pub fn position(self, at: Offset) -> Option<Pos> {
91 Some(Pos {
92 x: self.origin.x.checked_add_unsigned(at.x)?,
93 y: self.origin.y.checked_add_unsigned(at.y)?,
94 })
95 }
96}
97
98/// The rectangle a drawing reaches: the smallest and largest coordinates stamped into it.
99///
100/// **Two positions rather than an origin and a [`Size`]**, because `min` is a point in the plane and
101/// may be negative while the distance between the two is a magnitude: a span of `u16::MAX + 1` cells
102/// exists and no `Size` holds it, and a caller that wants the distance can derive it and decide what
103/// to do about the one span that does not fit. A drawing that stamped nothing reaches nothing, which
104/// is a state of its own rather than a rectangle at the origin.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct Footprint {
107 /// The smallest coordinate stamped, on both axes: the top-left corner, since `y` grows down.
108 pub min: Pos,
109 /// The largest coordinate stamped, on both axes.
110 pub max: Pos,
111}
112
113impl Footprint {
114 /// This footprint grown to hold `at`.
115 ///
116 /// The fold a [`FootprintSink`](crate::FootprintSink) performs once per stamp, kept here so
117 /// that "smallest" and "largest" are written once rather than at every caller that grows a
118 /// footprint.
119 #[must_use]
120 pub(crate) fn including(self, at: Pos) -> Self {
121 Self {
122 min: Pos {
123 x: self.min.x.min(at.x),
124 y: self.min.y.min(at.y),
125 },
126 max: Pos {
127 x: self.max.x.max(at.x),
128 y: self.max.y.max(at.y),
129 },
130 }
131 }
132}
133
134/// A way to move across the plane: up, right, down or left.
135///
136/// Not the same thing as a `Side`, which is a place on a cell rather than a way to move — see
137/// _Vocabulary_ in [`docs/model.md`](../../../docs/model.md). A figure reasons in directions; a
138/// fragment is told sides, and the translation between the two happens where a figure places its
139/// pieces.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum Direction {
142 /// Toward smaller `y`.
143 Up,
144 /// Toward larger `x`.
145 Right,
146 /// Toward larger `y`.
147 Down,
148 /// Toward smaller `x`.
149 Left,
150}
151
152/// Whether a run of cells lies along a row or a column.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum Orientation {
155 /// Along a row: positions differ in `x`.
156 Horizontal,
157 /// Along a column: positions differ in `y`.
158 Vertical,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::{Offset, Pos, Size, Window};
164
165 fn window(origin: Pos, width: u16, height: u16) -> Window {
166 Window {
167 origin,
168 size: Size { width, height },
169 }
170 }
171
172 /// A window of no size holds no position, not even the one at its corner.
173 #[test]
174 fn a_window_of_no_size_contains_no_position() {
175 let empty = window(Pos { x: 3, y: 4 }, 0, 0);
176
177 assert!(!empty.contains(Pos { x: 3, y: 4 }));
178 assert!(!empty.contains(Pos { x: 4, y: 4 }));
179 }
180
181 /// A negative origin is a corner like any other: it is inside, the position before it is not,
182 /// and the far edges are exclusive.
183 #[test]
184 fn a_window_contains_its_corner_and_stops_before_its_far_edges() {
185 let at_negative = window(Pos { x: -2, y: -3 }, 2, 2);
186
187 assert!(at_negative.contains(Pos { x: -2, y: -3 }));
188 assert!(at_negative.contains(Pos { x: -1, y: -2 }));
189 assert!(!at_negative.contains(Pos { x: -3, y: -3 }));
190 assert!(!at_negative.contains(Pos { x: -2, y: -4 }));
191 assert!(!at_negative.contains(Pos { x: 0, y: -3 }));
192 assert!(!at_negative.contains(Pos { x: -2, y: -1 }));
193 }
194
195 /// The corner offset reaches the origin, and every other offset adds to it.
196 #[test]
197 fn an_offset_is_counted_from_the_windows_corner() {
198 let window = window(Pos { x: 5, y: -4 }, 10, 10);
199
200 assert_eq!(
201 window.position(Offset { x: 0, y: 0 }),
202 Some(Pos { x: 5, y: -4 })
203 );
204 assert_eq!(
205 window.position(Offset { x: 2, y: 3 }),
206 Some(Pos { x: 7, y: -1 })
207 );
208 }
209
210 /// An offset the origin cannot reach is `None`, on either axis, and the largest one that does
211 /// fit is still a position.
212 #[test]
213 fn an_offset_the_origin_cannot_reach_is_none() {
214 let window = window(Pos { x: i16::MAX, y: 0 }, 1, 1);
215
216 assert_eq!(window.position(Offset { x: 1, y: 0 }), None);
217 assert_eq!(window.position(Offset { x: 0, y: u16::MAX }), None);
218 assert_eq!(
219 window.position(Offset {
220 x: 0,
221 y: i16::MAX as u16
222 }),
223 Some(Pos {
224 x: i16::MAX,
225 y: i16::MAX
226 })
227 );
228 }
229}