1use gpui_kit::{Bounds, Pixels, Point, Size, point, px, size};
11
12use crate::terminal::frame::MAX_GRID;
13use crate::terminal::selection::Cell;
14
15#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Geometry {
18 pub bounds: Bounds<Pixels>,
20 pub origin: Point<Pixels>,
22 pub cell: Size<Pixels>,
24 pub columns: u16,
26 pub rows: u16,
28}
29
30impl Geometry {
31 #[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 #[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 #[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 #[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 #[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 #[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 #[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}