Intermediate 16 min

Asynchronous tasks

Run cancellable work, keep completion on the UI thread, and prevent stale results.

Enable tasks explicitly

The tasks feature uses Tokio on native targets and browser-local futures on WebAssembly. Applications without it do not pull in Tokio.

Cargo.toml
[dependencies]
argui = { version = "0.3.2", features = ["tasks", "widget-input", "widget-button", "widget-vlist"] }

Replace stale work

spawn_latest stores ownership in a TaskSlot. Starting a new search cancels the previous delivery, including an older result already queued for the UI thread.

src/search.rs
fn search(&mut self, cx: &mut Context<Self>) -> Result<(), TaskError> {
    let query = self.query.clone();
    cx.spawn_latest(&mut self.task, load_results(query), |model, result, cx| {
        match result {
            Ok(Ok(items)) => model.results = items,
            Ok(Err(error)) => model.error = Some(error),
            Err(error) => model.error = Some(error.to_string()),
        }
        cx.notify();
    })?;
    Ok(())
}

Choose the right owner

ModelContext::spawn binds work to model lifetime. Context::spawn additionally binds it to one presentation. Dropping the handle, closing its scope, unmounting its owner, or shutting down the runtime cancels delivery.

Reference files

These are the implementation and guide files used for this chapter.

crates/argui-widget-gallery/src/pages/async_tasks.rsdocs/runtime/tasks.md
Compiled example

Cancellable search over 10,000 rows

The Rust file below is imported verbatim by this page and compiled into the WebAssembly application running underneath it.

Open exact source
app_examples/docs-examples/src/examples/tasks.rs
use std::time::Duration;

use argui::{
    runtime::{
        Context, Render,
        tasks::{TaskHandle, sleep},
    },
    text::TextStyle,
    ui::{Element, Sides, percent},
    widgets::{Button, default_theme},
};

#[derive(Default)]
pub struct Example {
    result: Option<String>,
    task: Option<TaskHandle>,
}

impl Render for Example {
    fn render(&mut self, cx: &mut Context<Self>) -> Element {
        let themes = default_theme(cx.environment());
        let theme = themes.resolve(cx.environment().color_scheme);
        let status = self.result.as_deref().unwrap_or("No task running");
        Element::column([
            Button::new("load", "Load asynchronously", theme.button())
                .enabled(self.task.is_none())
                .on_click(cx.event_handler(|app, _, cx| {
                    app.result = Some("Loading…".into());
                    app.task = cx
                        .spawn(
                            async { sleep(Duration::from_millis(650)).await },
                            |app, result, cx| {
                                result.expect("the local timer completes");
                                app.result = Some("Loaded without blocking the UI".into());
                                app.task = None;
                                cx.notify();
                            },
                        )
                        .ok();
                    cx.notify();
                }))
                .build(),
            Element::text(status).text_style(TextStyle {
                color: theme.foreground,
                ..TextStyle::default()
            }),
        ])
        .width(percent(1.0))
        .height(percent(1.0))
        .padding(Sides::length(28.0))
        .gap(16.0)
        .background(theme.background)
    }
}