Timers, Threads and Animations
This page mainly concerns itself with offloading callbacks to background threads, for example in order to keep the UI responsive while waiting for a large file to load.
Timers
If you've ever had to develop a website with JavaScript, you might be familiar
with the setInterval
and clearInterval
functions, which execute a callback
every X milliseconds. Azul features a similar concept called "timers",
but takes a different approach than JavaScript.
A Timer
is simply a callback function that is run in the event loop (on the main thread).
A Timer
has full mutable access to the data model and is equipped with:
- An optional timeout duration (maximum duration that the daemon should run)
- An interval (how fast the timer should ring, i.e.: run only every 2 seconds)
- A delay (if the timer should only start after a certain time)
The TimerCallback
returns two values: A TerminateTimer
that determines
whether the timer should terminate itself (for example if the timer should
run only once, you can set this to Terminate
, so that the timer is removed
after it's been called once) and an UpdateScreen
, which is used to determine
whether the layout()
function needs to be called again.
Additionally, timers have a TimerId
, which can be used to identify instances
of a timer running, i.e. to check if a timer (with the same ID) is already running.
If you try to add a timer, but a timer with the same ID is already running, nothing
will happen (the second timer won't get added). This is done to prevent timers
from executing twice, i.e. if a user clicks the button twice, the second
.add_timer()
will automatically be ignored.
The following example shows a timer that updates the state.stopwatch_time
field
based on the state.stopwatch_start
:
This timer, by itself, will count up towards infinity, but we can limit the timer to terminate itself after 5 seconds:
Now, in the layout()
function, we can format our timer nicely:
Here we have a fully featured stopwatch - it's that easy. If the button is clicked, a timer will start and automatically terminate itself after 5 seconds. It will update the screen at the fastest possible framerate (since we haven't limited the interval of the timer yet), so that might be something to improve.
Timers are always run on the main thread, before any callbacks are run - they shouldn't be used for IO or heavy calculation, rather they should be used as timers or as things that should check / poll for something every X seconds.
Threads
A Thread
is a slight abstraction in order to easily offload calculations to a
separate thread. They take a pure function (a function which has a signature of U -> T
)
and run it in a new thread (as a slight abstraction over std::thread
).
You must call .await()
on the thread, otherwise it will panic if it goes out of scope
while the thread is still running. As an example:
let thread_1 = new;
let thread_2 = new;
let thread_3 = new;
// thread_1, thread_2 and thread_3 run in parallel here...
let result_1 = thread_1.await;
let result_2 = thread_2.await;
let result_3 = thread_3.await;
assert_eq!;
assert_eq!;
assert_eq!;
Tasks
A Task
is more or less the same as a thread, but handled by the framework.
For example, you might want to wait for a file to load or a database connection
to be established while still keeping the UI responsive. For this purpose you
can create a Task
(which creates a std::thread
internally) and hand that
Task
to Azul, which will then automatically join it after the thread has
finished running (so joining the thread won't block the UI).
A TaskCallback
takes two arguments: an Arc<Mutex<T>>
and a DropCheck
type - the latter
is necessary so that Azul can determine if the thread has finished executing (when the
DropCheck
is dropped, Azul will try to join the thread). Please simply ignore this argument
and don't use it inside the callback.
A note: If you'd put the thread::sleep
inside of the .modify
closure, you'd block the main
thread from redrawing the UI. So please only use modify
and lock the data wnhen absolutely
necessary, i.e. when reading or writing data from / into the application data model. Also, after a
task is finished, the UI will always redraw itself, this should be configurable in the future
and is a work in progress.
A Task
can (optionally) have a Timer
attached to it (via .then()
) - this defines a timer
that is run immediately after the Task
has ended. This is useful when working with non-threadsafe
data, where some part of that data can be loaded on a background thread, but can't
be prepared for a UI visualization from a non-main thread.
Summary
In this chapter you've learned how to use timers, async IO and handle thread-safe and non-thread safe data within your data model.