mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
UI Texture 9 slice (#11600)
> Follow up to #10588 > Closes #11749 (Supersedes #11756) Enable Texture slicing for the following UI nodes: - `ImageBundle` - `ButtonBundle` <img width="739" alt="Screenshot 2024-01-29 at 13 57 43" src="https://github.com/bevyengine/bevy/assets/26703856/37675681-74eb-4689-ab42-024310cf3134"> I also added a collection of `fantazy-ui-borders` from [Kenney's](www.kenney.nl) assets, with the appropriate license (CC). If it's a problem I can use the same textures as the `sprite_slice` example # Work done Added the `ImageScaleMode` component to the targetted bundles, most of the logic is directly reused from `bevy_sprite`. The only additional internal component is the UI specific `ComputedSlices`, which does the same thing as its spritee equivalent but adapted to UI code. Again the slicing is not compatible with `TextureAtlas`, it's something I need to tackle more deeply in the future # Fixes * [x] I noticed that `TextureSlicer::compute_slices` could infinitely loop if the border was larger that the image half extents, now an error is triggered and the texture will fallback to being stretched * [x] I noticed that when using small textures with very small *tiling* options we could generate hundred of thousands of slices. Now I set a minimum size of 1 pixel per slice, which is already ridiculously small, and a warning will be sent at runtime when slice count goes above 1000 * [x] Sprite slicing with `flip_x` or `flip_y` would give incorrect results, correct flipping is now supported to both sprites and ui image nodes thanks to @odecay observation # GPU Alternative I create a separate branch attempting to implementing 9 slicing and tiling directly through the `ui.wgsl` fragment shader. It works but requires sending more data to the GPU: - slice border - tiling factors And more importantly, the actual quad *scale* which is hard to put in the shader with the current code, so that would be for a later iteration
This commit is contained in:
parent
ff77adc045
commit
ab16f5ed6a
17 changed files with 414 additions and 30 deletions
|
@ -21,6 +21,7 @@
|
|||
* Ground tile from [Kenney's Tower Defense Kit](https://www.kenney.nl/assets/tower-defense-kit) (CC0 1.0 Universal)
|
||||
* Game icons from [Kenney's Game Icons](https://www.kenney.nl/assets/game-icons) (CC0 1.0 Universal)
|
||||
* Space ships from [Kenny's Simple Space Kit](https://www.kenney.nl/assets/simple-space) (CC0 1.0 Universal)
|
||||
* UI borders from [Kenny's Fantasy UI Borders Kit](https://kenney.nl/assets/fantasy-ui-borders) (CC0 1.0 Universal)
|
||||
* glTF animated fox from [glTF Sample Models][fox]
|
||||
* Low poly fox [by PixelMannen] (CC0 1.0 Universal)
|
||||
* Rigging and animation [by @tomkranis on Sketchfab] ([CC-BY 4.0])
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -2482,6 +2482,17 @@ description = "Illustrates how to use TextureAtlases in UI"
|
|||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "ui_texture_slice"
|
||||
path = "examples/ui/ui_texture_slice.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.ui_texture_slice]
|
||||
name = "UI Texture Slice"
|
||||
description = "Illustrates how to use 9 Slicing in UI"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "viewport_debug"
|
||||
path = "examples/ui/viewport_debug.rs"
|
||||
|
|
30
assets/textures/fantasy_ui_borders/License.txt
Normal file
30
assets/textures/fantasy_ui_borders/License.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
|
||||
|
||||
Fantasy UI Borders (1.0)
|
||||
|
||||
Created/distributed by Kenney (www.kenney.nl)
|
||||
Creation date: 03-12-2023
|
||||
|
||||
For the sample image the font 'Aoboshi One' was used, OPL (Open Font License)
|
||||
|
||||
------------------------------
|
||||
|
||||
License: (Creative Commons Zero, CC0)
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
You can use this content for personal, educational, and commercial purposes.
|
||||
|
||||
Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
|
||||
|
||||
------------------------------
|
||||
|
||||
• Website : www.kenney.nl
|
||||
• Donate : www.kenney.nl/donate
|
||||
|
||||
• Patreon : patreon.com/kenney
|
||||
|
||||
Follow on social media for updates:
|
||||
|
||||
• Twitter: twitter.com/KenneyNL
|
||||
• Instagram: instagram.com/kenney_nl
|
||||
• Mastodon: mastodon.gamedev.place/@kenney
|
BIN
assets/textures/fantasy_ui_borders/panel-border-010.png
Normal file
BIN
assets/textures/fantasy_ui_borders/panel-border-010.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 B |
BIN
assets/textures/fantasy_ui_borders/panel-border-015.png
Normal file
BIN
assets/textures/fantasy_ui_borders/panel-border-015.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 136 B |
|
@ -39,10 +39,13 @@ pub struct SpriteBundle {
|
|||
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
|
||||
#[derive(Bundle, Clone, Default)]
|
||||
pub struct SpriteSheetBundle {
|
||||
/// Specifies the rendering properties of the sprite, such as color tint and flip.
|
||||
pub sprite: Sprite,
|
||||
/// Controls how the image is altered when scaled.
|
||||
pub scale_mode: ImageScaleMode,
|
||||
/// The local transform of the sprite, relative to its parent.
|
||||
pub transform: Transform,
|
||||
/// The absolute transform of the sprite. This should generally not be written to directly.
|
||||
pub global_transform: GlobalTransform,
|
||||
/// The sprite sheet base texture
|
||||
pub texture: Handle<Image>,
|
||||
|
@ -50,6 +53,7 @@ pub struct SpriteSheetBundle {
|
|||
pub atlas: TextureAtlas,
|
||||
/// User indication of whether an entity is visible
|
||||
pub visibility: Visibility,
|
||||
/// Inherited visibility of an entity.
|
||||
pub inherited_visibility: InheritedVisibility,
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
|
||||
pub view_visibility: ViewVisibility,
|
||||
|
|
|
@ -17,7 +17,7 @@ pub mod prelude {
|
|||
bundle::{SpriteBundle, SpriteSheetBundle},
|
||||
sprite::{ImageScaleMode, Sprite},
|
||||
texture_atlas::{TextureAtlas, TextureAtlasLayout},
|
||||
texture_slice::{BorderRect, SliceScaleMode, TextureSlicer},
|
||||
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
|
||||
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
|
||||
};
|
||||
}
|
||||
|
@ -124,7 +124,6 @@ impl Plugin for SpritePlugin {
|
|||
/// System calculating and inserting an [`Aabb`] component to entities with either:
|
||||
/// - a `Mesh2dHandle` component,
|
||||
/// - a `Sprite` and `Handle<Image>` components,
|
||||
/// - a `TextureAtlasSprite` and `Handle<TextureAtlas>` components,
|
||||
/// and without a [`NoFrustumCulling`] component.
|
||||
///
|
||||
/// Used in system set [`VisibilitySystems::CalculateBounds`].
|
||||
|
@ -137,7 +136,7 @@ pub fn calculate_bounds_2d(
|
|||
sprites_to_recalculate_aabb: Query<
|
||||
(Entity, &Sprite, &Handle<Image>, Option<&TextureAtlas>),
|
||||
(
|
||||
Or<(Without<Aabb>, Changed<Sprite>)>,
|
||||
Or<(Without<Aabb>, Changed<Sprite>, Changed<TextureAtlas>)>,
|
||||
Without<NoFrustumCulling>,
|
||||
),
|
||||
>,
|
||||
|
|
|
@ -31,17 +31,27 @@ impl ComputedTextureSlices {
|
|||
sprite: &'a Sprite,
|
||||
handle: &'a Handle<Image>,
|
||||
) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a {
|
||||
let mut flip = Vec2::ONE;
|
||||
let [mut flip_x, mut flip_y] = [false; 2];
|
||||
if sprite.flip_x {
|
||||
flip.x *= -1.0;
|
||||
flip_x = true;
|
||||
}
|
||||
if sprite.flip_y {
|
||||
flip.y *= -1.0;
|
||||
flip_y = true;
|
||||
}
|
||||
self.0.iter().map(move |slice| {
|
||||
let transform =
|
||||
transform.mul_transform(Transform::from_translation(slice.offset.extend(0.0)));
|
||||
let offset = (slice.offset * flip).extend(0.0);
|
||||
let transform = transform.mul_transform(Transform::from_translation(offset));
|
||||
ExtractedSprite {
|
||||
original_entity: Some(original_entity),
|
||||
color: sprite.color,
|
||||
transform,
|
||||
rect: Some(slice.texture_rect),
|
||||
custom_size: Some(slice.draw_size),
|
||||
flip_x: sprite.flip_x,
|
||||
flip_y: sprite.flip_y,
|
||||
flip_x,
|
||||
flip_y,
|
||||
image_handle_id: handle.id(),
|
||||
anchor: sprite.anchor.as_vec(),
|
||||
}
|
||||
|
|
|
@ -10,8 +10,9 @@ pub(crate) use computed_slices::{
|
|||
compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices,
|
||||
};
|
||||
|
||||
/// Single texture slice, representing a texture rect to draw in a given area
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TextureSlice {
|
||||
pub struct TextureSlice {
|
||||
/// texture area to draw
|
||||
pub texture_rect: Rect,
|
||||
/// slice draw size
|
||||
|
@ -39,16 +40,19 @@ impl TextureSlice {
|
|||
// Each tile expected size
|
||||
let expected_size = Vec2::new(
|
||||
if tile_x {
|
||||
rect_size.x * stretch_value
|
||||
// No slice should be less than 1 pixel wide
|
||||
(rect_size.x * stretch_value).max(1.0)
|
||||
} else {
|
||||
self.draw_size.x
|
||||
},
|
||||
if tile_y {
|
||||
rect_size.y * stretch_value
|
||||
// No slice should be less than 1 pixel high
|
||||
(rect_size.y * stretch_value).max(1.0)
|
||||
} else {
|
||||
self.draw_size.y
|
||||
},
|
||||
);
|
||||
)
|
||||
.min(self.draw_size);
|
||||
let mut slices = Vec::new();
|
||||
let base_offset = Vec2::new(
|
||||
-self.draw_size.x / 2.0,
|
||||
|
@ -81,6 +85,9 @@ impl TextureSlice {
|
|||
offset.y -= size_y / 2.0;
|
||||
remaining_columns -= size_y;
|
||||
}
|
||||
if slices.len() > 1_000 {
|
||||
bevy_log::warn!("One of your tiled textures has generated {} slices. You might want to use higher stretch values to avoid a great performance cost", slices.len());
|
||||
}
|
||||
slices
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,12 +199,23 @@ impl TextureSlicer {
|
|||
/// * `rect` - The section of the texture to slice in 9 parts
|
||||
/// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used.
|
||||
#[must_use]
|
||||
pub(crate) fn compute_slices(
|
||||
&self,
|
||||
rect: Rect,
|
||||
render_size: Option<Vec2>,
|
||||
) -> Vec<TextureSlice> {
|
||||
pub fn compute_slices(&self, rect: Rect, render_size: Option<Vec2>) -> Vec<TextureSlice> {
|
||||
let render_size = render_size.unwrap_or_else(|| rect.size());
|
||||
let rect_size = rect.size() / 2.0;
|
||||
if self.border.left >= rect_size.x
|
||||
|| self.border.right >= rect_size.x
|
||||
|| self.border.top >= rect_size.y
|
||||
|| self.border.bottom >= rect_size.y
|
||||
{
|
||||
bevy_log::error!(
|
||||
"TextureSlicer::border has out of bounds values. No slicing will be applied"
|
||||
);
|
||||
return vec![TextureSlice {
|
||||
texture_rect: rect,
|
||||
draw_size: render_size,
|
||||
offset: Vec2::ZERO,
|
||||
}];
|
||||
}
|
||||
let mut slices = Vec::with_capacity(9);
|
||||
// Corners
|
||||
let corners = self.corner_slices(rect, render_size);
|
||||
|
|
|
@ -21,6 +21,7 @@ mod geometry;
|
|||
mod layout;
|
||||
mod render;
|
||||
mod stack;
|
||||
mod texture_slice;
|
||||
mod ui_node;
|
||||
|
||||
pub use focus::*;
|
||||
|
@ -39,6 +40,9 @@ pub mod prelude {
|
|||
geometry::*, node_bundles::*, ui_material::*, ui_node::*, widget::Button, widget::Label,
|
||||
Interaction, UiMaterialPlugin, UiScale,
|
||||
};
|
||||
// `bevy_sprite` re-exports for texture slicing
|
||||
#[doc(hidden)]
|
||||
pub use bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer};
|
||||
}
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
@ -162,10 +166,17 @@ impl Plugin for UiPlugin {
|
|||
// They run independently since `widget::image_node_system` will only ever observe
|
||||
// its own UiImage, and `widget::text_system` & `bevy_text::update_text2d_layout`
|
||||
// will never modify a pre-existing `Image` asset.
|
||||
widget::update_image_content_size_system
|
||||
.before(UiSystem::Layout)
|
||||
.in_set(AmbiguousWithTextSystem)
|
||||
.in_set(AmbiguousWithUpdateText2DLayout),
|
||||
(
|
||||
widget::update_image_content_size_system
|
||||
.before(UiSystem::Layout)
|
||||
.in_set(AmbiguousWithTextSystem)
|
||||
.in_set(AmbiguousWithUpdateText2DLayout),
|
||||
(
|
||||
texture_slice::compute_slices_on_asset_event,
|
||||
texture_slice::compute_slices_on_image_change,
|
||||
),
|
||||
)
|
||||
.chain(),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ use bevy_render::{
|
|||
prelude::Color,
|
||||
view::{InheritedVisibility, ViewVisibility, Visibility},
|
||||
};
|
||||
use bevy_sprite::TextureAtlas;
|
||||
use bevy_sprite::{ImageScaleMode, TextureAtlas};
|
||||
#[cfg(feature = "bevy_text")]
|
||||
use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle};
|
||||
use bevy_transform::prelude::{GlobalTransform, Transform};
|
||||
|
@ -95,6 +95,8 @@ pub struct ImageBundle {
|
|||
///
|
||||
/// This component is set automatically
|
||||
pub image_size: UiImageSize,
|
||||
/// Controls how the image is altered when scaled.
|
||||
pub scale_mode: ImageScaleMode,
|
||||
/// Whether this node should block interaction with lower nodes
|
||||
pub focus_policy: FocusPolicy,
|
||||
/// The transform of the node
|
||||
|
@ -307,6 +309,8 @@ pub struct ButtonBundle {
|
|||
pub border_color: BorderColor,
|
||||
/// The image of the node
|
||||
pub image: UiImage,
|
||||
/// Controls how the image is altered when scaled.
|
||||
pub scale_mode: ImageScaleMode,
|
||||
/// The transform of the node
|
||||
///
|
||||
/// This component is automatically managed by the UI layout system.
|
||||
|
@ -343,6 +347,7 @@ impl Default for ButtonBundle {
|
|||
inherited_visibility: Default::default(),
|
||||
view_visibility: Default::default(),
|
||||
z_index: Default::default(),
|
||||
scale_mode: ImageScaleMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ pub use ui_material_pipeline::*;
|
|||
|
||||
use crate::graph::{LabelsUi, SubGraphUi};
|
||||
use crate::{
|
||||
BackgroundColor, BorderColor, CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline,
|
||||
Style, TargetCamera, UiImage, UiScale, Val,
|
||||
texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, CalculatedClip,
|
||||
ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val,
|
||||
};
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
@ -62,7 +62,6 @@ pub const UI_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(130128470471
|
|||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||
pub enum RenderUiSystem {
|
||||
ExtractNode,
|
||||
ExtractAtlasNode,
|
||||
}
|
||||
|
||||
pub fn build_ui_render(app: &mut App) {
|
||||
|
@ -86,10 +85,10 @@ pub fn build_ui_render(app: &mut App) {
|
|||
extract_default_ui_camera_view::<Camera2d>,
|
||||
extract_default_ui_camera_view::<Camera3d>,
|
||||
extract_uinodes.in_set(RenderUiSystem::ExtractNode),
|
||||
extract_uinode_borders.after(RenderUiSystem::ExtractAtlasNode),
|
||||
extract_uinode_borders,
|
||||
#[cfg(feature = "bevy_text")]
|
||||
extract_text_uinodes.after(RenderUiSystem::ExtractAtlasNode),
|
||||
extract_uinode_outlines.after(RenderUiSystem::ExtractAtlasNode),
|
||||
extract_text_uinodes,
|
||||
extract_uinode_outlines,
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
|
@ -377,6 +376,7 @@ pub fn extract_uinode_outlines(
|
|||
}
|
||||
|
||||
pub fn extract_uinodes(
|
||||
mut commands: Commands,
|
||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
|
@ -391,11 +391,22 @@ pub fn extract_uinodes(
|
|||
Option<&CalculatedClip>,
|
||||
Option<&TextureAtlas>,
|
||||
Option<&TargetCamera>,
|
||||
Option<&ComputedTextureSlices>,
|
||||
)>,
|
||||
>,
|
||||
) {
|
||||
for (entity, uinode, transform, color, maybe_image, view_visibility, clip, atlas, camera) in
|
||||
uinode_query.iter()
|
||||
for (
|
||||
entity,
|
||||
uinode,
|
||||
transform,
|
||||
color,
|
||||
maybe_image,
|
||||
view_visibility,
|
||||
clip,
|
||||
atlas,
|
||||
camera,
|
||||
slices,
|
||||
) in uinode_query.iter()
|
||||
{
|
||||
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
||||
else {
|
||||
|
@ -406,6 +417,15 @@ pub fn extract_uinodes(
|
|||
continue;
|
||||
}
|
||||
|
||||
if let Some((image, slices)) = maybe_image.zip(slices) {
|
||||
extracted_uinodes.uinodes.extend(
|
||||
slices
|
||||
.extract_ui_nodes(transform, uinode, color, image, clip, camera_entity)
|
||||
.map(|e| (commands.spawn_empty().id(), e)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (image, flip_x, flip_y) = if let Some(image) = maybe_image {
|
||||
(image.texture.id(), image.flip_x, image.flip_y)
|
||||
} else {
|
||||
|
|
185
crates/bevy_ui/src/texture_slice.rs
Normal file
185
crates/bevy_ui/src/texture_slice.rs
Normal file
|
@ -0,0 +1,185 @@
|
|||
// This module is mostly copied and pasted from `bevy_sprite::texture_slice`
|
||||
//
|
||||
// A more centralized solution should be investigated in the future
|
||||
|
||||
use bevy_asset::{AssetEvent, Assets};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{Rect, Vec2};
|
||||
use bevy_render::texture::Image;
|
||||
use bevy_sprite::{ImageScaleMode, TextureSlice};
|
||||
use bevy_transform::prelude::*;
|
||||
use bevy_utils::HashSet;
|
||||
|
||||
use crate::{widget::UiImageSize, BackgroundColor, CalculatedClip, ExtractedUiNode, Node, UiImage};
|
||||
|
||||
/// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`]
|
||||
///
|
||||
/// This component is automatically inserted and updated
|
||||
#[derive(Debug, Clone, Component)]
|
||||
pub struct ComputedTextureSlices {
|
||||
slices: Vec<TextureSlice>,
|
||||
image_size: Vec2,
|
||||
}
|
||||
|
||||
impl ComputedTextureSlices {
|
||||
/// Computes [`ExtractedUiNode`] iterator from the sprite slices
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `transform` - the sprite entity global transform
|
||||
/// * `original_entity` - the sprite entity
|
||||
/// * `sprite` - The sprite component
|
||||
/// * `handle` - The sprite texture handle
|
||||
#[must_use]
|
||||
pub(crate) fn extract_ui_nodes<'a>(
|
||||
&'a self,
|
||||
transform: &'a GlobalTransform,
|
||||
node: &'a Node,
|
||||
background_color: &'a BackgroundColor,
|
||||
image: &'a UiImage,
|
||||
clip: Option<&'a CalculatedClip>,
|
||||
camera_entity: Entity,
|
||||
) -> impl ExactSizeIterator<Item = ExtractedUiNode> + 'a {
|
||||
let mut flip = Vec2::new(1.0, -1.0);
|
||||
let [mut flip_x, mut flip_y] = [false; 2];
|
||||
if image.flip_x {
|
||||
flip.x *= -1.0;
|
||||
flip_x = true;
|
||||
}
|
||||
if image.flip_y {
|
||||
flip.y *= -1.0;
|
||||
flip_y = true;
|
||||
}
|
||||
self.slices.iter().map(move |slice| {
|
||||
let offset = (slice.offset * flip).extend(0.0);
|
||||
let transform = transform.mul_transform(Transform::from_translation(offset));
|
||||
let scale = slice.draw_size / slice.texture_rect.size();
|
||||
let mut rect = slice.texture_rect;
|
||||
rect.min *= scale;
|
||||
rect.max *= scale;
|
||||
let atlas_size = Some(self.image_size * scale);
|
||||
ExtractedUiNode {
|
||||
stack_index: node.stack_index,
|
||||
color: background_color.0,
|
||||
transform: transform.compute_matrix(),
|
||||
rect,
|
||||
flip_x,
|
||||
flip_y,
|
||||
image: image.texture.id(),
|
||||
atlas_size,
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
camera_entity,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
|
||||
/// will be computed according to the `image_handle` dimensions or the sprite rect.
|
||||
///
|
||||
/// Returns `None` if either:
|
||||
/// - The scale mode is [`ImageScaleMode::Stretched`]
|
||||
/// - The image asset is not loaded
|
||||
#[must_use]
|
||||
fn compute_texture_slices(
|
||||
draw_area: Vec2,
|
||||
scale_mode: &ImageScaleMode,
|
||||
image_handle: &UiImage,
|
||||
images: &Assets<Image>,
|
||||
) -> Option<ComputedTextureSlices> {
|
||||
if let ImageScaleMode::Stretched = scale_mode {
|
||||
return None;
|
||||
}
|
||||
let image_size = images.get(&image_handle.texture).map(|i| {
|
||||
Vec2::new(
|
||||
i.texture_descriptor.size.width as f32,
|
||||
i.texture_descriptor.size.height as f32,
|
||||
)
|
||||
})?;
|
||||
let texture_rect = Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: image_size,
|
||||
};
|
||||
let slices = match scale_mode {
|
||||
ImageScaleMode::Stretched => unreachable!(),
|
||||
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)),
|
||||
ImageScaleMode::Tiled {
|
||||
tile_x,
|
||||
tile_y,
|
||||
stretch_value,
|
||||
} => {
|
||||
let slice = TextureSlice {
|
||||
texture_rect,
|
||||
draw_size: draw_area,
|
||||
offset: Vec2::ZERO,
|
||||
};
|
||||
slice.tiled(*stretch_value, (*tile_x, *tile_y))
|
||||
}
|
||||
};
|
||||
Some(ComputedTextureSlices { slices, image_size })
|
||||
}
|
||||
|
||||
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
|
||||
/// on matching sprite entities
|
||||
pub(crate) fn compute_slices_on_asset_event(
|
||||
mut commands: Commands,
|
||||
mut events: EventReader<AssetEvent<Image>>,
|
||||
images: Res<Assets<Image>>,
|
||||
ui_nodes: Query<(
|
||||
Entity,
|
||||
&ImageScaleMode,
|
||||
&Node,
|
||||
Option<&UiImageSize>,
|
||||
&UiImage,
|
||||
)>,
|
||||
) {
|
||||
// We store the asset ids of added/modified image assets
|
||||
let added_handles: HashSet<_> = events
|
||||
.read()
|
||||
.filter_map(|e| match e {
|
||||
AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if added_handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
// We recompute the sprite slices for sprite entities with a matching asset handle id
|
||||
for (entity, scale_mode, ui_node, size, image) in &ui_nodes {
|
||||
if !added_handles.contains(&image.texture.id()) {
|
||||
continue;
|
||||
}
|
||||
let size = size.map(|s| s.size()).unwrap_or(ui_node.size());
|
||||
if let Some(slices) = compute_texture_slices(size, scale_mode, image, &images) {
|
||||
commands.entity(entity).insert(slices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
|
||||
pub(crate) fn compute_slices_on_image_change(
|
||||
mut commands: Commands,
|
||||
images: Res<Assets<Image>>,
|
||||
changed_nodes: Query<
|
||||
(
|
||||
Entity,
|
||||
&ImageScaleMode,
|
||||
&Node,
|
||||
Option<&UiImageSize>,
|
||||
&UiImage,
|
||||
),
|
||||
Or<(
|
||||
Changed<ImageScaleMode>,
|
||||
Changed<UiImage>,
|
||||
Changed<UiImageSize>,
|
||||
Changed<Node>,
|
||||
)>,
|
||||
>,
|
||||
) {
|
||||
for (entity, scale_mode, ui_node, size, image) in &changed_nodes {
|
||||
let size = size.map(|s| s.size()).unwrap_or(ui_node.size());
|
||||
if let Some(slices) = compute_texture_slices(size, scale_mode, image, &images) {
|
||||
commands.entity(entity).insert(slices);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
//! Showcases sprite 9 slice scaling
|
||||
//! Showcases sprite 9 slice scaling and tiling features, enabling usage of
|
||||
//! sprites in multiple resolutions while keeping it in proportion
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
|
|
|
@ -388,6 +388,7 @@ Example | Description
|
|||
[UI Material](../examples/ui/ui_material.rs) | Demonstrates creating and using custom Ui materials
|
||||
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI
|
||||
[UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI
|
||||
[UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI
|
||||
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
|
||||
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
|
||||
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.
|
||||
|
|
88
examples/ui/ui_texture_slice.rs
Normal file
88
examples/ui/ui_texture_slice.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
//! This example illustrates how to create a button that has its image sliced
|
||||
//! and kept in proportion instead of being stretched by the button dimensions
|
||||
|
||||
use bevy::{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), (Changed<Interaction>, With<Button>)>,
|
||||
mut text_query: Query<&mut Text>,
|
||||
) {
|
||||
for (interaction, children) in &mut interaction_query {
|
||||
let mut text = text_query.get_mut(children[0]).unwrap();
|
||||
match *interaction {
|
||||
Interaction::Pressed => {
|
||||
text.sections[0].value = "Press".to_string();
|
||||
}
|
||||
Interaction::Hovered => {
|
||||
text.sections[0].value = "Hover".to_string();
|
||||
}
|
||||
Interaction::None => {
|
||||
text.sections[0].value = "Button".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(Camera2dBundle::default());
|
||||
commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] {
|
||||
parent
|
||||
.spawn(ButtonBundle {
|
||||
style: Style {
|
||||
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()
|
||||
},
|
||||
image: image.clone().into(),
|
||||
scale_mode: ImageScaleMode::Sliced(slicer.clone()),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Button",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 40.0,
|
||||
color: Color::rgb(0.9, 0.9, 0.9),
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue