azul_core/task.rs
1use std::{
2 sync::{Arc, Mutex, Weak, atomic::{AtomicUsize, Ordering}},
3 thread::{self, JoinHandle},
4 time::{Duration, Instant},
5};
6use crate::{
7 FastHashMap,
8 callbacks::{
9 Redraw, DontRedraw, TimerCallback, TimerCallbackInfo, RefAny,
10 TimerCallbackReturn, TimerCallbackType, UpdateScreen,
11 },
12 app_resources::AppResources,
13};
14
15/// Should a timer terminate or not - used to remove active timers
16#[derive(Debug, Copy, Clone, PartialEq, Eq)]
17pub enum TerminateTimer {
18 /// Remove the timer from the list of active timers
19 Terminate,
20 /// Do nothing and let the timers continue to run
21 Continue,
22}
23
24static MAX_DAEMON_ID: AtomicUsize = AtomicUsize::new(0);
25
26/// ID for uniquely identifying a timer
27#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct TimerId { id: usize }
29
30impl TimerId {
31 /// Generates a new, unique `TimerId`.
32 pub fn new() -> Self {
33 TimerId { id: MAX_DAEMON_ID.fetch_add(1, Ordering::SeqCst) }
34 }
35}
36
37/// A `Timer` is a function that is run on every frame.
38///
39/// There are often a lot of visual tasks such as animations or fetching the
40/// next frame for a GIF or video, etc. - that need to run every frame or every X milliseconds,
41/// but they aren't heavy enough to warrant creating a thread - otherwise the framework
42/// would create too many threads, which leads to a lot of context switching and bad performance.
43///
44/// The callback of a `Timer` should be fast enough to run under 16ms,
45/// otherwise running timers will block the main UI thread.
46#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
47pub struct Timer {
48 /// Stores when the timer was created (usually acquired by `Instant::now()`)
49 pub created: Instant,
50 /// When the timer was last called (`None` only when the timer hasn't been called yet).
51 pub last_run: Option<Instant>,
52 /// If the timer shouldn't start instantly, but rather be delayed by a certain timeframe
53 pub delay: Option<Duration>,
54 /// How frequently the timer should run, i.e. set this to `Some(Duration::from_millis(16))`
55 /// to run the timer every 16ms. If this value is set to `None`, (the default), the timer
56 /// will execute the timer as-fast-as-possible (i.e. at a faster framerate
57 /// than the framework itself) - which might be performance intensive.
58 pub interval: Option<Duration>,
59 /// When to stop the timer (for example, you can stop the
60 /// execution after 5s using `Some(Duration::from_secs(5))`).
61 pub timeout: Option<Duration>,
62 /// Callback to be called for this timer
63 pub callback: TimerCallback,
64}
65
66impl Timer {
67
68 /// Create a new timer
69 pub fn new(callback: TimerCallbackType) -> Self {
70 Timer {
71 created: Instant::now(),
72 last_run: None,
73 delay: None,
74 interval: None,
75 timeout: None,
76 callback: TimerCallback(callback),
77 }
78 }
79
80 /// Delays the timer to not start immediately but rather
81 /// start after a certain time frame has elapsed.
82 #[inline]
83 pub fn with_delay(mut self, delay: Duration) -> Self {
84 self.delay = Some(delay);
85 self
86 }
87
88 /// Converts the timer into a timer, running the function only
89 /// if the given `Duration` has elapsed since the last run
90 #[inline]
91 pub fn with_interval(mut self, interval: Duration) -> Self {
92 self.interval = Some(interval);
93 self
94 }
95
96 /// Converts the timer into a countdown, by giving it a maximum duration
97 /// (counted from the creation of the Timer, not the first use).
98 #[inline]
99 pub fn with_timeout(mut self, timeout: Duration) -> Self {
100 self.timeout = Some(timeout);
101 self
102 }
103
104 /// Crate-internal: Invokes the timer if the timer and
105 /// the `self.timeout` allow it to
106 pub fn invoke<'a>(&mut self, info: TimerCallbackInfo<'a>) -> TimerCallbackReturn {
107
108 let instant_now = Instant::now();
109 let delay = self.delay.unwrap_or_else(|| Duration::from_millis(0));
110
111 // Check if the timers timeout is reached
112 if let Some(timeout) = self.timeout {
113 if instant_now - self.created > timeout {
114 return (DontRedraw, TerminateTimer::Terminate);
115 }
116 }
117
118 if let Some(interval) = self.interval {
119 let last_run = match self.last_run {
120 Some(s) => s,
121 None => self.created + delay,
122 };
123 if instant_now - last_run < interval {
124 return (DontRedraw, TerminateTimer::Continue);
125 }
126 }
127
128 let res = (self.callback.0)(info);
129
130 self.last_run = Some(instant_now);
131
132 res
133 }
134}
135
136/// Simple struct that is used by Azul internally to determine when the thread has finished executing.
137/// When this struct goes out of scope, Azul will call `.join()` on the thread (so in order to not
138/// block the main thread, simply let it go out of scope naturally.
139pub struct DropCheck(Arc<()>);
140
141/// A `Task` is a seperate thread that is owned by the framework.
142///
143/// In difference to a `Thread`, you don't have to `await()` the result of a `Task`,
144/// you can just hand the task to the framework (via `AppResources::add_task`) and
145/// the framework will automatically update the UI when the task is finished.
146/// This is useful to offload actions such as loading long files, etc. to a background thread.
147///
148/// Azul will join the thread automatically after it is finished (joining won't block the UI).
149pub struct Task {
150 // Thread handle of the currently in-progress task
151 join_handle: Option<JoinHandle<UpdateScreen>>,
152 dropcheck: Weak<()>,
153 /// Timer that will run directly after this task is completed.
154 pub after_completion_timer: Option<Timer>,
155}
156
157pub type TaskCallback<U> = fn(Arc<Mutex<U>>, DropCheck) -> UpdateScreen;
158
159impl Task {
160
161 /// Creates a new task from a callback and a set of input data - which has to be wrapped in an `Arc<Mutex<T>>>`.
162 pub fn new<U: Send + 'static>(data: Arc<Mutex<U>>, callback: TaskCallback<U>) -> Self {
163
164 let thread_check = Arc::new(());
165 let thread_weak = Arc::downgrade(&thread_check);
166 let thread_handle = thread::spawn(move || callback(data, DropCheck(thread_check)));
167
168 Self {
169 join_handle: Some(thread_handle),
170 dropcheck: thread_weak,
171 after_completion_timer: None,
172 }
173 }
174
175 /// Stores a `Timer` that will run after the task has finished.
176 ///
177 /// Often necessary to "clean up" or copy data from the background task into the UI.
178 #[inline]
179 pub fn then(mut self, timer: Timer) -> Self {
180 self.after_completion_timer = Some(timer);
181 self
182 }
183
184 /// Returns true if the task has been finished, false otherwise
185 pub fn is_finished(&self) -> bool {
186 self.dropcheck.upgrade().is_none()
187 }
188}
189
190impl Drop for Task {
191 fn drop(&mut self) {
192 if let Some(thread_handle) = self.join_handle.take() {
193 let _ = thread_handle.join().unwrap();
194 }
195 }
196}
197
198/// A `Thread` is a simple abstraction over `std::thread` that allows to offload a pure
199/// function to a different thread (essentially emulating async / await for older compilers).
200///
201/// # Warning
202///
203/// `Thread` panics if it goes out of scope before `.block()` was called.
204pub struct Thread<T> {
205 join_handle: Option<JoinHandle<T>>,
206}
207
208/// Error that can happen while calling `.block()`
209#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
210pub enum BlockError {
211 /// Arc::into_inner() failed
212 ArcUnlockError,
213 /// The background thread panicked
214 ThreadJoinError,
215 /// Mutex::into_inner() failed
216 MutexIntoInnerError,
217}
218
219impl<T> Thread<T> {
220
221 /// Creates a new thread that spawns a certain (pure) function on a separate thread.
222 /// This is a workaround until `await` is implemented. Note that invoking this function
223 /// will create an OS-level thread.
224 ///
225 /// **Warning**: You *must* call `.await()`, otherwise the `Thread` will panic when it is dropped!
226 ///
227 /// # Example
228 ///
229 /// ```rust
230 /// # extern crate azul_core;
231 /// # use azul_core::task::Thread;
232 /// #
233 /// fn pure_function(input: usize) -> usize { input + 1 }
234 ///
235 /// let thread_1 = Thread::new(5, pure_function);
236 /// let thread_2 = Thread::new(10, pure_function);
237 /// let thread_3 = Thread::new(20, pure_function);
238 ///
239 /// // thread_1, thread_2 and thread_3 run in parallel here...
240 ///
241 /// let result_1 = thread_1.block();
242 /// let result_2 = thread_2.block();
243 /// let result_3 = thread_3.block();
244 ///
245 /// assert_eq!(result_1, Ok(6));
246 /// assert_eq!(result_2, Ok(11));
247 /// assert_eq!(result_3, Ok(21));
248 /// ```
249 pub fn new<U>(initial_data: U, callback: fn(U) -> T) -> Self where T: Send + 'static, U: Send + 'static {
250 Self {
251 join_handle: Some(thread::spawn(move || callback(initial_data))),
252 }
253 }
254
255 /// Block until the internal thread has finished and return T
256 pub fn block(mut self) -> Result<T, BlockError> {
257 // .block() can only be called once, so these .unwrap()s are safe
258 let handle = self.join_handle.take().unwrap();
259 let data = handle.join().map_err(|_| BlockError::ThreadJoinError)?;
260 Ok(data)
261 }
262}
263
264impl<T> Drop for Thread<T> {
265 fn drop(&mut self) {
266 if self.join_handle.take().is_some() {
267 panic!("Thread has not been await()-ed correctly!");
268 }
269 }
270}
271
272/// Run all currently registered timers
273#[must_use]
274pub fn run_all_timers(
275 timers: &mut FastHashMap<TimerId, Timer>,
276 data: &mut RefAny,
277 resources: &mut AppResources,
278) -> UpdateScreen {
279
280 let mut should_update_screen = DontRedraw;
281 let mut timers_to_terminate = Vec::new();
282
283 for (key, timer) in timers.iter_mut() {
284 let (should_update, should_terminate) = timer.invoke(TimerCallbackInfo {
285 state: data,
286 app_resources: resources,
287 });
288
289 if should_update == Redraw {
290 should_update_screen = Redraw;
291 }
292
293 if should_terminate == TerminateTimer::Terminate {
294 timers_to_terminate.push(key.clone());
295 }
296 }
297
298 for key in timers_to_terminate {
299 timers.remove(&key);
300 }
301
302 should_update_screen
303}
304
305/// Remove all tasks that have finished executing
306#[must_use]
307pub fn clean_up_finished_tasks<T>(
308 tasks: &mut Vec<Task>,
309 timers: &mut FastHashMap<TimerId, Timer>,
310) -> UpdateScreen {
311
312 let old_count = tasks.len();
313 let mut timers_to_add = Vec::new();
314
315 tasks.retain(|task| {
316 if task.is_finished() {
317 if let Some(timer) = task.after_completion_timer {
318 timers_to_add.push((TimerId::new(), timer));
319 }
320 false
321 } else {
322 true
323 }
324 });
325
326 let timers_is_empty = timers_to_add.is_empty();
327 let new_count = tasks.len();
328
329 // Start all the timers that should run after the completion of the task
330 for (timer_id, timer) in timers_to_add {
331 timers.insert(timer_id, timer);
332 }
333
334 if old_count == new_count && timers_is_empty {
335 DontRedraw
336 } else {
337 Redraw
338 }
339}