
# 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>
469 lines
15 KiB
Rust
469 lines
15 KiB
Rust
use bevy_color::Color;
|
|
use bevy_ecs::{
|
|
component::Mutable,
|
|
prelude::*,
|
|
system::{Query, SystemParam},
|
|
};
|
|
use bevy_hierarchy::Children;
|
|
|
|
use crate::{TextColor, TextFont, TextSpan};
|
|
|
|
/// Helper trait for using the [`TextReader`] and [`TextWriter`] system params.
|
|
pub trait TextSpanAccess: Component<Mutability = Mutable> {
|
|
/// Gets the text span's string.
|
|
fn read_span(&self) -> &str;
|
|
/// Gets mutable reference to the text span's string.
|
|
fn write_span(&mut self) -> &mut String;
|
|
}
|
|
|
|
/// Helper trait for the root text component in a text block.
|
|
pub trait TextRoot: TextSpanAccess + From<String> {}
|
|
|
|
/// Helper trait for the text span components in a text block.
|
|
pub trait TextSpanComponent: TextSpanAccess + From<String> {}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub(crate) struct TextIterScratch {
|
|
stack: Vec<(&'static Children, usize)>,
|
|
}
|
|
|
|
impl TextIterScratch {
|
|
fn take<'a>(&mut self) -> Vec<(&'a Children, usize)> {
|
|
core::mem::take(&mut self.stack)
|
|
.into_iter()
|
|
.map(|_| -> (&Children, usize) { unreachable!() })
|
|
.collect()
|
|
}
|
|
|
|
fn recover(&mut self, mut stack: Vec<(&Children, usize)>) {
|
|
stack.clear();
|
|
self.stack = stack
|
|
.into_iter()
|
|
.map(|_| -> (&'static Children, usize) { unreachable!() })
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
/// System parameter for reading text spans in a text block.
|
|
///
|
|
/// `R` is the root text component.
|
|
#[derive(SystemParam)]
|
|
pub struct TextReader<'w, 's, R: TextRoot> {
|
|
// This is a local to avoid system ambiguities when TextReaders run in parallel.
|
|
scratch: Local<'s, TextIterScratch>,
|
|
roots: Query<
|
|
'w,
|
|
's,
|
|
(
|
|
&'static R,
|
|
&'static TextFont,
|
|
&'static TextColor,
|
|
Option<&'static Children>,
|
|
),
|
|
>,
|
|
spans: Query<
|
|
'w,
|
|
's,
|
|
(
|
|
&'static TextSpan,
|
|
&'static TextFont,
|
|
&'static TextColor,
|
|
Option<&'static Children>,
|
|
),
|
|
>,
|
|
}
|
|
|
|
impl<'w, 's, R: TextRoot> TextReader<'w, 's, R> {
|
|
/// Returns an iterator over text spans in a text block, starting with the root entity.
|
|
pub fn iter(&mut self, root_entity: Entity) -> TextSpanIter<R> {
|
|
let stack = self.scratch.take();
|
|
|
|
TextSpanIter {
|
|
scratch: &mut self.scratch,
|
|
root_entity: Some(root_entity),
|
|
stack,
|
|
roots: &self.roots,
|
|
spans: &self.spans,
|
|
}
|
|
}
|
|
|
|
/// Gets a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get(
|
|
&mut self,
|
|
root_entity: Entity,
|
|
index: usize,
|
|
) -> Option<(Entity, usize, &str, &TextFont, Color)> {
|
|
self.iter(root_entity).nth(index)
|
|
}
|
|
|
|
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get_text(&mut self, root_entity: Entity, index: usize) -> Option<&str> {
|
|
self.get(root_entity, index).map(|(_, _, text, _, _)| text)
|
|
}
|
|
|
|
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get_font(&mut self, root_entity: Entity, index: usize) -> Option<&TextFont> {
|
|
self.get(root_entity, index).map(|(_, _, _, font, _)| font)
|
|
}
|
|
|
|
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get_color(&mut self, root_entity: Entity, index: usize) -> Option<Color> {
|
|
self.get(root_entity, index)
|
|
.map(|(_, _, _, _, color)| color)
|
|
}
|
|
|
|
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
|
|
///
|
|
/// Panics if there is no span at the requested index.
|
|
pub fn text(&mut self, root_entity: Entity, index: usize) -> &str {
|
|
self.get_text(root_entity, index).unwrap()
|
|
}
|
|
|
|
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
|
|
///
|
|
/// Panics if there is no span at the requested index.
|
|
pub fn font(&mut self, root_entity: Entity, index: usize) -> &TextFont {
|
|
self.get_font(root_entity, index).unwrap()
|
|
}
|
|
|
|
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
|
|
///
|
|
/// Panics if there is no span at the requested index.
|
|
pub fn color(&mut self, root_entity: Entity, index: usize) -> Color {
|
|
self.get_color(root_entity, index).unwrap()
|
|
}
|
|
}
|
|
|
|
/// Iterator returned by [`TextReader::iter`].
|
|
///
|
|
/// Iterates all spans in a text block according to hierarchy traversal order.
|
|
/// Does *not* flatten interspersed ghost nodes. Only contiguous spans are traversed.
|
|
// TODO: Use this iterator design in UiChildrenIter to reduce allocations.
|
|
pub struct TextSpanIter<'a, R: TextRoot> {
|
|
scratch: &'a mut TextIterScratch,
|
|
root_entity: Option<Entity>,
|
|
/// Stack of (children, next index into children).
|
|
stack: Vec<(&'a Children, usize)>,
|
|
roots: &'a Query<
|
|
'a,
|
|
'a,
|
|
(
|
|
&'static R,
|
|
&'static TextFont,
|
|
&'static TextColor,
|
|
Option<&'static Children>,
|
|
),
|
|
>,
|
|
spans: &'a Query<
|
|
'a,
|
|
'a,
|
|
(
|
|
&'static TextSpan,
|
|
&'static TextFont,
|
|
&'static TextColor,
|
|
Option<&'static Children>,
|
|
),
|
|
>,
|
|
}
|
|
|
|
impl<'a, R: TextRoot> Iterator for TextSpanIter<'a, R> {
|
|
/// Item = (entity in text block, hierarchy depth in the block, span text, span style).
|
|
type Item = (Entity, usize, &'a str, &'a TextFont, Color);
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
// Root
|
|
if let Some(root_entity) = self.root_entity.take() {
|
|
if let Ok((text, text_font, color, maybe_children)) = self.roots.get(root_entity) {
|
|
if let Some(children) = maybe_children {
|
|
self.stack.push((children, 0));
|
|
}
|
|
return Some((root_entity, 0, text.read_span(), text_font, color.0));
|
|
}
|
|
return None;
|
|
}
|
|
|
|
// Span
|
|
loop {
|
|
let (children, idx) = self.stack.last_mut()?;
|
|
|
|
loop {
|
|
let Some(child) = children.get(*idx) else {
|
|
break;
|
|
};
|
|
|
|
// Increment to prep the next entity in this stack level.
|
|
*idx += 1;
|
|
|
|
let entity = *child;
|
|
let Ok((span, text_font, color, maybe_children)) = self.spans.get(entity) else {
|
|
continue;
|
|
};
|
|
|
|
let depth = self.stack.len();
|
|
if let Some(children) = maybe_children {
|
|
self.stack.push((children, 0));
|
|
}
|
|
return Some((entity, depth, span.read_span(), text_font, color.0));
|
|
}
|
|
|
|
// All children at this stack entry have been iterated.
|
|
self.stack.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, R: TextRoot> Drop for TextSpanIter<'a, R> {
|
|
fn drop(&mut self) {
|
|
// Return the internal stack.
|
|
let stack = core::mem::take(&mut self.stack);
|
|
self.scratch.recover(stack);
|
|
}
|
|
}
|
|
|
|
/// System parameter for reading and writing text spans in a text block.
|
|
///
|
|
/// `R` is the root text component, and `S` is the text span component on children.
|
|
#[derive(SystemParam)]
|
|
pub struct TextWriter<'w, 's, R: TextRoot> {
|
|
// This is a resource because two TextWriters can't run in parallel.
|
|
scratch: ResMut<'w, TextIterScratch>,
|
|
roots: Query<
|
|
'w,
|
|
's,
|
|
(
|
|
&'static mut R,
|
|
&'static mut TextFont,
|
|
&'static mut TextColor,
|
|
),
|
|
Without<TextSpan>,
|
|
>,
|
|
spans: Query<
|
|
'w,
|
|
's,
|
|
(
|
|
&'static mut TextSpan,
|
|
&'static mut TextFont,
|
|
&'static mut TextColor,
|
|
),
|
|
Without<R>,
|
|
>,
|
|
children: Query<'w, 's, &'static Children>,
|
|
}
|
|
|
|
impl<'w, 's, R: TextRoot> TextWriter<'w, 's, R> {
|
|
/// Gets a mutable reference to a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get(
|
|
&mut self,
|
|
root_entity: Entity,
|
|
index: usize,
|
|
) -> Option<(Entity, usize, Mut<String>, Mut<TextFont>, Mut<TextColor>)> {
|
|
// Root
|
|
if index == 0 {
|
|
let (text, font, color) = self.roots.get_mut(root_entity).ok()?;
|
|
return Some((
|
|
root_entity,
|
|
0,
|
|
text.map_unchanged(|t| t.write_span()),
|
|
font,
|
|
color,
|
|
));
|
|
}
|
|
|
|
// Prep stack.
|
|
let mut stack: Vec<(&Children, usize)> = self.scratch.take();
|
|
if let Ok(children) = self.children.get(root_entity) {
|
|
stack.push((children, 0));
|
|
}
|
|
|
|
// Span
|
|
let mut count = 1;
|
|
let (depth, entity) = 'l: loop {
|
|
let Some((children, idx)) = stack.last_mut() else {
|
|
self.scratch.recover(stack);
|
|
return None;
|
|
};
|
|
|
|
loop {
|
|
let Some(child) = children.get(*idx) else {
|
|
// All children at this stack entry have been iterated.
|
|
stack.pop();
|
|
break;
|
|
};
|
|
|
|
// Increment to prep the next entity in this stack level.
|
|
*idx += 1;
|
|
|
|
if !self.spans.contains(*child) {
|
|
continue;
|
|
};
|
|
count += 1;
|
|
|
|
if count - 1 == index {
|
|
let depth = stack.len();
|
|
self.scratch.recover(stack);
|
|
break 'l (depth, *child);
|
|
}
|
|
|
|
if let Ok(children) = self.children.get(*child) {
|
|
stack.push((children, 0));
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Note: We do this outside the loop due to borrow checker limitations.
|
|
let (text, font, color) = self.spans.get_mut(entity).unwrap();
|
|
Some((
|
|
entity,
|
|
depth,
|
|
text.map_unchanged(|t| t.write_span()),
|
|
font,
|
|
color,
|
|
))
|
|
}
|
|
|
|
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get_text(&mut self, root_entity: Entity, index: usize) -> Option<Mut<String>> {
|
|
self.get(root_entity, index).map(|(_, _, text, ..)| text)
|
|
}
|
|
|
|
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get_font(&mut self, root_entity: Entity, index: usize) -> Option<Mut<TextFont>> {
|
|
self.get(root_entity, index).map(|(_, _, _, font, _)| font)
|
|
}
|
|
|
|
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
|
|
pub fn get_color(&mut self, root_entity: Entity, index: usize) -> Option<Mut<TextColor>> {
|
|
self.get(root_entity, index)
|
|
.map(|(_, _, _, _, color)| color)
|
|
}
|
|
|
|
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
|
|
///
|
|
/// Panics if there is no span at the requested index.
|
|
pub fn text(&mut self, root_entity: Entity, index: usize) -> Mut<String> {
|
|
self.get_text(root_entity, index).unwrap()
|
|
}
|
|
|
|
/// Gets the [`TextFont`] of a text span within a text block at a specific index in the flattened span list.
|
|
///
|
|
/// Panics if there is no span at the requested index.
|
|
pub fn font(&mut self, root_entity: Entity, index: usize) -> Mut<TextFont> {
|
|
self.get_font(root_entity, index).unwrap()
|
|
}
|
|
|
|
/// Gets the [`TextColor`] of a text span within a text block at a specific index in the flattened span list.
|
|
///
|
|
/// Panics if there is no span at the requested index.
|
|
pub fn color(&mut self, root_entity: Entity, index: usize) -> Mut<TextColor> {
|
|
self.get_color(root_entity, index).unwrap()
|
|
}
|
|
|
|
/// Invokes a callback on each span in a text block, starting with the root entity.
|
|
pub fn for_each(
|
|
&mut self,
|
|
root_entity: Entity,
|
|
mut callback: impl FnMut(Entity, usize, Mut<String>, Mut<TextFont>, Mut<TextColor>),
|
|
) {
|
|
self.for_each_until(root_entity, |a, b, c, d, e| {
|
|
(callback)(a, b, c, d, e);
|
|
true
|
|
});
|
|
}
|
|
|
|
/// Invokes a callback on each span's string value in a text block, starting with the root entity.
|
|
pub fn for_each_text(&mut self, root_entity: Entity, mut callback: impl FnMut(Mut<String>)) {
|
|
self.for_each(root_entity, |_, _, text, _, _| {
|
|
(callback)(text);
|
|
});
|
|
}
|
|
|
|
/// Invokes a callback on each span's [`TextFont`] in a text block, starting with the root entity.
|
|
pub fn for_each_font(&mut self, root_entity: Entity, mut callback: impl FnMut(Mut<TextFont>)) {
|
|
self.for_each(root_entity, |_, _, _, font, _| {
|
|
(callback)(font);
|
|
});
|
|
}
|
|
|
|
/// Invokes a callback on each span's [`TextColor`] in a text block, starting with the root entity.
|
|
pub fn for_each_color(
|
|
&mut self,
|
|
root_entity: Entity,
|
|
mut callback: impl FnMut(Mut<TextColor>),
|
|
) {
|
|
self.for_each(root_entity, |_, _, _, _, color| {
|
|
(callback)(color);
|
|
});
|
|
}
|
|
|
|
/// Invokes a callback on each span in a text block, starting with the root entity.
|
|
///
|
|
/// Traversal will stop when the callback returns `false`.
|
|
// TODO: find a way to consolidate get and for_each_until, or provide a real iterator. Lifetime issues are challenging here.
|
|
pub fn for_each_until(
|
|
&mut self,
|
|
root_entity: Entity,
|
|
mut callback: impl FnMut(Entity, usize, Mut<String>, Mut<TextFont>, Mut<TextColor>) -> bool,
|
|
) {
|
|
// Root
|
|
let Ok((text, font, color)) = self.roots.get_mut(root_entity) else {
|
|
return;
|
|
};
|
|
if !(callback)(
|
|
root_entity,
|
|
0,
|
|
text.map_unchanged(|t| t.write_span()),
|
|
font,
|
|
color,
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Prep stack.
|
|
let mut stack: Vec<(&Children, usize)> = self.scratch.take();
|
|
if let Ok(children) = self.children.get(root_entity) {
|
|
stack.push((children, 0));
|
|
}
|
|
|
|
// Span
|
|
loop {
|
|
let depth = stack.len();
|
|
let Some((children, idx)) = stack.last_mut() else {
|
|
self.scratch.recover(stack);
|
|
return;
|
|
};
|
|
|
|
loop {
|
|
let Some(child) = children.get(*idx) else {
|
|
// All children at this stack entry have been iterated.
|
|
stack.pop();
|
|
break;
|
|
};
|
|
|
|
// Increment to prep the next entity in this stack level.
|
|
*idx += 1;
|
|
|
|
let entity = *child;
|
|
let Ok((text, font, color)) = self.spans.get_mut(entity) else {
|
|
continue;
|
|
};
|
|
|
|
if !(callback)(
|
|
entity,
|
|
depth,
|
|
text.map_unchanged(|t| t.write_span()),
|
|
font,
|
|
color,
|
|
) {
|
|
self.scratch.recover(stack);
|
|
return;
|
|
}
|
|
|
|
if let Ok(children) = self.children.get(entity) {
|
|
stack.push((children, 0));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|