Skip to main content

gpui_component/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::{PathCache, ShapeKey, StrokeStyle, 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    stroke_style: StrokeStyle,
18    dot: bool,
19    dot_size: Pixels,
20    dot_fill_color: Hsla,
21    dot_stroke_color: 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            stroke_style: Default::default(),
33            dot: false,
34            dot_size: px(4.),
35            dot_fill_color: gpui::transparent_black(),
36            dot_stroke_color: 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 the stroke style of the Line.
86    pub fn stroke_style(mut self, stroke_style: StrokeStyle) -> Self {
87        self.stroke_style = stroke_style;
88        self
89    }
90
91    /// Show dots on the Line.
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 color of the dots on the Line.
104    pub fn dot_fill_color(mut self, dot_fill_color: impl Into<Hsla>) -> Self {
105        self.dot_fill_color = dot_fill_color.into();
106        self
107    }
108
109    /// Set the stroke color of the dots on the Line.
110    pub fn dot_stroke_color(mut self, dot_stroke_color: impl Into<Hsla>) -> Self {
111        self.dot_stroke_color = Some(dot_stroke_color.into());
112        self
113    }
114
115    /// Paint the dots on the Line.
116    fn paint_dot(&self, dot: Point<Pixels>) -> PaintQuad {
117        quad(
118            gpui::bounds(dot, size(self.dot_size, self.dot_size)),
119            self.dot_size / 2.,
120            self.dot_fill_color,
121            px(1.),
122            self.dot_stroke_color.unwrap_or(self.dot_fill_color),
123            BorderStyle::default(),
124        )
125    }
126
127    /// The projected points relative to `origin`, and the dot quads to paint.
128    fn dots(&self, origin: Point<Pixels>) -> (Vec<Point<Pixels>>, Vec<PaintQuad>) {
129        let mut dots = vec![];
130        let mut paint_dots = vec![];
131
132        for v in self.data.iter() {
133            let x_tick = (self.x)(v);
134            let y_tick = (self.y)(v);
135
136            if let (Some(x), Some(y)) = (x_tick, y_tick) {
137                let pos = origin_point(px(x), px(y), origin);
138
139                if self.dot {
140                    let dot_radius = self.dot_size.as_f32() / 2.;
141                    let dot_pos = origin_point(px(x - dot_radius), px(y - dot_radius), origin);
142                    paint_dots.push(self.paint_dot(dot_pos));
143                }
144
145                dots.push(pos);
146            }
147        }
148
149        (dots, paint_dots)
150    }
151
152    /// The stroke through `dots`.
153    fn build_path(&self, dots: &[Point<Pixels>]) -> Option<Path<Pixels>> {
154        let mut builder = PathBuilder::stroke(self.stroke_width);
155
156        if dots.is_empty() {
157            return None;
158        }
159
160        if dots.len() == 1 {
161            builder.move_to(dots[0]);
162            return builder.build().ok();
163        }
164
165        match self.stroke_style {
166            StrokeStyle::Natural => {
167                builder.move_to(dots[0]);
168                let n = dots.len();
169                for i in 0..n - 1 {
170                    let p0 = if i == 0 { dots[0] } else { dots[i - 1] };
171                    let p1 = dots[i];
172                    let p2 = dots[i + 1];
173                    let p3 = if i + 2 < n { dots[i + 2] } else { dots[n - 1] };
174
175                    // Catmull-Rom to Bezier
176                    let c1 = Point::new(p1.x + (p2.x - p0.x) / 6.0, p1.y + (p2.y - p0.y) / 6.0);
177                    let c2 = Point::new(p2.x - (p3.x - p1.x) / 6.0, p2.y - (p3.y - p1.y) / 6.0);
178
179                    builder.cubic_bezier_to(p2, c1, c2);
180                }
181            }
182            StrokeStyle::Linear => {
183                builder.move_to(dots[0]);
184                for p in &dots[1..] {
185                    builder.line_to(*p);
186                }
187            }
188            StrokeStyle::StepAfter => {
189                builder.move_to(dots[0]);
190                for (i, p) in dots.windows(2).enumerate() {
191                    builder.line_to(Point::new(p[1].x, p[0].y));
192                    // Don't draw the vertical line for the last point
193                    if i < dots.len() - 2 {
194                        builder.line_to(p[1]);
195                    }
196                }
197            }
198        }
199
200        builder.build().ok()
201    }
202
203    fn path(&self, bounds: &Bounds<Pixels>) -> (Option<Path<Pixels>>, Vec<PaintQuad>) {
204        let (dots, paint_dots) = self.dots(bounds.origin);
205        (self.build_path(&dots), paint_dots)
206    }
207
208    /// Paint the Line, reusing the stroke tessellated by an earlier paint
209    /// while the projected points, stroke width and curve style are unchanged.
210    ///
211    /// Use this from a [`Plot`](crate::plot::Plot) that keeps a
212    /// [`PathCache`] per line: the plot repaints on every frame it is on
213    /// screen, and tessellating the stroke is most of what a line costs.
214    pub fn paint_cached(
215        &self,
216        bounds: &Bounds<Pixels>,
217        cache: &mut PathCache,
218        window: &mut Window,
219    ) {
220        let (dots, paint_dots) = self.dots(Point::default());
221        let mut key = ShapeKey::new((self.stroke_style, self.stroke_width.as_f32().to_bits()));
222        for dot in &dots {
223            key.point(*dot);
224        }
225        if let Some(path) = cache.get(key.finish(), bounds.origin, || self.build_path(&dots)) {
226            window.paint_path(path, self.stroke);
227        }
228        // Dots are quads: cheap, and positioned at this frame's origin.
229        for dot in paint_dots {
230            let mut dot = dot;
231            dot.bounds.origin = dot.bounds.origin + bounds.origin;
232            window.paint_quad(dot);
233        }
234    }
235
236    /// Paint the Line.
237    pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
238        let (path, dots) = self.path(bounds);
239        if let Some(path) = path {
240            window.paint_path(path, self.stroke);
241        }
242        for dot in dots {
243            window.paint_quad(dot);
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    use gpui::{Bounds, point, px};
253
254    #[test]
255    fn test_line_path() {
256        let data = vec![1., 2., 3.];
257        let line = Line::new()
258            .data(data.clone())
259            .x(|v| Some(*v))
260            .y(|v| Some(*v * 2.));
261
262        let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
263        let (path, dots) = line.path(&bounds);
264
265        assert!(path.is_some());
266        assert!(dots.is_empty());
267
268        let line_with_dots = Line::new()
269            .data(data)
270            .x(|v| Some(*v))
271            .y(|v| Some(*v * 2.))
272            .dot();
273
274        let (_, dots) = line_with_dots.path(&bounds);
275        assert_eq!(dots.len(), 3);
276    }
277}