Skip to main content

gpui_base/plot/shape/
line.rs

1// @reference: https://d3js.org/d3-shape/line
2
3use gpui::{
4    Background, BorderStyle, Bounds, Hsla, PaintQuad, Path, PathBuilder, Pixels, Point, Window, px,
5    quad, size,
6};
7
8use crate::plot::{Curve, PathCache, ShapeKey, origin_point};
9
10#[allow(clippy::type_complexity)]
11pub struct Line<T> {
12    data: Vec<T>,
13    x: Box<dyn Fn(&T) -> Option<f32>>,
14    y: Box<dyn Fn(&T) -> Option<f32>>,
15    stroke: Background,
16    stroke_width: Pixels,
17    curve: Curve,
18    dot: bool,
19    dot_size: Pixels,
20    dot_fill: Background,
21    dot_stroke: Option<Hsla>,
22}
23
24impl<T> Default for Line<T> {
25    fn default() -> Self {
26        Self {
27            data: Vec::new(),
28            x: Box::new(|_| None),
29            y: Box::new(|_| None),
30            stroke: Default::default(),
31            stroke_width: px(1.),
32            curve: Curve::default(),
33            dot: false,
34            dot_size: px(4.),
35            dot_fill: gpui::transparent_black().into(),
36            dot_stroke: None,
37        }
38    }
39}
40
41impl<T> Line<T> {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Set the data of the Line.
47    pub fn data<I>(mut self, data: I) -> Self
48    where
49        I: IntoIterator<Item = T>,
50    {
51        self.data = data.into_iter().collect();
52        self
53    }
54
55    /// Set the x of the Line.
56    pub fn x<F>(mut self, x: F) -> Self
57    where
58        F: Fn(&T) -> Option<f32> + 'static,
59    {
60        self.x = Box::new(x);
61        self
62    }
63
64    /// Set the y of the Line.
65    pub fn y<F>(mut self, y: F) -> Self
66    where
67        F: Fn(&T) -> Option<f32> + 'static,
68    {
69        self.y = Box::new(y);
70        self
71    }
72
73    /// Set the stroke color of the Line.
74    pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
75        self.stroke = stroke.into();
76        self
77    }
78
79    /// Set the stroke width of the Line.
80    pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
81        self.stroke_width = stroke_width.into();
82        self
83    }
84
85    /// Set how the Line connects its points. Defaults to [`Curve::Natural`].
86    pub fn curve(mut self, curve: Curve) -> Self {
87        self.curve = curve;
88        self
89    }
90
91    /// Draw a dot on every point.
92    pub fn dot(mut self) -> Self {
93        self.dot = true;
94        self
95    }
96
97    /// Set the size of the dots on the Line.
98    pub fn dot_size(mut self, dot_size: impl Into<Pixels>) -> Self {
99        self.dot_size = dot_size.into();
100        self
101    }
102
103    /// Set the fill of the dots on the Line.
104    pub fn dot_fill(mut self, fill: impl Into<Background>) -> Self {
105        self.dot_fill = fill.into();
106        self
107    }
108
109    /// Set the 1px border color of the dots on the Line. Defaults to the dot
110    /// fill when it is a solid color.
111    pub fn dot_stroke(mut self, stroke: impl Into<Hsla>) -> Self {
112        self.dot_stroke = Some(stroke.into());
113        self
114    }
115
116    #[deprecated(since = "0.7.0", note = "use `dot_fill`")]
117    pub fn dot_fill_color(self, color: impl Into<Hsla>) -> Self {
118        self.dot_fill(color.into())
119    }
120
121    #[deprecated(since = "0.7.0", note = "use `dot_stroke`")]
122    pub fn dot_stroke_color(self, color: impl Into<Hsla>) -> Self {
123        self.dot_stroke(color)
124    }
125
126    /// Paint the dots on the Line.
127    fn paint_dot(&self, dot: Point<Pixels>) -> PaintQuad {
128        quad(
129            gpui::bounds(dot, size(self.dot_size, self.dot_size)),
130            self.dot_size / 2.,
131            self.dot_fill,
132            px(1.),
133            self.dot_stroke
134                .or_else(|| self.dot_fill.as_solid())
135                .unwrap_or_default(),
136            BorderStyle::default(),
137        )
138    }
139
140    /// The projected points relative to `origin`, and the dot quads to paint.
141    fn dots(&self, origin: Point<Pixels>) -> (Vec<Point<Pixels>>, Vec<PaintQuad>) {
142        let mut dots = vec![];
143        let mut paint_dots = vec![];
144
145        for v in self.data.iter() {
146            let x_tick = (self.x)(v);
147            let y_tick = (self.y)(v);
148
149            if let (Some(x), Some(y)) = (x_tick, y_tick) {
150                let pos = origin_point(px(x), px(y), origin);
151
152                if self.dot {
153                    let dot_radius = self.dot_size.as_f32() / 2.;
154                    let dot_pos = origin_point(px(x - dot_radius), px(y - dot_radius), origin);
155                    paint_dots.push(self.paint_dot(dot_pos));
156                }
157
158                dots.push(pos);
159            }
160        }
161
162        (dots, paint_dots)
163    }
164
165    /// The stroke through `dots`.
166    fn build_path(&self, dots: &[Point<Pixels>]) -> Option<Path<Pixels>> {
167        let mut builder = PathBuilder::stroke(self.stroke_width);
168
169        if dots.is_empty() {
170            return None;
171        }
172
173        if dots.len() == 1 {
174            builder.move_to(dots[0]);
175            return builder.build().ok();
176        }
177
178        match self.curve {
179            Curve::Natural => {
180                builder.move_to(dots[0]);
181                let n = dots.len();
182                for i in 0..n - 1 {
183                    let p0 = if i == 0 { dots[0] } else { dots[i - 1] };
184                    let p1 = dots[i];
185                    let p2 = dots[i + 1];
186                    let p3 = if i + 2 < n { dots[i + 2] } else { dots[n - 1] };
187
188                    // Catmull-Rom to Bezier
189                    let c1 = Point::new(p1.x + (p2.x - p0.x) / 6.0, p1.y + (p2.y - p0.y) / 6.0);
190                    let c2 = Point::new(p2.x - (p3.x - p1.x) / 6.0, p2.y - (p3.y - p1.y) / 6.0);
191
192                    builder.cubic_bezier_to(p2, c1, c2);
193                }
194            }
195            Curve::Linear => {
196                builder.move_to(dots[0]);
197                for p in &dots[1..] {
198                    builder.line_to(*p);
199                }
200            }
201            Curve::StepAfter => {
202                builder.move_to(dots[0]);
203                for (i, p) in dots.windows(2).enumerate() {
204                    builder.line_to(Point::new(p[1].x, p[0].y));
205                    // Don't draw the vertical line for the last point
206                    if i < dots.len() - 2 {
207                        builder.line_to(p[1]);
208                    }
209                }
210            }
211        }
212
213        builder.build().ok()
214    }
215
216    fn path(&self, bounds: &Bounds<Pixels>) -> (Option<Path<Pixels>>, Vec<PaintQuad>) {
217        let (dots, paint_dots) = self.dots(bounds.origin);
218        (self.build_path(&dots), paint_dots)
219    }
220
221    /// Paint the Line, reusing the stroke tessellated by an earlier paint
222    /// while the projected points, stroke width and curve style are unchanged.
223    ///
224    /// Use this from a [`Plot`](crate::plot::Plot) that keeps a
225    /// [`PathCache`] per line: the plot repaints on every frame it is on
226    /// screen, and tessellating the stroke is most of what a line costs.
227    pub fn paint_cached(
228        &self,
229        bounds: &Bounds<Pixels>,
230        cache: &mut PathCache,
231        window: &mut Window,
232    ) {
233        let (dots, paint_dots) = self.dots(Point::default());
234        let mut key = ShapeKey::new((self.curve, self.stroke_width.as_f32().to_bits()));
235        for dot in &dots {
236            key.point(*dot);
237        }
238        if let Some(path) = cache.get(key.finish(), bounds.origin, || self.build_path(&dots)) {
239            window.paint_path(path, self.stroke);
240        }
241        // Dots are quads: cheap, and positioned at this frame's origin.
242        for dot in paint_dots {
243            let mut dot = dot;
244            dot.bounds.origin = dot.bounds.origin + bounds.origin;
245            window.paint_quad(dot);
246        }
247    }
248
249    /// Paint the Line.
250    pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
251        let (path, dots) = self.path(bounds);
252        if let Some(path) = path {
253            window.paint_path(path, self.stroke);
254        }
255        for dot in dots {
256            window.paint_quad(dot);
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    use gpui::{Bounds, point, px};
266
267    #[test]
268    fn test_line_path() {
269        let data = vec![1., 2., 3.];
270        let line = Line::new()
271            .data(data.clone())
272            .x(|v| Some(*v))
273            .y(|v| Some(*v * 2.));
274
275        let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
276        let (path, dots) = line.path(&bounds);
277
278        assert!(path.is_some());
279        assert!(dots.is_empty());
280
281        let line_with_dots = Line::new()
282            .data(data)
283            .x(|v| Some(*v))
284            .y(|v| Some(*v * 2.))
285            .dot();
286
287        let (_, dots) = line_with_dots.path(&bounds);
288        assert_eq!(dots.len(), 3);
289    }
290}