egui/widgets/
progress_bar.rs

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