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