use crate::{
lerp, vec2, Color32, NumExt, Pos2, Rect, Response, Rgba, Rounding, Sense, Shape, Stroke,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
};
enum ProgressBarText {
Custom(WidgetText),
Percentage,
}
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct ProgressBar {
progress: f32,
desired_width: Option<f32>,
desired_height: Option<f32>,
text: Option<ProgressBarText>,
fill: Option<Color32>,
animate: bool,
rounding: Option<Rounding>,
}
impl ProgressBar {
pub fn new(progress: f32) -> Self {
Self {
progress: progress.clamp(0.0, 1.0),
desired_width: None,
desired_height: None,
text: None,
fill: None,
animate: false,
rounding: None,
}
}
#[inline]
pub fn desired_width(mut self, desired_width: f32) -> Self {
self.desired_width = Some(desired_width);
self
}
#[inline]
pub fn desired_height(mut self, desired_height: f32) -> Self {
self.desired_height = Some(desired_height);
self
}
#[inline]
pub fn fill(mut self, color: Color32) -> Self {
self.fill = Some(color);
self
}
#[inline]
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
self.text = Some(ProgressBarText::Custom(text.into()));
self
}
#[inline]
pub fn show_percentage(mut self) -> Self {
self.text = Some(ProgressBarText::Percentage);
self
}
#[inline]
pub fn animate(mut self, animate: bool) -> Self {
self.animate = animate;
self
}
#[inline]
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = Some(rounding.into());
self
}
}
impl Widget for ProgressBar {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
progress,
desired_width,
desired_height,
text,
fill,
animate,
rounding,
} = self;
let animate = animate && progress < 1.0;
let desired_width =
desired_width.unwrap_or_else(|| ui.available_size_before_wrap().x.at_least(96.0));
let height = desired_height.unwrap_or(ui.spacing().interact_size.y);
let (outer_rect, response) =
ui.allocate_exact_size(vec2(desired_width, height), Sense::hover());
response.widget_info(|| {
let mut info = if let Some(ProgressBarText::Custom(text)) = &text {
WidgetInfo::labeled(WidgetType::ProgressIndicator, ui.is_enabled(), text.text())
} else {
WidgetInfo::new(WidgetType::ProgressIndicator)
};
info.value = Some((progress as f64 * 100.0).floor());
info
});
if ui.is_rect_visible(response.rect) {
if animate {
ui.ctx().request_repaint();
}
let visuals = ui.style().visuals.clone();
let is_custom_rounding = rounding.is_some();
let corner_radius = outer_rect.height() / 2.0;
let rounding = rounding.unwrap_or_else(|| corner_radius.into());
ui.painter()
.rect(outer_rect, rounding, visuals.extreme_bg_color, Stroke::NONE);
let min_width = 2.0 * rounding.sw.at_least(rounding.nw).at_most(corner_radius);
let filled_width = (outer_rect.width() * progress).at_least(min_width);
let inner_rect =
Rect::from_min_size(outer_rect.min, vec2(filled_width, outer_rect.height()));
let (dark, bright) = (0.7, 1.0);
let color_factor = if animate {
let time = ui.input(|i| i.time);
lerp(dark..=bright, time.cos().abs())
} else {
bright
};
ui.painter().rect(
inner_rect,
rounding,
Color32::from(
Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32,
),
Stroke::NONE,
);
if animate && !is_custom_rounding {
let n_points = 20;
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
let circle_radius = corner_radius - 2.0;
let points: Vec<Pos2> = (0..n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
let (sin, cos) = angle.sin_cos();
inner_rect.right_center()
+ circle_radius * vec2(cos as f32, sin as f32)
+ vec2(-corner_radius, 0.0)
})
.collect();
ui.painter()
.add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
}
if let Some(text_kind) = text {
let text = match text_kind {
ProgressBarText::Custom(text) => text,
ProgressBarText::Percentage => {
format!("{}%", (progress * 100.0) as usize).into()
}
};
let galley = text.into_galley(
ui,
Some(TextWrapMode::Extend),
f32::INFINITY,
TextStyle::Button,
);
let text_pos = outer_rect.left_center() - Vec2::new(0.0, galley.size().y / 2.0)
+ vec2(ui.spacing().item_spacing.x, 0.0);
let text_color = visuals
.override_text_color
.unwrap_or(visuals.selection.stroke.color);
ui.painter()
.with_clip_rect(outer_rect)
.galley(text_pos, galley, text_color);
}
}
response
}
}