Replies: 9 comments 42 replies
-
|
Beta Was this translation helpful? Give feedback.
-
I tried out your approach and I really like it! I have one question though: Let's call the "main" system that you run with bevy ...
.with_system(run_ui.exclusive_system())
... Do you have a smart way of passing the pub fn run_ui(world: &mut World) {
let mut egui_ctx = world.remove_resource::<EguiContext>().unwrap();
egui::CentralPanel::default.show(egui_ctx.ctx.mut(), ...);
world.insert_resource(egui_ctx);
} but that feels a bit wonky. Is there another approach to this? |
Beta Was this translation helpful? Give feedback.
-
I've been working with Egui in Bevy for a little bit and ran into similar challenges. So far my solution was just to put everything in a massive system parameter that had everything the entire UI tree needed, and while it worked, that system param kept getting bigger and bigger. 🙃 Then I ran into the sixteen-field limit on the derive and had to start nesting custom system parameters in each-other to get around it. Anyway, this looks great, and thanks for sharing! I'm going to try it out. |
Beta Was this translation helpful? Give feedback.
-
I just tried out an idea to let you write widgets as normal Bevy systems with a special expected For example, here's what I wanted to be able to do: fn main_menu_widget(
In(ui): In<WidgetSystemInput>,
mut menu_page: Local<MenuPage>,
) {
match *menu_page {
// home_menu_widget is another widget system like main_menu_widget
MenuPage::Main => widget(ui.with("home"), home_menu_widget),
}
} I almost got it working, but we've got an issue with the nested widget that I think might be logically impossible to workaround. Essentially, because rendering a new widget needs exclusive access to world, because it's In @aevyrie's original example, we don't have the issue because system params aren't passed in directly as args, just the Anyway, I think it's logically flawed, but I figured I'd share just in case somebody could prove me wrong. :) Here's the code: pub trait WidgetSystem<Out, Param: SystemParam, Marker>:
for<'a> SystemParamFunction<WidgetSystemInput<'a>, Out, Param, Marker>
{
type Param: SystemParam;
}
impl<
Out,
Param: SystemParam,
Marker,
T: for<'a> SystemParamFunction<WidgetSystemInput<'a>, Out, Param, Marker>,
> WidgetSystem<Out, Param, Marker> for T
{
type Param = Param;
}
pub struct WidgetSystemInput<'a> {
id: WidgetId,
ui: &'a mut egui::Ui,
world: &'a mut World,
}
impl<'a> WidgetSystemInput<'a> {
pub fn with(mut self, name: &str) -> Self {
self.id = self.id.with(name);
self
}
}
impl<'a> Deref for WidgetSystemInput<'a> {
type Target = egui::Ui;
fn deref(&self) -> &Self::Target {
&self.ui
}
}
impl<'a> DerefMut for WidgetSystemInput<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ui
}
}
pub fn widget<W, Out: 'static, Param: 'static, Marker: 'static>(
ui: WidgetSystemInput,
mut widget_system: W,
) where
W: WidgetSystem<Out, Param, Marker, Param = Param>,
Param: SystemParam,
{
let WidgetSystemInput { ui, world, id } = ui;
// We need to cache `SystemState` to allow for a system's locally tracked state
if !world.contains_resource::<StateInstances<Out, Param, Marker, W>>() {
// Note, this message should only appear once! If you see it twice in the logs, the function
// may have been called recursively, and will panic.
trace!("Init widget system state {}", std::any::type_name::<W>());
world.insert_resource(StateInstances::<Out, Param, Marker, W> {
instances: HashMap::new(),
});
}
world.resource_scope(
|world, mut states: Mut<StateInstances<Out, Param, Marker, W>>| {
if !states.instances.contains_key(&id) {
trace!(
"Registering widget system state for widget {id:?} of type {}",
std::any::type_name::<W>()
);
states.instances.insert(id, SystemState::new(world));
}
let cached_state = states.instances.get_mut(&id).unwrap();
// Can't borrow world mutably twice. 🙁
widget_system.run(
WidgetSystemInput { ui, world, id },
cached_state.get_mut(world),
);
cached_state.apply(world);
},
);
}
/// A UI widget may have multiple instances. We need to ensure the local state of these instances is
/// not shared. This hashmap allows us to dynamically store instance states.
#[derive(Default)]
struct StateInstances<Out, Param, Marker, T>
where
Param: SystemParam,
T: WidgetSystem<Out, Param, Marker>,
{
instances: HashMap<WidgetId, SystemState<T::Param>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WidgetId(pub u64);
impl WidgetId {
pub fn new(name: &str) -> Self {
let bytes = name.as_bytes();
let mut hasher = FxHasher32::default();
hasher.write(bytes);
WidgetId(hasher.finish())
}
pub fn with(&self, name: &str) -> WidgetId {
Self::new(&format!("{}{name}", self.0))
}
}
impl From<&str> for WidgetId {
fn from(s: &str) -> Self {
Self::new(s)
}
} |
Beta Was this translation helpful? Give feedback.
-
@mvlabat Not sure if you've seen this, but you might find it interesting or useful as the |
Beta Was this translation helpful? Give feedback.
-
Here's a small tweak I made in my project. I had a widget that needed to take a simple index value to tell it which player is was related to. Unfortunately now most widgets that don't take extra arguments have an extra ignored argument that looks like this pub trait WidgetSystem: SystemParam {
type Args;
fn system(
world: &mut World,
state: &mut SystemState<Self>,
ui: &mut egui::Ui,
id: WidgetId,
// Now you can pass arbitrary, yet statically typed, data into the widget
args: Self::Args,
);
}
#[derive(SystemParam)]
struct PlayerIndicator<'w, 's> {
commands: Commands<'w, 's>,
}
impl<'w, 's> WidgetSystem for PlayerIndicator<'w, 's> {
type Args = usize;
fn system(
world: &mut World,
state: &mut SystemState<Self>,
ui: &mut egui::Ui,
id: WidgetId,
player_idx: usize,
) {
// Widget code
}
}
#[derive(SystemParam)]
struct WithoutCustomArgs<'w, 's> {
commands: Commands<'w, 's>,
}
impl<'w, 's> WidgetSystem for WithoutCustomArgs<'w, 's> {
type Args = ();
fn system(
world: &mut World,
state: &mut SystemState<Self>,
ui: &mut egui::Ui,
id: WidgetId,
// Just ignore custom arguments
_: (),
) {
// Widget code
}
} |
Beta Was this translation helpful? Give feedback.
-
Update: The Original content: If you're ok not having access to I'm honestly not sure if the trade-off is worth it, but I'm going down this path for now. use bevy::prelude::*;
use bevy_egui::egui;
pub trait WidgetSystem: Default + Clone + Send + Sync {
fn run(&mut self, world: &mut World, ui: &mut egui::Ui, id: egui::Id);
}
pub fn widget<S: WidgetSystem + 'static>(world: &mut World, ui: &mut egui::Ui, id: egui::Id) {
let mut state = ui
.memory_mut(|mem| mem.data.get_temp::<S>(id))
.unwrap_or(S::default());
state.run(world, ui, id);
ui.memory_mut(|mem| mem.data.insert_temp(id, state));
} Usage: #[derive(Default, Clone)]
pub struct CountingButton {
count: usize,
}
impl WidgetSystem for CountingButton {
fn run(&mut self, world: &mut World, ui: &mut egui::Ui, _id: egui::Id) {
if ui.button(format!("click {}", &self.count)).clicked() {
world.spawn(Name::new(format!("ClickEntity{}", self.count)));
self.count += 1;
}
}
} |
Beta Was this translation helpful? Give feedback.
-
With @ItsDoot's recent work evolving this discussion, I came up with a way to do the egui-based widget calls without Preview (see link): fn display_number(In((egui_ctx, num)): In<(egui::Context, u32), ... your system parameters ...) -> egui::Context
{
...
egui_ctxt
}
let egui_ui = named_syscall(&mut world, "fps", (egui_ui, fps), display_number);
let egui_ui = named_syscall(&mut world, "health", (egui_ui, health), display_number); |
Beta Was this translation helpful? Give feedback.
-
With regards to nesting widgets with this approach, I guess this doesn't really play well with loops over queries? The query implicitly still borrows |
Beta Was this translation helpful? Give feedback.
-
At Foresight we're using bevy and egui to build 3D CAD desktop applications. I'd like to share a solution we've developed to solve some of the growing pains we've experienced. This solution gives us UI widgets that are easy to compose, can directly query the ECS, yet have a uniform function signature.
While bevy is not going to use
egui
for its first party UI solution, the purpose of this post is to provide some ideas for the future ofbevy_ui
. Speaking for myself, I'd love to switch over tobevy_ui
fromegui
if we can build a UI library that better integrates with the ECS, fills current accessibility gaps, and improves iteration times.Foreword
I'd like to credit @Ratysz and @TheRawMeatball for their help in pointing out bugs and shoring up my implementation with their in-depth knowledge of bevy's ECS, as well as @alice-i-cecile for her work on one-shot systems and pointing me in the right direction.
What do those fancy words mean?
There's a bit of jargon-y text in the intro. Here's what I mean:
Skip to the fun part, what does this look like?
In general, adding a
MyCoolWidget
widget looks like this:For a practical example showing an actual use case, let's start by looking at a window that has a few different modes. The user can be in one of these modes depending on whether they are viewing a list of entries, editing, or creating a new entry:
Note that all these widgets have the same signature,
widget::<MyWidget>(world, ui, "")
. Because this is a generic function, we have the same three parameters provided for any widget: theWorld
, the eguiUi
context, and a unique ID for that instance of the widget. Theid.with()
helper function simply concatenates the parent widget's id with this instance's id, resulting in a unique namespaced id.Widgets can be added inside other widgets; inside
ListView
we use common button widgets that are also used in other views:Background
egui is an immediate mode UI library. This means that the state of the UI is built from scratch every frame, but it also means the state is entirely transparent, without the need for propagating state through a retained tree. In bevy, using bevy_egui, this state is accessed through the
EguiContext
resource - a single struct that contains the entire UI state. When you have mutable access to theEguiContext
, you can add widgets, updating the state.Problem
Before using this widget solution, the most natural way to write an egui app in bevy is to use systems and functions:
This is fine, but things start to get more difficult as the GUI grows. Let's start adding a ribbon and tabs to the application:
As we add more stuff, the function signature of our bevy UI system grows, and grows, and grows. We can't just move the logic in our function to a new system, because we need the
Ui
to be in the correct state, e.g. inside thatWindow
closure.A more significant problem is that this makes leaky abstractions inevitable. Instead of being able to define self-contained widgets inside a "feature" crate, all of the implementation details of that feature end up leaking all over the UI code. As the UI grows more complex and you have layers and layers of function calls (i.e. window > panel > tab view > button), the parent function signatures start to grow rapidly. Every time you need to access a resource or component in your widget, you now have to add that to the function signature of every single parent function, all the way up to the top level system. Adding a new resource suddenly becomes a chore as you have to fix every call site that you just broke.
Solutions
It was pretty clear at this point that what we were doing was becoming more and more unmaintainable by the day.
The API I wanted to have, without really knowing if it was possible, was the ability to run functions as if they were their own systems. Instead of nested function calls, imagine if each widget could be called without needing to pass in resources. This would alleviate the problem of the ever-growing ui system function signature.
Passing around the
World
The simplest solution is to just pass the
World
as your only function parameter, and run the UI as an exclusive system. Your function can now get whatever it needs from the world. Performance isn't a problem, it's not as if you were running the UI in parallel before. This doesn't make for a great API surface, and it's hard to enforce any sort of guarantees if you just toss a function theWorld
and tell it to go have fun.The magic of
SystemState
Browsing through the ECS docs, trying to find some way to improve the situation, I ran across the
SystemState
docs. This is what tracks the state of bevy systems under the hood, it's the thing keeping track of query mutations and system local variables between runs of a system. Without going into too much detail, the final solution is a widget trait implemented on your widget, and a generic function that allows you to pass in thetype
of your widget:The
widget
function handles the creation and storage of every widget'sSystemState
in a resource. If you do want to get into the details, you can see the implementation below:SHOW ME THE CODE
Finally, let's see what it takes to define a widget. We start off by defining the widget's type. This is similar to defining a system:
This widget/system only needs access to
Commands
and a local resource. Pretty boring. Let's define what happens when we show our widget:That's all there is to it. We now have a widget we can add, and each instance of our
SpawnButton
widget will have its own localcounter
state. We can add as many instances as we need, and each one will have independent state:This is a pretty simple example, but it works with anything you could use in a bevy system.
Outcome
I'm pretty happy with the result. It's by no means perfect (we'll talk about that next), but it solved the biggest issues I was experiencing when scaling the application up. We can now develop features in independent crates, and only make widgets public, but keep all implementation details private to the crate. We're also looking to build off of this, adding stylesheets to our widgets.
Limitations and Future Work
This technique, in its current form has some known limitations.
Exclusive
World
accessThe ui system is an
exclusive_system
, meaning no other systems can run while this system is running. This might be intuitive - the UI needs mutable access to theWorld
, which means nothing else can access it while mutably borrowed.In practice, this doesn't seem to be a problem. Building the UI doesn't take long with egui, it's designed to build the entire UI from scratch in a few milliseconds, at most. And while we can't run other systems in parallel, it doesn't stop us from doing other useful work in an independent task pool.
This limitation could be removed if one-shot systems were allowed to be dynamically added to the schedule, and only mutably borrowed the components and resources they use, like normal systems.
Boilerplate
Unfortunately, there's no way I'm aware of to elide the lifetimes in a
SystemParam
. If you don't use one of the two lifetimes, you will need to add aPhantomData
field to ensure all lifetimes are used in the struct:While it looks ugly, it's not really a problem in practice. When widgets are used in the UI, it looks nice and is relatively information dense. Plus, you will only see these lifetimes in the struct where the widget is defined.
Nested widgets with conflicting data access
If you nest widgets that need access to the same data, there are cases where building recursive widgets is impossible. To be clear, the widgets presented here can be nested arbitrarily, it's an edge case we found that used a self-referential data structure.
Closing thoughts
While egui is quite different from bevy_ui, I think this is an interesting avenue worth exploring. Using systems as widgets makes accessing and updating ECS state through the UI both intuitive and composable, and makes it much easier to scale up GUI-heavy applications. If we end up merging one-shot systems, widgets like this might become even more ergonomic to write.
Beta Was this translation helpful? Give feedback.
All reactions