1use std::{f32::consts::PI, fmt::Debug};
4
5use gpui::{Background, Bounds, Path, PathBuilder, Pixels, Point, Window, point, px};
6
7use crate::plot::{PathCache, ShapeKey};
8
9const EPSILON: f32 = 1e-12;
10const HALF_PI: f32 = PI / 2.;
11
12#[non_exhaustive]
15pub struct ArcData<'a, T> {
16 pub data: &'a T,
17 pub index: usize,
18 pub value: f32,
19 pub start_angle: f32,
20 pub end_angle: f32,
21 pub pad_angle: f32,
22}
23
24impl<'a, T> ArcData<'a, T> {
25 pub fn new(data: &'a T, index: usize, value: f32, start_angle: f32, end_angle: f32) -> Self {
28 Self {
29 data,
30 index,
31 value,
32 start_angle,
33 end_angle,
34 pad_angle: 0.,
35 }
36 }
37}
38
39impl<T> Debug for ArcData<'_, T> {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 write!(
42 f,
43 "ArcData {{ index: {}, value: {}, start_angle: {}, end_angle: {}, pad_angle: {} }}",
44 self.index, self.value, self.start_angle, self.end_angle, self.pad_angle
45 )
46 }
47}
48
49pub struct Arc {
50 inner_radius: f32,
51 outer_radius: f32,
52}
53
54impl Default for Arc {
55 fn default() -> Self {
56 Self {
57 inner_radius: 0.,
58 outer_radius: 0.,
59 }
60 }
61}
62
63impl Arc {
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn inner_radius(mut self, inner_radius: f32) -> Self {
70 self.inner_radius = inner_radius;
71 self
72 }
73
74 pub fn outer_radius(mut self, outer_radius: f32) -> Self {
76 self.outer_radius = outer_radius;
77 self
78 }
79
80 pub fn centroid<T>(&self, arc: &ArcData<T>) -> Point<f32> {
82 let start_angle = arc.start_angle - HALF_PI;
83 let end_angle = arc.end_angle - HALF_PI;
84 let r = (self.inner_radius + self.outer_radius) / 2.;
85 let a = (start_angle + end_angle) / 2.;
86
87 point(r * a.cos(), r * a.sin())
88 }
89
90 fn path<T>(&self, arc: &ArcData<T>, bounds: &Bounds<Pixels>) -> Option<Path<Pixels>> {
91 let start_angle = arc.start_angle - HALF_PI;
92 let end_angle = arc.end_angle - HALF_PI;
93 let da = end_angle - start_angle;
94 let pad_angle = if da >= PI {
95 0.0001
98 } else {
99 arc.pad_angle
100 };
101 let r0 = self.inner_radius.max(0.);
102 let r1 = self.outer_radius.max(0.);
103
104 let center_x = bounds.origin.x.as_f32() + bounds.size.width.as_f32() / 2.;
106 let center_y = bounds.origin.y.as_f32() + bounds.size.height.as_f32() / 2.;
107
108 if r1 < EPSILON || da.abs() < EPSILON {
110 return None;
111 }
112
113 let (a0_outer, a1_outer, a0_inner, a1_inner) = if r0 > EPSILON && pad_angle > 0.0 {
115 let pad_width = r1 * pad_angle;
116 let pad_angle_outer = pad_width / r1;
117 let mut pad_angle_inner = pad_width / r0;
118 let max_inner_pad = da * 0.8;
119 if pad_angle_inner > max_inner_pad {
120 pad_angle_inner = max_inner_pad;
121 }
122 (
123 start_angle + pad_angle_outer * 0.5,
124 end_angle - pad_angle_outer * 0.5,
125 start_angle + pad_angle_inner * 0.5,
126 end_angle - pad_angle_inner * 0.5,
127 )
128 } else {
129 let pad = pad_angle * 0.5;
130 (
131 start_angle + pad,
132 end_angle - pad,
133 start_angle + pad,
134 end_angle - pad,
135 )
136 };
137
138 let da_outer = a1_outer - a0_outer;
139 if da_outer <= 0. {
140 return None;
141 }
142
143 let x01 = center_x + r1 * a0_outer.cos();
145 let y01 = center_y + r1 * a0_outer.sin();
146 let x11 = center_x + r1 * a1_outer.cos();
147 let y11 = center_y + r1 * a1_outer.sin();
148
149 let mut builder = PathBuilder::fill();
150
151 builder.move_to(point(px(x01), px(y01)));
153
154 let large_arc = (a1_outer - a0_outer).abs() > PI;
156 builder.arc_to(
157 point(px(r1), px(r1)),
158 px(0.),
159 large_arc,
160 true,
161 point(px(x11), px(y11)),
162 );
163
164 if r0 > EPSILON {
165 let x10 = center_x + r0 * a1_inner.cos();
167 let y10 = center_y + r0 * a1_inner.sin();
168 builder.line_to(point(px(x10), px(y10)));
169
170 let x00 = center_x + r0 * a0_inner.cos();
172 let y00 = center_y + r0 * a0_inner.sin();
173 let large_arc_inner = (a1_inner - a0_inner).abs() > PI;
174 builder.arc_to(
175 point(px(r0), px(r0)),
176 px(0.),
177 large_arc_inner,
178 false,
179 point(px(x00), px(y00)),
180 );
181 } else {
182 builder.line_to(point(px(center_x), px(center_y)));
184 }
185
186 builder.build().ok()
187 }
188
189 pub fn contains<T>(
192 &self,
193 arc: &ArcData<T>,
194 position: Point<f32>,
195 bounds: &Bounds<Pixels>,
196 ) -> bool {
197 let dx = position.x - bounds.size.width.as_f32() / 2.;
198 let dy = position.y - bounds.size.height.as_f32() / 2.;
199 let radius = dx.hypot(dy);
200 let r0 = self.inner_radius.max(0.);
201 let r1 = self.outer_radius.max(0.);
202 if radius < r0 || radius > r1 {
203 return false;
204 }
205
206 let angle = (dy.atan2(dx) + HALF_PI).rem_euclid(2. * PI);
208 (arc.start_angle..arc.end_angle).contains(&angle)
209 }
210
211 pub fn paint_cached<T>(
218 &self,
219 arc: &ArcData<T>,
220 fill: impl Into<Background>,
221 bounds: &Bounds<Pixels>,
222 cache: &mut PathCache,
223 window: &mut Window,
224 ) {
225 let key = ShapeKey::new((
226 bounds.size.width.as_f32().to_bits(),
227 bounds.size.height.as_f32().to_bits(),
228 ))
229 .f32(arc.start_angle)
230 .f32(arc.end_angle)
231 .f32(arc.pad_angle)
232 .f32(self.inner_radius)
233 .f32(self.outer_radius)
234 .finish();
235 let local = Bounds::new(Point::default(), bounds.size);
236 let path = cache.get(key, bounds.origin, || self.path(arc, &local));
237 if let Some(path) = path {
238 window.paint_path(path, fill);
239 }
240 }
241
242 pub fn paint<T>(
244 &self,
245 arc: &ArcData<T>,
246 fill: impl Into<Background>,
247 bounds: &Bounds<Pixels>,
248 window: &mut Window,
249 ) {
250 if let Some(path) = self.path(arc, bounds) {
251 window.paint_path(path, fill);
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_arc_default() {
262 let arc = Arc::default();
263 assert_eq!(arc.inner_radius, 0.);
264 assert_eq!(arc.outer_radius, 0.);
265 }
266
267 #[test]
268 fn test_arc_builder() {
269 let arc = Arc::new().inner_radius(10.).outer_radius(20.);
270
271 assert_eq!(arc.inner_radius, 10.);
272 assert_eq!(arc.outer_radius, 20.);
273 }
274
275 #[test]
276 fn test_arc_centroid() {
277 let arc = Arc::new().inner_radius(10.).outer_radius(20.);
278
279 let arc_data = ArcData {
280 data: &(),
281 index: 0,
282 value: 1.,
283 start_angle: 0.,
284 end_angle: PI,
285 pad_angle: 0.,
286 };
287
288 let centroid = arc.centroid(&arc_data);
289 let expected_radius = (10. + 20.) / 2.;
290 let expected_angle = (0. + PI - 2. * HALF_PI) / 2.;
291
292 assert_eq!(centroid.x, expected_radius * expected_angle.cos());
293 assert_eq!(centroid.y, expected_radius * expected_angle.sin());
294 }
295
296 #[test]
297 fn test_arc_contains() {
298 use gpui::{point, px, size};
299
300 let arc = Arc::new().inner_radius(10.).outer_radius(40.);
302 let right_half = ArcData {
303 data: &(),
304 index: 0,
305 value: 1.,
306 start_angle: 0.,
307 end_angle: PI,
308 pad_angle: 0.,
309 };
310 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
311
312 assert!(arc.contains(&right_half, point(80., 50.), &bounds));
314 assert!(!arc.contains(&right_half, point(20., 50.), &bounds));
316 assert!(!arc.contains(&right_half, point(55., 50.), &bounds));
318 assert!(!arc.contains(&right_half, point(95., 50.), &bounds));
319 let wider = Arc::new().inner_radius(10.).outer_radius(50.);
321 assert!(wider.contains(&right_half, point(95., 50.), &bounds));
322 assert!(arc.contains(&right_half, point(50., 20.), &bounds));
324 assert!(!arc.contains(&right_half, point(50., 80.), &bounds));
325 }
326}