bevyengine / bevyengine/bevy

Allow one-shot systems to be replaced, re-using the same SystemId

Open
#13,978 0 comments 1 reaction 0 assignees View on GitHub
A-ECS C-Usability D-Straightforward
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 22h
Merged PRs (30d)
161

Description

## What problem does this solve or what need does it fill?

A common use case for one-shot systems is callbacks and event handlers. However, these are often closures with capture variables. It will frequently be the case that the values of these capture variables will change as a consequence of the callback.

Take for example a checkbox which uses a one-shot system as a click event handler. The click handler will toggle the 'checked' state between true and false. This is fine if the checked state is stored on a Component or otherwise in the world, but what if it's stored somewhere else (either a local variable or a property of `self` such as `self.checked`)? In this case, changing the checked state won't have any effect on the one-shot system because it has already captured it's values at the time of registration. (For obvious reasons, `self` typically cannot be captured.)

One approach is to make all of these state variables lazily evaluated so that the closure can get the current value. But capturing lazy variables in closures is a fairly complex design challenge in Rust.

Of particular concern is callbacks invoked by event handlers from bevy_mod_picking. Again, in the checkbox example, the mod_picking click handler might call an "on_change" system. This is typical of the way widgets are designed: low-level events such as "click" and "pointer move" are often transformed into higher-level events such as "value changed" or "item selected". This is relatively straightforward to implement since the mod_picking handler can capture the SystemId of the higher-level handler as a closure capture.

However, it's problematic to be constantly removing and inserting bevy_mod_picking event handlers, which is what you would need to do in order to refresh their capture variables. In particular, removing the drag event handlers in mid-drag *cancels* the drag. So it's generally desirable to install the mod_picking handlers once and leave them alone - but this leaves us with no easy way to update the callback SystemId if it changed.

## What solution would you like?

I propose that we allow a one-shot SystemId to be re-used, replacing the function/closure with another function of the same type (that is, having the same input and output types - obviously the actual Fn type will be different since no two closures have the same type in Rust.)

This lets us replace the callback, but any closures which have captured the SystemId will still be valid.

The API I am envisioning is simple: it simply takes the old system id as a parameter, along with the new closure Fn.

## What alternative(s) have you considered?

Several :) The most likely alternate approach is to store the SystemId in some kind of lazy container, but this is hard because such a container is no longer Copy (making it hard to capture) unless it's a handle to some external state. One of the nice things about SystemIds is that they are Copy making them easy to capture.

## Additional context

In my UI framework, I provide a more friendly wrapper for creating one-shot callbacks, similar to React/Solid hooks:

```rust
let callback = cx.create_callback(|value: In| {
info!("Checked state: {}", *value);
})
```
Currently this creates the one-shot the first time it's evaluated, and never replaces it. However, it would be helpful if `create_callback` could be smart enough to replace the closure (possibly by adding a `deps` argument which is used to detect whether anything changed).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.