gpui_component/plot/shape/
line.rs1use 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 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 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 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 pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
78 self.stroke = stroke.into();
79 self
80 }
81
82 pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
84 self.stroke_width = stroke_width.into();
85 self
86 }
87
88 pub fn stroke_style(mut self, stroke_style: StrokeStyle) -> Self {
90 self.stroke_style = stroke_style;
91 self
92 }
93
94 pub fn dot(mut self) -> Self {
96 self.dot = true;
97 self
98 }
99
100 pub fn dot_size(mut self, dot_size: impl Into<Pixels>) -> Self {
102 self.dot_size = dot_size.into();
103 self
104 }
105
106 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 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 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 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 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}