Source Code
//! Async Tasks Example
//!
//! This example demonstrates different async patterns in GPUI:
//!
//! 1. `cx.spawn` - Foreground tasks for UI updates
//! 2. `cx.background_spawn` - Background tasks for heavy computation
//! 3. Task management - Storing, canceling, and detaching tasks
//! 4. Progress updates - Communicating from background to UI
#[path = "../prelude.rs"]
mod example_prelude;
use std::time::Duration;
use gpui::{
App, Application, Bounds, Colors, Context, Entity, Render, Task, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
};
// Example 1: Simple Foreground Task
//
// `cx.spawn` runs an async closure on the foreground thread.
// Use it when you need to perform async work that updates UI.
struct ForegroundTaskDemo {
message: String,
is_loading: bool,
}
impl ForegroundTaskDemo {
fn new() -> Self {
Self {
message: "Click to start a foreground task".into(),
is_loading: false,
}
}
fn start_task(&mut self, cx: &mut Context<Self>) {
self.is_loading = true;
self.message = "Loading...".into();
cx.notify();
cx.spawn(async move |this, cx| {
cx.background_spawn(async {
std::thread::sleep(Duration::from_secs(1));
})
.await;
this.update(cx, |this, cx| {
this.message = "Task completed!".into();
this.is_loading = false;
cx.notify();
})
.ok();
})
.detach();
}
}
// Example 2: Background Task with Progress
//
// `cx.background_spawn` runs work off the UI thread.
// Use it for heavy computation that shouldn't block the UI.
struct BackgroundTaskDemo {
progress: u32,
result: Option<u64>,
is_computing: bool,
}
impl BackgroundTaskDemo {
fn new() -> Self {
Self {
progress: 0,
result: None,
is_computing: false,
}
}
fn start_computation(&mut self, cx: &mut Context<Self>) {
self.is_computing = true;
self.progress = 0;
self.result = None;
cx.notify();
cx.spawn(async move |this, cx| {
for i in 0..100 {
let computation = cx.background_spawn(async move {
std::thread::sleep(Duration::from_millis(10));
(i + 1) as u64
});
let partial_result = computation.await;
this.update(cx, |this, cx| {
this.progress = i as u32 + 1;
this.result = Some(partial_result);
cx.notify();
})
.ok();
}
this.update(cx, |this, cx| {
this.is_computing = false;
cx.notify();
})
.ok();
})
.detach();
}
}
// Example 3: Cancellable Task
//
// Tasks can be cancelled by dropping them.
// Store a task in a field to keep it running.
struct CancellableTaskDemo {
counter: u32,
counting_task: Option<Task<()>>,
}
impl CancellableTaskDemo {
fn new() -> Self {
Self {
counter: 0,
counting_task: None,
}
}
fn is_running(&self) -> bool {
self.counting_task.is_some()
}
fn toggle(&mut self, cx: &mut Context<Self>) {
if self.counting_task.is_some() {
self.counting_task = None;
cx.notify();
} else {
self.counting_task = Some(cx.spawn(async move |this, cx| {
loop {
cx.background_spawn(async {
std::thread::sleep(Duration::from_millis(100));
})
.await;
let should_continue = this
.update(cx, |this, cx| {
this.counter += 1;
cx.notify();
true
})
.unwrap_or(false);
if !should_continue {
break;
}
}
}));
cx.notify();
}
}
}
// Example 4: Task with Return Value
//
// Tasks can return values that you can await.
struct ReturnValueDemo {
numbers: Vec<i32>,
sum: Option<i32>,
is_calculating: bool,
}
impl ReturnValueDemo {
fn new() -> Self {
Self {
numbers: vec![1, 2, 3, 4, 5],
sum: None,
is_calculating: false,
}
}
fn calculate_sum(&mut self, cx: &mut Context<Self>) {
self.is_calculating = true;
cx.notify();
let numbers = self.numbers.clone();
cx.spawn(async move |this, cx| {
let result = cx
.background_spawn(async move {
std::thread::sleep(Duration::from_millis(500));
numbers.iter().sum::<i32>()
})
.await;
this.update(cx, |this, cx| {
this.sum = Some(result);
this.is_calculating = false;
cx.notify();
})
.ok();
})
.detach();
}
fn randomize(&mut self, cx: &mut Context<Self>) {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let hasher = RandomState::new().build_hasher().finish();
self.numbers = (0..5)
.map(|i| ((hasher >> (i * 8)) & 0xFF) as i32 % 100)
.collect();
self.sum = None;
cx.notify();
}
}
// Main Application
struct AsyncTasksExample {
foreground_demo: Entity<ForegroundTaskDemo>,
background_demo: Entity<BackgroundTaskDemo>,
cancellable_demo: Entity<CancellableTaskDemo>,
return_demo: Entity<ReturnValueDemo>,
}
impl AsyncTasksExample {
fn new(cx: &mut Context<Self>) -> Self {
Self {
foreground_demo: cx.new(|_| ForegroundTaskDemo::new()),
background_demo: cx.new(|_| BackgroundTaskDemo::new()),
cancellable_demo: cx.new(|_| CancellableTaskDemo::new()),
return_demo: cx.new(|_| ReturnValueDemo::new()),
}
}
}
impl Render for AsyncTasksExample {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let foreground = self.foreground_demo.read(cx);
let background = self.background_demo.read(cx);
let cancellable = self.cancellable_demo.read(cx);
let return_demo = self.return_demo.read(cx);
div()
.id("main")
.size_full()
.p_6()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_6()
.max_w(px(500.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Async Tasks"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Spawning, background work, and task management"),
),
)
.child(demo_section(
&colors,
"1. Foreground Task (cx.spawn)",
"Runs async work on the UI thread. Good for sequential async operations.",
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_sm()
.text_color(colors.text)
.child(foreground.message.clone()),
)
.child(
button(&colors, "foreground-btn", "Start Task", foreground.is_loading)
.on_click(cx.listener(|this, _, _, cx| {
this.foreground_demo.update(cx, |demo, cx| {
demo.start_task(cx);
});
})),
),
))
.child(demo_section(
&colors,
"2. Background Task (cx.background_spawn)",
"Runs heavy computation off the UI thread with progress updates.",
div()
.flex()
.flex_col()
.gap_2()
.child(progress_bar(&colors, background.progress))
.child(
div()
.text_sm()
.text_color(colors.text)
.child(format!(
"Progress: {}% | Result: {}",
background.progress,
background
.result
.map(|r| r.to_string())
.unwrap_or_else(|| "-".into())
)),
)
.child(
button(&colors, "background-btn", "Compute", background.is_computing)
.on_click(cx.listener(|this, _, _, cx| {
this.background_demo.update(cx, |demo, cx| {
demo.start_computation(cx);
});
})),
),
))
.child(demo_section(
&colors,
"3. Cancellable Task",
"Store Task in a field to keep it running. Drop to cancel.",
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_2xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child(format!("{}", cancellable.counter)),
)
.child({
let is_running = cancellable.is_running();
let (bg, bg_hover) = if is_running {
(colors.error, colors.error_hover)
} else {
(colors.success, colors.success_hover)
};
div()
.id("cancel-btn")
.px_3()
.py_1p5()
.rounded_md()
.text_sm()
.text_color(colors.text)
.cursor_pointer()
.bg(bg)
.hover(move |style| style.bg(bg_hover))
.child(if is_running { "Stop" } else { "Start Counter" })
.on_click(cx.listener(|this, _, _, cx| {
this.cancellable_demo.update(cx, |demo, cx| {
demo.toggle(cx);
});
}))
}),
))
.child(demo_section(
&colors,
"4. Task with Return Value",
"Tasks can return values that can be awaited or used in chained operations.",
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child(format!("Numbers: {:?}", return_demo.numbers)),
)
.child(
div()
.text_sm()
.text_color(colors.text)
.child(format!(
"Sum: {}",
if return_demo.is_calculating {
"Calculating...".into()
} else {
return_demo
.sum
.map(|s| s.to_string())
.unwrap_or_else(|| "Not calculated".into())
}
)),
)
.child(
div()
.flex()
.gap_2()
.child(
button(
&colors,
"sum-btn",
"Calculate Sum",
return_demo.is_calculating,
)
.on_click(cx.listener(|this, _, _, cx| {
this.return_demo.update(cx, |demo, cx| {
demo.calculate_sum(cx);
});
})),
)
.child(
secondary_button(&colors, "random-btn", "Randomize")
.on_click(cx.listener(|this, _, _, cx| {
this.return_demo.update(cx, |demo, cx| {
demo.randomize(cx);
});
})),
),
),
))
)
}
}
// Helper Components
fn demo_section(
colors: &Colors,
title: &'static str,
description: &'static str,
content: impl IntoElement,
) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded_lg()
.bg(colors.surface)
.border_1()
.border_color(colors.border)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child(title),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child(description),
),
)
.child(content)
}
fn button(
colors: &Colors,
id: impl Into<gpui::ElementId>,
label: &'static str,
disabled: bool,
) -> gpui::Stateful<gpui::Div> {
let disabled_bg = colors.surface_hover;
let bg = colors.accent;
let bg_hover = colors.accent_hover;
let bg_active = colors.accent_active;
let text = colors.selected_text;
div()
.id(id)
.px_3()
.py_1p5()
.rounded_md()
.text_sm()
.text_color(text)
.when(disabled, |el| {
el.bg(disabled_bg).cursor_not_allowed().opacity(0.6)
})
.when(!disabled, |el| {
el.bg(bg)
.cursor_pointer()
.hover(move |style| style.bg(bg_hover))
.active(move |style| style.bg(bg_active))
})
.child(label)
}
fn secondary_button(
colors: &Colors,
id: impl Into<gpui::ElementId>,
label: &'static str,
) -> gpui::Stateful<gpui::Div> {
let bg = colors.surface_hover;
let bg_hover = colors.border;
let text = colors.text;
div()
.id(id)
.px_3()
.py_1p5()
.rounded_md()
.text_sm()
.text_color(text)
.bg(bg)
.cursor_pointer()
.hover(move |style| style.bg(bg_hover))
.child(label)
}
fn progress_bar(colors: &Colors, progress: u32) -> impl IntoElement {
let clamped = progress.min(100);
let bar_bg = colors.surface_hover;
let bar_fill = colors.success;
div()
.h_2()
.w_full()
.rounded_full()
.bg(bar_bg)
.overflow_hidden()
.child(
div()
.h_full()
.rounded_full()
.bg(bar_fill)
.w(gpui::relative(clamped as f32 / 100.0)),
)
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(550.), px(850.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|cx| AsyncTasksExample::new(cx)),
)
.expect("Failed to open window");
example_prelude::init_example(cx, "Async Tasks");
});
}