1use 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: Background,
38 dot_stroke: 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: gpui::transparent_black().into(),
54 dot_stroke: 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(mut self, fill: impl Into<Background>) -> Self {
138 self.dot_fill = fill.into();
139 self
140 }
141
142 pub fn dot_stroke(mut self, stroke: impl Into<Hsla>) -> Self {
145 self.dot_stroke = Some(stroke.into());
146 self
147 }
148
149 #[deprecated(since = "0.7.0", note = "use `dot_fill`")]
150 pub fn dot_fill_color(self, color: impl Into<Hsla>) -> Self {
151 self.dot_fill(color.into())
152 }
153
154 #[deprecated(since = "0.7.0", note = "use `dot_stroke`")]
155 pub fn dot_stroke_color(self, color: impl Into<Hsla>) -> Self {
156 self.dot_stroke(color)
157 }
158
159 fn paint_dot(&self, dot: Point<Pixels>) -> PaintQuad {
161 quad(
162 gpui::bounds(dot, size(self.dot_size, self.dot_size)),
163 self.dot_size / 2.,
164 self.dot_fill,
165 px(1.),
166 self.dot_stroke
167 .or_else(|| self.dot_fill.as_solid())
168 .unwrap_or_default(),
169 BorderStyle::default(),
170 )
171 }
172
173 fn points(&self, bounds: &Bounds<Pixels>) -> Vec<Point<Pixels>> {
175 let center_x = bounds.origin.x.as_f32() + bounds.size.width.as_f32() / 2.;
176 let center_y = bounds.origin.y.as_f32() + bounds.size.height.as_f32() / 2.;
177
178 self.data
179 .iter()
180 .enumerate()
181 .filter_map(|(i, v)| {
182 let angle = (self.angle)(v, i)? - HALF_PI;
183 let radius = (self.radius)(v, i)?;
184
185 Some(point(
186 px(center_x + radius * angle.cos()),
187 px(center_y + radius * angle.sin()),
188 ))
189 })
190 .collect()
191 }
192
193 fn path(
194 &self,
195 bounds: &Bounds<Pixels>,
196 ) -> (Option<Path<Pixels>>, Option<Path<Pixels>>, Vec<PaintQuad>) {
197 let points = self.points(bounds);
198 let mut paint_dots = vec![];
199
200 if self.dot {
201 let dot_radius = self.dot_size / 2.;
202 for p in &points {
203 paint_dots.push(self.paint_dot(point(p.x - dot_radius, p.y - dot_radius)));
204 }
205 }
206
207 if points.is_empty() {
208 return (None, None, paint_dots);
209 }
210
211 let fill_path = self.fill.and_then(|_| {
212 if points.len() < 3 {
213 return None;
214 }
215
216 let mut builder = PathBuilder::fill();
217 builder.add_polygon(&points, true);
218 builder.build().ok()
219 });
220
221 let mut builder = PathBuilder::stroke(self.stroke_width);
222 builder.move_to(points[0]);
223 for p in &points[1..] {
224 builder.line_to(*p);
225 }
226 if self.closed && points.len() > 2 {
227 builder.close();
228 }
229
230 (fill_path, builder.build().ok(), paint_dots)
231 }
232
233 pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
235 let (fill_path, stroke_path, dots) = self.path(bounds);
236
237 if let (Some(path), Some(fill)) = (fill_path, self.fill) {
238 window.paint_path(path, fill);
239 }
240 if let Some(path) = stroke_path {
241 window.paint_path(path, self.stroke);
242 }
243 for dot in dots {
244 window.paint_quad(dot);
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use std::f32::consts::TAU;
252
253 use super::*;
254
255 use gpui::{Bounds, point, px};
256
257 #[test]
258 fn test_radial_line_points() {
259 let data = vec![1., 1., 1., 1.];
260 let line = RadialLine::new()
261 .data(data)
262 .angle(|_, i| Some(i as f32 * TAU / 4.))
263 .radius(|v, _| Some(*v * 10.));
264
265 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
266 let points = line.points(&bounds);
267
268 assert_eq!(points.len(), 4);
270 let expected = [(50., 40.), (60., 50.), (50., 60.), (40., 50.)];
271 for (p, (x, y)) in points.iter().zip(expected) {
272 assert!((p.x.as_f32() - x).abs() < 1e-4);
273 assert!((p.y.as_f32() - y).abs() < 1e-4);
274 }
275 }
276
277 #[test]
278 fn test_radial_line_path() {
279 let data = vec![1., 2., 3.];
280 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
281
282 let line = RadialLine::new()
283 .data(data.clone())
284 .angle(|_, i| Some(i as f32 * TAU / 3.))
285 .radius(|v, _| Some(*v * 10.));
286
287 let (fill_path, stroke_path, dots) = line.path(&bounds);
288 assert!(fill_path.is_none());
289 assert!(stroke_path.is_some());
290 assert!(dots.is_empty());
291
292 let line = RadialLine::new()
293 .data(data)
294 .angle(|_, i| Some(i as f32 * TAU / 3.))
295 .radius(|v, _| Some(*v * 10.))
296 .closed()
297 .fill(gpui::black())
298 .dot();
299
300 let (fill_path, stroke_path, dots) = line.path(&bounds);
301 assert!(fill_path.is_some());
302 assert!(stroke_path.is_some());
303 assert_eq!(dots.len(), 3);
304 }
305}