1#![allow(clippy::unusual_byte_groupings)]
3
4use std::f32::consts::{PI, TAU};
5
6use crate::motion::StyledSlot;
7use crate::theme::{ActiveTheme, Theme};
8use gpui::{
9 canvas, div, point, prelude::FluentBuilder, px, relative, App, Bounds, Hsla, IntoElement,
10 ParentElement, PathBuilder, Pixels, RenderOnce, StyleRefinement, Styled, Window,
11};
12
13use crate::button::ButtonVariant;
14use crate::chrome::{box_shadow, button_chrome};
15use crate::spinner::{paint_dot, paint_ring, point_on_circle, spinner_paints, SpinnerTone};
16
17#[derive(IntoElement)]
22pub struct Progress {
23 value: f32,
24 theme: Option<Theme>,
25 style: StyleRefinement,
26}
27
28impl Progress {
29 pub fn new(value: f32) -> Self {
30 Self {
31 value: normalize(value),
32 theme: None,
33 style: StyleRefinement::default(),
34 }
35 }
36
37 pub fn theme(mut self, theme: Theme) -> Self {
39 self.theme = Some(theme);
40 self
41 }
42}
43
44impl Styled for Progress {
45 fn style(&mut self) -> &mut StyleRefinement {
46 &mut self.style
47 }
48}
49
50impl RenderOnce for Progress {
51 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
52 let theme = self.theme.unwrap_or_else(|| cx.theme());
53 let track = button_chrome(theme, ButtonVariant::Outline);
54 let fill = button_chrome(theme, ButtonVariant::Primary);
55
56 div()
57 .w(px(280.))
58 .h(px(8.))
59 .flex_shrink_0()
60 .overflow_hidden()
61 .rounded(px(4.))
62 .border_1()
63 .border_color(track.border)
64 .bg(track.bg)
65 .shadow(vec![
66 box_shadow(0., 1., track.inset, 0., 0.),
67 box_shadow(0., track.shadow_y, track.shadow, track.shadow_blur, 0.),
68 ])
69 .refine_style(&self.style)
70 .when(self.value > 0.0, |el| {
71 el.child(
72 div()
73 .h_full()
74 .w(relative(self.value))
75 .flex_shrink_0()
76 .rounded(px(4.))
77 .bg(fill.bg)
78 .shadow(vec![box_shadow(0., 1., fill.inset, 0., 0.)]),
79 )
80 })
81 }
82}
83
84#[derive(IntoElement)]
86pub struct CircularProgress {
87 value: f32,
88 theme: Option<Theme>,
89 style: StyleRefinement,
90}
91
92impl CircularProgress {
93 pub fn new(value: f32) -> Self {
94 Self {
95 value: normalize(value),
96 theme: None,
97 style: StyleRefinement::default().flex_shrink_0(),
98 }
99 }
100
101 pub fn theme(mut self, theme: Theme) -> Self {
103 self.theme = Some(theme);
104 self
105 }
106}
107
108impl Styled for CircularProgress {
109 fn style(&mut self) -> &mut StyleRefinement {
110 &mut self.style
111 }
112}
113
114impl RenderOnce for CircularProgress {
115 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
116 let theme = self.theme.unwrap_or_else(|| cx.theme());
117 let (track, fill) = spinner_paints(theme, SpinnerTone::Default);
118 let value = self.value;
119
120 div()
121 .size(px(24.))
122 .flex_shrink_0()
123 .overflow_hidden()
124 .refine_style(&self.style)
125 .child(
126 canvas(
127 |_, _, _| {},
128 move |bounds, _, window, _| {
129 paint_circular_progress(window, bounds, value, track, fill);
130 },
131 )
132 .size_full(),
133 )
134 }
135}
136
137fn normalize(value: f32) -> f32 {
138 if value.is_finite() {
139 value.clamp(0.0, 1.0)
140 } else {
141 0.0
142 }
143}
144
145fn paint_circular_progress(
146 window: &mut Window,
147 bounds: Bounds<Pixels>,
148 value: f32,
149 track: Hsla,
150 fill: Hsla,
151) {
152 let stroke = px(2.);
153 let center = bounds.center();
154 let radius = (bounds.size.width.min(bounds.size.height) / 2.0) * 0.75;
155 paint_ring(window, center, radius, stroke, track);
156
157 if value <= 0.0 {
158 return;
159 }
160 if value >= 1.0 {
161 paint_ring(window, center, radius, stroke, fill);
162 return;
163 }
164
165 let start = point_on_circle(center, radius, -PI / 2.0);
166 let end = point_on_circle(center, radius, -PI / 2.0 + TAU * value);
167 let mut path = PathBuilder::stroke(stroke);
168 path.move_to(start);
169 path.arc_to(point(radius, radius), px(0.), value > 0.5, true, end);
170 if let Ok(path) = path.build() {
171 window.paint_path(path, fill);
172 }
173
174 let cap = stroke / 2.0;
175 paint_dot(window, start, cap, fill);
176 paint_dot(window, end, cap, fill);
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::theme::{paint, rgb};
183
184 #[test]
185 fn values_are_clamped() {
186 assert_eq!(normalize(-1.0), 0.0);
187 assert_eq!(normalize(0.4), 0.4);
188 assert_eq!(normalize(2.0), 1.0);
189 assert_eq!(normalize(f32::NAN), 0.0);
190 }
191
192 #[test]
193 fn linear_material_matches_paper() {
194 let light_track = button_chrome(Theme::light(), ButtonVariant::Outline);
195 let light_fill = button_chrome(Theme::light(), ButtonVariant::Primary);
196 assert_eq!(light_track.bg, paint(0xFFFFFF_47));
197 assert_eq!(light_track.border, paint(0xFFFFFF_9E));
198 assert_eq!(light_fill.bg, paint(0x18181B_B8));
199 assert_eq!(light_fill.inset, paint(0xFFFFFF_38));
200
201 let dark_track = button_chrome(Theme::dark(), ButtonVariant::Outline);
202 let dark_fill = button_chrome(Theme::dark(), ButtonVariant::Primary);
203 assert_eq!(dark_track.bg, paint(0xFFFFFF_0A));
204 assert_eq!(dark_track.border, paint(0xFFFFFF_24));
205 assert_eq!(dark_fill.bg, paint(0xFFFFFF_29));
206 assert_eq!(dark_fill.inset, paint(0xFFFFFF_47));
207 }
208
209 #[test]
210 fn circular_paints_match_paper() {
211 assert_eq!(
212 spinner_paints(Theme::light(), SpinnerTone::Default),
213 (paint(0x18181B_33), rgb(0x18181B))
214 );
215 assert_eq!(
216 spinner_paints(Theme::dark(), SpinnerTone::Default),
217 (paint(0xFAFAFA_2E), rgb(0xFAFAFA))
218 );
219 }
220}