mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
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>
This commit is contained in:
parent
1e47604506
commit
4e02d3cdb9
17 changed files with 189 additions and 114 deletions
|
@ -8,12 +8,6 @@ use bevy_render::{
|
|||
use bevy_transform::components::{GlobalTransform, Transform};
|
||||
|
||||
/// A [`Bundle`] of components for drawing a single sprite from an image.
|
||||
///
|
||||
/// # Extra behaviors
|
||||
///
|
||||
/// You may add one or both of the following components to enable additional behaviors:
|
||||
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
|
||||
/// - [`TextureAtlas`](crate::TextureAtlas) to draw a specific section of the texture
|
||||
#[derive(Bundle, Clone, Debug, Default)]
|
||||
#[deprecated(
|
||||
since = "0.15.0",
|
||||
|
|
|
@ -30,7 +30,7 @@ pub mod prelude {
|
|||
#[doc(hidden)]
|
||||
pub use crate::{
|
||||
bundle::SpriteBundle,
|
||||
sprite::{ImageScaleMode, Sprite},
|
||||
sprite::{Sprite, SpriteImageMode},
|
||||
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
|
||||
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
|
||||
ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder,
|
||||
|
@ -106,7 +106,7 @@ impl Plugin for SpritePlugin {
|
|||
app.init_asset::<TextureAtlasLayout>()
|
||||
.register_asset_reflect::<TextureAtlasLayout>()
|
||||
.register_type::<Sprite>()
|
||||
.register_type::<ImageScaleMode>()
|
||||
.register_type::<SpriteImageMode>()
|
||||
.register_type::<TextureSlicer>()
|
||||
.register_type::<Anchor>()
|
||||
.register_type::<TextureAtlas>()
|
||||
|
|
|
@ -34,6 +34,8 @@ pub struct Sprite {
|
|||
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 {
|
||||
|
@ -79,9 +81,12 @@ impl From<Handle<Image>> for Sprite {
|
|||
}
|
||||
|
||||
/// Controls how the image is altered when scaled.
|
||||
#[derive(Component, Debug, Clone, Reflect)]
|
||||
#[reflect(Component, Debug)]
|
||||
pub enum ImageScaleMode {
|
||||
#[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`
|
||||
|
@ -96,6 +101,17 @@ pub enum ImageScaleMode {
|
|||
},
|
||||
}
|
||||
|
||||
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)]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{ExtractedSprite, ImageScaleMode, Sprite, TextureAtlasLayout};
|
||||
use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout};
|
||||
|
||||
use super::TextureSlice;
|
||||
use bevy_asset::{AssetEvent, Assets};
|
||||
|
@ -8,7 +8,7 @@ use bevy_render::texture::Image;
|
|||
use bevy_transform::prelude::*;
|
||||
use bevy_utils::HashSet;
|
||||
|
||||
/// Component storing texture slices for sprite entities with a [`ImageScaleMode`]
|
||||
/// Component storing texture slices for tiled or sliced sprite entities
|
||||
///
|
||||
/// This component is automatically inserted and updated
|
||||
#[derive(Debug, Clone, Component)]
|
||||
|
@ -69,24 +69,19 @@ impl ComputedTextureSlices {
|
|||
}
|
||||
}
|
||||
|
||||
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
|
||||
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices
|
||||
/// will be computed according to the `image_handle` dimensions or the sprite rect.
|
||||
///
|
||||
/// Returns `None` if the image asset is not loaded
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sprite` - The sprite component, will be used to find the draw area size
|
||||
/// * `scale_mode` - The image scaling component
|
||||
/// * `image_handle` - The texture to slice or tile
|
||||
/// * `sprite` - The sprite component with the image handle and image mode
|
||||
/// * `images` - The image assets, use to retrieve the image dimensions
|
||||
/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section
|
||||
/// of the texture
|
||||
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
|
||||
#[must_use]
|
||||
fn compute_sprite_slices(
|
||||
sprite: &Sprite,
|
||||
scale_mode: &ImageScaleMode,
|
||||
images: &Assets<Image>,
|
||||
atlas_layouts: &Assets<TextureAtlasLayout>,
|
||||
) -> Option<ComputedTextureSlices> {
|
||||
|
@ -111,9 +106,9 @@ fn compute_sprite_slices(
|
|||
(size, rect)
|
||||
}
|
||||
};
|
||||
let slices = match scale_mode {
|
||||
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
|
||||
ImageScaleMode::Tiled {
|
||||
let slices = match &sprite.image_mode {
|
||||
SpriteImageMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
|
||||
SpriteImageMode::Tiled {
|
||||
tile_x,
|
||||
tile_y,
|
||||
stretch_value,
|
||||
|
@ -125,18 +120,21 @@ fn compute_sprite_slices(
|
|||
};
|
||||
slice.tiled(*stretch_value, (*tile_x, *tile_y))
|
||||
}
|
||||
SpriteImageMode::Auto => {
|
||||
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
|
||||
}
|
||||
};
|
||||
Some(ComputedTextureSlices(slices))
|
||||
}
|
||||
|
||||
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
|
||||
/// on matching sprite entities with a [`ImageScaleMode`] component
|
||||
/// on sprite entities with a matching [`SpriteImageMode`]
|
||||
pub(crate) fn compute_slices_on_asset_event(
|
||||
mut commands: Commands,
|
||||
mut events: EventReader<AssetEvent<Image>>,
|
||||
images: Res<Assets<Image>>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
sprites: Query<(Entity, &ImageScaleMode, &Sprite)>,
|
||||
sprites: Query<(Entity, &Sprite)>,
|
||||
) {
|
||||
// We store the asset ids of added/modified image assets
|
||||
let added_handles: HashSet<_> = events
|
||||
|
@ -150,29 +148,31 @@ pub(crate) fn compute_slices_on_asset_event(
|
|||
return;
|
||||
}
|
||||
// We recompute the sprite slices for sprite entities with a matching asset handle id
|
||||
for (entity, scale_mode, sprite) in &sprites {
|
||||
for (entity, sprite) in &sprites {
|
||||
if !sprite.image_mode.uses_slices() {
|
||||
continue;
|
||||
}
|
||||
if !added_handles.contains(&sprite.image.id()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
|
||||
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
|
||||
commands.entity(entity).insert(slices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
|
||||
/// on matching sprite entities with a [`ImageScaleMode`] component
|
||||
/// System reacting to changes on the [`Sprite`] component to compute the sprite slices
|
||||
pub(crate) fn compute_slices_on_sprite_change(
|
||||
mut commands: Commands,
|
||||
images: Res<Assets<Image>>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
changed_sprites: Query<
|
||||
(Entity, &ImageScaleMode, &Sprite),
|
||||
Or<(Changed<ImageScaleMode>, Changed<Sprite>)>,
|
||||
>,
|
||||
changed_sprites: Query<(Entity, &Sprite), Changed<Sprite>>,
|
||||
) {
|
||||
for (entity, scale_mode, sprite) in &changed_sprites {
|
||||
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
|
||||
for (entity, sprite) in &changed_sprites {
|
||||
if !sprite.image_mode.uses_slices() {
|
||||
continue;
|
||||
}
|
||||
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
|
||||
commands.entity(entity).insert(slices);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use bevy_reflect::Reflect;
|
|||
/// sections will be scaled or tiled.
|
||||
///
|
||||
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
|
||||
#[derive(Debug, Clone, Reflect)]
|
||||
#[derive(Debug, Clone, Reflect, PartialEq)]
|
||||
pub struct TextureSlicer {
|
||||
/// The sprite borders, defining the 9 sections of the image
|
||||
pub border: BorderRect,
|
||||
|
@ -23,7 +23,7 @@ pub struct TextureSlicer {
|
|||
}
|
||||
|
||||
/// Defines how a texture slice scales when resized
|
||||
#[derive(Debug, Copy, Clone, Default, Reflect)]
|
||||
#[derive(Debug, Copy, Clone, Default, Reflect, PartialEq)]
|
||||
pub enum SliceScaleMode {
|
||||
/// The slice will be stretched to fit the area
|
||||
#[default]
|
||||
|
|
|
@ -221,7 +221,7 @@ pub fn ui_layout_system(
|
|||
|| node.is_changed()
|
||||
|| content_size
|
||||
.as_ref()
|
||||
.map(|c| c.measure.is_some())
|
||||
.map(|c| c.is_changed() || c.measure.is_some())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let layout_context = LayoutContext::new(
|
||||
|
|
|
@ -62,7 +62,7 @@ pub mod prelude {
|
|||
Interaction, MaterialNode, UiMaterialPlugin, UiScale,
|
||||
},
|
||||
// `bevy_sprite` re-exports for texture slicing
|
||||
bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer},
|
||||
bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -57,12 +57,6 @@ pub struct NodeBundle {
|
|||
}
|
||||
|
||||
/// A UI node that is an image
|
||||
///
|
||||
/// # Extra behaviors
|
||||
///
|
||||
/// You may add one or both of the following components to enable additional behaviors:
|
||||
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
|
||||
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
|
||||
#[derive(Bundle, Debug, Default)]
|
||||
#[deprecated(
|
||||
since = "0.15.0",
|
||||
|
@ -110,12 +104,6 @@ pub struct ImageBundle {
|
|||
}
|
||||
|
||||
/// A UI node that is a button
|
||||
///
|
||||
/// # Extra behaviors
|
||||
///
|
||||
/// You may add one or both of the following components to enable additional behaviors:
|
||||
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
|
||||
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
|
||||
#[derive(Bundle, Clone, Debug)]
|
||||
#[deprecated(
|
||||
since = "0.15.0",
|
||||
|
|
|
@ -41,7 +41,7 @@ use bevy_render::{
|
|||
ExtractSchedule, Render,
|
||||
};
|
||||
use bevy_sprite::TextureAtlasLayout;
|
||||
use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents};
|
||||
use bevy_sprite::{BorderRect, SpriteAssetEvents};
|
||||
|
||||
use crate::{Display, Node};
|
||||
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
|
||||
|
@ -309,18 +309,15 @@ pub fn extract_uinode_images(
|
|||
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
uinode_query: Extract<
|
||||
Query<
|
||||
(
|
||||
Entity,
|
||||
&ComputedNode,
|
||||
&GlobalTransform,
|
||||
&ViewVisibility,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&TargetCamera>,
|
||||
&UiImage,
|
||||
),
|
||||
Without<ImageScaleMode>,
|
||||
>,
|
||||
Query<(
|
||||
Entity,
|
||||
&ComputedNode,
|
||||
&GlobalTransform,
|
||||
&ViewVisibility,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&TargetCamera>,
|
||||
&UiImage,
|
||||
)>,
|
||||
>,
|
||||
mapping: Extract<Query<RenderEntity>>,
|
||||
) {
|
||||
|
@ -338,6 +335,7 @@ pub fn extract_uinode_images(
|
|||
if !view_visibility.get()
|
||||
|| image.color.is_fully_transparent()
|
||||
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
|
||||
|| image.image_mode.uses_slices()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ use bevy_render::{
|
|||
Extract, ExtractSchedule, Render, RenderSet,
|
||||
};
|
||||
use bevy_sprite::{
|
||||
ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlasLayout, TextureSlicer,
|
||||
SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureAtlasLayout, TextureSlicer,
|
||||
};
|
||||
use bevy_transform::prelude::GlobalTransform;
|
||||
use bevy_utils::HashMap;
|
||||
|
@ -232,7 +232,7 @@ pub struct ExtractedUiTextureSlice {
|
|||
pub clip: Option<Rect>,
|
||||
pub camera_entity: Entity,
|
||||
pub color: LinearRgba,
|
||||
pub image_scale_mode: ImageScaleMode,
|
||||
pub image_scale_mode: SpriteImageMode,
|
||||
pub flip_x: bool,
|
||||
pub flip_y: bool,
|
||||
pub main_entity: MainEntity,
|
||||
|
@ -257,14 +257,11 @@ pub fn extract_ui_texture_slices(
|
|||
Option<&CalculatedClip>,
|
||||
Option<&TargetCamera>,
|
||||
&UiImage,
|
||||
&ImageScaleMode,
|
||||
)>,
|
||||
>,
|
||||
mapping: Extract<Query<RenderEntity>>,
|
||||
) {
|
||||
for (entity, uinode, transform, view_visibility, clip, camera, image, image_scale_mode) in
|
||||
&slicers_query
|
||||
{
|
||||
for (entity, uinode, transform, view_visibility, clip, camera, image) in &slicers_query {
|
||||
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
||||
else {
|
||||
continue;
|
||||
|
@ -274,6 +271,22 @@ pub fn extract_ui_texture_slices(
|
|||
continue;
|
||||
};
|
||||
|
||||
let image_scale_mode = match image.image_mode.clone() {
|
||||
widget::NodeImageMode::Sliced(texture_slicer) => {
|
||||
SpriteImageMode::Sliced(texture_slicer)
|
||||
}
|
||||
widget::NodeImageMode::Tiled {
|
||||
tile_x,
|
||||
tile_y,
|
||||
stretch_value,
|
||||
} => SpriteImageMode::Tiled {
|
||||
tile_x,
|
||||
tile_y,
|
||||
stretch_value,
|
||||
},
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Skip invisible images
|
||||
if !view_visibility.get()
|
||||
|| image.color.is_fully_transparent()
|
||||
|
@ -312,7 +325,7 @@ pub fn extract_ui_texture_slices(
|
|||
clip: clip.map(|clip| clip.clip),
|
||||
image: image.image.id(),
|
||||
camera_entity,
|
||||
image_scale_mode: image_scale_mode.clone(),
|
||||
image_scale_mode,
|
||||
atlas_rect,
|
||||
flip_x: image.flip_x,
|
||||
flip_y: image.flip_y,
|
||||
|
@ -719,10 +732,10 @@ impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {
|
|||
fn compute_texture_slices(
|
||||
image_size: Vec2,
|
||||
target_size: Vec2,
|
||||
image_scale_mode: &ImageScaleMode,
|
||||
image_scale_mode: &SpriteImageMode,
|
||||
) -> [[f32; 4]; 3] {
|
||||
match image_scale_mode {
|
||||
ImageScaleMode::Sliced(TextureSlicer {
|
||||
SpriteImageMode::Sliced(TextureSlicer {
|
||||
border: border_rect,
|
||||
center_scale_mode,
|
||||
sides_scale_mode,
|
||||
|
@ -775,7 +788,7 @@ fn compute_texture_slices(
|
|||
],
|
||||
]
|
||||
}
|
||||
ImageScaleMode::Tiled {
|
||||
SpriteImageMode::Tiled {
|
||||
tile_x,
|
||||
tile_y,
|
||||
stretch_value,
|
||||
|
@ -784,6 +797,9 @@ fn compute_texture_slices(
|
|||
let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value);
|
||||
[[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]]
|
||||
}
|
||||
SpriteImageMode::Auto => {
|
||||
unreachable!("Slices should not be computed for ImageScaleMode::Stretch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,14 +5,14 @@ use bevy_ecs::prelude::*;
|
|||
use bevy_math::{Rect, UVec2, Vec2};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_render::texture::{Image, TRANSPARENT_IMAGE_HANDLE};
|
||||
use bevy_sprite::{TextureAtlas, TextureAtlasLayout};
|
||||
use bevy_sprite::{TextureAtlas, TextureAtlasLayout, TextureSlicer};
|
||||
use bevy_window::{PrimaryWindow, Window};
|
||||
use taffy::{MaybeMath, MaybeResolve};
|
||||
|
||||
/// The 2D texture displayed for this UI node
|
||||
#[derive(Component, Clone, Debug, Reflect)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
#[require(Node, UiImageSize, ContentSize)]
|
||||
#[require(Node, UiImageSize)]
|
||||
pub struct UiImage {
|
||||
/// The tint color used to draw the image.
|
||||
///
|
||||
|
@ -23,11 +23,11 @@ pub struct UiImage {
|
|||
///
|
||||
/// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
|
||||
pub image: Handle<Image>,
|
||||
/// The (optional) texture atlas used to render the image
|
||||
/// The (optional) texture atlas used to render the image.
|
||||
pub texture_atlas: Option<TextureAtlas>,
|
||||
/// Whether the image should be flipped along its x-axis
|
||||
/// Whether the image should be flipped along its x-axis.
|
||||
pub flip_x: bool,
|
||||
/// Whether the image should be flipped along its y-axis
|
||||
/// Whether the image should be flipped along its y-axis.
|
||||
pub flip_y: bool,
|
||||
/// An optional rectangle representing the region of the image to render, instead of rendering
|
||||
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
|
||||
|
@ -35,6 +35,8 @@ pub struct UiImage {
|
|||
/// When used with a [`TextureAtlas`], the rect
|
||||
/// is offset by the atlas's minimal (top-left) corner position.
|
||||
pub rect: Option<Rect>,
|
||||
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space to allocate for the image.
|
||||
pub image_mode: NodeImageMode,
|
||||
}
|
||||
|
||||
impl Default for UiImage {
|
||||
|
@ -56,6 +58,7 @@ impl Default for UiImage {
|
|||
flip_x: false,
|
||||
flip_y: false,
|
||||
rect: None,
|
||||
image_mode: NodeImageMode::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +84,7 @@ impl UiImage {
|
|||
flip_y: false,
|
||||
texture_atlas: None,
|
||||
rect: None,
|
||||
image_mode: NodeImageMode::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +123,12 @@ impl UiImage {
|
|||
self.rect = Some(rect);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn with_mode(mut self, mode: NodeImageMode) -> Self {
|
||||
self.image_mode = mode;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Handle<Image>> for UiImage {
|
||||
|
@ -127,6 +137,39 @@ impl From<Handle<Image>> for UiImage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image
|
||||
#[derive(Default, Debug, Clone, Reflect)]
|
||||
pub enum NodeImageMode {
|
||||
/// The image will be sized automatically by taking the size of the source image and applying any layout constraints.
|
||||
#[default]
|
||||
Auto,
|
||||
/// The image will be resized to match the size of the node. The image's original size and aspect ratio will be ignored.
|
||||
Stretch,
|
||||
/// 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 NodeImageMode {
|
||||
/// Returns true if this mode uses slices internally ([`NodeImageMode::Sliced`] or [`NodeImageMode::Tiled`])
|
||||
#[inline]
|
||||
pub fn uses_slices(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
NodeImageMode::Sliced(..) | NodeImageMode::Tiled { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of the image's texture
|
||||
///
|
||||
/// This component is updated automatically by [`update_image_content_size_system`]
|
||||
|
@ -216,7 +259,7 @@ pub fn update_image_content_size_system(
|
|||
textures: Res<Assets<Image>>,
|
||||
|
||||
atlases: Res<Assets<TextureAtlasLayout>>,
|
||||
mut query: Query<(&mut ContentSize, &UiImage, &mut UiImageSize), UpdateImageFilter>,
|
||||
mut query: Query<(&mut ContentSize, Ref<UiImage>, &mut UiImageSize), UpdateImageFilter>,
|
||||
) {
|
||||
let combined_scale_factor = windows
|
||||
.get_single()
|
||||
|
@ -225,6 +268,14 @@ pub fn update_image_content_size_system(
|
|||
* ui_scale.0;
|
||||
|
||||
for (mut content_size, image, mut image_size) in &mut query {
|
||||
if !matches!(image.image_mode, NodeImageMode::Auto) {
|
||||
if image.is_changed() {
|
||||
// Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
|
||||
content_size.measure = None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(size) = match &image.texture_atlas {
|
||||
Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
|
||||
None => textures.get(&image.image).map(Image::size),
|
||||
|
|
|
@ -19,55 +19,65 @@ fn spawn_sprites(
|
|||
) {
|
||||
let cases = [
|
||||
// Reference sprite
|
||||
("Original", style.clone(), Vec2::splat(100.0), None),
|
||||
(
|
||||
"Original",
|
||||
style.clone(),
|
||||
Vec2::splat(100.0),
|
||||
SpriteImageMode::Auto,
|
||||
),
|
||||
// Scaled regular sprite
|
||||
("Stretched", style.clone(), Vec2::new(100.0, 200.0), None),
|
||||
(
|
||||
"Stretched",
|
||||
style.clone(),
|
||||
Vec2::new(100.0, 200.0),
|
||||
SpriteImageMode::Auto,
|
||||
),
|
||||
// Stretched Scaled sliced sprite
|
||||
(
|
||||
"With Slicing",
|
||||
style.clone(),
|
||||
Vec2::new(100.0, 200.0),
|
||||
Some(ImageScaleMode::Sliced(TextureSlicer {
|
||||
SpriteImageMode::Sliced(TextureSlicer {
|
||||
border: BorderRect::square(slice_border),
|
||||
center_scale_mode: SliceScaleMode::Stretch,
|
||||
..default()
|
||||
})),
|
||||
}),
|
||||
),
|
||||
// Scaled sliced sprite
|
||||
(
|
||||
"With Tiling",
|
||||
style.clone(),
|
||||
Vec2::new(100.0, 200.0),
|
||||
Some(ImageScaleMode::Sliced(TextureSlicer {
|
||||
SpriteImageMode::Sliced(TextureSlicer {
|
||||
border: BorderRect::square(slice_border),
|
||||
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 },
|
||||
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
|
||||
..default()
|
||||
})),
|
||||
}),
|
||||
),
|
||||
// Scaled sliced sprite horizontally
|
||||
(
|
||||
"With Tiling",
|
||||
style.clone(),
|
||||
Vec2::new(300.0, 200.0),
|
||||
Some(ImageScaleMode::Sliced(TextureSlicer {
|
||||
SpriteImageMode::Sliced(TextureSlicer {
|
||||
border: BorderRect::square(slice_border),
|
||||
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
|
||||
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 },
|
||||
..default()
|
||||
})),
|
||||
}),
|
||||
),
|
||||
// Scaled sliced sprite horizontally with max scale
|
||||
(
|
||||
"With Corners Constrained",
|
||||
style,
|
||||
Vec2::new(300.0, 200.0),
|
||||
Some(ImageScaleMode::Sliced(TextureSlicer {
|
||||
SpriteImageMode::Sliced(TextureSlicer {
|
||||
border: BorderRect::square(slice_border),
|
||||
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 },
|
||||
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
|
||||
max_corner_scale: 0.2,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -77,13 +87,11 @@ fn spawn_sprites(
|
|||
Sprite {
|
||||
image: texture_handle.clone(),
|
||||
custom_size: Some(size),
|
||||
image_mode: scale_mode,
|
||||
..default()
|
||||
},
|
||||
Transform::from_translation(position),
|
||||
));
|
||||
if let Some(scale_mode) = scale_mode {
|
||||
cmd.insert(scale_mode);
|
||||
}
|
||||
cmd.with_children(|builder| {
|
||||
builder.spawn((
|
||||
Text2d::new(label),
|
||||
|
|
|
@ -26,14 +26,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
current: 128.0,
|
||||
speed: 50.0,
|
||||
});
|
||||
commands.spawn((
|
||||
Sprite::from_image(asset_server.load("branding/icon.png")),
|
||||
ImageScaleMode::Tiled {
|
||||
commands.spawn(Sprite {
|
||||
image: asset_server.load("branding/icon.png"),
|
||||
image_mode: SpriteImageMode::Tiled {
|
||||
tile_x: true,
|
||||
tile_y: true,
|
||||
stretch_value: 0.5, // The image will tile every 128px
|
||||
},
|
||||
));
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
fn animate(mut sprites: Query<&mut Sprite>, mut state: ResMut<AnimationState>, time: Res<Time>) {
|
||||
|
|
|
@ -9,6 +9,7 @@ use bevy::{
|
|||
input::mouse::{MouseScrollUnit, MouseWheel},
|
||||
picking::focus::HoverMap,
|
||||
prelude::*,
|
||||
ui::widget::NodeImageMode,
|
||||
winit::WinitSettings,
|
||||
};
|
||||
|
||||
|
@ -280,7 +281,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
// bevy logo (image)
|
||||
parent
|
||||
.spawn((
|
||||
UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png")),
|
||||
UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png"))
|
||||
.with_mode(NodeImageMode::Stretch),
|
||||
Node {
|
||||
width: Val::Px(500.0),
|
||||
height: Val::Px(125.0),
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
use bevy::{
|
||||
color::palettes::css::{GOLD, ORANGE},
|
||||
prelude::*,
|
||||
ui::widget::NodeImageMode,
|
||||
winit::WinitSettings,
|
||||
};
|
||||
|
||||
|
@ -87,7 +88,8 @@ fn setup(
|
|||
index: idx,
|
||||
layout: atlas_layout_handle.clone(),
|
||||
},
|
||||
),
|
||||
)
|
||||
.with_mode(NodeImageMode::Sliced(slicer.clone())),
|
||||
Node {
|
||||
width: Val::Px(w),
|
||||
height: Val::Px(h),
|
||||
|
@ -98,7 +100,6 @@ fn setup(
|
|||
margin: UiRect::all(Val::Px(20.0)),
|
||||
..default()
|
||||
},
|
||||
ImageScaleMode::Sliced(slicer.clone()),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
use bevy::{
|
||||
color::palettes::css::{GOLD, ORANGE},
|
||||
prelude::*,
|
||||
ui::widget::NodeImageMode,
|
||||
winit::WinitSettings,
|
||||
};
|
||||
|
||||
|
@ -77,20 +78,18 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
margin: UiRect::all(Val::Px(20.0)),
|
||||
..default()
|
||||
},
|
||||
UiImage::new(image.clone()),
|
||||
ImageScaleMode::Sliced(slicer.clone()),
|
||||
UiImage::new(image.clone())
|
||||
.with_mode(NodeImageMode::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)),
|
||||
));
|
||||
});
|
||||
.with_child((
|
||||
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)),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use bevy::{
|
||||
prelude::*,
|
||||
render::texture::{ImageLoaderSettings, ImageSampler},
|
||||
ui::widget::NodeImageMode,
|
||||
winit::WinitSettings,
|
||||
};
|
||||
|
||||
|
@ -61,6 +62,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
image: image.clone(),
|
||||
flip_x,
|
||||
flip_y,
|
||||
image_mode: NodeImageMode::Sliced(slicer.clone()),
|
||||
..default()
|
||||
},
|
||||
Node {
|
||||
|
@ -68,7 +70,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
height: Val::Px(height),
|
||||
..default()
|
||||
},
|
||||
ImageScaleMode::Sliced(slicer.clone()),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue