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