mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
59e9ee3a1a
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>
272 lines
8.9 KiB
Rust
272 lines
8.9 KiB
Rust
//! This example demonstrates the behavior of `NodeImageMode::Auto` and `NodeImageMode::Stretch` by allowing keyboard input to resize an `ImageGroup` container.
|
|
//! It visually shows how images are sized automatically versus stretched to fit their container.
|
|
|
|
use bevy::{color::palettes::tailwind, prelude::*};
|
|
|
|
const MIN_RESIZE_VAL: f32 = 1.0;
|
|
const IMAGE_GROUP_BOX_MIN_WIDTH: f32 = 50.0;
|
|
const IMAGE_GROUP_BOX_MAX_WIDTH: f32 = 100.0;
|
|
const IMAGE_GROUP_BOX_MIN_HEIGHT: f32 = 10.0;
|
|
const IMAGE_GROUP_BOX_MAX_HEIGHT: f32 = 50.0;
|
|
const IMAGE_GROUP_BOX_INIT_WIDTH: f32 =
|
|
(IMAGE_GROUP_BOX_MIN_WIDTH + IMAGE_GROUP_BOX_MAX_WIDTH) / 2.;
|
|
const IMAGE_GROUP_BOX_INIT_HEIGHT: f32 =
|
|
(IMAGE_GROUP_BOX_MIN_HEIGHT + IMAGE_GROUP_BOX_MAX_HEIGHT) / 2.;
|
|
const TEXT_PREFIX: &str = "Compare NodeImageMode(Auto, Stretch) press `Up`/`Down` to resize height, press `Left`/`Right` to resize width\n";
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
// Enable for image outline
|
|
.insert_resource(GlobalUiDebugOptions {
|
|
enabled: true,
|
|
..default()
|
|
})
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, update)
|
|
.add_observer(on_trigger_image_group)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
struct ImageGroup;
|
|
|
|
#[derive(Debug, Event)]
|
|
enum ImageGroupResize {
|
|
HeightGrow,
|
|
HeightShrink,
|
|
WidthGrow,
|
|
WidthShrink,
|
|
}
|
|
|
|
// Text data for easy modification
|
|
#[derive(Debug, Component)]
|
|
struct TextData {
|
|
height: f32,
|
|
width: f32,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum Direction {
|
|
Height,
|
|
Width,
|
|
}
|
|
|
|
#[derive(Debug, EntityEvent)]
|
|
struct TextUpdate {
|
|
entity: Entity,
|
|
direction: Direction,
|
|
change: f32,
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
let image_handle = asset_server.load("branding/icon.png");
|
|
let full_text = format!(
|
|
"{}height : {}%, width : {}%",
|
|
TEXT_PREFIX, IMAGE_GROUP_BOX_INIT_HEIGHT, IMAGE_GROUP_BOX_INIT_WIDTH,
|
|
);
|
|
|
|
commands.spawn(Camera2d);
|
|
|
|
let container = commands
|
|
.spawn((
|
|
Node {
|
|
display: Display::Grid,
|
|
width: percent(100),
|
|
height: percent(100),
|
|
grid_template_rows: vec![GridTrack::min_content(), GridTrack::flex(1.0)],
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::WHITE),
|
|
))
|
|
.id();
|
|
|
|
// Keyboard Text
|
|
commands
|
|
.spawn((
|
|
TextData {
|
|
height: IMAGE_GROUP_BOX_INIT_HEIGHT,
|
|
width: IMAGE_GROUP_BOX_INIT_WIDTH,
|
|
},
|
|
Text::new(full_text),
|
|
TextColor::BLACK,
|
|
Node {
|
|
grid_row: GridPlacement::span(1),
|
|
padding: px(6).all(),
|
|
..default()
|
|
},
|
|
UiDebugOptions {
|
|
enabled: false,
|
|
..default()
|
|
},
|
|
ChildOf(container),
|
|
))
|
|
.observe(update_text);
|
|
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
display: Display::Flex,
|
|
grid_row: GridPlacement::span(1),
|
|
flex_direction: FlexDirection::Column,
|
|
justify_content: JustifyContent::SpaceAround,
|
|
padding: px(10.).all(),
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::BLACK),
|
|
ChildOf(container),
|
|
))
|
|
.with_children(|builder| {
|
|
// `NodeImageMode::Auto` will resize the image automatically by taking the size of the source image and applying any layout constraints.
|
|
builder
|
|
.spawn((
|
|
ImageGroup,
|
|
Node {
|
|
display: Display::Flex,
|
|
justify_content: JustifyContent::Start,
|
|
width: percent(IMAGE_GROUP_BOX_INIT_WIDTH),
|
|
height: percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::from(tailwind::BLUE_100)),
|
|
))
|
|
.with_children(|parent| {
|
|
for _ in 0..4 {
|
|
// child node will apply Flex layout
|
|
parent.spawn((
|
|
Node::default(),
|
|
ImageNode {
|
|
image: image_handle.clone(),
|
|
image_mode: NodeImageMode::Auto,
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
});
|
|
// `NodeImageMode::Stretch` will resize the image to match the size of the `Node` component
|
|
builder
|
|
.spawn((
|
|
ImageGroup,
|
|
Node {
|
|
display: Display::Flex,
|
|
justify_content: JustifyContent::Start,
|
|
width: percent(IMAGE_GROUP_BOX_INIT_WIDTH),
|
|
height: percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::from(tailwind::BLUE_100)),
|
|
))
|
|
.with_children(|parent| {
|
|
for width in [10., 20., 30., 40.] {
|
|
parent.spawn((
|
|
Node {
|
|
height: percent(100),
|
|
width: percent(width),
|
|
..default()
|
|
},
|
|
ImageNode {
|
|
image: image_handle.clone(),
|
|
image_mode: NodeImageMode::Stretch,
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Trigger event
|
|
fn update(
|
|
keycode: Res<ButtonInput<KeyCode>>,
|
|
mut commands: Commands,
|
|
query: Query<Entity, With<TextData>>,
|
|
) {
|
|
let entity = query.single().unwrap();
|
|
if keycode.pressed(KeyCode::ArrowUp) {
|
|
commands.trigger(ImageGroupResize::HeightGrow);
|
|
commands.trigger(TextUpdate {
|
|
entity,
|
|
direction: Direction::Height,
|
|
change: MIN_RESIZE_VAL,
|
|
});
|
|
}
|
|
if keycode.pressed(KeyCode::ArrowDown) {
|
|
commands.trigger(ImageGroupResize::HeightShrink);
|
|
commands.trigger(TextUpdate {
|
|
entity,
|
|
direction: Direction::Height,
|
|
change: -MIN_RESIZE_VAL,
|
|
});
|
|
}
|
|
if keycode.pressed(KeyCode::ArrowLeft) {
|
|
commands.trigger(ImageGroupResize::WidthShrink);
|
|
commands.trigger(TextUpdate {
|
|
entity,
|
|
direction: Direction::Width,
|
|
change: -MIN_RESIZE_VAL,
|
|
});
|
|
}
|
|
if keycode.pressed(KeyCode::ArrowRight) {
|
|
commands.trigger(ImageGroupResize::WidthGrow);
|
|
commands.trigger(TextUpdate {
|
|
entity,
|
|
direction: Direction::Width,
|
|
change: MIN_RESIZE_VAL,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn update_text(
|
|
event: On<TextUpdate>,
|
|
mut textmeta: Single<&mut TextData>,
|
|
mut text: Single<&mut Text>,
|
|
) {
|
|
let mut new_text = Text::new(TEXT_PREFIX);
|
|
match event.direction {
|
|
Direction::Height => {
|
|
textmeta.height = (textmeta.height + event.change)
|
|
.clamp(IMAGE_GROUP_BOX_MIN_HEIGHT, IMAGE_GROUP_BOX_MAX_HEIGHT);
|
|
new_text.push_str(&format!(
|
|
"height : {}%, width : {}%",
|
|
textmeta.height, textmeta.width
|
|
));
|
|
}
|
|
Direction::Width => {
|
|
textmeta.width = (textmeta.width + event.change)
|
|
.clamp(IMAGE_GROUP_BOX_MIN_WIDTH, IMAGE_GROUP_BOX_MAX_WIDTH);
|
|
new_text.push_str(&format!(
|
|
"height : {}%, width : {}%",
|
|
textmeta.height, textmeta.width
|
|
));
|
|
}
|
|
}
|
|
text.0 = new_text.0;
|
|
}
|
|
|
|
fn on_trigger_image_group(event: On<ImageGroupResize>, query: Query<&mut Node, With<ImageGroup>>) {
|
|
for mut node in query {
|
|
match event.event() {
|
|
ImageGroupResize::HeightGrow => {
|
|
if let Val::Percent(val) = &mut node.height {
|
|
*val = (*val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_HEIGHT);
|
|
}
|
|
}
|
|
ImageGroupResize::HeightShrink => {
|
|
if let Val::Percent(val) = &mut node.height {
|
|
*val = (*val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_HEIGHT);
|
|
}
|
|
}
|
|
ImageGroupResize::WidthGrow => {
|
|
if let Val::Percent(val) = &mut node.width {
|
|
*val = (*val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_WIDTH);
|
|
}
|
|
}
|
|
ImageGroupResize::WidthShrink => {
|
|
if let Val::Percent(val) = &mut node.width {
|
|
*val = (*val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_WIDTH);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|