Skip to main content

ferroui_xaml_interactions/core/
timer_trigger.rs

1//! Port of `Core/TimerTrigger.cs`.
2
3use ferroui_base::reactive::IDisposable;
4use ferroui_base::threading::DispatcherTimer;
5use ferroui_base::utilities::EventArgs;
6use ferroui_base::{
7    ferro_class, ferro_class_info, ferro_properties, instantiate, BoxedValue, FerroObjectImpl, FerroProperty, Ref,
8    StyledElementImpl, StyledProperty,
9};
10use ferroui_xaml_interactivity::{
11    EventTriggerBase, EventTriggerBaseImpl, Interaction, StyledElementBehaviorImpl, StyledElementBehaviorImplExt,
12};
13use std::cell::{Cell, RefCell};
14use std::rc::Rc;
15use std::time::Duration;
16
17/// A trigger that invokes its actions after a specified interval.
18///
19/// The timer starts when the trigger is attached; the event of the trigger (see
20/// [`EventTriggerBase`]) restarts it.
21#[repr(C)]
22pub struct TimerTrigger {
23    base: EventTriggerBase,
24    timer: RefCell<Option<(Rc<DispatcherTimer>, Rc<dyn IDisposable>)>>,
25    current_tick: Cell<i32>,
26}
27
28ferro_class!(TimerTrigger: EventTriggerBase);
29ferro_class_info!(TimerTrigger { new: TimerTrigger::new });
30ferroui_base::ferro_impl_classes!(TimerTrigger: FerroObjectImpl, StyledElementImpl);
31
32ferro_properties! {
33    impl TimerTrigger {
34        /// Identifies the `MillisecondsPerTick` property.
35        pub fn milliseconds_per_tick_property() -> StyledProperty<i32> {
36            FerroProperty::register::<TimerTrigger, _>("MillisecondsPerTick", 1000)
37        }
38
39        /// Identifies the `TotalTicks` property.
40        pub fn total_ticks_property() -> StyledProperty<i32> {
41            FerroProperty::register::<TimerTrigger, _>("TotalTicks", 1)
42        }
43
44        /// Identifies the `RepeatForever` property.
45        pub fn repeat_forever_property() -> StyledProperty<bool> {
46            FerroProperty::register::<TimerTrigger, _>("RepeatForever", false)
47        }
48    }
49}
50
51ferroui_base::ferro_overrides! {
52    impl StyledElementBehaviorImpl for TimerTrigger {
53        fn on_attached(this: &Self) {
54            Self::parent_on_attached(this);
55            this.start_timer();
56        }
57
58        fn on_detaching(this: &Self) {
59            this.stop_timer();
60            Self::parent_on_detaching(this);
61        }
62    }
63}
64
65ferroui_base::ferro_overrides! {
66    impl EventTriggerBaseImpl for TimerTrigger {
67        /// Restarts the timer.
68        fn on_event(this: &Self, _event_args: Option<&BoxedValue>) {
69            this.stop_timer();
70            this.start_timer();
71        }
72    }
73}
74
75impl TimerTrigger {
76    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
77    pub fn construct() -> Self {
78        Self { base: EventTriggerBase::construct(), timer: RefCell::new(None), current_tick: Cell::new(0) }
79    }
80
81    /// Creates a trigger.
82    pub fn new() -> Ref<Self> {
83        instantiate(Self::construct())
84    }
85
86    /// Gets or sets the time, in milliseconds, between timer ticks.
87    pub fn milliseconds_per_tick(&self) -> i32 {
88        self.get_value(Self::milliseconds_per_tick_property())
89    }
90
91    pub fn set_milliseconds_per_tick(&self, value: i32) {
92        self.set_value(Self::milliseconds_per_tick_property(), value)
93    }
94
95    /// Gets or sets the number of ticks after which the trigger stops firing.
96    pub fn total_ticks(&self) -> i32 {
97        self.get_value(Self::total_ticks_property())
98    }
99
100    pub fn set_total_ticks(&self, value: i32) {
101        self.set_value(Self::total_ticks_property(), value)
102    }
103
104    /// Gets or sets a value indicating whether the timer repeats indefinitely.
105    pub fn repeat_forever(&self) -> bool {
106        self.get_value(Self::repeat_forever_property())
107    }
108
109    pub fn set_repeat_forever(&self, value: bool) {
110        self.set_value(Self::repeat_forever_property(), value)
111    }
112
113    fn start_timer(&self) {
114        if self.timer.borrow().is_some() {
115            return;
116        }
117
118        self.current_tick.set(0);
119        let milliseconds_per_tick = self.milliseconds_per_tick();
120        if milliseconds_per_tick < 0 {
121            panic!("TimeSpan period must be greater than or equal to zero. (Parameter 'value')");
122        }
123
124        let timer = DispatcherTimer::new();
125        timer.set_interval(Duration::from_millis(milliseconds_per_tick as u64));
126        let trigger = self.to_ref().downgrade();
127        let tick = timer.tick(move |_timer: &DispatcherTimer| {
128            if let Some(trigger) = trigger.upgrade() {
129                trigger.on_tick();
130            }
131        });
132        timer.start();
133        *self.timer.borrow_mut() = Some((timer, tick));
134    }
135
136    fn stop_timer(&self) {
137        let timer = self.timer.borrow_mut().take();
138        let Some((timer, tick)) = timer else {
139            return;
140        };
141
142        tick.dispose();
143        timer.stop();
144    }
145
146    fn on_tick(&self) {
147        self.current_tick.set(self.current_tick.get() + 1);
148        self.execute(Some(&(Rc::new(EventArgs::EMPTY) as BoxedValue)));
149
150        if !self.repeat_forever() && self.current_tick.get() >= self.total_ticks() {
151            self.stop_timer();
152        }
153    }
154
155    fn execute(&self, parameter: Option<&BoxedValue>) {
156        if !self.is_enabled() {
157            return;
158        }
159
160        let sender = self.associated_object().map(|associated_object| Rc::new(associated_object) as BoxedValue);
161        Interaction::execute_actions(sender.as_ref(), self.actions().as_ref(), parameter);
162    }
163}