2024-09-04 19:31:41 +00:00
//! This example illustrates how to how to flip and tile images with 9-slicing in the UI.
2024-09-06 06:00:43 +00:00
use bevy ::{
2024-11-10 06:54:38 +00:00
image ::{ ImageLoaderSettings , ImageSampler } ,
2024-09-06 06:00:43 +00:00
prelude ::* ,
Improved `UiImage` and `Sprite` scaling and slicing APIs (#16088)
# Objective
1. UI texture slicing chops and scales an image to fit the size of a
node and isn't meant to place any constraints on the size of the node
itself, but because the required components changes required `ImageSize`
and `ContentSize` for nodes with `UiImage`, texture sliced nodes are
laid out using an `ImageMeasure`.
2. In 0.14 users could spawn a `(UiImage, NodeBundle)` which would
display an image stretched to fill the UI node's bounds ignoring the
image's instrinsic size. Now that `UiImage` requires `ContentSize`,
there's no option to display an image without its size placing
constrains on the UI layout (unless you force the `Node` to a fixed
size, but that's not a solution).
3. It's desirable that the `Sprite` and `UiImage` share similar APIs.
Fixes #16109
## Solution
* Remove the `Component` impl from `ImageScaleMode`.
* Add a `Stretch` variant to `ImageScaleMode`.
* Add a field `scale_mode: ImageScaleMode` to `Sprite`.
* Add a field `mode: UiImageMode` to `UiImage`.
* Add an enum `UiImageMode` similar to `ImageScaleMode` but with
additional UI specific variants.
* Remove the queries for `ImageScaleMode` from Sprite and UI extraction,
and refer to the new fields instead.
* Change `ui_layout_system` to update measure funcs on any change to
`ContentSize`s to enable manual clearing without removing the component.
* Don't add a measure unless `UiImageMode::Auto` is set in
`update_image_content_size_system`. Mutably deref the `Mut<ContentSize>`
if the `UiImage` is changed to force removal of any existing measure
func.
## Testing
Remove all the constraints from the ui_texture_slice example:
```rust
//! This example illustrates how to create buttons with their textures sliced
//! and kept in proportion instead of being stretched by the button dimensions
use bevy::{
color::palettes::css::{GOLD, ORANGE},
prelude::*,
winit::WinitSettings,
};
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, button_system)
.run();
}
fn button_system(
mut interaction_query: Query<
(&Interaction, &Children, &mut UiImage),
(Changed<Interaction>, With<Button>),
>,
mut text_query: Query<&mut Text>,
) {
for (interaction, children, mut image) in &mut interaction_query {
let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction {
Interaction::Pressed => {
**text = "Press".to_string();
image.color = GOLD.into();
}
Interaction::Hovered => {
**text = "Hover".to_string();
image.color = ORANGE.into();
}
Interaction::None => {
**text = "Button".to_string();
image.color = Color::WHITE;
}
}
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let image = asset_server.load("textures/fantasy_ui_borders/panel-border-010.png");
let slicer = TextureSlicer {
border: BorderRect::square(22.0),
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
// ui camera
commands.spawn(Camera2d);
commands
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
})
.with_children(|parent| {
for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] {
parent
.spawn((
Button,
Node {
// width: Val::Px(w),
// height: Val::Px(h),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
margin: UiRect::all(Val::Px(20.0)),
..default()
},
UiImage::new(image.clone()),
ImageScaleMode::Sliced(slicer.clone()),
))
.with_children(|parent| {
// parent.spawn((
// Text::new("Button"),
// TextFont {
// font: asset_server.load("fonts/FiraSans-Bold.ttf"),
// font_size: 33.0,
// ..default()
// },
// TextColor(Color::srgb(0.9, 0.9, 0.9)),
// ));
});
}
});
}
```
This should result in a blank window, since without any constraints the
texture slice image nodes should be zero-sized. But in main the image
nodes are given the size of the underlying unsliced source image
`textures/fantasy_ui_borders/panel-border-010.png`:
<img width="321" alt="slicing"
src="https://github.com/user-attachments/assets/cbd74c9c-14cd-4b4d-93c6-7c0152bb05ee">
For this PR need to change the lines:
```
UiImage::new(image.clone()),
ImageScaleMode::Sliced(slicer.clone()),
```
to
```
UiImage::new(image.clone()).with_mode(UiImageMode::Sliced(slicer.clone()),
```
and then nothing should be rendered, as desired.
---------
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
2024-11-04 15:14:03 +00:00
ui ::widget ::NodeImageMode ,
2024-09-06 06:00:43 +00:00
winit ::WinitSettings ,
} ;
2024-09-04 19:31:41 +00:00
fn main ( ) {
App ::new ( )
. add_plugins ( DefaultPlugins )
. insert_resource ( UiScale ( 2. ) )
// Only run the app when there is user input. This will significantly reduce CPU/GPU use for UI-only apps.
. insert_resource ( WinitSettings ::desktop_app ( ) )
. add_systems ( Startup , setup )
. run ( ) ;
}
fn setup ( mut commands : Commands , asset_server : Res < AssetServer > ) {
let image = asset_server . load_with_settings (
" textures/fantasy_ui_borders/numbered_slices.png " ,
| settings : & mut ImageLoaderSettings | {
// Need to use nearest filtering to avoid bleeding between the slices with tiling
settings . sampler = ImageSampler ::nearest ( ) ;
} ,
) ;
let slicer = TextureSlicer {
// `numbered_slices.png` is 48 pixels square. `BorderRect::square(16.)` insets the slicing line from each edge by 16 pixels, resulting in nine slices that are each 16 pixels square.
border : BorderRect ::square ( 16. ) ,
2024-10-20 01:03:27 +00:00
// With `SliceScaleMode::Tile` the side and center slices are tiled to fill the side and center sections of the target.
2024-09-04 19:31:41 +00:00
// And with a `stretch_value` of `1.` the tiles will have the same size as the corresponding slices in the source image.
center_scale_mode : SliceScaleMode ::Tile { stretch_value : 1. } ,
sides_scale_mode : SliceScaleMode ::Tile { stretch_value : 1. } ,
.. default ( )
} ;
// ui camera
2024-10-05 01:59:52 +00:00
commands . spawn ( Camera2d ) ;
2024-09-04 19:31:41 +00:00
commands
Merge Style properties into Node. Use ComputedNode for computed properties. (#15975)
# Objective
Continue improving the user experience of our UI Node API in the
direction specified by [Bevy's Next Generation Scene / UI
System](https://github.com/bevyengine/bevy/discussions/14437)
## Solution
As specified in the document above, merge `Style` fields into `Node`,
and move "computed Node fields" into `ComputedNode` (I chose this name
over something like `ComputedNodeLayout` because it currently contains
more than just layout info. If we want to break this up / rename these
concepts, lets do that in a separate PR). `Style` has been removed.
This accomplishes a number of goals:
## Ergonomics wins
Specifying both `Node` and `Style` is now no longer required for
non-default styles
Before:
```rust
commands.spawn((
Node::default(),
Style {
width: Val::Px(100.),
..default()
},
));
```
After:
```rust
commands.spawn(Node {
width: Val::Px(100.),
..default()
});
```
## Conceptual clarity
`Style` was never a comprehensive "style sheet". It only defined "core"
style properties that all `Nodes` shared. Any "styled property" that
couldn't fit that mold had to be in a separate component. A "real" style
system would style properties _across_ components (`Node`, `Button`,
etc). We have plans to build a true style system (see the doc linked
above).
By moving the `Style` fields to `Node`, we fully embrace `Node` as the
driving concept and remove the "style system" confusion.
## Next Steps
* Consider identifying and splitting out "style properties that aren't
core to Node". This should not happen for Bevy 0.15.
---
## Migration Guide
Move any fields set on `Style` into `Node` and replace all `Style`
component usage with `Node`.
Before:
```rust
commands.spawn((
Node::default(),
Style {
width: Val::Px(100.),
..default()
},
));
```
After:
```rust
commands.spawn(Node {
width: Val::Px(100.),
..default()
});
```
For any usage of the "computed node properties" that used to live on
`Node`, use `ComputedNode` instead:
Before:
```rust
fn system(nodes: Query<&Node>) {
for node in &nodes {
let computed_size = node.size();
}
}
```
After:
```rust
fn system(computed_nodes: Query<&ComputedNode>) {
for computed_node in &computed_nodes {
let computed_size = computed_node.size();
}
}
```
2024-10-18 22:25:33 +00:00
. spawn ( Node {
width : Val ::Percent ( 100. ) ,
height : Val ::Percent ( 100. ) ,
justify_content : JustifyContent ::Center ,
align_content : AlignContent ::Center ,
flex_wrap : FlexWrap ::Wrap ,
column_gap : Val ::Px ( 10. ) ,
row_gap : Val ::Px ( 10. ) ,
.. default ( )
} )
2024-09-04 19:31:41 +00:00
. with_children ( | parent | {
2024-11-22 00:45:07 +00:00
for [ columns , rows ] in [ [ 3. , 3. ] , [ 4. , 4. ] , [ 5. , 4. ] , [ 4. , 5. ] , [ 5. , 5. ] ] {
for ( flip_x , flip_y ) in [ ( false , false ) , ( false , true ) , ( true , false ) , ( true , true ) ]
{
parent . spawn ( (
ImageNode {
image : image . clone ( ) ,
flip_x ,
flip_y ,
image_mode : NodeImageMode ::Sliced ( slicer . clone ( ) ) ,
.. default ( )
} ,
Node {
width : Val ::Px ( 16. * columns ) ,
height : Val ::Px ( 16. * rows ) ,
.. default ( )
} ,
) ) ;
}
2024-09-04 19:31:41 +00:00
}
} ) ;
}