egui/widgets/
progress_bar.rs

1use crate::{
2    lerp, vec2, Color32, NumExt, Pos2, Rect, Response, Rgba, Rounding, 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    rounding: Option<Rounding>,
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            rounding: 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::rounding`] 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::rounding`] 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 rounding(mut self, rounding: impl Into<Rounding>) -> Self {
94        self.rounding = Some(rounding.into());
95        self
96    }
97}
98
99impl Widget for ProgressBar {
100    fn ui(self, ui: &mut Ui) -> Response {
101        let Self {
102            progress,
103            desired_width,
104            desired_height,
105            text,
106            fill,
107            animate,
108            rounding,
109        } = self;
110
111        let animate = animate && progress < 1.0;
112
113        let desired_width =
114            desired_width.unwrap_or_else(|| ui.available_size_before_wrap().x.at_least(96.0));
115        let height = desired_height.unwrap_or(ui.spacing().interact_size.y);
116        let (outer_rect, response) =
117            ui.allocate_exact_size(vec2(desired_width, height), Sense::hover());
118
119        response.widget_info(|| {
120            let mut info = if let Some(ProgressBarText::Custom(text)) = &text {
121                WidgetInfo::labeled(WidgetType::ProgressIndicator, ui.is_enabled(), text.text())
122            } else {
123                WidgetInfo::new(WidgetType::ProgressIndicator)
124            };
125            info.value = Some((progress as f64 * 100.0).floor());
126
127            info
128        });
129
130        if ui.is_rect_visible(response.rect) {
131            if animate {
132                ui.ctx().request_repaint();
133            }
134
135            let visuals = ui.style().visuals.clone();
136            let is_custom_rounding = rounding.is_some();
137            let corner_radius = outer_rect.height() / 2.0;
138            let rounding = rounding.unwrap_or_else(|| corner_radius.into());
139            ui.painter()
140                .rect(outer_rect, rounding, visuals.extreme_bg_color, Stroke::NONE);
141            let min_width = 2.0 * rounding.sw.at_least(rounding.nw).at_most(corner_radius);
142            let filled_width = (outer_rect.width() * progress).at_least(min_width);
143            let inner_rect =
144                Rect::from_min_size(outer_rect.min, vec2(filled_width, outer_rect.height()));
145
146            let (dark, bright) = (0.7, 1.0);
147            let color_factor = if animate {
148                let time = ui.input(|i| i.time);
149                lerp(dark..=bright, time.cos().abs())
150            } else {
151                bright
152            };
153
154            ui.painter().rect(
155                inner_rect,
156                rounding,
157                Color32::from(
158                    Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32,
159                ),
160                Stroke::NONE,
161            );
162
163            if animate && !is_custom_rounding {
164                let n_points = 20;
165                let time = ui.input(|i| i.time);
166                let start_angle = time * std::f64::consts::TAU;
167                let end_angle = start_angle + 240f64.to_radians() * time.sin();
168                let circle_radius = corner_radius - 2.0;
169                let points: Vec<Pos2> = (0..n_points)
170                    .map(|i| {
171                        let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
172                        let (sin, cos) = angle.sin_cos();
173                        inner_rect.right_center()
174                            + circle_radius * vec2(cos as f32, sin as f32)
175                            + vec2(-corner_radius, 0.0)
176                    })
177                    .collect();
178                ui.painter()
179                    .add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
180            }
181
182            if let Some(text_kind) = text {
183                let text = match text_kind {
184                    ProgressBarText::Custom(text) => text,
185                    ProgressBarText::Percentage => {
186                        format!("{}%", (progress * 100.0) as usize).into()
187                    }
188                };
189                let galley = text.into_galley(
190                    ui,
191                    Some(TextWrapMode::Extend),
192                    f32::INFINITY,
193                    TextStyle::Button,
194                );
195                let text_pos = outer_rect.left_center() - Vec2::new(0.0, galley.size().y / 2.0)
196                    + vec2(ui.spacing().item_spacing.x, 0.0);
197                let text_color = visuals
198                    .override_text_color
199                    .unwrap_or(visuals.selection.stroke.color);
200                ui.painter()
201                    .with_clip_rect(outer_rect)
202                    .galley(text_pos, galley, text_color);
203            }
204        }
205
206        response
207    }
208}