Skip to main content

gpui_base/plot/
grid.rs

1use gpui::{Background, Bounds, Pixels, Point, Window, fill, point, px, size};
2
3use super::origin_point;
4
5/// Axis-aligned grid lines across a plot, at the given x and y positions.
6#[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    /// Set the positions of the vertical lines, from the left of the plot.
20    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    /// Set the positions of the horizontal lines, from the top of the plot.
26    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    /// Set the stroke of the grid lines.
32    pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
33        self.stroke = stroke.into();
34        self
35    }
36
37    /// Set the dash array of the grid lines.
38    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    /// Paint the Grid.
74    ///
75    /// Grid lines are axis-aligned, so each one (or each dash of one) is a
76    /// 1px quad rather than a stroked path: a chart repaints its grid on
77    /// every frame while it scrolls, and tessellating each line — measuring
78    /// and sampling it first when dashed — was the largest cost of painting a
79    /// chart card.
80    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
89/// The box a 1px stroke of the axis-aligned line `start`–`end` covers:
90/// centred on the coordinate, as `PathBuilder::stroke(px(1.))` draws it.
91fn 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
108/// Splits the line `start`–`end` into the dashes of `dash_array`, walked from
109/// `start` with the SVG `stroke-dasharray` rules `PathBuilder` follows: values
110/// alternate dash and gap, and an odd-length array repeats to an even one.
111/// Without a dash array the whole line is one segment.
112fn 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        // 5,3,2 is 5 on, 3 off, 2 on, 5 off, 3 on, 2 off.
183        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}