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 familiarwith the setInterval and clearInterval functions, which execute a callbackevery 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 determineswhether the timer should terminate itself (for example if the timer shouldrun only once, you can set this to Terminate, so that the timer is removedafter it's been called once) and an UpdateScreen, which is used to determinewhether the layout() function needs to be called again.

Additionally, timers have a TimerId, which can be used to identify instancesof 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, nothingwill happen (the second timer won't get added). This is done to prevent timersfrom 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 fieldbased on the state.stopwatch_start:

  1. fn timer_callback(state: &mut MyDataModel, _: &mut AppResources) -> (UpdateScreen, TerminateTimer) {
  2. state.stopwatch_time = Instant::now() - state.stopwatch_start;
  3. (Redraw, TerminateTimer::Continue)
  4. }

This timer, by itself, will count up towards infinity, but we can limit the timer toterminate itself after 5 seconds:

  1. fn on_start_timer_btn_clicked(app_state: &mut AppState<MyDataModel>, _: &mut CallbackInfo<MyDataModel>)
  2. -> UpdateScreen
  3. {
  4. app_state.modify(|state| state.stopwatch_start = Instant::now());
  5. let timer = Timer::new(timer_callback).with_timeout(Duration::from_secs(5));
  6. app_state.add_timer(TimerId::new(), timer);
  7. Redraw
  8. }

Now, in the layout() function, we can format our timer nicely:

  1. impl Layout for TimerApplication {
  2. fn layout(&self, _: LayoutInfo<Self>) -> Dom<Self> {
  3.  
  4. let sec_total = self.stopwatch_time.as_secs();
  5. let min = sec_total / 60;
  6. let secs = sec_total % 60;
  7. let ms = self.stopwatch_time.subsec_millis();
  8.  
  9. let current_time = Label::new(format!("{:02}:{:02}:{:02}", min, sec, ms)).dom();
  10. let start_button = Button::with_label("Start timer").dom()
  11. .with_callback(On::Click, Callback(on_start_timer_btn_clicked));
  12.  
  13. Dom::div()
  14. .with_child(current_time)
  15. .with_child(start_button)
  16. }
  17. }

Here we have a fully featured stopwatch - it's that easy. If the button isclicked, 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 limitedthe 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 - theyshouldn't be used for IO or heavy calculation, rather they should be used as timersor 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 aseparate 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 scopewhile the thread is still running. As an example:

  1. fn pure_function(input: usize) -> usize { input + 1 }
  2.  
  3. let thread_1 = Thread::new(5, pure_function);
  4. let thread_2 = Thread::new(10, pure_function);
  5. let thread_3 = Thread::new(20, pure_function);
  6.  
  7. // thread_1, thread_2 and thread_3 run in parallel here...
  8.  
  9. let result_1 = thread_1.await();
  10. let result_2 = thread_2.await();
  11. let result_3 = thread_3.await();
  12.  
  13. assert_eq!(result_1, Ok(6));
  14. assert_eq!(result_2, Ok(11));
  15. assert_eq!(result_3, Ok(21));

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 connectionto be established while still keeping the UI responsive. For this purpose youcan create a Task (which creates a std::thread internally) and hand thatTask to Azul, which will then automatically join it after the thread hasfinished running (so joining the thread won't block the UI).

A TaskCallback takes two arguments: an Arc<Mutex<T>> and a DropCheck type - the latteris necessary so that Azul can determine if the thread has finished executing (when theDropCheck is dropped, Azul will try to join the thread). Please simply ignore this argumentand don't use it inside the callback.

  1. fn do_something_async(app_data: Arc<Mutex<DataModel>>, _: DropCheck) {
  2. thread::sleep(Duration::from_secs(10));
  3. app_data.modify(|state| state.is_thread_finished = true);
  4. }
  5.  
  6. fn start_thread_on_btn_click(app_state: &mut AppState<MyDataModel>, _: &mut CallbackInfo<MyDataModel>)
  7. -> UpdateScreen
  8. {
  9. // data.clone() only clones the Arc, not a deep copy
  10. app_state.add_task(Task::new(self.data.clone(), connect_to_db_async));
  11. Redraw
  12. }

A note: If you'd put the thread::sleep inside of the .modify closure, you'd block the mainthread from redrawing the UI. So please only use modify and lock the data wnhen absolutelynecessary, i.e. when reading or writing data from / into the application data model. Also, after atask is finished, the UI will always redraw itself, this should be configurable in the futureand is a work in progress.

A Task can (optionally) have a Timer attached to it (via .then()) - this defines a timerthat is run immediately after the Task has ended. This is useful when working with non-threadsafedata, where some part of that data can be loaded on a background thread, but can'tbe 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 datawithin your data model.