1use gpui::prelude::*;
11use gpui::{canvas, div, point, px, App, Hsla, IntoElement, PathBuilder, SharedString, Window};
12
13use crate::style::ColorValue;
14use crate::theme::{theme, Size};
15
16use super::{arc_point, series_color, slice_spans};
17use crate::devtools::Probed;
18
19#[derive(IntoElement)]
24pub struct PieChart {
25 values: Vec<f32>,
26 labels: Vec<SharedString>,
27 colors: Vec<ColorValue>,
28 size: f32,
29 donut: Option<f32>,
30}
31
32impl PieChart {
33 pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
34 PieChart {
35 values: values.into_iter().collect(),
36 labels: Vec::new(),
37 colors: Vec::new(),
38 size: 160.0,
39 donut: None,
40 }
41 }
42
43 pub fn entries(entries: impl IntoIterator<Item = (impl Into<SharedString>, f32)>) -> Self {
45 let (labels, values): (Vec<SharedString>, Vec<f32>) = entries
46 .into_iter()
47 .map(|(label, value)| (label.into(), value))
48 .unzip();
49 PieChart {
50 labels,
51 ..PieChart::new(values)
52 }
53 }
54
55 pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
57 self.colors = vec![color.into()];
58 self
59 }
60
61 pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
63 self.colors = colors.into_iter().map(Into::into).collect();
64 self
65 }
66
67 pub fn size(mut self, size: f32) -> Self {
69 self.size = size;
70 self
71 }
72
73 pub fn donut(mut self, inner_fraction: f32) -> Self {
76 self.donut = Some(inner_fraction.clamp(0.05, 0.95));
77 self
78 }
79}
80
81impl RenderOnce for PieChart {
82 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
83 let t = theme(cx);
84 let n = self.values.len();
85 let slice_colors: Vec<Hsla> = (0..n).map(|i| series_color(t, &self.colors, i)).collect();
86 let legend_colors = slice_colors.clone();
87 let dimmed = t.dimmed().hsla();
88 let font_xs = t.font_size(Size::Xs);
89 let spans = slice_spans(&self.values);
90 let donut = self.donut;
91 let diameter = self.size;
92
93 let plot = canvas(
94 |_, _, _| (),
95 move |bounds, _, window, _cx| {
96 let w = f32::from(bounds.size.width);
97 let h = f32::from(bounds.size.height);
98 let radius = w.min(h) / 2.0;
99 if radius <= 0.0 {
100 return;
101 }
102 let center = (
103 f32::from(bounds.origin.x) + w / 2.0,
104 f32::from(bounds.origin.y) + h / 2.0,
105 );
106 for (i, &(start, end)) in spans.iter().enumerate() {
107 if end - start <= f32::EPSILON {
108 continue;
109 }
110 let color = slice_colors[i];
111 let inner = donut.map(|f| radius * f);
112 if end - start >= 0.999 {
113 let mid = start + 0.5;
116 paint_slice(window, center, radius, inner, start, mid, color);
117 paint_slice(window, center, radius, inner, mid, start + 1.0, color);
118 } else {
119 paint_slice(window, center, radius, inner, start, end, color);
120 }
121 }
122 },
123 )
124 .w(px(diameter))
125 .h(px(diameter));
126
127 let mut root = div()
128 .flex()
129 .flex_col()
130 .items_center()
131 .gap(px(8.0))
132 .child(plot);
133
134 if !self.labels.is_empty() && !legend_colors.is_empty() {
136 let items = self.labels.iter().enumerate().map(|(i, label)| {
137 div()
138 .flex()
139 .flex_row()
140 .items_center()
141 .gap(px(6.0))
142 .child(
143 div()
144 .w(px(8.0))
145 .h(px(8.0))
146 .rounded(px(4.0))
147 .bg(legend_colors[i % legend_colors.len()]),
148 )
149 .child(
150 div()
151 .text_size(px(font_xs))
152 .text_color(dimmed)
153 .child(label.clone()),
154 )
155 });
156 root = root.child(
157 div()
158 .flex()
159 .flex_row()
160 .flex_wrap()
161 .justify_center()
162 .gap(px(12.0))
163 .children(items),
164 );
165 }
166 root.probe("PieChart")
167 }
168}
169
170fn paint_slice(
174 window: &mut Window,
175 center: (f32, f32),
176 radius: f32,
177 inner: Option<f32>,
178 start: f32,
179 end: f32,
180 color: Hsla,
181) {
182 let large = end - start > 0.5;
183 let to_pt = |(x, y): (f32, f32)| point(px(x), px(y));
184 let outer_start = to_pt(arc_point(center, radius, start));
185 let outer_end = to_pt(arc_point(center, radius, end));
186 let radii = point(px(radius), px(radius));
187
188 let mut pb = PathBuilder::fill();
189 match inner {
190 Some(hole) if hole > 0.0 => {
193 let inner_radii = point(px(hole), px(hole));
194 pb.move_to(outer_start);
195 pb.arc_to(radii, px(0.0), large, true, outer_end);
196 pb.line_to(to_pt(arc_point(center, hole, end)));
197 pb.arc_to(
198 inner_radii,
199 px(0.0),
200 large,
201 false,
202 to_pt(arc_point(center, hole, start)),
203 );
204 pb.close();
205 }
206 _ => {
208 pb.move_to(to_pt(center));
209 pb.line_to(outer_start);
210 pb.arc_to(radii, px(0.0), large, true, outer_end);
211 pb.close();
212 }
213 }
214 if let Ok(path) = pb.build() {
215 window.paint_path(path, color);
216 }
217}