
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 * 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. 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>
152 lines
4.9 KiB
Rust
152 lines
4.9 KiB
Rust
use bevy_asset::Handle;
|
|
use bevy_color::Color;
|
|
use bevy_ecs::{component::Component, reflect::ReflectComponent};
|
|
use bevy_math::{Rect, Vec2};
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|
use bevy_render::{sync_world::SyncToRenderWorld, texture::Image, view::Visibility};
|
|
use bevy_transform::components::Transform;
|
|
|
|
use crate::{TextureAtlas, TextureSlicer};
|
|
|
|
/// Describes a sprite to be rendered to a 2D camera
|
|
#[derive(Component, Debug, Default, Clone, Reflect)]
|
|
#[require(Transform, Visibility, SyncToRenderWorld)]
|
|
#[reflect(Component, Default, Debug)]
|
|
pub struct Sprite {
|
|
/// The image used to render the sprite
|
|
pub image: Handle<Image>,
|
|
/// The (optional) texture atlas used to render the sprite
|
|
pub texture_atlas: Option<TextureAtlas>,
|
|
/// The sprite's color tint
|
|
pub color: Color,
|
|
/// Flip the sprite along the `X` axis
|
|
pub flip_x: bool,
|
|
/// Flip the sprite along the `Y` axis
|
|
pub flip_y: bool,
|
|
/// An optional custom size for the sprite that will be used when rendering, instead of the size
|
|
/// of the sprite's image
|
|
pub custom_size: Option<Vec2>,
|
|
/// An optional rectangle representing the region of the sprite's image to render, instead of rendering
|
|
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
|
|
///
|
|
/// When used with a [`TextureAtlas`], the rect
|
|
/// is offset by the atlas's minimal (top-left) corner position.
|
|
pub rect: Option<Rect>,
|
|
/// [`Anchor`] point of the sprite in the world
|
|
pub anchor: Anchor,
|
|
/// How the sprite's image will be scaled.
|
|
pub image_mode: SpriteImageMode,
|
|
}
|
|
|
|
impl Sprite {
|
|
/// Create a Sprite with a custom size
|
|
pub fn sized(custom_size: Vec2) -> Self {
|
|
Sprite {
|
|
custom_size: Some(custom_size),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Create a sprite from an image
|
|
pub fn from_image(image: Handle<Image>) -> Self {
|
|
Self {
|
|
image,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Create a sprite from an image, with an associated texture atlas
|
|
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
|
|
Self {
|
|
image,
|
|
texture_atlas: Some(atlas),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Create a sprite from a solid color
|
|
pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
|
|
Self {
|
|
color: color.into(),
|
|
custom_size: Some(size),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Handle<Image>> for Sprite {
|
|
fn from(image: Handle<Image>) -> Self {
|
|
Self::from_image(image)
|
|
}
|
|
}
|
|
|
|
/// Controls how the image is altered when scaled.
|
|
#[derive(Default, Debug, Clone, Reflect, PartialEq)]
|
|
#[reflect(Debug)]
|
|
pub enum SpriteImageMode {
|
|
/// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
|
|
#[default]
|
|
Auto,
|
|
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
|
|
Sliced(TextureSlicer),
|
|
/// The texture will be repeated if stretched beyond `stretched_value`
|
|
Tiled {
|
|
/// Should the image repeat horizontally
|
|
tile_x: bool,
|
|
/// Should the image repeat vertically
|
|
tile_y: bool,
|
|
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
|
|
/// *original texture size* are above this value.
|
|
stretch_value: f32,
|
|
},
|
|
}
|
|
|
|
impl SpriteImageMode {
|
|
/// Returns true if this mode uses slices internally ([`SpriteImageMode::Sliced`] or [`SpriteImageMode::Tiled`])
|
|
#[inline]
|
|
pub fn uses_slices(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
|
|
)
|
|
}
|
|
}
|
|
|
|
/// How a sprite is positioned relative to its [`Transform`].
|
|
/// It defaults to `Anchor::Center`.
|
|
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
|
|
#[reflect(Component, Default, Debug, PartialEq)]
|
|
#[doc(alias = "pivot")]
|
|
pub enum Anchor {
|
|
#[default]
|
|
Center,
|
|
BottomLeft,
|
|
BottomCenter,
|
|
BottomRight,
|
|
CenterLeft,
|
|
CenterRight,
|
|
TopLeft,
|
|
TopCenter,
|
|
TopRight,
|
|
/// Custom anchor point. Top left is `(-0.5, 0.5)`, center is `(0.0, 0.0)`. The value will
|
|
/// be scaled with the sprite size.
|
|
Custom(Vec2),
|
|
}
|
|
|
|
impl Anchor {
|
|
pub fn as_vec(&self) -> Vec2 {
|
|
match self {
|
|
Anchor::Center => Vec2::ZERO,
|
|
Anchor::BottomLeft => Vec2::new(-0.5, -0.5),
|
|
Anchor::BottomCenter => Vec2::new(0.0, -0.5),
|
|
Anchor::BottomRight => Vec2::new(0.5, -0.5),
|
|
Anchor::CenterLeft => Vec2::new(-0.5, 0.0),
|
|
Anchor::CenterRight => Vec2::new(0.5, 0.0),
|
|
Anchor::TopLeft => Vec2::new(-0.5, 0.5),
|
|
Anchor::TopCenter => Vec2::new(0.0, 0.5),
|
|
Anchor::TopRight => Vec2::new(0.5, 0.5),
|
|
Anchor::Custom(point) => *point,
|
|
}
|
|
}
|
|
}
|