gpui_component/plot/shape/
radial_line.rs1use std::f32::consts::PI;
4
5use gpui::{
6 Background, BorderStyle, Bounds, Hsla, PaintQuad, Path, PathBuilder, Pixels, Point, Window,
7 point, px, quad, size,
8};
9
10const HALF_PI: f32 = PI / 2.;
11
12#[allow(clippy::type_complexity)]
27pub struct RadialLine<T> {
28 data: Vec<T>,
29 angle: Box<dyn Fn(&T, usize) -> Option<f32>>,
30 radius: Box<dyn Fn(&T, usize) -> Option<f32>>,
31 closed: bool,
32 fill: Option<Background>,
33 stroke: Background,
34 stroke_width: Pixels,
35 dot: bool,
36 dot_size: Pixels,
37 dot_fill_color: Hsla,
38 dot_stroke_color: Option<Hsla>,
39}
40
41impl<T> Default for RadialLine<T> {
42 fn default() -> Self {
43 Self {
44 data: Vec::new(),
45 angle: Box::new(|_, _| None),
46 radius: Box::new(|_, _| None),
47 closed: false,
48 fill: None,
49 stroke: Default::default(),
50 stroke_width: px(1.),
51 dot: false,
52 dot_size: px(4.),
53 dot_fill_color: gpui::transparent_black(),
54 dot_stroke_color: None,
55 }
56 }
57}
58
59impl<T> RadialLine<T> {
60 pub fn new() -> Self {
61 Self::default()
62 }
63
64 pub fn data<I>(mut self, data: I) -> Self
66 where
67 I: IntoIterator<Item = T>,
68 {
69 self.data = data.into_iter().collect();
70 self
71 }
72
73 pub fn angle<F>(mut self, angle: F) -> Self
79 where
80 F: Fn(&T, usize) -> Option<f32> + 'static,
81 {
82 self.angle = Box::new(angle);
83 self
84 }
85
86 pub fn radius<F>(mut self, radius: F) -> Self
91 where
92 F: Fn(&T, usize) -> Option<f32> + 'static,
93 {
94 self.radius = Box::new(radius);
95 self
96 }
97
98 pub fn closed(mut self) -> Self {
100 self.closed = true;
101 self
102 }
103
104 pub fn fill(mut self, fill: impl Into<Background>) -> Self {
108 self.fill = Some(fill.into());
109 self
110 }
111
112 pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
114 self.stroke = stroke.into();
115 self
116 }
117
118 pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
120 self.stroke_width = stroke_width.into();
121 self
122 }
123
124 pub fn dot(mut self) -> Self {
126 self.dot = true;
127 self
128 }
129
130 pub fn dot_size(mut self, dot_size: impl Into<Pixels>) -> Self {
132 self.dot_size = dot_size.into();
133 self
134 }
135
136 pub fn dot_fill_color(mut self, dot_fill_color: impl Into<Hsla>) -> Self {
138 self.dot_fill_color = dot_fill_color.into();
139 self
140 }
141
142 pub fn dot_stroke_color(mut self, dot_stroke_color: impl Into<Hsla>) -> Self {
144 self.dot_stroke_color = Some(dot_stroke_color.into());
145 self
146 }
147
148 fn paint_dot(&self, dot: Point<Pixels>) -> PaintQuad {
150 quad(
151 gpui::bounds(dot, size(self.dot_size, self.dot_size)),
152 self.dot_size / 2.,
153 self.dot_fill_color,
154 px(1.),
155 self.dot_stroke_color.unwrap_or(self.dot_fill_color),
156 BorderStyle::default(),
157 )
158 }
159
160 fn points(&self, bounds: &Bounds<Pixels>) -> Vec<Point<Pixels>> {
162 let center_x = bounds.origin.x.as_f32() + bounds.size.width.as_f32() / 2.;
163 let center_y = bounds.origin.y.as_f32() + bounds.size.height.as_f32() / 2.;
164
165 self.data
166 .iter()
167 .enumerate()
168 .filter_map(|(i, v)| {
169 let angle = (self.angle)(v, i)? - HALF_PI;
170 let radius = (self.radius)(v, i)?;
171
172 Some(point(
173 px(center_x + radius * angle.cos()),
174 px(center_y + radius * angle.sin()),
175 ))
176 })
177 .collect()
178 }
179
180 fn path(
181 &self,
182 bounds: &Bounds<Pixels>,
183 ) -> (Option<Path<Pixels>>, Option<Path<Pixels>>, Vec<PaintQuad>) {
184 let points = self.points(bounds);
185 let mut paint_dots = vec![];
186
187 if self.dot {
188 let dot_radius = self.dot_size / 2.;
189 for p in &points {
190 paint_dots.push(self.paint_dot(point(p.x - dot_radius, p.y - dot_radius)));
191 }
192 }
193
194 if points.is_empty() {
195 return (None, None, paint_dots);
196 }
197
198 let fill_path = self.fill.and_then(|_| {
199 if points.len() < 3 {
200 return None;
201 }
202
203 let mut builder = PathBuilder::fill();
204 builder.add_polygon(&points, true);
205 builder.build().ok()
206 });
207
208 let mut builder = PathBuilder::stroke(self.stroke_width);
209 builder.move_to(points[0]);
210 for p in &points[1..] {
211 builder.line_to(*p);
212 }
213 if self.closed && points.len() > 2 {
214 builder.close();
215 }
216
217 (fill_path, builder.build().ok(), paint_dots)
218 }
219
220 pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
222 let (fill_path, stroke_path, dots) = self.path(bounds);
223
224 if let (Some(path), Some(fill)) = (fill_path, self.fill) {
225 window.paint_path(path, fill);
226 }
227 if let Some(path) = stroke_path {
228 window.paint_path(path, self.stroke);
229 }
230 for dot in dots {
231 window.paint_quad(dot);
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use std::f32::consts::TAU;
239
240 use super::*;
241
242 use gpui::{Bounds, point, px};
243
244 #[test]
245 fn test_radial_line_points() {
246 let data = vec![1., 1., 1., 1.];
247 let line = RadialLine::new()
248 .data(data)
249 .angle(|_, i| Some(i as f32 * TAU / 4.))
250 .radius(|v, _| Some(*v * 10.));
251
252 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
253 let points = line.points(&bounds);
254
255 assert_eq!(points.len(), 4);
257 let expected = [(50., 40.), (60., 50.), (50., 60.), (40., 50.)];
258 for (p, (x, y)) in points.iter().zip(expected) {
259 assert!((p.x.as_f32() - x).abs() < 1e-4);
260 assert!((p.y.as_f32() - y).abs() < 1e-4);
261 }
262 }
263
264 #[test]
265 fn test_radial_line_path() {
266 let data = vec![1., 2., 3.];
267 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
268
269 let line = RadialLine::new()
270 .data(data.clone())
271 .angle(|_, i| Some(i as f32 * TAU / 3.))
272 .radius(|v, _| Some(*v * 10.));
273
274 let (fill_path, stroke_path, dots) = line.path(&bounds);
275 assert!(fill_path.is_none());
276 assert!(stroke_path.is_some());
277 assert!(dots.is_empty());
278
279 let line = RadialLine::new()
280 .data(data)
281 .angle(|_, i| Some(i as f32 * TAU / 3.))
282 .radius(|v, _| Some(*v * 10.))
283 .closed()
284 .fill(gpui::black())
285 .dot();
286
287 let (fill_path, stroke_path, dots) = line.path(&bounds);
288 assert!(fill_path.is_some());
289 assert!(stroke_path.is_some());
290 assert_eq!(dots.len(), 3);
291 }
292}