I am going to make an incredibly strong claim here: Async is the solution not only to UI, but also a perfect complement to bevy's ECS in general, and a great tool to build features ontop of and expose to advanced users.
https://notgull.net/async-gui/
Pre-reading
> If you treat events as what they actually are— things that are waiting to happen— then you can do a lot more with them.
NOTE- I have been working on this problem for a long time, and as such my code examples span multiple different versions of the code I was working on. I will note if a code example is REAL, as in it fully worked when written, or if it is SPECULATIVE.
Bevy is a data driven architecture, that really excels at certain things. The ECS means that it has a whole section of problem space under lockdown. Batch processing, interconnected systems, event readers and writers, queries. Systems running in orders, piping things, etc.
Observers in their current iteration are also their own very useful thing.
I am going to argue that async is in many ways a complement, the missing half of bevy.
When you're writing an event handler, like with an observer, what you're doing is *waiting for something to happen.*
With observers, this works perfectly fine as is for one-off interactions. But trying to interleave complexity of multiple events in different scenarios gets complicated. You start to have to share intermediate state through components that act as stateful message holders of some sort.
As an example, let's take a slider widget from the bevy headless widgets.
```rust=
/// Component used to manage the state of a slider during dragging.
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct CoreSliderDragState {
/// Whether the slider is currently being dragged.
pub dragging: bool,
/// The value of the slider when dragging started.
offset: f32,
}
```
You have this CoreSliderDragState, which contains this dragging flag, in order to communicate that state of the drag across multiple systems or observers.
```rust=
pub(crate) fn slider_on_drag_start(
mut drag_start: On<Pointer<DragStart>>,
mut q_slider: Query<
(
&SliderValue,
&mut CoreSliderDragState,
Has<InteractionDisabled>,
),
With<Slider>,
>,
) {
if let Ok((value, mut drag, disabled)) = q_slider.get_mut(drag_start.entity) {
drag_start.propagate(false);
if !disabled {
drag.dragging = true;
drag.offset = value.0;
}
}
}
pub(crate) fn slider_on_drag(
mut event: On<Pointer<Drag>>,
mut q_slider: Query<(
&ComputedNode,
&Slider,
&SliderRange,
Option<&SliderPrecision>,
&UiGlobalTransform,
&mut CoreSliderDragState,
Has<InteractionDisabled>,
)>,
q_thumb: Query<&ComputedNode, With<SliderThumb>>,
q_children: Query<&Children>,
mut commands: Commands,
ui_scale: Res<UiScale>,
) {
if let Ok((node, slider, range, precision, transform, drag, disabled)) =
q_slider.get_mut(event.entity)
{
event.propagate(false);
if drag.dragging && !disabled {
let mut distance = event.distance / ui_scale.0;
distance.y *= -1.;
let distance = transform.transform_vector2(distance);
// Find thumb size by searching descendants for the first entity with SliderThumb
let thumb_size = q_children
.iter_descendants(event.entity)
.find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
.unwrap_or(0.0);
let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
let span = range.span();
let new_value = if span > 0. {
drag.offset + (distance.x * span) / slider_width
} else {
range.start() + span * 0.5
};
let rounded_value = range.clamp(
precision
.map(|prec| prec.round(new_value))
.unwrap_or(new_value),
);
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(event.entity)
.insert(SliderValue(rounded_value));
} else {
commands.notify_with(
&slider.on_change,
ValueChange {
source: event.entity,
value: rounded_value,
},
);
}
}
}
}
pub(crate) fn slider_on_drag_end(
mut drag_end: On<Pointer<DragEnd>>,
mut q_slider: Query<(&Slider, &mut CoreSliderDragState)>,
) {
if let Ok((_slider, mut drag)) = q_slider.get_mut(drag_end.entity) {
drag_end.propagate(false);
if drag.dragging {
drag.dragging = false;
}
}
}
```
What is important to notice here, is that there is lots of duplicated logic, and structure between these 3 observers.
All of them individually requistion access to this `CoreSliderDragState` so that way they can share state between eachother, and they all also have to go through the process of getting the variable out of the query.
Worst of all, the logic is not linearly layed out.
In another world, your ideal code might look something like this.
**Speculative**:
```rust=
fn slider_spawned(slider_entity) {
loop {
// wait until we start dragging
wait::<DragStart>();
let offset = ...;
loop {
let drag = either(wait::<Drag>(), wait::<DragEnd>()) {
Drag(drag) => drag,
DragEnd(_) => break,
};
// do drag logic
}
}
}
```
This kind of logic prevents the need for a `CoreSliderState`, because the kind of state you're tracking, whether or not the `drag` has started, is just tracked by the linear flow of the program. You know if the drag has started because you waited unti the `DragStart` event happened.
All the information about what your program does is simplified and show in one place.
Something like `offset` can either be tracked locally as a variable, or if you want it exposed to the rest of the ecs you could store it in a component you modify. The choice is yours rather than it being forced onto you.
However, obviously this code doesn't function. You can't busy wait on events without bringing your whole program to a screeching halt. But with the use of async, we actually *can* write code this way.
Previously I used a crate called bevy_defer, with an addon I created, in order to write async ui code.
This was a prior version with some stuff stripped out **REAL**:
```rust=
fn on_add(trigger: Trigger<OnAdd, CoreSlider>, mut commands: Commands) {
let slider = trigger.target();
commands.spawn_task(async move || {
let slider = AsyncWorld.entity(slider);
let thumb = 'outer: loop {
...//yield every frame until we get a thumb child
};
let thumb = AsyncWorld.entity(thumb);
let drag_start = AsyncTrigger::<Pointer<DragStart>>::new(slider.id());
let drag = AsyncTrigger::<Pointer<Drag>>::new(slider.id());
let drag_end = AsyncTrigger::<Pointer<DragEnd>>::new(slider.id());
let drag_loop = async {
loop {
// wait until we start dragging
let _ = drag_start.clone().await;
// loop through
loop {
// every time we get a new drag event we do something with it
let drag = match future::select(drag.clone(), drag_end.clone()).await {
Either::Left((drag, _)) => drag,
Either::Right(_drag_end) => {
// when we get a drag end then we stop
break;
}
};
// do our dragging calculation logic
}
}
};
drag_loop.await;
Ok(())
});
}
```
This is it with *all* the code.
**Real**:
```rust=
impl CoreSlider {
fn on_add(trigger: Trigger<OnAdd, CoreSlider>, mut commands: Commands) {
let slider = trigger.target();
commands.spawn_task(async move || {
let slider = AsyncWorld.entity(slider);
let thumb = 'outer: loop {
let children = slider.component::<Children>().exists_watch()
.await.get(|children| children.iter().collect::<Vec<_>>())?;
for child in children.iter() {
if AsyncWorld.entity(*child).component::<CoreSliderThumb>().exists() {
break 'outer *child;
}
}
AsyncWorld.yield_now().await;
};
let thumb = AsyncWorld.entity(thumb);
let drag_start = AsyncTrigger::<Pointer<DragStart>>::new(slider.id());
let drag = AsyncTrigger::<Pointer<Drag>>::new(slider.id());
let drag_end = AsyncTrigger::<Pointer<DragEnd>>::new(slider.id());
let drag_loop = async {
loop {
// We wait until we get our drag start event
let _ = drag_start.clone().await;
let offset = slider.component::<SliderValue>().get(Clone::clone)?.0;
slider.component::<CoreSliderDragState>().set_offset(offset)?;
loop {
// We loop getting our drag event every frame until we get our drag end event
let (drag, _) = match future::select(drag.clone(), drag_end.clone()).await {
Either::Left((drag, _)) => drag,
Either::Right(_drag_end) => break,
};
let mut distance = AsyncWorld.resource_scope(|ui_scale: Mut<UiScale>|
drag.distance / ui_scale.0);
distance.y *= -1.0;
let thumb_size = thumb.component::<ComputedNode>().cloned()?.size.x;
let node = slider.component::<ComputedNode>().cloned()?;
let slider_width = ((node.size.x - thumb_size) * node.inverse_scale_factor).max(1.0);
let range = slider.component::<SliderRange>().get(Clone::clone)?;
let span = range.span();
let new_value = if span > 0. {
range.clamp(
slider.component::<CoreSliderDragState>().offset()?
+ (distance.x * span) / slider_width,
)
} else {
range.start() + span * 0.5
};
match slider.component::<CoreSlider>().cloned()?.on_change {
None => {
thumb.component::<Node>().get_mut(|thumb_node| {
thumb_node.left =
Val::Percent(range.thumb_position(new_value) * 97.5);
})?;
slider.insert(SliderValue(new_value))?;
}
Some(on_change) => {
AsyncWorld.run_system_with(on_change, new_value)?;
}
}
}
}
};
let thing: Result<Never, AccessError> = drag_loop.await;
thing.unwrap();
Ok(())
});
}
}
```
This works, and reduces the amount of code and what you have to track by quite a bit. Obviously however this isn't ideal.
I've been working on my own crate to increase the ergonomics, reduce the complexity, and reduce the overhead of async ui and async ecs in bevy.
The crate is not finished, but with that crate, we could rewrite this code as follows **SPECULATIVE**:
```rust=
// imagine this is a component in bsn where we get access to the #Thumb entity
Reactive(async |slider: Entity, cx: AsyncContext| loop {
//wait until we get our drag start event
cx.on::<Pointer<DragStart>, ()>(slider).await;
// we store the offset right when the drag starts, locally.
let offset: f32 = slider.component::<SliderValue>().get(&mut cx)?.0;
let drag = cx.on::<Pointer<Drag>, ()>(slider);
let drag_end = cx.on::<Pointer<DragEnd, ()>(slider);
loop {
// We loop getting our drag event every frame until we get our drag end event
let drag = match future::select(drag, drag_end).await {
Either::Left((drag, _)) => drag.distance * Vec2::new(1.0, -1.0),
// When the drag ends, we break the inner loop
Either::Right(_drag_end) => break,
};
let distance = drag / cx.resource::<UiScale>().0;
let thumb_size = #Thumb.component::<ComputedNode>().get(&mut cx)?.size.x;
let node_size = slider.component::<ComputedNode>().get(&mut cx)?.size.x;
let inverse_scale_factor = slider.component::<ComputedNode>().get(&mut cx)?.inverse_scale_factor;
let slider_width = ((node_size - thumb_size) * inverse_scale_factor).max(1.0);
let range = slider.component::<SliderRange>().get(&mut cx)?.clone();
let span = range.span();
let new_value = if span > 0. {
range.clamp(
offset + (distance.x * span) / slider_width,
)
} else {
range.start() + span * 0.5
};
let on_change = slider.component::<CoreSlider>().get(&mut cx)?.on_change.cloned();
match on_change {
None => {
thumb.component::<Node>().get(&mut cx)?.left = Val::Percent(range.thumb_position(new_value) * 97.5);
slider.insert(SliderValue(new_value)).await?;
}
Some(on_change) => {
cx.world_mut().await.run_system_with(on_change, new_value);
}
}
}
});
```
By using the power of futures, we can linearly lay out our logic more simply.
To further understand the power of being able to use async, here is a dropdown list i created using a prior version **REAL**:
```rust=
/// Component that indicates whether a menu is currently in an opened or state.
#[derive(Component, Default, Debug)]
pub struct Opened;
#[derive(Component)]
#[component(on_add = DropdownList::<T>::on_add)]
#[derive(Clone)]
pub struct DropdownList<T: std::fmt::Display + Clone + EntityEvent + PartialEq>(pub Vec<T>);
#[derive(Component, Clone)]
pub struct DropdownButton<T: std::fmt::Display + Clone + EntityEvent + PartialEq>(pub T);
impl<T: std::fmt::Display + Clone + EntityEvent + PartialEq> DropdownList<T> {
fn on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let dropdown = AsyncWorld.entity(entity);
world.commands().spawn_task(async move || {
// wait one frame so children are spawned
AsyncWorld.yield_now().await;
let button_entity = dropdown
.descendant_with::<DropdownButton<T>>()
.expect("didn't spawn a dropdown button with a dropdown list");
let button = button_entity.component::<DropdownButton<T>>();
loop {
let string = button.get(|b| b.0.to_string())?;
let button_child = button_entity.child(0)?;
button_child.component::<Text>().set(Text(string))?;
On::<Add, Pressed>::entity(button_entity).await;
dropdown.insert(Opened)?;
let mut dropdown_selected: Pin<Box<dyn Future<Output = _>>> =
Box::pin(On::<Add, Pressed>::entity(button_entity));
let mut to_cleanup = vec![];
for item in dropdown.component::<DropdownList<T>>().cloned()?.0 {
if item == button.cloned()?.0 {
continue;
}
let c = dropdown.spawn_child((
children![(
button_child.component::<TextFont>().cloned()?,
button_child.component::<TextColor>().cloned()?,
button_child.component::<TextShadow>().cloned()?,
Text(item.to_string()),
)],
button_entity.component::<Node>().cloned()?,
button_entity.component::<BorderColor>().cloned()?,
button_entity.component::<BorderRadius>().cloned()?,
BackgroundColor(HOVERED_BUTTON),
CoreButton { on_click: None },
DropdownButton(item),
))?;
dropdown_selected = combine(dropdown_selected, On::<Add, Pressed>::entity(c));
to_cleanup.push(c);
}
let new_selection = AsyncWorld
.entity(dropdown_selected.await.1)
.component::<DropdownButton<T>>()
.cloned()?;
button.set(new_selection.clone())?;
dropdown.trigger(new_selection.0.clone())?;
dropdown.remove::<Opened>()?;
to_cleanup.iter().for_each(AsyncEntityMut::despawn);
}
});
}
}
```
What is important to note here is line 50 and line 54.
We can combine all the futures of our dropdown options into one, and then poll for the first one, and get the entity out of it!
The logical crazyness that this would take with normal observers or systems would be so much more complicated.
In many ways the following I think is a really neat example of how concisce you can get, when you're able to use multiple different observers linearly in a single block of logic.
Here is a link to a video of this dropdown functioning: https://youtu.be/C_mdVOe0XZg
**Real**:
```rust=
fn on_add(trigger: On<Add, DemoButton>, mut commands: Commands) {
let button = AsyncWorld.entity(trigger.target().unwrap());
commands.spawn_task(async move || loop {
futures::select! {
_ = On::<Add, Pressed>::entity(button.id()).fuse() => (),
_ = On::<Remove, Pressed>::entity(button.id()).fuse() => (),
_ = On::<Add, InteractionDisabled>::entity(button.id()).fuse() => (),
_ = On::<Remove, InteractionDisabled>::entity(button.id()).fuse() => (),
_ = On::<Insert, Hovered>::entity(button.id()).fuse() => (),
}
let (t, c, b) = match (
button.component::<InteractionDisabled>().exists(),
button.component::<Hovered>().cloned()?.0,
button.component::<Pressed>().exists(),
) {
(true, _, _) => ("Disabled", NORMAL_BUTTON.into(), GRAY ), // Disabled button
(false, true, true) => ("Press", PRESSED_BUTTON.into(), RED ), // Pressed and hovered button
(false, true, false) => ("Hover", HOVERED_BUTTON.into(), WHITE), // Hovered, unpressed button
(false, false, _) => ("Button", NORMAL_BUTTON.into(), BLACK), // Unhovered button
};
button.child(0)?.component::<Text>().get_mut(|text| text.0 = t.to_string())?;
button.component::<BackgroundColor>().get_mut(|color| color.0 = c)?;
button.component::<BorderColor>().get_mut(|color| *color.set_all(b))?;
});
}
```
Again this was written with the prior version. A newer version would look like the following **SPECULATIVE**:
```rust=
fn on_add(trigger: On<Add, DemoButton>, mut commands: Commands) {
commands.insert(Reactive(async |button, mut cx| loop {
futures::select! {
_ = cx.on::<Add, Pressed>(button).fuse() => (),
_ = cx.on::<Remove, Pressed>(button).fuse() => (),
_ = cx.on::<Add, InteractionDisabled>(button).fuse() => (),
_ = cx.on::<Remove, InteractionDisabled>(button).fuse() => (),
_ = cx.on::<Insert, Hovered>(button).fuse() => (),
}
let (t, c, b) = match (
button.component::<InteractionDisabled>().exists(&cx),
button.component::<Hovered>().cloned(&cx)?.0,
button.component::<Pressed>().exists(&cx),
) {
(true, _, _) => ("Disabled", NORMAL_BUTTON.into(), GRAY ), // Disabled button
(false, true, true) => ("Press", PRESSED_BUTTON.into(), RED ), // Pressed and hovered button
(false, true, false) => ("Hover", HOVERED_BUTTON.into(), WHITE), // Hovered, unpressed button
(false, false, _) => ("Button", NORMAL_BUTTON.into(), BLACK), // Unhovered button
};
let child = button.child(&cx, 0)?;
child.component::<Text>().get(&mut cx)?.0 = t.to_string();
button.component::<BackgroundColor>().get(&mut cx)?.0 = c;
button.component::<BorderColor>().get(&mut cx)?.set_all(b);
}));
}
```
The newer versions of these that I am showcasing are still not finished, I am working on the crate for them and some but not all the functionality showcased is currently working.
There's other interesting and useful functionality one can use with async, as an example auto-cleanup **REAL**:
```rust=
cx.cleanup(async |mut cx| {
let e = cx.spawn_cleanup(Name::new("Hello")).await;
Ok(())
})
.await?;
```
Which cleans up the entities spawned with `spawn_cleanup` within it.
You are also able to wait upon the insertion of components, the removal of components, the spawning and deleting of entities.
Another useful but not yet implemented feature would be a generalized animation/timing feature **SPECUALTIVE**:
```rust=
let start = Duration::from_secs(1);
let end = Duration::from_secs(3);
let starting_position = transform.get(cx).x;
cx.animate(start, end, async |cx, delta| {
transform.get(cx).x = starting_position.lerp(starting_position+10.0, delta);
}).await;
```
How this would work, is delta is a number from 0.0 to 1.0 representing how far along the animation is.
The function *guarentees* that the closure will be called at least once with the value 1.0. It may or may not be called and arbitrary amount of times with values [0.0, 1.0], and it is monotonically increasing.
Timed commands could be implemented similarly.
Another useful side effect is the ability to integrate web requests or other network / io things.
You could easily create a UI that has a button that, once clicked, sends a web request, and then polls a second time on either the button being clicked again, or the web request finishing, and if the button is clicked again before the web request finishes it displays an error.
**SPECULATIVE**:
```rust=
let button_press = cx.on::<Pointer<Pressed>, ()>(button);
button_press.await;
let web_request = match future::select(web_request, button_press).await {
// Web request finishes
Either::Left((web_request, _)) => web_request,
// We press the button again before the request finishes
Either::Right((web_request_future, _)) => {
cx.spawn_cleanup(async move |cx| {
cx.spawn(Text::new("Please wait web request finishing")).await;
Ok(web_request_future.await)
}).await?
},
};
```
In general though, integrating async like this into bevy is useful for *more* than just UI.
There is desire to create Plugins as Entities, but also have plugins be async, waiting on resources and such.
With resources as entities, and this async ecs integration, we can easily integrate async plugins, without having to build specialized async infrastructure.
Bevy Remote Protocol can be simplified by integrating directly into inline async queries.
We can provide tools to integrate the asset system more closely with the ECS, by having the ability to asyncily interface with the world.
You might think that this will take a ton of code, but i already have quite a bit working in my current crate and it's only at 690 lines of code and we currently have auto cleanup, async observers, awaiting spawning and inserting components on entities and removing components on entities, and yielding for a frame, and yeilding to get back a &mut world. I think I will be able to have everything working in less than 2 thousand, likely more. This makes it easy to audit and not such a burden on contributors who don't want to touch much async code.