Skip to main content

ferroui_controls/
progress_bar.rs

1use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
2use crate::primitives::{RangeBase, TemplateAppliedEventArgs, TemplatedControlImpl};
3use crate::{Border, ControlImpl};
4use ferroui_base::data::BindingMode;
5use ferroui_base::input::InputElementImpl;
6use ferroui_base::interactivity::InteractiveImpl;
7use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt, Orientation};
8use ferroui_base::reactive::IDisposable;
9use ferroui_base::{
10    ferro_class, ferro_impl_classes, ferro_property, instantiate, DirectProperty, FerroObject, FerroObjectExtensions,
11    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, Size, StaticType,
12    StyledElementImpl, StyledProperty, StyledPropertyMetadata, Visual, VisualImpl,
13};
14use std::cell::{Cell, RefCell};
15use std::rc::Rc;
16
17const PC_VERTICAL: &str = ":vertical";
18const PC_HORIZONTAL: &str = ":horizontal";
19const PC_INDETERMINATE: &str = ":indeterminate";
20
21/// Provides calculated values for use with the [`ProgressBar`]'s control
22/// theme or template.
23///
24/// This class is NOT intended for general use outside of control templates.
25#[repr(C)]
26pub struct ProgressBarTemplateSettings {
27    base: FerroObject,
28    container2_width: Cell<f64>,
29    container_width: Cell<f64>,
30    container_animation_start_position: Cell<f64>,
31    container_animation_end_position: Cell<f64>,
32    container2_animation_start_position: Cell<f64>,
33    container2_animation_end_position: Cell<f64>,
34    indeterminate_starting_offset: Cell<f64>,
35    indeterminate_ending_offset: Cell<f64>,
36}
37
38ferro_class!(ProgressBarTemplateSettings: FerroObject);
39ferroui_base::ferro_class_info!(ProgressBarTemplateSettings {
40    new: ProgressBarTemplateSettings::new,
41});
42ferro_impl_classes!(ProgressBarTemplateSettings: FerroObjectImpl);
43
44ferroui_base::ferro_properties! { impl ProgressBarTemplateSettings {
45    ferro_property!(
46        /// Defines the `ContainerAnimationStartPosition` property.
47        pub fn container_animation_start_position_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
48            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
49                "ContainerAnimationStartPosition",
50                |p| p.container_animation_start_position(),
51                Some(|p, o| p.set_container_animation_start_position(o)),
52                0.0,
53            )
54        }
55    );
56
57    ferro_property!(
58        /// Defines the `ContainerAnimationEndPosition` property.
59        pub fn container_animation_end_position_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
60            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
61                "ContainerAnimationEndPosition",
62                |p| p.container_animation_end_position(),
63                Some(|p, o| p.set_container_animation_end_position(o)),
64                0.0,
65            )
66        }
67    );
68
69    ferro_property!(
70        /// Defines the `Container2AnimationStartPosition` property.
71        pub fn container2_animation_start_position_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
72            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
73                "Container2AnimationStartPosition",
74                |p| p.container2_animation_start_position(),
75                Some(|p, o| p.set_container2_animation_start_position(o)),
76                0.0,
77            )
78        }
79    );
80
81    ferro_property!(
82        /// Defines the `Container2AnimationEndPosition` property.
83        pub fn container2_animation_end_position_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
84            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
85                "Container2AnimationEndPosition",
86                |p| p.container2_animation_end_position(),
87                Some(|p, o| p.set_container2_animation_end_position(o)),
88                0.0,
89            )
90        }
91    );
92
93    ferro_property!(
94        /// Defines the `Container2Width` property.
95        pub fn container2_width_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
96            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
97                "Container2Width",
98                |p| p.container2_width(),
99                Some(|p, o| p.set_container2_width(o)),
100                0.0,
101            )
102        }
103    );
104
105    ferro_property!(
106        /// Defines the `ContainerWidth` property.
107        pub fn container_width_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
108            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
109                "ContainerWidth",
110                |p| p.container_width(),
111                Some(|p, o| p.set_container_width(o)),
112                0.0,
113            )
114        }
115    );
116
117    ferro_property!(
118        /// Defines the `IndeterminateStartingOffset` property.
119        pub fn indeterminate_starting_offset_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
120            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
121                "IndeterminateStartingOffset",
122                |p| p.indeterminate_starting_offset(),
123                Some(|p, o| p.set_indeterminate_starting_offset(o)),
124                0.0,
125            )
126        }
127    );
128
129    ferro_property!(
130        /// Defines the `IndeterminateEndingOffset` property.
131        pub fn indeterminate_ending_offset_property() -> DirectProperty<ProgressBarTemplateSettings, f64> {
132            FerroProperty::register_direct::<ProgressBarTemplateSettings, _>(
133                "IndeterminateEndingOffset",
134                |p| p.indeterminate_ending_offset(),
135                Some(|p, o| p.set_indeterminate_ending_offset(o)),
136                0.0,
137            )
138        }
139    );
140} }
141
142impl ProgressBarTemplateSettings {
143    pub fn new() -> Ref<Self> {
144        instantiate(Self {
145            base: FerroObject::construct(),
146            container2_width: Cell::new(0.0),
147            container_width: Cell::new(0.0),
148            container_animation_start_position: Cell::new(0.0),
149            container_animation_end_position: Cell::new(0.0),
150            container2_animation_start_position: Cell::new(0.0),
151            container2_animation_end_position: Cell::new(0.0),
152            indeterminate_starting_offset: Cell::new(0.0),
153            indeterminate_ending_offset: Cell::new(0.0),
154        })
155    }
156
157    /// Used by the fluent theme to define the first indeterminate
158    /// indicator's width.
159    pub fn container_width(&self) -> f64 {
160        self.container_width.get()
161    }
162
163    pub fn set_container_width(&self, value: f64) {
164        self.set_and_raise_cell(Self::container_width_property(), &self.container_width, value);
165    }
166
167    /// Used by the fluent theme to define the second indeterminate
168    /// indicator's width.
169    pub fn container2_width(&self) -> f64 {
170        self.container2_width.get()
171    }
172
173    pub fn set_container2_width(&self, value: f64) {
174        self.set_and_raise_cell(Self::container2_width_property(), &self.container2_width, value);
175    }
176
177    /// Used by the fluent theme to define the first indeterminate
178    /// indicator's start position when animated.
179    pub fn container_animation_start_position(&self) -> f64 {
180        self.container_animation_start_position.get()
181    }
182
183    pub fn set_container_animation_start_position(&self, value: f64) {
184        self.set_and_raise_cell(
185            Self::container_animation_start_position_property(),
186            &self.container_animation_start_position,
187            value,
188        );
189    }
190
191    /// Used by the fluent theme to define the first indeterminate
192    /// indicator's end position when animated.
193    pub fn container_animation_end_position(&self) -> f64 {
194        self.container_animation_end_position.get()
195    }
196
197    pub fn set_container_animation_end_position(&self, value: f64) {
198        self.set_and_raise_cell(
199            Self::container_animation_end_position_property(),
200            &self.container_animation_end_position,
201            value,
202        );
203    }
204
205    /// Used by the fluent theme to define the second indeterminate
206    /// indicator's start position when animated.
207    pub fn container2_animation_start_position(&self) -> f64 {
208        self.container2_animation_start_position.get()
209    }
210
211    pub fn set_container2_animation_start_position(&self, value: f64) {
212        self.set_and_raise_cell(
213            Self::container2_animation_start_position_property(),
214            &self.container2_animation_start_position,
215            value,
216        );
217    }
218
219    /// Used by the fluent theme to define the second indeterminate
220    /// indicator's end position when animated.
221    pub fn container2_animation_end_position(&self) -> f64 {
222        self.container2_animation_end_position.get()
223    }
224
225    pub fn set_container2_animation_end_position(&self, value: f64) {
226        self.set_and_raise_cell(
227            Self::container2_animation_end_position_property(),
228            &self.container2_animation_end_position,
229            value,
230        );
231    }
232
233    /// Used by the simple theme to define the starting point of its
234    /// indeterminate animation.
235    pub fn indeterminate_starting_offset(&self) -> f64 {
236        self.indeterminate_starting_offset.get()
237    }
238
239    pub fn set_indeterminate_starting_offset(&self, value: f64) {
240        self.set_and_raise_cell(
241            Self::indeterminate_starting_offset_property(),
242            &self.indeterminate_starting_offset,
243            value,
244        );
245    }
246
247    /// Used by the simple theme to define the ending point of its
248    /// indeterminate animation.
249    pub fn indeterminate_ending_offset(&self) -> f64 {
250        self.indeterminate_ending_offset.get()
251    }
252
253    pub fn set_indeterminate_ending_offset(&self, value: f64) {
254        self.set_and_raise_cell(
255            Self::indeterminate_ending_offset_property(),
256            &self.indeterminate_ending_offset,
257            value,
258        );
259    }
260}
261
262/// A control used to indicate the progress of an operation.
263#[repr(C)]
264pub struct ProgressBar {
265    base: RangeBase,
266    percentage: Cell<f64>,
267    indicator: RefCell<Option<Ref<Border>>>,
268    track_size_changed_listener: RefCell<Option<Rc<dyn IDisposable>>>,
269    template_settings: Ref<ProgressBarTemplateSettings>,
270}
271
272ferro_class!(ProgressBar: RangeBase);
273// The `markup:` part is declared here and not generated: `TemplateSettings` is a plain property
274// (a property of the class that is not a registered one), which the bindings of the control
275// themes read (`{Binding $parent[ProgressBar].TemplateSettings.ContainerWidth}`).
276ferroui_base::ferro_class_info!(ProgressBar {
277    new: ProgressBar::new,
278    markup: {
279        properties: [
280            TemplateSettings: Ref<ProgressBarTemplateSettings> { get: ProgressBar::template_settings },
281        ],
282        attributes: [
283            TemplatePart("PART_Indicator", type(Ref<Border>), IsRequired = true),
284            PseudoClasses(":vertical", ":horizontal", ":indeterminate"),
285        ],
286    },
287});
288ferro_impl_classes!(ProgressBar: StyledElementImpl, VisualImpl, InteractiveImpl, InputElementImpl);
289
290impl ControlImpl for ProgressBar {
291    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
292        crate::automation::peers::ProgressBarAutomationPeer::new(this).upcast()
293    }
294}
295
296impl FerroObjectImpl for ProgressBar {
297    fn constructed(this: &Self) {
298        Self::parent_constructed(this);
299        this.update_pseudo_classes(Some(this.is_indeterminate()), Some(this.orientation()));
300    }
301
302    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
303        Self::parent_on_property_changed(this, change);
304
305        let property = change.property();
306
307        if property == RangeBase::value_property().as_property()
308            || property == RangeBase::minimum_property().as_property()
309            || property == RangeBase::maximum_property().as_property()
310            || property == Self::is_indeterminate_property().as_property()
311            || property == Self::orientation_property().as_property()
312        {
313            this.update_indicator();
314        }
315
316        if property == Self::is_indeterminate_property().as_property() {
317            this.update_pseudo_classes(Some(change.get_new_value::<bool>()), None);
318        } else if property == Self::orientation_property().as_property() {
319            this.update_pseudo_classes(None, Some(change.get_new_value::<Orientation>()));
320        }
321    }
322}
323
324impl LayoutableImpl for ProgressBar {
325    fn arrange_override(this: &Self, final_size: Size) -> Size {
326        let result = Self::parent_arrange_override(this, final_size);
327        this.update_indicator();
328        result
329    }
330}
331
332impl TemplatedControlImpl for ProgressBar {
333    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
334        // Dispose any previous track size listener.
335        let previous = this.track_size_changed_listener.borrow_mut().take();
336        if let Some(previous) = previous {
337            previous.dispose();
338        }
339
340        let indicator = e.name_scope().get_as::<Border>("PART_Indicator");
341        *this.indicator.borrow_mut() = Some(indicator.clone());
342
343        // Listen to size changes of the indicator's track (parent) and update
344        // the indicator there.
345        let listener = indicator.parent().map(|parent| {
346            let weak = this.to_ref().downgrade();
347            parent.get_property_changed_observable(Visual::bounds_property().as_property()).subscribe(move |_| {
348                if let Some(this) = weak.upgrade() {
349                    this.update_indicator();
350                }
351            })
352        });
353        *this.track_size_changed_listener.borrow_mut() = listener;
354
355        this.update_indicator();
356    }
357}
358
359impl ProgressBar {
360    /// The named parts expected in the control template.
361    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] =
362        &[TemplatePartAttribute::required("PART_Indicator", <Border as StaticType>::TYPE)];
363
364    /// The pseudoclasses set by the class.
365    pub const PSEUDO_CLASSES: PseudoClassesAttribute =
366        PseudoClassesAttribute::new(&[PC_VERTICAL, PC_HORIZONTAL, PC_INDETERMINATE]);
367}
368
369ferroui_base::ferro_properties! { impl ProgressBar {
370    ferro_property!(
371        /// Defines the `IsIndeterminate` property.
372        pub fn is_indeterminate_property() -> StyledProperty<bool> {
373            FerroProperty::register::<ProgressBar, _>("IsIndeterminate", false)
374        }
375    );
376
377    ferro_property!(
378        /// Defines the `ShowProgressText` property.
379        pub fn show_progress_text_property() -> StyledProperty<bool> {
380            FerroProperty::register::<ProgressBar, _>("ShowProgressText", false)
381        }
382    );
383
384    ferro_property!(
385        /// Defines the `ProgressTextFormat` property.
386        pub fn progress_text_format_property() -> StyledProperty<String> {
387            FerroProperty::register::<ProgressBar, _>("ProgressTextFormat", "{1:0}%".to_string())
388        }
389    );
390
391    ferro_property!(
392        /// Defines the `Orientation` property.
393        pub fn orientation_property() -> StyledProperty<Orientation> {
394            FerroProperty::register::<ProgressBar, _>("Orientation", Orientation::Horizontal)
395        }
396    );
397
398    ferro_property!(
399        /// Defines the `Percentage` property.
400        pub fn percentage_property() -> DirectProperty<ProgressBar, f64> {
401            FerroProperty::register_direct::<ProgressBar, _>("Percentage", |o| o.percentage(), None, 0.0)
402        }
403    );
404} }
405
406impl ProgressBar {
407    fn static_constructor() {
408        RangeBase::value_property().override_metadata::<ProgressBar>(
409            StyledPropertyMetadata::new(None).with_default_binding_mode(BindingMode::OneWay),
410        );
411    }
412
413    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
414    pub fn construct() -> Self {
415        Self {
416            base: RangeBase::construct(),
417            percentage: Cell::new(0.0),
418            indicator: RefCell::new(None),
419            track_size_changed_listener: RefCell::new(None),
420            template_settings: ProgressBarTemplateSettings::new(),
421        }
422    }
423
424    pub fn new() -> Ref<Self> {
425        instantiate(Self::construct())
426    }
427
428    /// The overall percentage complete of the progress.
429    ///
430    /// This read-only property is automatically calculated using the current
431    /// `Value` and the effective range (`Maximum` - `Minimum`).
432    pub fn percentage(&self) -> f64 {
433        self.percentage.get()
434    }
435
436    fn set_percentage(&self, value: f64) {
437        self.set_and_raise_cell(Self::percentage_property(), &self.percentage, value);
438    }
439
440    /// The template settings of the progress bar.
441    pub fn template_settings(&self) -> Ref<ProgressBarTemplateSettings> {
442        self.template_settings.clone()
443    }
444
445    /// Whether the progress bar shows the actual value or a generic,
446    /// continuous progress indicator (indeterminate state).
447    pub fn is_indeterminate(&self) -> bool {
448        self.get_value(Self::is_indeterminate_property())
449    }
450
451    pub fn set_is_indeterminate(&self, value: bool) {
452        self.set_value(Self::is_indeterminate_property(), value)
453    }
454
455    /// Whether progress text will be shown.
456    pub fn show_progress_text(&self) -> bool {
457        self.get_value(Self::show_progress_text_property())
458    }
459
460    pub fn set_show_progress_text(&self, value: bool) {
461        self.set_value(Self::show_progress_text_property(), value)
462    }
463
464    /// The format string applied to the internally calculated progress text
465    /// before it is shown.
466    pub fn progress_text_format(&self) -> String {
467        self.get_value(Self::progress_text_format_property())
468    }
469
470    pub fn set_progress_text_format(&self, value: impl Into<String>) {
471        self.set_value(Self::progress_text_format_property(), value.into())
472    }
473
474    /// The orientation of the progress bar.
475    pub fn orientation(&self) -> Orientation {
476        self.get_value(Self::orientation_property())
477    }
478
479    pub fn set_orientation(&self, value: Orientation) {
480        self.set_value(Self::orientation_property(), value)
481    }
482
483    fn update_indicator(&self) {
484        let Some(indicator) = self.indicator.borrow().clone() else {
485            return;
486        };
487
488        // Gets the size of the parent indicator container.
489        let bar_size = indicator.visual_parent().map_or_else(|| self.bounds().size(), |parent| parent.bounds().size());
490
491        if self.is_indeterminate() {
492            // Pulled from ModernWPF.
493
494            let dim = if self.orientation() == Orientation::Horizontal { bar_size.width } else { bar_size.height };
495            let bar_indicator_width = dim * 0.4; // Indicator width at 40% of the progress bar
496            let bar_indicator_width2 = dim * 0.6; // Indicator width at 60% of the progress bar
497
498            let settings = &self.template_settings;
499
500            settings.set_container_width(bar_indicator_width);
501            settings.set_container2_width(bar_indicator_width2);
502
503            settings.set_container_animation_start_position(bar_indicator_width * -1.8); // Position at -180%
504            settings.set_container_animation_end_position(bar_indicator_width * 3.0); // Position at 300%
505
506            settings.set_container2_animation_start_position(bar_indicator_width2 * -1.5); // Position at -150%
507            settings.set_container2_animation_end_position(bar_indicator_width2 * 1.66); // Position at 166%
508
509            settings.set_indeterminate_starting_offset(-dim);
510            settings.set_indeterminate_ending_offset(dim);
511        } else {
512            let (minimum, maximum) = (self.minimum(), self.maximum());
513            // The smallest positive value: the range is empty only when the
514            // bounds are equal.
515            let percent =
516                if (maximum - minimum).abs() < 5e-324 { 1.0 } else { (self.value() - minimum) / (maximum - minimum) };
517
518            // When the orientation changed, the indicator's width or height
519            // should be set to NaN. The indicator size calculation should
520            // consider the padding of the progress bar.
521            let margin = indicator.margin();
522            if self.orientation() == Orientation::Horizontal {
523                let width = (bar_size.width - margin.left - margin.right) * percent;
524                indicator.set_width(if width > 0.0 { width } else { 0.0 });
525                indicator.set_height(f64::NAN);
526            } else {
527                indicator.set_width(f64::NAN);
528                let height = (bar_size.height - margin.top - margin.bottom) * percent;
529                indicator.set_height(if height > 0.0 { height } else { 0.0 });
530            }
531
532            self.set_percentage(percent * 100.0);
533        }
534    }
535
536    fn update_pseudo_classes(&self, is_indeterminate: Option<bool>, o: Option<Orientation>) {
537        if let Some(is_indeterminate) = is_indeterminate {
538            self.pseudo_classes().set(PC_INDETERMINATE, is_indeterminate);
539        }
540
541        let Some(o) = o else {
542            return;
543        };
544        self.pseudo_classes().set(PC_VERTICAL, o == Orientation::Vertical);
545        self.pseudo_classes().set(PC_HORIZONTAL, o == Orientation::Horizontal);
546    }
547}