1use super::*;
2use crate::node::WeakFlexBox;
3use crate::Border;
4
5const PROGRESS_BORDER_WIDTH: f32 = 1.0;
6
7fn inset_radius(radius: f32) -> f32 {
8 (radius - PROGRESS_BORDER_WIDTH).max(0.0)
9}
10
11#[derive(Clone, Copy)]
12struct ResolvedProgressBarSizing {
13 length: f32,
14 thickness: f32,
15}
16
17fn resolve_sizing(sizing: Option<ProgressBarSizing>) -> ResolvedProgressBarSizing {
18 let sizing = sizing.unwrap_or_default();
19 ResolvedProgressBarSizing {
20 length: if sizing.has_length() {
21 sizing.length_px()
22 } else {
23 PROGRESS_LENGTH
24 },
25 thickness: if sizing.has_thickness() {
26 sizing.thickness_px()
27 } else {
28 PROGRESS_THICKNESS
29 },
30 }
31}
32
33#[derive(Clone)]
34pub struct ProgressBar {
35 root: FlexBox,
36 fill: FlexBox,
37 min: Rc<Cell<f32>>,
38 max: Rc<Cell<f32>>,
39 value: Rc<Cell<f32>>,
40 orientation: Rc<Cell<Orientation>>,
41 sizing_value: Rc<Cell<Option<ProgressBarSizing>>>,
42 colors_value: Rc<Cell<Option<ProgressBarColors>>>,
43 weak_root: WeakFlexBox,
44 weak_fill: WeakFlexBox,
45}
46
47impl Default for ProgressBar {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl ProgressBar {
54 pub fn new() -> Self {
55 let root = flex_box();
56 let fill = flex_box();
57 root.semantic_orientation(Orientation::Horizontal)
58 .semantic_value_range(0.0, 0.0, 100.0)
59 .child(&fill);
60 let control = Self {
61 weak_root: root.downgrade(),
62 weak_fill: fill.downgrade(),
63 root,
64 fill,
65 min: Rc::new(Cell::new(0.0)),
66 max: Rc::new(Cell::new(100.0)),
67 value: Rc::new(Cell::new(0.0)),
68 orientation: Rc::new(Cell::new(Orientation::Horizontal)),
69 sizing_value: Rc::new(Cell::new(None)),
70 colors_value: Rc::new(Cell::new(None)),
71 };
72 control.install_visual_subscriptions();
73 control.set_value(0.0);
74 control.handle_theme_changed();
75 control
76 }
77
78 pub fn min(&self, value: f32) -> &Self {
79 self.min.set(value);
80 if self.max.get() < value {
81 self.max.set(value);
82 }
83 self.set_value(self.value.get());
84 self
85 }
86
87 pub fn max(&self, value: f32) -> &Self {
88 self.max.set(value);
89 if self.min.get() > value {
90 self.min.set(value);
91 }
92 self.set_value(self.value.get());
93 self
94 }
95
96 pub fn value(&self, value: f32) -> &Self {
97 self.set_value(value);
98 self
99 }
100
101 pub fn length(&self, value: f32) -> &Self {
102 let value = clamp_direct_dimension("length", value);
103 let current = self.sizing_value.get().unwrap_or_default();
104 let mut sizing = ProgressBarSizing::new().length(value);
105 if current.has_thickness() {
106 sizing = sizing.thickness(current.thickness_px());
107 }
108 self.sizing_value.set(Some(sizing));
109 self.sync_geometry();
110 self
111 }
112
113 pub fn thickness(&self, value: f32) -> &Self {
114 let value = clamp_direct_dimension("thickness", value);
115 let current = self.sizing_value.get().unwrap_or_default();
116 let mut sizing = ProgressBarSizing::new().thickness(value);
117 if current.has_length() {
118 sizing = sizing.length(current.length_px());
119 }
120 self.sizing_value.set(Some(sizing));
121 self.sync_geometry();
122 self.sync_visual_state();
123 self
124 }
125
126 pub fn orientation(&self, orientation: Orientation) -> &Self {
127 let orientation = if orientation == Orientation::Vertical {
128 Orientation::Vertical
129 } else {
130 Orientation::Horizontal
131 };
132 self.orientation.set(orientation);
133 self.root.semantic_orientation(orientation);
134 self.sync_geometry();
135 self
136 }
137
138 pub fn sizing(&self, sizing: ProgressBarSizing) -> &Self {
139 self.sizing_value.set(Some(sizing));
140 self.sync_geometry();
141 self.sync_visual_state();
142 self
143 }
144
145 pub fn clear_sizing(&self) -> &Self {
146 self.sizing_value.set(None);
147 self.sync_geometry();
148 self.sync_visual_state();
149 self
150 }
151
152 pub fn colors(&self, colors: ProgressBarColors) -> &Self {
153 self.colors_value.set(Some(colors));
154 self.sync_visual_state();
155 self
156 }
157
158 pub fn clear_colors(&self) -> &Self {
159 self.colors_value.set(None);
160 self.sync_visual_state();
161 self
162 }
163
164 pub fn corner_radius(&self, radius: f32) -> &Self {
165 self.root.corner_radius(radius);
166 self.fill.corner_radius(inset_radius(radius));
167 self
168 }
169
170 pub fn current_value(&self) -> f32 {
171 self.value.get()
172 }
173
174 fn install_visual_subscriptions(&self) {
175 let weak_root = self.weak_root.clone();
176 let weak_fill = self.weak_fill.clone();
177 let sizing_value = self.sizing_value.clone();
178 let colors_value = self.colors_value.clone();
179 let guard = subscribe(move |_theme| {
180 let Some(root) = weak_root.upgrade() else {
181 return;
182 };
183 let Some(fill) = weak_fill.upgrade() else {
184 return;
185 };
186 sync_progress_visual_state(
187 &root,
188 &fill,
189 resolve_sizing(sizing_value.get()),
190 colors_value.get(),
191 );
192 });
193 self.root
194 .retained_node_ref()
195 .retain_attachment(Rc::new(guard));
196 }
197
198 fn handle_theme_changed(&self) {
199 self.sync_visual_state();
200 self.sync_geometry();
201 }
202
203 fn set_value(&self, value: f32) {
204 self.value.set(value.clamp(self.min.get(), self.max.get()));
205 self.sync_geometry();
206 }
207
208 fn sync_geometry(&self) {
209 sync_progress_geometry(
210 &self.root,
211 &self.fill,
212 self.min.get(),
213 self.max.get(),
214 self.value.get(),
215 self.orientation.get(),
216 resolve_sizing(self.sizing_value.get()),
217 );
218 }
219
220 fn sync_visual_state(&self) {
221 sync_progress_visual_state(
222 &self.root,
223 &self.fill,
224 resolve_sizing(self.sizing_value.get()),
225 self.colors_value.get(),
226 );
227 }
228}
229
230#[derive(Clone)]
231struct ProgressBarThemeTarget {
232 root: WeakFlexBox,
233 fill: WeakFlexBox,
234 min: Rc<Cell<f32>>,
235 max: Rc<Cell<f32>>,
236 value: Rc<Cell<f32>>,
237 orientation: Rc<Cell<Orientation>>,
238 sizing_value: Rc<Cell<Option<ProgressBarSizing>>>,
239 colors_value: Rc<Cell<Option<ProgressBarColors>>>,
240}
241
242impl ProgressBarThemeTarget {
243 fn upgrade(&self) -> Option<ProgressBar> {
244 Some(ProgressBar {
245 root: self.root.upgrade()?,
246 fill: self.fill.upgrade()?,
247 min: self.min.clone(),
248 max: self.max.clone(),
249 value: self.value.clone(),
250 orientation: self.orientation.clone(),
251 sizing_value: self.sizing_value.clone(),
252 colors_value: self.colors_value.clone(),
253 weak_root: self.root.clone(),
254 weak_fill: self.fill.clone(),
255 })
256 }
257}
258
259fn clamp_direct_dimension(method: &str, value: f32) -> f32 {
260 if value > 0.0 {
261 return value;
262 }
263 crate::logger::warn(
264 "Layout",
265 &format!("ProgressBar.{method}() received {value}; clamping to 1.0."),
266 );
267 1.0
268}
269
270impl Node for ProgressBar {
271 fn retained_node_ref(&self) -> NodeRef {
272 self.root.retained_node_ref()
273 }
274
275 fn build_self(&self) {
276 self.sync_visual_state();
277 self.sync_geometry();
278 self.root.build_self();
279 }
280}
281
282impl HasFlexBoxRoot for ProgressBar {
283 fn flex_box_root(&self) -> &FlexBox {
284 &self.root
285 }
286}
287
288impl ThemeBindable for ProgressBar {
289 fn theme_binding_node(&self) -> NodeRef {
290 self.root.retained_node_ref()
291 }
292
293 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
294 let target = ProgressBarThemeTarget {
295 root: self.weak_root.clone(),
296 fill: self.weak_fill.clone(),
297 min: self.min.clone(),
298 max: self.max.clone(),
299 value: self.value.clone(),
300 orientation: self.orientation.clone(),
301 sizing_value: self.sizing_value.clone(),
302 colors_value: self.colors_value.clone(),
303 };
304 Box::new(move || target.upgrade())
305 }
306}
307
308fn sync_progress_geometry(
309 root: &FlexBox,
310 fill: &FlexBox,
311 min: f32,
312 max: f32,
313 current_value: f32,
314 orientation: Orientation,
315 sizing: ResolvedProgressBarSizing,
316) {
317 let range = max - min;
318 let fraction = if range > 0.0 {
319 ((current_value - min) / range).clamp(0.0, 1.0)
320 } else {
321 0.0
322 };
323 let inset = PROGRESS_BORDER_WIDTH * 2.0;
324 let inner_length = (sizing.length - inset).max(0.0);
325 let inner_thickness = (sizing.thickness - inset).max(0.0);
326 let fill_length = inner_length * fraction;
327 if orientation == Orientation::Vertical {
328 root.flex_direction(FlexDirection::Column)
329 .justify_content(JustifyContent::End)
330 .width(sizing.thickness, Unit::Pixel)
331 .height(sizing.length, Unit::Pixel);
332 fill.width(inner_thickness, Unit::Pixel)
333 .height(fill_length, Unit::Pixel);
334 } else {
335 root.flex_direction(FlexDirection::Row)
336 .justify_content(JustifyContent::Start)
337 .width(sizing.length, Unit::Pixel)
338 .height(sizing.thickness, Unit::Pixel);
339 fill.width(fill_length, Unit::Pixel)
340 .height(inner_thickness, Unit::Pixel);
341 }
342 root.semantic_value_range(current_value, min, max)
343 .default_semantic_label(format!(
344 "Progress bar, value {}, range {} to {}",
345 current_value, min, max
346 ));
347}
348
349fn sync_progress_visual_state(
350 root: &FlexBox,
351 fill: &FlexBox,
352 sizing: ResolvedProgressBarSizing,
353 colors: Option<ProgressBarColors>,
354) {
355 let theme = current_theme();
356 let track_color = colors
357 .filter(ProgressBarColors::has_track)
358 .map(|colors| colors.track_color())
359 .unwrap_or(theme.colors.scrollbar_track);
360 let fill_color = colors
361 .filter(ProgressBarColors::has_fill)
362 .map(|colors| colors.fill_color())
363 .unwrap_or(theme.colors.accent);
364 root.apply_presenter_style(
365 crate::PresenterHostStyle::new()
366 .background(track_color)
367 .border(Border::solid(PROGRESS_BORDER_WIDTH, theme.colors.border))
368 .corners(crate::Corners::all(sizing.thickness * 0.5)),
369 );
370 let corners = root
371 .resolved_host_style()
372 .corners
373 .unwrap_or_else(|| crate::Corners::all(sizing.thickness * 0.5));
374 fill.corners(
375 inset_radius(corners.top_left),
376 inset_radius(corners.top_right),
377 inset_radius(corners.bottom_right),
378 inset_radius(corners.bottom_left),
379 )
380 .bg_color(fill_color);
381}