mirror of
https://github.com/bevyengine/bevy
synced 2024-12-29 22:43:14 +00:00
a35811d088
# Objective - Fixes #16208 ## Solution - Added an associated type to `Component`, `Mutability`, which flags whether a component is mutable, or immutable. If `Mutability= Mutable`, the component is mutable. If `Mutability= Immutable`, the component is immutable. - Updated `derive_component` to default to mutable unless an `#[component(immutable)]` attribute is added. - Updated `ReflectComponent` to check if a component is mutable and, if not, panic when attempting to mutate. ## Testing - CI - `immutable_components` example. --- ## Showcase Users can now mark a component as `#[component(immutable)]` to prevent safe mutation of a component while it is attached to an entity: ```rust #[derive(Component)] #[component(immutable)] struct Foo { // ... } ``` This prevents creating an exclusive reference to the component while it is attached to an entity. This is particularly powerful when combined with component hooks, as you can now fully track a component's value, ensuring whatever invariants you desire are upheld. Before this would be done my making a component private, and manually creating a `QueryData` implementation which only permitted read access. <details> <summary>Using immutable components as an index</summary> ```rust /// This is an example of a component like [`Name`](bevy::prelude::Name), but immutable. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Component)] #[component( immutable, on_insert = on_insert_name, on_replace = on_replace_name, )] pub struct Name(pub &'static str); /// This index allows for O(1) lookups of an [`Entity`] by its [`Name`]. #[derive(Resource, Default)] struct NameIndex { name_to_entity: HashMap<Name, Entity>, } impl NameIndex { fn get_entity(&self, name: &'static str) -> Option<Entity> { self.name_to_entity.get(&Name(name)).copied() } } fn on_insert_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) { let Some(&name) = world.entity(entity).get::<Name>() else { unreachable!() }; let Some(mut index) = world.get_resource_mut::<NameIndex>() else { return; }; index.name_to_entity.insert(name, entity); } fn on_replace_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) { let Some(&name) = world.entity(entity).get::<Name>() else { unreachable!() }; let Some(mut index) = world.get_resource_mut::<NameIndex>() else { return; }; index.name_to_entity.remove(&name); } // Setup our name index world.init_resource::<NameIndex>(); // Spawn some entities! let alyssa = world.spawn(Name("Alyssa")).id(); let javier = world.spawn(Name("Javier")).id(); // Check our index let index = world.resource::<NameIndex>(); assert_eq!(index.get_entity("Alyssa"), Some(alyssa)); assert_eq!(index.get_entity("Javier"), Some(javier)); // Changing the name of an entity is also fully capture by our index world.entity_mut(javier).insert(Name("Steven")); // Javier changed their name to Steven let steven = javier; // Check our index let index = world.resource::<NameIndex>(); assert_eq!(index.get_entity("Javier"), None); assert_eq!(index.get_entity("Steven"), Some(steven)); ``` </details> Additionally, users can use `Component<Mutability = ...>` in trait bounds to enforce that a component _is_ mutable or _is_ immutable. When using `Component` as a trait bound without specifying `Mutability`, any component is applicable. However, methods which only work on mutable or immutable components are unavailable, since the compiler must be pessimistic about the type. ## Migration Guide - When implementing `Component` manually, you must now provide a type for `Mutability`. The type `Mutable` provides equivalent behaviour to earlier versions of `Component`: ```rust impl Component for Foo { type Mutability = Mutable; // ... } ``` - When working with generic components, you may need to specify that your generic parameter implements `Component<Mutability = Mutable>` rather than `Component` if you require mutable access to said component. - The entity entry API has had to have some changes made to minimise friction when working with immutable components. Methods which previously returned a `Mut<T>` will now typically return an `OccupiedEntry<T>` instead, requiring you to add an `into_mut()` to get the `Mut<T>` item again. ## Draft Release Notes Components can now be made immutable while stored within the ECS. Components are the fundamental unit of data within an ECS, and Bevy provides a number of ways to work with them that align with Rust's rules around ownership and borrowing. One part of this is hooks, which allow for defining custom behavior at key points in a component's lifecycle, such as addition and removal. However, there is currently no way to respond to _mutation_ of a component using hooks. The reasons for this are quite technical, but to summarize, their addition poses a significant challenge to Bevy's core promises around performance. Without mutation hooks, it's relatively trivial to modify a component in such a way that breaks invariants it intends to uphold. For example, you can use `core::mem::swap` to swap the components of two entities, bypassing the insertion and removal hooks. This means the only way to react to this modification is via change detection in a system, which then begs the question of what happens _between_ that alteration and the next run of that system? Alternatively, you could make your component private to prevent mutation, but now you need to provide commands and a custom `QueryData` implementation to allow users to interact with your component at all. Immutable components solve this problem by preventing the creation of an exclusive reference to the component entirely. Without an exclusive reference, the only way to modify an immutable component is via removal or replacement, which is fully captured by component hooks. To make a component immutable, simply add `#[component(immutable)]`: ```rust #[derive(Component)] #[component(immutable)] struct Foo { // ... } ``` When implementing `Component` manually, there is an associated type `Mutability` which controls this behavior: ```rust impl Component for Foo { type Mutability = Mutable; // ... } ``` Note that this means when working with generic components, you may need to specify that a component is mutable to gain access to certain methods: ```rust // Before fn bar<C: Component>() { // ... } // After fn bar<C: Component<Mutability = Mutable>>() { // ... } ``` With this new tool, creating index components, or caching data on an entity should be more user friendly, allowing libraries to provide APIs relying on components and hooks to uphold their invariants. ## Notes - ~~I've done my best to implement this feature, but I'm not happy with how reflection has turned out. If any reflection SMEs know a way to improve this situation I'd greatly appreciate it.~~ There is an outstanding issue around the fallibility of mutable methods on `ReflectComponent`, but the DX is largely unchanged from `main` now. - I've attempted to prevent all safe mutable access to a component that does not implement `Component<Mutability = Mutable>`, but there may still be some methods I have missed. Please indicate so and I will address them, as they are bugs. - Unsafe is an escape hatch I am _not_ attempting to prevent. Whatever you do with unsafe is between you and your compiler. - I am marking this PR as ready, but I suspect it will undergo fairly major revisions based on SME feedback. - I've marked this PR as _Uncontroversial_ based on the feature, not the implementation. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Benjamin Brienen <benjamin.brienen@outlook.com> Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Co-authored-by: Nuutti Kotivuori <naked@iki.fi>
459 lines
18 KiB
Rust
459 lines
18 KiB
Rust
//! Demonstrates how Display and Visibility work in the UI.
|
|
|
|
use bevy::{
|
|
color::palettes::css::{DARK_CYAN, DARK_GRAY, YELLOW},
|
|
ecs::component::Mutable,
|
|
prelude::*,
|
|
winit::WinitSettings,
|
|
};
|
|
|
|
const PALETTE: [&str; 4] = ["27496D", "466B7A", "669DB3", "ADCBE3"];
|
|
const HIDDEN_COLOR: Color = Color::srgb(1.0, 0.7, 0.7);
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
|
.insert_resource(WinitSettings::desktop_app())
|
|
.add_systems(Startup, setup)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
buttons_handler::<Display>,
|
|
buttons_handler::<Visibility>,
|
|
text_hover,
|
|
),
|
|
)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Target<T> {
|
|
id: Entity,
|
|
phantom: std::marker::PhantomData<T>,
|
|
}
|
|
|
|
impl<T> Target<T> {
|
|
fn new(id: Entity) -> Self {
|
|
Self {
|
|
id,
|
|
phantom: std::marker::PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
trait TargetUpdate {
|
|
type TargetComponent: Component<Mutability = Mutable>;
|
|
const NAME: &'static str;
|
|
fn update_target(&self, target: &mut Self::TargetComponent) -> String;
|
|
}
|
|
|
|
impl TargetUpdate for Target<Display> {
|
|
type TargetComponent = Node;
|
|
const NAME: &'static str = "Display";
|
|
fn update_target(&self, node: &mut Self::TargetComponent) -> String {
|
|
node.display = match node.display {
|
|
Display::Flex => Display::None,
|
|
Display::None => Display::Flex,
|
|
Display::Block | Display::Grid => unreachable!(),
|
|
};
|
|
format!("{}::{:?} ", Self::NAME, node.display)
|
|
}
|
|
}
|
|
|
|
impl TargetUpdate for Target<Visibility> {
|
|
type TargetComponent = Visibility;
|
|
const NAME: &'static str = "Visibility";
|
|
fn update_target(&self, visibility: &mut Self::TargetComponent) -> String {
|
|
*visibility = match *visibility {
|
|
Visibility::Inherited => Visibility::Visible,
|
|
Visibility::Visible => Visibility::Hidden,
|
|
Visibility::Hidden => Visibility::Inherited,
|
|
};
|
|
format!("{}::{visibility:?}", Self::NAME)
|
|
}
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
let palette: [Color; 4] = PALETTE.map(|hex| Srgba::hex(hex).unwrap().into());
|
|
|
|
let text_font = TextFont {
|
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
|
..default()
|
|
};
|
|
|
|
commands.spawn(Camera2d);
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
width: Val::Percent(100.),
|
|
height: Val::Percent(100.),
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::SpaceEvenly,
|
|
..Default::default()
|
|
},
|
|
BackgroundColor(Color::BLACK),
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
Text::new("Use the panel on the right to change the Display and Visibility properties for the respective nodes of the panel on the left"),
|
|
text_font.clone(),
|
|
TextLayout::new_with_justify(JustifyText::Center),
|
|
Node {
|
|
margin: UiRect::bottom(Val::Px(10.)),
|
|
..Default::default()
|
|
},
|
|
));
|
|
|
|
parent
|
|
.spawn(Node {
|
|
width: Val::Percent(100.),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
let mut target_ids = vec![];
|
|
parent
|
|
.spawn(Node {
|
|
width: Val::Percent(50.),
|
|
height: Val::Px(520.),
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
target_ids = spawn_left_panel(parent, &palette);
|
|
});
|
|
|
|
parent
|
|
.spawn(Node {
|
|
width: Val::Percent(50.),
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
spawn_right_panel(parent, text_font, &palette, target_ids);
|
|
});
|
|
});
|
|
|
|
parent
|
|
.spawn(Node {
|
|
flex_direction: FlexDirection::Row,
|
|
align_items: AlignItems::Start,
|
|
justify_content: JustifyContent::Start,
|
|
column_gap: Val::Px(10.),
|
|
..default()
|
|
})
|
|
.with_children(|builder| {
|
|
let text_font = TextFont {
|
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
|
..default()
|
|
};
|
|
|
|
builder.spawn((
|
|
Text::new("Display::None\nVisibility::Hidden\nVisibility::Inherited"),
|
|
text_font.clone(),
|
|
TextColor(HIDDEN_COLOR),
|
|
TextLayout::new_with_justify(JustifyText::Center),
|
|
));
|
|
builder.spawn((
|
|
Text::new("-\n-\n-"),
|
|
text_font.clone(),
|
|
TextColor(DARK_GRAY.into()),
|
|
TextLayout::new_with_justify(JustifyText::Center),
|
|
));
|
|
builder.spawn((Text::new("The UI Node and its descendants will not be visible and will not be allotted any space in the UI layout.\nThe UI Node will not be visible but will still occupy space in the UI layout.\nThe UI node will inherit the visibility property of its parent. If it has no parent it will be visible."), text_font));
|
|
});
|
|
});
|
|
}
|
|
|
|
fn spawn_left_panel(builder: &mut ChildBuilder, palette: &[Color; 4]) -> Vec<Entity> {
|
|
let mut target_ids = vec![];
|
|
builder
|
|
.spawn((
|
|
Node {
|
|
padding: UiRect::all(Val::Px(10.)),
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::WHITE),
|
|
))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((Node::default(), BackgroundColor(Color::BLACK)))
|
|
.with_children(|parent| {
|
|
let id = parent
|
|
.spawn((
|
|
Node {
|
|
align_items: AlignItems::FlexEnd,
|
|
justify_content: JustifyContent::FlexEnd,
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[0]),
|
|
Outline {
|
|
width: Val::Px(4.),
|
|
color: DARK_CYAN.into(),
|
|
offset: Val::Px(10.),
|
|
},
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn(Node {
|
|
width: Val::Px(100.),
|
|
height: Val::Px(500.),
|
|
..default()
|
|
});
|
|
|
|
let id = parent
|
|
.spawn((
|
|
Node {
|
|
height: Val::Px(400.),
|
|
align_items: AlignItems::FlexEnd,
|
|
justify_content: JustifyContent::FlexEnd,
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[1]),
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn(Node {
|
|
width: Val::Px(100.),
|
|
height: Val::Px(400.),
|
|
..default()
|
|
});
|
|
|
|
let id = parent
|
|
.spawn((
|
|
Node {
|
|
height: Val::Px(300.),
|
|
align_items: AlignItems::FlexEnd,
|
|
justify_content: JustifyContent::FlexEnd,
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[2]),
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn(Node {
|
|
width: Val::Px(100.),
|
|
height: Val::Px(300.),
|
|
..default()
|
|
});
|
|
|
|
let id = parent
|
|
.spawn((
|
|
Node {
|
|
width: Val::Px(200.),
|
|
height: Val::Px(200.),
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[3]),
|
|
))
|
|
.id();
|
|
target_ids.push(id);
|
|
})
|
|
.id();
|
|
target_ids.push(id);
|
|
})
|
|
.id();
|
|
target_ids.push(id);
|
|
})
|
|
.id();
|
|
target_ids.push(id);
|
|
});
|
|
});
|
|
target_ids
|
|
}
|
|
|
|
fn spawn_right_panel(
|
|
parent: &mut ChildBuilder,
|
|
text_font: TextFont,
|
|
palette: &[Color; 4],
|
|
mut target_ids: Vec<Entity>,
|
|
) {
|
|
let spawn_buttons = |parent: &mut ChildBuilder, target_id| {
|
|
spawn_button::<Display>(parent, text_font.clone(), target_id);
|
|
spawn_button::<Visibility>(parent, text_font.clone(), target_id);
|
|
};
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
padding: UiRect::all(Val::Px(10.)),
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::WHITE),
|
|
))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
width: Val::Px(500.),
|
|
height: Val::Px(500.),
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::FlexEnd,
|
|
justify_content: JustifyContent::SpaceBetween,
|
|
padding: UiRect {
|
|
left: Val::Px(5.),
|
|
top: Val::Px(5.),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[0]),
|
|
Outline {
|
|
width: Val::Px(4.),
|
|
color: DARK_CYAN.into(),
|
|
offset: Val::Px(10.),
|
|
},
|
|
))
|
|
.with_children(|parent| {
|
|
spawn_buttons(parent, target_ids.pop().unwrap());
|
|
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
width: Val::Px(400.),
|
|
height: Val::Px(400.),
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::FlexEnd,
|
|
justify_content: JustifyContent::SpaceBetween,
|
|
padding: UiRect {
|
|
left: Val::Px(5.),
|
|
top: Val::Px(5.),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[1]),
|
|
))
|
|
.with_children(|parent| {
|
|
spawn_buttons(parent, target_ids.pop().unwrap());
|
|
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
width: Val::Px(300.),
|
|
height: Val::Px(300.),
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::FlexEnd,
|
|
justify_content: JustifyContent::SpaceBetween,
|
|
padding: UiRect {
|
|
left: Val::Px(5.),
|
|
top: Val::Px(5.),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[2]),
|
|
))
|
|
.with_children(|parent| {
|
|
spawn_buttons(parent, target_ids.pop().unwrap());
|
|
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
width: Val::Px(200.),
|
|
height: Val::Px(200.),
|
|
align_items: AlignItems::FlexStart,
|
|
justify_content: JustifyContent::SpaceBetween,
|
|
flex_direction: FlexDirection::Column,
|
|
padding: UiRect {
|
|
left: Val::Px(5.),
|
|
top: Val::Px(5.),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
BackgroundColor(palette[3]),
|
|
))
|
|
.with_children(|parent| {
|
|
spawn_buttons(parent, target_ids.pop().unwrap());
|
|
|
|
parent.spawn(Node {
|
|
width: Val::Px(100.),
|
|
height: Val::Px(100.),
|
|
..default()
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
fn spawn_button<T>(parent: &mut ChildBuilder, text_font: TextFont, target: Entity)
|
|
where
|
|
T: Default + std::fmt::Debug + Send + Sync + 'static,
|
|
Target<T>: TargetUpdate,
|
|
{
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
Node {
|
|
align_self: AlignSelf::FlexStart,
|
|
padding: UiRect::axes(Val::Px(5.), Val::Px(1.)),
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::BLACK.with_alpha(0.5)),
|
|
Target::<T>::new(target),
|
|
))
|
|
.with_children(|builder| {
|
|
builder.spawn((
|
|
Text(format!("{}::{:?}", Target::<T>::NAME, T::default())),
|
|
text_font,
|
|
TextLayout::new_with_justify(JustifyText::Center),
|
|
));
|
|
});
|
|
}
|
|
|
|
fn buttons_handler<T>(
|
|
mut left_panel_query: Query<&mut <Target<T> as TargetUpdate>::TargetComponent>,
|
|
mut visibility_button_query: Query<(&Target<T>, &Interaction, &Children), Changed<Interaction>>,
|
|
mut text_query: Query<(&mut Text, &mut TextColor)>,
|
|
) where
|
|
T: Send + Sync,
|
|
Target<T>: TargetUpdate + Component,
|
|
{
|
|
for (target, interaction, children) in visibility_button_query.iter_mut() {
|
|
if matches!(interaction, Interaction::Pressed) {
|
|
let mut target_value = left_panel_query.get_mut(target.id).unwrap();
|
|
for &child in children {
|
|
if let Ok((mut text, mut text_color)) = text_query.get_mut(child) {
|
|
**text = target.update_target(target_value.as_mut());
|
|
text_color.0 = if text.contains("None") || text.contains("Hidden") {
|
|
Color::srgb(1.0, 0.7, 0.7)
|
|
} else {
|
|
Color::WHITE
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn text_hover(
|
|
mut button_query: Query<(&Interaction, &mut BackgroundColor, &Children), Changed<Interaction>>,
|
|
mut text_query: Query<(&Text, &mut TextColor)>,
|
|
) {
|
|
for (interaction, mut color, children) in button_query.iter_mut() {
|
|
match interaction {
|
|
Interaction::Hovered => {
|
|
*color = Color::BLACK.with_alpha(0.6).into();
|
|
for &child in children {
|
|
if let Ok((_, mut text_color)) = text_query.get_mut(child) {
|
|
// Bypass change detection to avoid recomputation of the text when only changing the color
|
|
text_color.bypass_change_detection().0 = YELLOW.into();
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
*color = Color::BLACK.with_alpha(0.5).into();
|
|
for &child in children {
|
|
if let Ok((text, mut text_color)) = text_query.get_mut(child) {
|
|
text_color.bypass_change_detection().0 =
|
|
if text.contains("None") || text.contains("Hidden") {
|
|
HIDDEN_COLOR
|
|
} else {
|
|
Color::WHITE
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|