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}