Skip to main content

gpui_component/plot/
grid.rs

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    /// Set the x of the Grid.
24    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    /// Set the y of the Grid.
30    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    /// Set the stroke color of the Grid.
36    pub fn stroke(mut self, stroke: impl Into<Hsla>) -> Self {
37        self.stroke = stroke.into();
38        self
39    }
40
41    /// Set the dash array of the Grid.
42    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    /// Paint the Grid.
78    ///
79    /// Grid lines are axis-aligned, so each one (or each dash of one) is a
80    /// 1px quad rather than a stroked path: a chart repaints its grid on
81    /// every frame while it scrolls, and tessellating each line — measuring
82    /// and sampling it first when dashed — was the largest cost of painting a
83    /// chart card.
84    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
93/// The box a 1px stroke of the axis-aligned line `start`–`end` covers:
94/// centred on the coordinate, as `PathBuilder::stroke(px(1.))` draws it.
95fn 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
112/// Splits the line `start`–`end` into the dashes of `dash_array`, walked from
113/// `start` with the SVG `stroke-dasharray` rules `PathBuilder` follows: values
114/// alternate dash and gap, and an odd-length array repeats to an even one.
115/// Without a dash array the whole line is one segment.
116fn 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        // 5,3,2 is 5 on, 3 off, 2 on, 5 off, 3 on, 2 off.
187        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}