ratatui_core/layout/position.rs
1#![warn(missing_docs)]
2use core::fmt;
3use core::ops::{Add, AddAssign, Sub, SubAssign};
4
5use crate::layout::{Offset, Rect};
6
7/// Position in the terminal coordinate system.
8///
9/// The position is relative to the top left corner of the terminal window, with the top left corner
10/// being (0, 0). The x axis is horizontal increasing to the right, and the y axis is vertical
11/// increasing downwards.
12///
13/// `Position` is used throughout the layout system to represent specific points in the terminal.
14/// It can be created from coordinates, tuples, or extracted from rectangular areas.
15///
16/// # Construction
17///
18/// - [`new`](Self::new) - Create a new position from x and y coordinates
19/// - [`default`](Default::default) - Create at origin (0, 0)
20///
21/// # Conversion
22///
23/// - [`from((u16, u16))`](Self::from) - Create from `(u16, u16)` tuple
24/// - [`from(Rect)`](Self::from) - Create from [`Rect`] (uses top-left corner)
25/// - [`into((u16, u16))`] - Convert to `(u16, u16)` tuple
26///
27/// # Movement
28///
29/// - [`offset`](Self::offset) - Move by an [`Offset`]
30/// - [`Add<Offset>`](core::ops::Add) and [`Sub<Offset>`](core::ops::Sub) - Shift by offsets with
31/// clamping
32/// - [`AddAssign<Offset>`](core::ops::AddAssign) and [`SubAssign<Offset>`](core::ops::SubAssign) -
33/// In-place shifting
34///
35/// # Examples
36///
37/// ```
38/// use ratatui_core::layout::{Offset, Position, Rect};
39///
40/// // the following are all equivalent
41/// let position = Position { x: 1, y: 2 };
42/// let position = Position::new(1, 2);
43/// let position = Position::from((1, 2));
44/// let position = Position::from(Rect::new(1, 2, 3, 4));
45///
46/// // position can be converted back into the components when needed
47/// let (x, y) = position.into();
48///
49/// // movement by offsets
50/// let position = Position::new(5, 5) + Offset::new(2, -3);
51/// assert_eq!(position, Position::new(7, 2));
52/// ```
53///
54/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
55#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct Position {
58 /// The x coordinate of the position
59 ///
60 /// The x coordinate is relative to the left edge of the terminal window, with the left edge
61 /// being 0.
62 pub x: u16,
63
64 /// The y coordinate of the position
65 ///
66 /// The y coordinate is relative to the top edge of the terminal window, with the top edge
67 /// being 0.
68 pub y: u16,
69}
70
71impl Position {
72 /// Position at the origin, the top left edge at 0,0
73 pub const ORIGIN: Self = Self::new(0, 0);
74
75 /// Position at the minimum x and y values
76 pub const MIN: Self = Self::ORIGIN;
77
78 /// Position at the maximum x and y values
79 pub const MAX: Self = Self::new(u16::MAX, u16::MAX);
80
81 /// Create a new position
82 pub const fn new(x: u16, y: u16) -> Self {
83 Self { x, y }
84 }
85
86 /// Moves the position by the given offset.
87 ///
88 /// Positive offsets move right and down, negative offsets move left and up. Values that would
89 /// move the position outside the `u16` range are clamped to the nearest edge.
90 #[must_use = "method returns the modified value"]
91 pub fn offset(self, offset: Offset) -> Self {
92 self + offset
93 }
94}
95
96impl From<(u16, u16)> for Position {
97 fn from((x, y): (u16, u16)) -> Self {
98 Self { x, y }
99 }
100}
101
102impl From<Position> for (u16, u16) {
103 fn from(position: Position) -> Self {
104 (position.x, position.y)
105 }
106}
107
108impl From<Rect> for Position {
109 fn from(rect: Rect) -> Self {
110 rect.as_position()
111 }
112}
113
114impl fmt::Display for Position {
115 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
116 write!(f, "({}, {})", self.x, self.y)
117 }
118}
119
120impl Add<Offset> for Position {
121 type Output = Self;
122
123 /// Moves the position by the given offset.
124 ///
125 /// Values that would move the position outside the `u16` range are clamped to the nearest
126 /// edge.
127 fn add(self, offset: Offset) -> Self {
128 let max = i32::from(u16::MAX);
129 let x = i32::from(self.x).saturating_add(offset.x).clamp(0, max) as u16;
130 let y = i32::from(self.y).saturating_add(offset.y).clamp(0, max) as u16;
131 Self { x, y }
132 }
133}
134
135impl Add<Position> for Offset {
136 type Output = Position;
137
138 /// Moves the position by the given offset.
139 ///
140 /// Values that would move the position outside the `u16` range are clamped to the nearest
141 /// edge.
142 fn add(self, position: Position) -> Position {
143 position + self
144 }
145}
146
147impl Sub<Offset> for Position {
148 type Output = Self;
149
150 /// Moves the position by the inverse of the given offset.
151 ///
152 /// Values that would move the position outside the `u16` range are clamped to the nearest
153 /// edge.
154 fn sub(self, offset: Offset) -> Self {
155 let max = i32::from(u16::MAX);
156 let x = i32::from(self.x).saturating_sub(offset.x).clamp(0, max) as u16;
157 let y = i32::from(self.y).saturating_sub(offset.y).clamp(0, max) as u16;
158 Self { x, y }
159 }
160}
161
162impl AddAssign<Offset> for Position {
163 /// Moves the position in place by the given offset.
164 ///
165 /// Values that would move the position outside the `u16` range are clamped to the nearest
166 /// edge.
167 fn add_assign(&mut self, offset: Offset) {
168 *self = *self + offset;
169 }
170}
171
172impl SubAssign<Offset> for Position {
173 /// Moves the position in place by the inverse of the given offset.
174 ///
175 /// Values that would move the position outside the `u16` range are clamped to the nearest
176 /// edge.
177 fn sub_assign(&mut self, offset: Offset) {
178 *self = *self - offset;
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use alloc::string::ToString;
185
186 use super::*;
187
188 #[test]
189 fn new() {
190 let position = Position::new(1, 2);
191
192 assert_eq!(position, Position { x: 1, y: 2 });
193 }
194
195 #[test]
196 fn from_tuple() {
197 let position = Position::from((1, 2));
198
199 assert_eq!(position, Position { x: 1, y: 2 });
200 }
201
202 #[test]
203 fn into_tuple() {
204 let position = Position::new(1, 2);
205 let (x, y) = position.into();
206 assert_eq!(x, 1);
207 assert_eq!(y, 2);
208 }
209
210 #[test]
211 fn from_rect() {
212 let rect = Rect::new(1, 2, 3, 4);
213 let position = Position::from(rect);
214
215 assert_eq!(position, Position { x: 1, y: 2 });
216 }
217
218 #[test]
219 fn to_string() {
220 let position = Position::new(1, 2);
221 assert_eq!(position.to_string(), "(1, 2)");
222 }
223
224 #[test]
225 fn offset_moves_position() {
226 let position = Position::new(2, 3).offset(Offset::new(5, 7));
227
228 assert_eq!(position, Position::new(7, 10));
229 }
230
231 #[test]
232 fn offset_clamps_to_bounds() {
233 let position = Position::new(1, 1).offset(Offset::MAX);
234
235 assert_eq!(position, Position::MAX);
236 }
237
238 #[test]
239 fn add_and_subtract_offset() {
240 let position = Position::new(10, 10) + Offset::new(-3, 4) - Offset::new(5, 20);
241
242 assert_eq!(position, Position::new(2, 0));
243 }
244
245 #[test]
246 fn add_assign_and_sub_assign_offset() {
247 let mut position = Position::new(5, 5);
248 position += Offset::new(2, 3);
249 position -= Offset::new(10, 1);
250
251 assert_eq!(position, Position::new(0, 7));
252 }
253}