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