1use gpui::{Background, Bounds, Pixels, Point, Window, fill, point, px, size};
2
3use super::origin_point;
4
5#[derive(Default)]
7pub struct Grid {
8 x: Vec<Pixels>,
9 y: Vec<Pixels>,
10 stroke: Background,
11 dash_array: Option<Vec<Pixels>>,
12}
13
14impl Grid {
15 pub fn new() -> Self {
16 Self::default()
17 }
18
19 pub fn x(mut self, x: impl IntoIterator<Item = impl Into<Pixels>>) -> Self {
21 self.x = x.into_iter().map(Into::into).collect();
22 self
23 }
24
25 pub fn y(mut self, y: impl IntoIterator<Item = impl Into<Pixels>>) -> Self {
27 self.y = y.into_iter().map(Into::into).collect();
28 self
29 }
30
31 pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
33 self.stroke = stroke.into();
34 self
35 }
36
37 pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self {
39 self.dash_array = Some(dash_array.to_vec());
40 self
41 }
42
43 fn points(&self, bounds: &Bounds<Pixels>) -> Vec<(Point<Pixels>, Point<Pixels>)> {
44 let size = bounds.size;
45 let origin = bounds.origin;
46
47 let mut x = self
48 .x
49 .iter()
50 .map(|x| {
51 (
52 origin_point(*x, px(0.), origin),
53 origin_point(*x, size.height, origin),
54 )
55 })
56 .collect::<Vec<_>>();
57
58 let y = self
59 .y
60 .iter()
61 .map(|y| {
62 (
63 origin_point(px(0.), *y, origin),
64 origin_point(size.width, *y, origin),
65 )
66 })
67 .collect::<Vec<_>>();
68
69 x.extend(y);
70 x
71 }
72
73 pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
81 for (start, end) in self.points(bounds) {
82 for (start, end) in dash_segments(start, end, self.dash_array.as_deref()) {
83 window.paint_quad(fill(line_bounds(start, end), self.stroke));
84 }
85 }
86 }
87}
88
89fn line_bounds(start: Point<Pixels>, end: Point<Pixels>) -> Bounds<Pixels> {
92 let half = px(0.5);
93 if start.x == end.x {
94 let top = start.y.min(end.y);
95 Bounds::new(
96 point(start.x - half, top),
97 size(px(1.), start.y.max(end.y) - top),
98 )
99 } else {
100 let left = start.x.min(end.x);
101 Bounds::new(
102 point(left, start.y - half),
103 size(start.x.max(end.x) - left, px(1.)),
104 )
105 }
106}
107
108fn dash_segments(
113 start: Point<Pixels>,
114 end: Point<Pixels>,
115 dash_array: Option<&[Pixels]>,
116) -> Vec<(Point<Pixels>, Point<Pixels>)> {
117 let Some(dash_array) = dash_array.filter(|dashes| !dashes.is_empty()) else {
118 return vec![(start, end)];
119 };
120 let length = ((end.x - start.x).as_f32().powi(2) + (end.y - start.y).as_f32().powi(2)).sqrt();
121 if length <= 0. || dash_array.iter().all(|dash| dash.as_f32() <= 0.) {
122 return vec![(start, end)];
123 }
124 let at = |distance: f32| {
125 let t = distance / length;
126 point(
127 start.x + (end.x - start.x) * t,
128 start.y + (end.y - start.y) * t,
129 )
130 };
131 let pattern_len = if dash_array.len() % 2 == 1 {
132 dash_array.len() * 2
133 } else {
134 dash_array.len()
135 };
136 let mut segments = Vec::new();
137 let mut position = 0.;
138 let mut index = 0;
139 while position < length {
140 let dash = dash_array[index % dash_array.len()].as_f32().max(0.);
141 let next = (position + dash).min(length);
142 if index % 2 == 0 && next > position {
143 segments.push((at(position), at(next)));
144 }
145 position = next;
146 index = (index + 1) % pattern_len;
147 }
148 segments
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 fn xs(segments: &[(Point<Pixels>, Point<Pixels>)]) -> Vec<(f32, f32)> {
156 segments
157 .iter()
158 .map(|(start, end)| (start.x.as_f32(), end.x.as_f32()))
159 .collect()
160 }
161
162 #[test]
163 fn solid_line_is_one_segment() {
164 let segments = dash_segments(point(px(0.), px(5.)), point(px(10.), px(5.)), None);
165 assert_eq!(xs(&segments), vec![(0., 10.)]);
166 let segments = dash_segments(point(px(0.), px(5.)), point(px(10.), px(5.)), Some(&[]));
167 assert_eq!(xs(&segments), vec![(0., 10.)]);
168 }
169
170 #[test]
171 fn dashes_alternate_and_clip_at_the_end() {
172 let segments = dash_segments(
173 point(px(0.), px(5.)),
174 point(px(11.), px(5.)),
175 Some(&[px(4.), px(2.)]),
176 );
177 assert_eq!(xs(&segments), vec![(0., 4.), (6., 10.)]);
178 }
179
180 #[test]
181 fn odd_dash_array_repeats_like_svg() {
182 let segments = dash_segments(
184 point(px(0.), px(0.)),
185 point(px(0.), px(20.)),
186 Some(&[px(5.), px(3.), px(2.)]),
187 );
188 let ys: Vec<_> = segments
189 .iter()
190 .map(|(start, end)| (start.y.as_f32(), end.y.as_f32()))
191 .collect();
192 assert_eq!(ys, vec![(0., 5.), (8., 10.), (15., 18.)]);
193 }
194
195 #[test]
196 fn line_box_is_one_pixel_centred_on_the_coordinate() {
197 let vertical = line_bounds(point(px(10.), px(0.)), point(px(10.), px(40.)));
198 assert_eq!(vertical.origin, point(px(9.5), px(0.)));
199 assert_eq!(vertical.size, size(px(1.), px(40.)));
200 let horizontal = line_bounds(point(px(40.), px(7.)), point(px(0.), px(7.)));
201 assert_eq!(horizontal.origin, point(px(0.), px(6.5)));
202 assert_eq!(horizontal.size, size(px(40.), px(1.)));
203 }
204}