use crate::automation::peers::{AutomationPeer, ProgressRingAutomationPeer};
use crate::automation::{AccessibilityView, AutomationProperties};
use crate::primitives::{
ProgressRingTemplateSettings, TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl,
TemplatedControlImplExt,
};
use crate::shapes::Arc;
use crate::ControlImpl;
use ferroui_base::animation::{Animatable, IClock, IGlobalClock, TimeSpan};
use ferroui_base::input::InputElementImpl;
use ferroui_base::interactivity::InteractiveImpl;
use ferroui_base::layout::LayoutableImpl;
use ferroui_base::reactive::{AnonymousObserver, IDisposable};
use ferroui_base::{
ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_property, instantiate, DirectProperty,
FerroLocator, FerroObject, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs,
LocatorExtensions, Ref, StyledElementImpl, StyledProperty, Visual, VisualImpl, VisualImplExt,
VisualTreeAttachmentEventArgs,
};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
const PC_ACTIVE: &str = ":active";
const PC_DETERMINATE_ACTIVE: &str = ":determinate-active";
const PC_INACTIVE: &str = ":inactive";
const TIMELINE_SECONDS: f64 = 2.0;
struct ResetOnDrop<'a>(&'a Cell<bool>);
impl Drop for ResetOnDrop<'_> {
fn drop(&mut self) {
self.0.set(false);
}
}
#[repr(C)]
pub struct ProgressRing {
base: TemplatedControl,
settings: Ref<ProgressRingTemplateSettings>,
range_base_property_updating: Cell<bool>,
attached: Cell<bool>,
indicator: RefCell<Option<Ref<Arc>>>,
visibility_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
animation_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
animation_epoch: Cell<u64>,
animation_start: Cell<Option<TimeSpan>>,
animation_from: Cell<f64>,
animation_target: Cell<f64>,
displayed_progress: Cell<f64>,
indeterminate_phase: Cell<f64>,
}
ferro_class!(ProgressRing: TemplatedControl);
ferro_class_info!(ProgressRing {
new: ProgressRing::new,
markup: {
namespace: "FerroUI.Controls",
attributes: [
TemplatePart("PART_Indicator", type(Ref<Arc>)),
PseudoClasses(":active", ":determinate-active", ":inactive"),
],
},
});
ferro_impl_classes!(ProgressRing: StyledElementImpl, LayoutableImpl, InteractiveImpl, InputElementImpl);
impl FerroObjectImpl for ProgressRing {
fn constructed(this: &Self) {
Self::parent_constructed(this);
let weak = this.to_ref().downgrade();
let subscription = this.is_effectively_visible_changed(move || {
if let Some(owner) = weak.upgrade() {
owner.update_animation();
}
});
*this.visibility_subscription.borrow_mut() = Some(subscription);
this.update_states();
}
fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
Self::parent_on_property_changed(this, change);
if !change.is_effective_value_change() {
return;
}
let property = change.property();
if property == Self::value_property().as_property() {
this.on_range_base_property_changed(|ring| ring.coerce_value());
} else if property == Self::maximum_property().as_property() {
this.on_range_base_property_changed(|ring| {
ring.coerce_minimum();
ring.coerce_value();
});
} else if property == Self::minimum_property().as_property() {
this.on_range_base_property_changed(|ring| {
ring.coerce_maximum();
ring.coerce_value();
});
} else if property == Self::is_active_property().as_property()
|| property == Self::is_indeterminate_property().as_property()
{
this.stop_animation();
this.indeterminate_phase.set(0.0);
this.update_states();
this.update_progress();
} else if property == Animatable::clock_property().as_property() {
this.stop_animation();
this.update_animation();
} else if property == Visual::bounds_property().as_property() {
this.settings.update(this.bounds().width);
}
}
}
impl VisualImpl for ProgressRing {
fn on_attached_to_visual_tree(this: &Self, args: &VisualTreeAttachmentEventArgs) {
Self::parent_on_attached_to_visual_tree(this, args);
this.attached.set(true);
this.update_animation();
}
fn on_detached_from_visual_tree(this: &Self, args: &VisualTreeAttachmentEventArgs) {
this.attached.set(false);
this.stop_animation();
Self::parent_on_detached_from_visual_tree(this, args);
}
}
impl ControlImpl for ProgressRing {
fn on_create_automation_peer(this: &Self) -> Ref<AutomationPeer> {
ProgressRingAutomationPeer::new(this).upcast()
}
}
impl TemplatedControlImpl for ProgressRing {
fn on_apply_template(this: &Self, args: &TemplateAppliedEventArgs) {
this.stop_animation();
Self::parent_on_apply_template(this, args);
*this.indicator.borrow_mut() = args.name_scope().find_as::<Arc>("PART_Indicator");
this.settings.update(this.bounds().width);
this.update_states();
this.update_progress();
}
}
ferro_properties! { impl ProgressRing {
ferro_property!(
pub fn is_active_property() -> StyledProperty<bool> {
FerroProperty::register::<ProgressRing, _>("IsActive", true)
}
);
ferro_property!(
pub fn is_indeterminate_property() -> StyledProperty<bool> {
FerroProperty::register::<ProgressRing, _>("IsIndeterminate", true)
}
);
ferro_property!(
pub fn value_property() -> StyledProperty<f64> {
FerroProperty::register::<ProgressRing, _>("Value", 0.0)
}
);
ferro_property!(
pub fn minimum_property() -> StyledProperty<f64> {
FerroProperty::register::<ProgressRing, _>("Minimum", 0.0)
}
);
ferro_property!(
pub fn maximum_property() -> StyledProperty<f64> {
FerroProperty::register::<ProgressRing, _>("Maximum", 100.0)
}
);
ferro_property!(
pub fn template_settings_property() -> DirectProperty<ProgressRing, Ref<ProgressRingTemplateSettings>> {
FerroProperty::register_direct::<ProgressRing, _>(
"TemplateSettings",
|owner| owner.template_settings(),
None,
ProgressRingTemplateSettings::new(),
)
}
);
} }
impl ProgressRing {
pub fn construct() -> Self {
Self {
base: TemplatedControl::construct(),
settings: ProgressRingTemplateSettings::new(),
range_base_property_updating: Cell::new(false),
attached: Cell::new(false),
indicator: RefCell::new(None),
visibility_subscription: RefCell::new(None),
animation_subscription: RefCell::new(None),
animation_epoch: Cell::new(0),
animation_start: Cell::new(None),
animation_from: Cell::new(0.0),
animation_target: Cell::new(0.0),
displayed_progress: Cell::new(0.0),
indeterminate_phase: Cell::new(0.0),
}
}
pub fn new() -> Ref<Self> {
instantiate(Self::construct())
}
pub fn is_active(&self) -> bool {
self.get_value(Self::is_active_property())
}
pub fn set_is_active(&self, value: bool) {
FerroObject::set_value(self, Self::is_active_property(), value);
}
pub fn is_indeterminate(&self) -> bool {
self.get_value(Self::is_indeterminate_property())
}
pub fn set_is_indeterminate(&self, value: bool) {
FerroObject::set_value(self, Self::is_indeterminate_property(), value);
}
pub fn value(&self) -> f64 {
self.get_value(Self::value_property())
}
pub fn set_range_value(&self, value: f64) {
FerroObject::set_value(self, Self::value_property(), value);
}
pub fn minimum(&self) -> f64 {
self.get_value(Self::minimum_property())
}
pub fn set_minimum(&self, value: f64) {
FerroObject::set_value(self, Self::minimum_property(), value);
}
pub fn maximum(&self) -> f64 {
self.get_value(Self::maximum_property())
}
pub fn set_maximum(&self, value: f64) {
FerroObject::set_value(self, Self::maximum_property(), value);
}
pub fn template_settings(&self) -> Ref<ProgressRingTemplateSettings> {
self.settings.clone()
}
pub fn normalized_value(&self) -> f64 {
let range = self.maximum() - self.minimum();
let fraction = (self.value() - self.minimum()) / range;
if range > 0.0 && range.is_finite() && fraction.is_finite() {
fraction.clamp(0.0, 1.0)
} else {
0.0
}
}
fn on_range_base_property_changed(&self, coerce: impl FnOnce(&Self)) {
if self.range_base_property_updating.replace(true) {
return;
}
let _reset = ResetOnDrop(&self.range_base_property_updating);
coerce(self);
if !self.is_indeterminate() {
self.update_progress();
}
}
fn coerce_minimum(&self) {
let max = self.maximum();
if self.minimum() > max {
self.set_current_value(Self::minimum_property(), max);
}
}
fn coerce_maximum(&self) {
let min = self.minimum();
if self.maximum() < min {
self.set_current_value(Self::maximum_property(), min);
}
}
fn coerce_value(&self) {
let value = self.value();
if !value.is_nan() && !(value >= self.minimum() && value <= self.maximum()) {
let max = self.maximum();
self.set_current_value(Self::value_property(), if value > max { max } else { self.minimum() });
}
}
fn update_states(&self) {
let active = self.is_active();
let indeterminate = self.is_indeterminate();
let classes = self.pseudo_classes();
classes.set(PC_ACTIVE, active && indeterminate);
classes.set(PC_DETERMINATE_ACTIVE, active && !indeterminate);
classes.set(PC_INACTIVE, !active);
AutomationProperties::set_accessibility_view(
self,
if active { AccessibilityView::Content } else { AccessibilityView::Raw },
);
}
fn update_progress(&self) {
let target = self.normalized_value();
self.animation_from.set(self.displayed_progress.get());
self.animation_target.set(target);
if !self.is_indeterminate() {
self.animation_start.set(None);
if target <= self.displayed_progress.get()
|| !self.attached.get()
|| !self.is_active()
|| !self.is_effectively_visible()
{
self.displayed_progress.set(target);
}
}
self.update_geometry();
self.update_animation();
}
fn wants_animation(&self) -> bool {
self.attached.get()
&& self.is_active()
&& self.is_effectively_visible()
&& self.indicator.borrow().is_some()
&& (self.is_indeterminate() || self.displayed_progress.get() < self.animation_target.get())
}
fn update_animation(&self) {
if !self.wants_animation() {
self.stop_animation();
if !self.is_indeterminate() {
self.displayed_progress.set(self.animation_target.get());
self.update_geometry();
}
return;
}
if self.animation_subscription.borrow().is_some() {
return;
}
let clock: Option<Rc<dyn IClock>> = self.clock().or_else(|| {
FerroLocator::current().get_service::<dyn IGlobalClock>().map(|clock| clock as Rc<dyn IClock>)
});
let Some(clock) = clock else {
return;
};
let epoch = self.animation_epoch.get();
let weak = self.to_ref().downgrade();
let subscription = clock.subscribe(Rc::new(AnonymousObserver::new(move |time| {
if let Some(owner) = weak.upgrade() {
if owner.animation_epoch.get() == epoch {
owner.animation_tick(time);
}
}
})));
if self.animation_epoch.get() == epoch && self.wants_animation() {
*self.animation_subscription.borrow_mut() = Some(subscription);
} else {
subscription.dispose();
}
}
fn stop_animation(&self) {
self.animation_epoch.set(self.animation_epoch.get().wrapping_add(1));
self.animation_start.set(None);
let subscription = self.animation_subscription.borrow_mut().take();
if let Some(subscription) = subscription {
subscription.dispose();
}
}
fn animation_tick(&self, time: TimeSpan) {
if !self.wants_animation() {
self.stop_animation();
return;
}
let start = self.animation_start.get().unwrap_or_else(|| {
self.animation_start.set(Some(time));
time
});
let elapsed = (time - start).total_seconds().max(0.0);
if self.is_indeterminate() {
self.indeterminate_phase.set((elapsed / TIMELINE_SECONDS).fract());
} else {
let progress = self.animation_from.get() + elapsed / TIMELINE_SECONDS;
if progress >= self.animation_target.get() {
self.displayed_progress.set(self.animation_target.get());
self.stop_animation();
} else {
self.displayed_progress.set(progress);
}
}
self.update_geometry();
}
fn update_geometry(&self) {
let indicator = self.indicator.borrow().clone();
let Some(indicator) = indicator else { return };
if self.is_indeterminate() {
let phase = self.indeterminate_phase.get();
let (start, end) = if phase < 0.5 { (0.0, 0.0001 + phase * 2.0 * (0.5 - 0.0001)) } else { (phase - 0.5, 0.5) };
indicator.set_start_angle(-90.0 + phase * 900.0 + start * 360.0);
indicator.set_sweep_angle((end - start) * 360.0);
} else {
indicator.set_start_angle(-90.0);
indicator.set_sweep_angle(self.displayed_progress.get() * 360.0);
}
}
}
impl Drop for ProgressRing {
fn drop(&mut self) {
if let Some(subscription) = self.animation_subscription.get_mut().take() {
subscription.dispose();
}
if let Some(subscription) = self.visibility_subscription.get_mut().take() {
subscription.dispose();
}
}
}