egui/widgets/
progress_bar.rs1use crate::{
2 lerp, vec2, Color32, CornerRadius, NumExt, Pos2, Rect, Response, Rgba, Sense, Shape, Stroke,
3 TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
4};
5
6enum ProgressBarText {
7 Custom(WidgetText),
8 Percentage,
9}
10
11#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
15pub struct ProgressBar {
16 progress: f32,
17 desired_width: Option<f32>,
18 desired_height: Option<f32>,
19 text: Option<ProgressBarText>,
20 fill: Option<Color32>,
21 animate: bool,
22 corner_radius: Option<CornerRadius>,
23}
24
25impl ProgressBar {
26 pub fn new(progress: f32) -> Self {
28 Self {
29 progress: progress.clamp(0.0, 1.0),
30 desired_width: None,
31 desired_height: None,
32 text: None,
33 fill: None,
34 animate: false,
35 corner_radius: None,
36 }
37 }
38
39 #[inline]
41 pub fn desired_width(mut self, desired_width: f32) -> Self {
42 self.desired_width = Some(desired_width);
43 self
44 }
45
46 #[inline]
48 pub fn desired_height(mut self, desired_height: f32) -> Self {
49 self.desired_height = Some(desired_height);
50 self
51 }
52
53 #[inline]
55 pub fn fill(mut self, color: Color32) -> Self {
56 self.fill = Some(color);
57 self
58 }
59
60 #[inline]
62 pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
63 self.text = Some(ProgressBarText::Custom(text.into()));
64 self
65 }
66
67 #[inline]
69 pub fn show_percentage(mut self) -> Self {
70 self.text = Some(ProgressBarText::Percentage);
71 self
72 }
73
74 #[inline]
82 pub fn animate(mut self, animate: bool) -> Self {
83 self.animate = animate;
84 self
85 }
86
87 #[inline]
93 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
94 self.corner_radius = Some(corner_radius.into());
95 self
96 }
97
98 #[inline]
99 #[deprecated = "Renamed to `corner_radius`"]
100 pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
101 self.corner_radius(corner_radius)
102 }
103}
104
105impl Widget for ProgressBar {
106 fn ui(self, ui: &mut Ui) -> Response {
107 let Self {
108 progress,
109 desired_width,
110 desired_height,
111 text,
112 fill,
113 animate,
114 corner_radius,
115 } = self;
116
117 let animate = animate && progress < 1.0;
118
119 let desired_width =
120 desired_width.unwrap_or_else(|| ui.available_size_before_wrap().x.at_least(96.0));
121 let height = desired_height.unwrap_or(ui.spacing().interact_size.y);
122 let (outer_rect, response) =
123 ui.allocate_exact_size(vec2(desired_width, height), Sense::hover());
124
125 response.widget_info(|| {
126 let mut info = if let Some(ProgressBarText::Custom(text)) = &text {
127 WidgetInfo::labeled(WidgetType::ProgressIndicator, ui.is_enabled(), text.text())
128 } else {
129 WidgetInfo::new(WidgetType::ProgressIndicator)
130 };
131 info.value = Some((progress as f64 * 100.0).floor());
132
133 info
134 });
135
136 if ui.is_rect_visible(response.rect) {
137 if animate {
138 ui.ctx().request_repaint();
139 }
140
141 let visuals = ui.style().visuals.clone();
142 let has_custom_cr = corner_radius.is_some();
143 let half_height = outer_rect.height() / 2.0;
144 let corner_radius = corner_radius.unwrap_or_else(|| half_height.into());
145 ui.painter()
146 .rect_filled(outer_rect, corner_radius, visuals.extreme_bg_color);
147 let min_width =
148 2.0 * f32::max(corner_radius.sw as _, corner_radius.nw as _).at_most(half_height);
149 let filled_width = (outer_rect.width() * progress).at_least(min_width);
150 let inner_rect =
151 Rect::from_min_size(outer_rect.min, vec2(filled_width, outer_rect.height()));
152
153 let (dark, bright) = (0.7, 1.0);
154 let color_factor = if animate {
155 let time = ui.input(|i| i.time);
156 lerp(dark..=bright, time.cos().abs())
157 } else {
158 bright
159 };
160
161 ui.painter().rect_filled(
162 inner_rect,
163 corner_radius,
164 Color32::from(
165 Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32,
166 ),
167 );
168
169 if animate && !has_custom_cr {
170 let n_points = 20;
171 let time = ui.input(|i| i.time);
172 let start_angle = time * std::f64::consts::TAU;
173 let end_angle = start_angle + 240f64.to_radians() * time.sin();
174 let circle_radius = half_height - 2.0;
175 let points: Vec<Pos2> = (0..n_points)
176 .map(|i| {
177 let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
178 let (sin, cos) = angle.sin_cos();
179 inner_rect.right_center()
180 + circle_radius * vec2(cos as f32, sin as f32)
181 + vec2(-half_height, 0.0)
182 })
183 .collect();
184 ui.painter()
185 .add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
186 }
187
188 if let Some(text_kind) = text {
189 let text = match text_kind {
190 ProgressBarText::Custom(text) => text,
191 ProgressBarText::Percentage => {
192 format!("{}%", (progress * 100.0) as usize).into()
193 }
194 };
195 let galley = text.into_galley(
196 ui,
197 Some(TextWrapMode::Extend),
198 f32::INFINITY,
199 TextStyle::Button,
200 );
201 let text_pos = outer_rect.left_center() - Vec2::new(0.0, galley.size().y / 2.0)
202 + vec2(ui.spacing().item_spacing.x, 0.0);
203 let text_color = visuals
204 .override_text_color
205 .unwrap_or(visuals.selection.stroke.color);
206 ui.painter()
207 .with_clip_rect(outer_rect)
208 .galley(text_pos, galley, text_color);
209 }
210 }
211
212 response
213 }
214}