Files
bevy/examples/testbed/full_ui.rs
Trashtalk217 59e9ee3a1a Store Resources as components on singleton entities (#20934)
This is part of #19731.

# Resources as Components

## Motivation

More things should be entities. This simplifies the API, the lower-level
implementation and the tools we have for entities and components can be
used for other things in the engine. In particular, for resources, it is
really handy to have observers, which we currently don't have. See
#20821 under 1A, for a more specific use.

## Current Work

This removes the `resources` field from the world storage and instead
store the resources on singleton entities. For easy lookup, we add a
`HashMap<ComponentId, Entity>` to `World`, in order to quickly find the
singleton entity where the resource is stored.

Because we store resources on entities, we derive `Component` alongside
`Resource`, this means that

```rust
#[derive(Resource)]
struct Foo;
```
turns into
```rust
#[derive(Resource, Component)]
struct Foo;
```

This was also done for reflections, meaning that

```rust
#[derive(Resource, Reflect)]
#[refect(Resource)]
struct Bar;
```
becomes
```rust
#[derive(Resource, Component, Reflect)]
#[refect(Resource, Component)]
struct Bar;
```

In order to distinguish resource entities, they are tagged with the
`IsResource` component. Additionally, to ensure that they aren't queried
by accident, they are also tagged as being internal entities, which
means that they don't show up in queries by default.

## Drawbacks

- Currently you can't have a struct that is both a `Resource` and a
`Component`, because `Resource` expands to also implement `Component`,
this means that this throws a compiler error as it's implemented twice.
- Because every reflected Resource must also implement
`ReflectComponent` you need to import
`bevy_ecs::reflect::ReflectComponent` every time you use
`#[reflect(Resource)]`. This is kind of unintuitive.

## Future Work

- Simplify `Access` in the ECS, to only deal with components (and not
components *and* resources).
- Newtype `Res<Resource>` to `Single<Ref<Resource>>` (or something
similair).
- Eliminate `ReflectResource`.
- Take stabs at simplifying the public facing API.

---------

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Dimitrios Loukadakis <dloukadakis@users.noreply.github.com>
2026-02-10 18:53:12 +00:00

474 lines
20 KiB
Rust

//! This example illustrates the various features of Bevy UI.
use std::f32::consts::PI;
use accesskit::{Node as Accessible, Role};
use bevy::{
a11y::AccessibilityNode,
color::palettes::{
basic::LIME,
css::{DARK_GRAY, NAVY},
},
input::mouse::{MouseScrollUnit, MouseWheel},
picking::hover::HoverMap,
prelude::*,
ui::widget::NodeImageMode,
ui_widgets::Scrollbar,
};
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, update_scroll_position);
#[cfg(feature = "bevy_ui_debug")]
app.add_systems(Update, toggle_debug_overlay);
app.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Camera
commands.spawn((Camera2d, IsDefaultUiCamera, BoxShadowSamples(6)));
// root node
commands
.spawn(Node {
width: percent(100),
height: percent(100),
justify_content: JustifyContent::SpaceBetween,
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
// left vertical fill (border)
parent
.spawn((
Node {
width: px(200),
border: UiRect::all(px(2)),
..default()
},
BackgroundColor(Color::srgb(0.65, 0.65, 0.65)),
))
.with_children(|parent| {
// left vertical fill (content)
parent
.spawn((
Node {
width: percent(100),
flex_direction: FlexDirection::Column,
padding: UiRect::all(px(5)),
row_gap: px(5),
..default()
},
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
Visibility::Visible,
))
.with_children(|parent| {
// text
parent.spawn((
Text::new("Text Example"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
font_size: FontSize::Px(25.0),
..default()
},
// Because this is a distinct label widget and
// not button/list item text, this is necessary
// for accessibility to treat the text accordingly.
Label,
));
#[cfg(feature = "bevy_ui_debug")]
{
// Debug overlay text
parent.spawn((
Text::new("Press Space to toggle debug outlines."),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
..default()
},
Label,
));
parent.spawn((
Text::new("V: toggle UI root's visibility"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
font_size: FontSize::Px(12.),
..default()
},
Label,
));
parent.spawn((
Text::new("S: toggle outlines for hidden nodes"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
font_size: FontSize::Px(12.),
..default()
},
Label,
));
parent.spawn((
Text::new("C: toggle outlines for clipped nodes"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
font_size: FontSize::Px(12.),
..default()
},
Label,
));
}
#[cfg(not(feature = "bevy_ui_debug"))]
parent.spawn((
Text::new("Try enabling feature \"bevy_ui_debug\"."),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
..default()
},
Label,
));
});
});
// right vertical fill
parent
.spawn(Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: px(200),
..default()
})
.with_children(|parent| {
// Title
parent.spawn((
Text::new("Scrolling list"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
font_size: FontSize::Px(21.),
..default()
},
Label,
));
// Scrolling list
parent
.spawn((
Node {
flex_direction: FlexDirection::Column,
align_self: AlignSelf::Stretch,
height: percent(50),
overflow: Overflow::scroll_y(),
..default()
},
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
))
.with_children(|parent| {
parent
.spawn((
Node {
flex_direction: FlexDirection::Column,
..Default::default()
},
BackgroundGradient::from(LinearGradient::to_bottom(vec![
ColorStop::auto(NAVY),
ColorStop::auto(Color::BLACK),
])),
Pickable {
should_block_lower: false,
..Default::default()
},
))
.with_children(|parent| {
// List items
for i in 0..25 {
parent
.spawn((
Text(format!("Item {i}")),
TextFont {
font: asset_server
.load("fonts/FiraSans-Bold.ttf")
.into(),
..default()
},
Label,
AccessibilityNode(Accessible::new(Role::ListItem)),
))
.insert(Pickable {
should_block_lower: false,
..default()
});
}
});
});
});
parent
.spawn(Node {
left: px(210),
bottom: px(10),
position_type: PositionType::Absolute,
..default()
})
.with_children(|parent| {
parent
.spawn((
Node {
width: px(200),
height: px(200),
border: UiRect::all(px(20)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
..default()
},
BorderColor::all(LIME),
BackgroundColor(Color::srgb(0.8, 0.8, 1.)),
))
.with_children(|parent| {
parent.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_light.png")),
// Uses the transform to rotate the logo image by 45 degrees
Node {
border_radius: BorderRadius::all(px(10)),
..Default::default()
},
UiTransform {
rotation: Rot2::radians(0.25 * PI),
..Default::default()
},
Outline {
width: px(2),
offset: px(4),
color: DARK_GRAY.into(),
},
));
});
});
let shadow_style = ShadowStyle {
color: Color::BLACK.with_alpha(0.5),
blur_radius: px(2),
x_offset: px(10),
y_offset: px(10),
..default()
};
// render order test: reddest in the back, whitest in the front (flex center)
parent
.spawn(Node {
width: percent(100),
height: percent(100),
position_type: PositionType::Absolute,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
parent
.spawn((
Node {
width: px(100),
height: px(100),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.0, 0.)),
BoxShadow::from(shadow_style),
))
.with_children(|parent| {
parent.spawn((
Node {
// Take the size of the parent node.
width: percent(100),
height: percent(100),
position_type: PositionType::Absolute,
left: px(20),
bottom: px(20),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.3, 0.3)),
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
width: percent(100),
height: percent(100),
position_type: PositionType::Absolute,
left: px(40),
bottom: px(40),
..default()
},
BackgroundColor(Color::srgb(1.0, 0.5, 0.5)),
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
width: percent(100),
height: percent(100),
position_type: PositionType::Absolute,
left: px(60),
bottom: px(60),
..default()
},
BackgroundColor(Color::srgb(0.0, 0.7, 0.7)),
BoxShadow::from(shadow_style),
));
// alpha test
parent.spawn((
Node {
width: percent(100),
height: percent(100),
position_type: PositionType::Absolute,
left: px(80),
bottom: px(80),
..default()
},
BackgroundColor(Color::srgba(1.0, 0.9, 0.9, 0.4)),
BoxShadow::from(ShadowStyle {
color: Color::BLACK.with_alpha(0.3),
..shadow_style
}),
));
});
});
// bevy logo (flex center)
parent
.spawn(Node {
width: percent(100),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexStart,
..default()
})
.with_children(|parent| {
// bevy logo (image)
parent
.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_dark_big.png"))
.with_mode(NodeImageMode::Stretch),
Node {
width: px(500),
height: px(125),
margin: UiRect::top(vmin(5)),
..default()
},
))
.with_children(|parent| {
// alt text
// This UI node takes up no space in the layout and the `Text` component is used by the accessibility module
// and is not rendered.
parent.spawn((
Node {
display: Display::None,
..default()
},
Text::new("Bevy logo"),
));
});
});
// four bevy icons demonstrating image flipping
parent
.spawn(Node {
width: percent(100),
height: percent(100),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd,
column_gap: px(10),
padding: UiRect::all(px(10)),
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
for (flip_x, flip_y) in
[(false, false), (false, true), (true, true), (true, false)]
{
parent.spawn((
ImageNode {
image: asset_server.load("branding/icon.png"),
flip_x,
flip_y,
..default()
},
Node {
// The height will be chosen automatically to preserve the image's aspect ratio
width: px(75),
..default()
},
));
}
});
});
}
#[cfg(feature = "bevy_ui_debug")]
// The system that will enable/disable the debug outlines around the nodes
fn toggle_debug_overlay(
input: Res<ButtonInput<KeyCode>>,
mut debug_options: ResMut<GlobalUiDebugOptions>,
mut root_node_query: Query<&mut Visibility, (With<Node>, Without<ChildOf>)>,
) {
info_once!("The debug outlines are enabled, press Space to turn them on/off");
if input.just_pressed(KeyCode::Space) {
// The toggle method will enable the debug overlay if disabled and disable if enabled
debug_options.toggle();
}
if input.just_pressed(KeyCode::KeyS) {
// Toggle debug outlines for nodes with `ViewVisibility` set to false.
debug_options.show_hidden = !debug_options.show_hidden;
}
if input.just_pressed(KeyCode::KeyC) {
// Toggle outlines for clipped UI nodes.
debug_options.show_clipped = !debug_options.show_clipped;
}
if input.just_pressed(KeyCode::KeyV) {
for mut visibility in root_node_query.iter_mut() {
// Toggle the UI root node's visibility
visibility.toggle_inherited_hidden();
}
}
}
/// Updates the scroll position of scrollable nodes in response to mouse input
pub fn update_scroll_position(
mut mouse_wheel_reader: MessageReader<MouseWheel>,
hover_map: Res<HoverMap>,
mut scrolled_node_query: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
for mouse_wheel in mouse_wheel_reader.read() {
let (mut dx, mut dy) = match mouse_wheel.unit {
MouseScrollUnit::Line => (mouse_wheel.x * 20., mouse_wheel.y * 20.),
MouseScrollUnit::Pixel => (mouse_wheel.x, mouse_wheel.y),
};
if keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight)
{
std::mem::swap(&mut dx, &mut dy);
}
for (_pointer, pointer_map) in hover_map.iter() {
for (entity, _hit) in pointer_map.iter() {
if let Ok((mut scroll_position, scroll_content)) =
scrolled_node_query.get_mut(*entity)
{
let visible_size = scroll_content.size();
let content_size = scroll_content.content_size();
let range = (content_size.y - visible_size.y).max(0.)
* scroll_content.inverse_scale_factor;
scroll_position.x -= dx;
scroll_position.y = (scroll_position.y - dy).clamp(0., range);
}
}
}
}
}