1use egui::{
9 Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget,
10 WidgetInfo, WidgetType,
11};
12
13use crate::theme::{Accent, Theme};
14
15#[derive(Debug, Clone)]
25#[must_use = "Add with `ui.add(...)`."]
26pub struct ProgressBar {
27 fraction: f32,
28 height: f32,
29 desired_width: Option<f32>,
30 color: Option<Color32>,
31 accent: Option<Accent>,
32 text: Option<String>,
33}
34
35impl ProgressBar {
36 pub fn new(fraction: f32) -> Self {
39 Self {
40 fraction: if fraction.is_nan() {
41 0.0
42 } else {
43 fraction.clamp(0.0, 1.0)
44 },
45 height: 22.0,
46 desired_width: None,
47 color: None,
48 accent: None,
49 text: None,
50 }
51 }
52
53 pub fn height(mut self, height: f32) -> Self {
55 self.height = height;
56 self
57 }
58
59 pub fn desired_width(mut self, width: f32) -> Self {
61 self.desired_width = Some(width);
62 self
63 }
64
65 pub fn color(mut self, color: Color32) -> Self {
67 self.color = Some(color);
68 self.accent = None;
69 self
70 }
71
72 pub fn accent(mut self, accent: Accent) -> Self {
75 self.accent = Some(accent);
76 self.color = None;
77 self
78 }
79
80 pub fn text(mut self, text: impl Into<String>) -> Self {
83 self.text = Some(text.into());
84 self
85 }
86}
87
88impl Widget for ProgressBar {
89 fn ui(self, ui: &mut Ui) -> Response {
90 let theme = Theme::current(ui.ctx());
91 let p = &theme.palette;
92 let fill_color = match (self.color, self.accent) {
93 (Some(c), _) => c,
94 (_, Some(a)) => p.accent_fill(a),
95 _ => p.sky,
96 };
97
98 let width = self
99 .desired_width
100 .unwrap_or_else(|| ui.available_width())
101 .max(self.height * 2.0);
102 let (rect, response) =
103 ui.allocate_exact_size(Vec2::new(width, self.height), Sense::hover());
104
105 if ui.is_rect_visible(rect) {
106 let painter = ui.painter();
107 let radius = CornerRadius::same((self.height * 0.5).round() as u8);
108
109 painter.rect(
111 rect,
112 radius,
113 p.input_bg,
114 Stroke::new(1.0, p.border),
115 StrokeKind::Inside,
116 );
117
118 let fill_w = rect.width() * self.fraction;
120 let fill_rect = Rect::from_min_size(rect.min, Vec2::new(fill_w, rect.height()));
121 if fill_w > 0.5 {
122 painter
123 .with_clip_rect(fill_rect)
124 .rect_filled(rect, radius, fill_color);
125 }
126
127 let label = match self.text.as_deref() {
129 Some(s) => s.to_owned(),
130 None => format!("{}%", (self.fraction * 100.0).round() as u32),
131 };
132 if !label.is_empty() {
133 let font_size = (self.height * 0.55).max(11.0);
134 let galley =
135 crate::theme::placeholder_galley(ui, &label, font_size, true, f32::INFINITY);
136 let text_pos = Pos2::new(
137 rect.center().x - galley.size().x * 0.5,
138 rect.center().y - galley.size().y * 0.5,
139 );
140
141 let empty_rect =
143 Rect::from_min_max(Pos2::new(rect.min.x + fill_w, rect.min.y), rect.max);
144 painter
145 .with_clip_rect(empty_rect)
146 .galley(text_pos, galley.clone(), p.text_muted);
147
148 painter
150 .with_clip_rect(fill_rect)
151 .galley(text_pos, galley, Color32::WHITE);
152 }
153 }
154
155 response
156 .widget_info(|| WidgetInfo::labeled(WidgetType::ProgressIndicator, true, "progress"));
157 response
158 }
159}