Skip to main content

gpui_cn/terminal/
geometry.rs

1//! Cell geometry and hit testing for the grid.
2//!
3//! Every pixel-to-cell conversion goes through [`Geometry`] so the element,
4//! the pointer handlers, the IME candidate rectangle and the resize request
5//! cannot disagree about the grid.
6//!
7//! Derived from Herdr (Apache-2.0). Flooring the content box to whole cells
8//! is adapted from Muxy v2 (MIT).
9
10use gpui_kit::{Bounds, Pixels, Point, Size, point, px, size};
11
12use crate::terminal::frame::MAX_GRID;
13use crate::terminal::selection::Cell;
14
15/// Where the grid sits inside the element and how large one cell is.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Geometry {
18    /// The element bounds.
19    pub bounds: Bounds<Pixels>,
20    /// The top left of cell (0, 0).
21    pub origin: Point<Pixels>,
22    /// One cell.
23    pub cell: Size<Pixels>,
24    /// Whole columns that fit.
25    pub columns: u16,
26    /// Whole rows that fit.
27    pub rows: u16,
28}
29
30impl Geometry {
31    /// Geometry for `bounds` with `padding` on every side.
32    ///
33    /// The space left over after whole cells sits at the right and bottom,
34    /// as in Ghostty. With `balance` it is split between both sides.
35    #[must_use]
36    pub fn new(bounds: Bounds<Pixels>, cell: Size<Pixels>, padding: Pixels, balance: bool) -> Self {
37        let width = (bounds.size.width - padding * 2.0).max(px(0.0));
38        let height = (bounds.size.height - padding * 2.0).max(px(0.0));
39        let columns = whole_cells(width, cell.width);
40        let rows = whole_cells(height, cell.height);
41        let mut origin = bounds.origin + point(padding, padding);
42        if balance {
43            let spare_x = (width - cell.width * f32::from(columns)).max(px(0.0));
44            let spare_y = (height - cell.height * f32::from(rows)).max(px(0.0));
45            origin += point((spare_x / 2.0).floor(), (spare_y / 2.0).floor());
46        }
47        Self {
48            bounds,
49            origin,
50            cell,
51            columns,
52            rows,
53        }
54    }
55
56    /// The bounds of one cell.
57    #[must_use]
58    pub fn cell_bounds(self, column: u16, row: u16) -> Bounds<Pixels> {
59        Bounds::new(
60            self.origin
61                + point(
62                    self.cell.width * f32::from(column),
63                    self.cell.height * f32::from(row),
64                ),
65            self.cell,
66        )
67    }
68
69    /// The bounds of a run of columns on one row.
70    #[must_use]
71    pub fn span_bounds(self, columns: std::ops::Range<u16>, row: u16) -> Bounds<Pixels> {
72        let width = self.cell.width * f32::from(columns.end.saturating_sub(columns.start));
73        Bounds::new(
74            self.origin
75                + point(
76                    self.cell.width * f32::from(columns.start),
77                    self.cell.height * f32::from(row),
78                ),
79            size(width, self.cell.height),
80        )
81    }
82
83    /// The top left of a row.
84    #[must_use]
85    pub fn row_origin(self, row: u16) -> Point<Pixels> {
86        self.origin + point(px(0.0), self.cell.height * f32::from(row))
87    }
88
89    /// The bounds of the whole grid.
90    #[must_use]
91    pub fn grid_bounds(self) -> Bounds<Pixels> {
92        Bounds::new(
93            self.origin,
94            size(
95                self.cell.width * f32::from(self.columns),
96                self.cell.height * f32::from(self.rows),
97            ),
98        )
99    }
100
101    /// The nearest cell boundary, for selection ends.
102    #[must_use]
103    pub fn selection_boundary_at(self, position: Point<Pixels>) -> Cell {
104        Cell::new(
105            axis_cell(
106                position.x - self.origin.x + self.cell.width * 0.5,
107                self.cell.width,
108                self.columns,
109            ),
110            self.cell_at(position).row,
111        )
112    }
113
114    /// The cell under a window position, clamped into the grid.
115    #[must_use]
116    pub fn cell_at(self, position: Point<Pixels>) -> Cell {
117        Cell::new(
118            axis_cell(
119                position.x - self.origin.x,
120                self.cell.width,
121                self.columns.saturating_sub(1),
122            ),
123            axis_cell(
124                position.y - self.origin.y,
125                self.cell.height,
126                self.rows.saturating_sub(1),
127            ),
128        )
129    }
130}
131
132fn whole_cells(available: Pixels, cell: Pixels) -> u16 {
133    if cell <= px(0.0) {
134        return 1;
135    }
136    let count = (available / cell).floor();
137    if !count.is_finite() || count < 1.0 {
138        return 1;
139    }
140    if count >= f32::from(MAX_GRID) {
141        return MAX_GRID;
142    }
143    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
144    {
145        count as u16
146    }
147}
148
149fn axis_cell(offset: Pixels, cell: Pixels, last: u16) -> u16 {
150    if cell <= px(0.0) {
151        return 0;
152    }
153    let index = (offset / cell).floor();
154    if !index.is_finite() || index < 0.0 {
155        return 0;
156    }
157    if index >= f32::from(last) {
158        return last;
159    }
160    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
161    {
162        index as u16
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    fn geometry(padding: Pixels) -> Geometry {
171        Geometry::new(
172            Bounds::new(point(px(10.0), px(20.0)), size(px(108.0), px(84.0))),
173            size(px(8.0), px(16.0)),
174            padding,
175            false,
176        )
177    }
178
179    #[test]
180    fn padding_moves_the_grid_and_reduces_the_usable_cells() {
181        let unpadded = geometry(px(0.0));
182        assert_eq!(unpadded.origin, point(px(10.0), px(20.0)));
183        assert_eq!((unpadded.columns, unpadded.rows), (13, 5));
184        let padded = geometry(px(6.0));
185        assert_eq!(padded.origin, point(px(16.0), px(26.0)));
186        assert_eq!((padded.columns, padded.rows), (12, 4));
187    }
188
189    #[test]
190    fn a_tiny_element_still_reports_a_usable_grid() {
191        let tiny = Geometry::new(Bounds::default(), size(px(8.0), px(16.0)), px(0.0), false);
192        assert_eq!((tiny.columns, tiny.rows), (1, 1));
193    }
194
195    #[test]
196    fn balancing_splits_the_spare_space_between_both_sides() {
197        let balanced = Geometry::new(
198            Bounds::new(point(px(0.0), px(0.0)), size(px(108.0), px(84.0))),
199            size(px(8.0), px(16.0)),
200            px(0.0),
201            true,
202        );
203        assert_eq!((balanced.columns, balanced.rows), (13, 5));
204        assert_eq!(balanced.origin, point(px(2.0), px(2.0)));
205    }
206
207    #[test]
208    fn positions_map_to_cells_and_clamp_at_the_edges() {
209        let geometry = geometry(px(0.0));
210        assert_eq!(geometry.cell_at(point(px(10.0), px(20.0))), Cell::new(0, 0));
211        assert_eq!(geometry.cell_at(point(px(18.0), px(36.0))), Cell::new(1, 1));
212        assert_eq!(geometry.cell_at(point(px(0.0), px(0.0))), Cell::new(0, 0));
213        assert_eq!(
214            geometry.cell_at(point(px(9999.0), px(9999.0))),
215            Cell::new(12, 4)
216        );
217        assert_eq!(
218            geometry.selection_boundary_at(point(px(13.9), px(20.0))),
219            Cell::new(0, 0)
220        );
221        assert_eq!(
222            geometry.selection_boundary_at(point(px(14.1), px(20.0))),
223            Cell::new(1, 0)
224        );
225        assert_eq!(
226            geometry.selection_boundary_at(point(px(900.0), px(20.0))),
227            Cell::new(13, 0)
228        );
229    }
230
231    #[test]
232    fn cell_and_span_bounds_line_up_with_the_grid() {
233        let geometry = geometry(px(0.0));
234        assert_eq!(
235            geometry.cell_bounds(2, 1),
236            Bounds::new(point(px(26.0), px(36.0)), size(px(8.0), px(16.0)))
237        );
238        assert_eq!(
239            geometry.span_bounds(2..5, 1),
240            Bounds::new(point(px(26.0), px(36.0)), size(px(24.0), px(16.0)))
241        );
242        assert_eq!(geometry.grid_bounds().size, size(px(104.0), px(80.0)));
243    }
244}