gpui_component/plot/shape/
line.rs

1// @reference: https://d3js.org/d3-shape/line
2
3use gpui::{
4    px, quad, size, Background, BorderStyle, Bounds, Hsla, PaintQuad, Path, PathBuilder, Pixels,
5    Point, Window,
6};
7
8use crate::{
9    plot::{origin_point, StrokeStyle},
10    PixelsExt,
11};
12
13#[allow(clippy::type_complexity)]
14pub struct Line<T> {
15    data: Vec<T>,
16    x: Box<dyn Fn(&T) -> Option<f32>>,
17    y: Box<dyn Fn(&T) -> Option<f32>>,
18    stroke: Background,
19    stroke_width: Pixels,
20    stroke_style: StrokeStyle,
21    dot: bool,
22    dot_size: Pixels,
23    dot_fill_color: Hsla,
24    dot_stroke_color: Option<Hsla>,
25}
26
27impl<T> Default for Line<T> {
28    fn default() -> Self {
29        Self {
30            data: Vec::new(),
31            x: Box::new(|_| None),
32            y: Box::new(|_| None),
33            stroke: Default::default(),
34            stroke_width: px(1.),
35            stroke_style: Default::default(),
36            dot: false,
37            dot_size: px(4.),
38            dot_fill_color: gpui::transparent_black(),
39            dot_stroke_color: None,
40        }
41    }
42}
43
44impl<T> Line<T> {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Set the data of the Line.
50    pub fn data<I>(mut self, data: I) -> Self
51    where
52        I: IntoIterator<Item = T>,
53    {
54        self.data = data.into_iter().collect();
55        self
56    }
57
58    /// Set the x of the Line.
59    pub fn x<F>(mut self, x: F) -> Self
60    where
61        F: Fn(&T) -> Option<f32> + 'static,
62    {
63        self.x = Box::new(x);
64        self
65    }
66
67    /// Set the y of the Line.
68    pub fn y<F>(mut self, y: F) -> Self
69    where
70        F: Fn(&T) -> Option<f32> + 'static,
71    {
72        self.y = Box::new(y);
73        self
74    }
75
76    /// Set the stroke color of the Line.
77    pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
78        self.stroke = stroke.into();
79        self
80    }
81
82    /// Set the stroke width of the Line.
83    pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
84        self.stroke_width = stroke_width.into();
85        self
86    }
87
88    /// Set the stroke style of the Line.
89    pub fn stroke_style(mut self, stroke_style: StrokeStyle) -> Self {
90        self.stroke_style = stroke_style;
91        self
92    }
93
94    /// Show dots on the Line.
95    pub fn dot(mut self) -> Self {
96        self.dot = true;
97        self
98    }
99
100    /// Set the size of the dots on the Line.
101    pub fn dot_size(mut self, dot_size: impl Into<Pixels>) -> Self {
102        self.dot_size = dot_size.into();
103        self
104    }
105
106    /// Set the fill color of the dots on the Line.
107    pub fn dot_fill_color(mut self, dot_fill_color: impl Into<Hsla>) -> Self {
108        self.dot_fill_color = dot_fill_color.into();
109        self
110    }
111
112    /// Set the stroke color of the dots on the Line.
113    pub fn dot_stroke_color(mut self, dot_stroke_color: impl Into<Hsla>) -> Self {
114        self.dot_stroke_color = Some(dot_stroke_color.into());
115        self
116    }
117
118    /// Paint the dots on the Line.
119    fn paint_dot(&self, dot: Point<Pixels>) -> PaintQuad {
120        quad(
121            gpui::bounds(dot, size(self.dot_size, self.dot_size)),
122            self.dot_size / 2.,
123            self.dot_fill_color,
124            px(1.),
125            self.dot_stroke_color.unwrap_or(self.dot_fill_color),
126            BorderStyle::default(),
127        )
128    }
129
130    fn path(&self, bounds: &Bounds<Pixels>) -> (Option<Path<Pixels>>, Vec<PaintQuad>) {
131        let origin = bounds.origin;
132        let mut builder = PathBuilder::stroke(self.stroke_width);
133        let mut dots = vec![];
134        let mut paint_dots = vec![];
135
136        for v in self.data.iter() {
137            let x_tick = (self.x)(v);
138            let y_tick = (self.y)(v);
139
140            if let (Some(x), Some(y)) = (x_tick, y_tick) {
141                let pos = origin_point(px(x), px(y), origin);
142
143                if self.dot {
144                    let dot_radius = self.dot_size.as_f32() / 2.;
145                    let dot_pos = origin_point(px(x - dot_radius), px(y - dot_radius), origin);
146                    paint_dots.push(self.paint_dot(dot_pos));
147                }
148
149                dots.push(pos);
150            }
151        }
152
153        if dots.is_empty() {
154            return (None, paint_dots);
155        }
156
157        if dots.len() == 1 {
158            builder.move_to(dots[0]);
159            return (builder.build().ok(), paint_dots);
160        }
161
162        match self.stroke_style {
163            StrokeStyle::Natural => {
164                builder.move_to(dots[0]);
165                let n = dots.len();
166                for i in 0..n - 1 {
167                    let p0 = if i == 0 { dots[0] } else { dots[i - 1] };
168                    let p1 = dots[i];
169                    let p2 = dots[i + 1];
170                    let p3 = if i + 2 < n { dots[i + 2] } else { dots[n - 1] };
171
172                    // Catmull-Rom to Bezier
173                    let c1 = Point::new(p1.x + (p2.x - p0.x) / 6.0, p1.y + (p2.y - p0.y) / 6.0);
174                    let c2 = Point::new(p2.x - (p3.x - p1.x) / 6.0, p2.y - (p3.y - p1.y) / 6.0);
175
176                    builder.cubic_bezier_to(p2, c1, c2);
177                }
178            }
179            StrokeStyle::Linear => {
180                builder.move_to(dots[0]);
181                for p in &dots[1..] {
182                    builder.line_to(*p);
183                }
184            }
185        }
186
187        (builder.build().ok(), paint_dots)
188    }
189
190    /// Paint the Line.
191    pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
192        let (path, dots) = self.path(bounds);
193        if let Some(path) = path {
194            window.paint_path(path, self.stroke);
195        }
196        for dot in dots {
197            window.paint_quad(dot);
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    use gpui::{point, px, Bounds};
207
208    #[test]
209    fn test_line_path() {
210        let data = vec![1., 2., 3.];
211        let line = Line::new()
212            .data(data.clone())
213            .x(|v| Some(*v))
214            .y(|v| Some(*v * 2.));
215
216        let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
217        let (path, dots) = line.path(&bounds);
218
219        assert!(path.is_some());
220        assert!(dots.is_empty());
221
222        let line_with_dots = Line::new()
223            .data(data)
224            .x(|v| Some(*v))
225            .y(|v| Some(*v * 2.))
226            .dot();
227
228        let (_, dots) = line_with_dots.path(&bounds);
229        assert_eq!(dots.len(), 3);
230    }
231}