egui/widgets/
progress_bar.rs

1use 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/// A simple progress bar.
12///
13/// See also: [`crate::Spinner`].
14#[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    /// Progress in the `[0, 1]` range, where `1` means "completed".
27    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    /// The desired width of the bar. Will use all horizontal space if not set.
40    #[inline]
41    pub fn desired_width(mut self, desired_width: f32) -> Self {
42        self.desired_width = Some(desired_width);
43        self
44    }
45
46    /// The desired height of the bar. Will use the default interaction size if not set.
47    #[inline]
48    pub fn desired_height(mut self, desired_height: f32) -> Self {
49        self.desired_height = Some(desired_height);
50        self
51    }
52
53    /// The fill color of the bar.
54    #[inline]
55    pub fn fill(mut self, color: Color32) -> Self {
56        self.fill = Some(color);
57        self
58    }
59
60    /// A custom text to display on the progress bar.
61    #[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    /// Show the progress in percent on the progress bar.
68    #[inline]
69    pub fn show_percentage(mut self) -> Self {
70        self.text = Some(ProgressBarText::Percentage);
71        self
72    }
73
74    /// Whether to display a loading animation when progress `< 1`.
75    /// Note that this will cause the UI to be redrawn.
76    /// Defaults to `false`.
77    ///
78    /// If [`Self::corner_radius`] and [`Self::animate`] are used simultaneously, the animation is not
79    /// rendered, since it requires a perfect circle to render correctly. However, the UI is still
80    /// redrawn.
81    #[inline]
82    pub fn animate(mut self, animate: bool) -> Self {
83        self.animate = animate;
84        self
85    }
86
87    /// Set the rounding of the progress bar.
88    ///
89    /// If [`Self::corner_radius`] and [`Self::animate`] are used simultaneously, the animation is not
90    /// rendered, since it requires a perfect circle to render correctly. However, the UI is still
91    /// redrawn.
92    #[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}