diff --git a/Cargo.toml b/Cargo.toml index b0920d9ecd0fa..74b9e37b8dd30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1402,6 +1402,16 @@ description = "Illustrates creating and updating a button" category = "UI (User Interface)" wasm = true +[[example]] +name = "image_button" +path = "examples/ui/image_button.rs" + +[package.metadata.example.image_button] +name = "Image Button" +description = "Illustrates creating and updating a button using images" +category = "UI (User Interface)" +wasm = true + [[example]] name = "font_atlas_debug" path = "examples/ui/font_atlas_debug.rs" diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 0a5a171ffccaa..dce00433d62a6 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -188,28 +188,51 @@ pub fn extract_uinodes( >, ) { extracted_uinodes.uinodes.clear(); - for (uinode, transform, color, image, visibility, clip) in uinode_query.iter() { + for (uinode, transform, color, ui_image, visibility, clip) in uinode_query.iter() { if !visibility.is_visible() { continue; } - let image = image.0.clone_weak(); - // Skip loading images - if !images.contains(&image) { + let image = ui_image.handle.clone_weak(); + + let image_size = if let Some(raw_image) = images.get(&image) { + raw_image.size() + } else { + // Skip loading images continue; - } + }; + + let (atlas_size, scale, rect) = if ui_image.offset.size() == Vec2::ZERO { + ( + None, + Vec2::ONE, + bevy_sprite::Rect { + min: Vec2::ZERO, + max: uinode.size, + }, + ) + } else { + ( + Some(image_size), + // Compute image scale to fill the entire node size + (uinode.size / ui_image.offset.size()), + // UiImage::offset::max is inclusive + bevy_sprite::Rect { + min: ui_image.offset.min, + max: ui_image.offset.max + Vec2::ONE, + }, + ) + }; + // Skip completely transparent nodes if color.0.a() == 0.0 { continue; } extracted_uinodes.uinodes.push(ExtractedUiNode { - transform: transform.compute_matrix(), + transform: transform.compute_matrix() * Mat4::from_scale(scale.extend(1.0)), color: color.0, - rect: bevy_sprite::Rect { - min: Vec2::ZERO, - max: uinode.size, - }, + rect, image, - atlas_size: None, + atlas_size, clip: clip.map(|clip| clip.clip), }); } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index b603d6813b932..0a85aec58f711 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,6 +1,5 @@ use crate::{Size, UiRect}; use bevy_asset::Handle; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_math::Vec2; use bevy_reflect::prelude::*; @@ -8,6 +7,7 @@ use bevy_render::{ color::Color, texture::{Image, DEFAULT_IMAGE_HANDLE}, }; +use bevy_utils::default; use serde::{Deserialize, Serialize}; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; @@ -387,19 +387,31 @@ impl From for UiColor { } /// The image of the node -#[derive(Component, Clone, Debug, Reflect, Deref, DerefMut)] +#[derive(Component, Clone, Debug, Reflect)] #[reflect(Component, Default)] -pub struct UiImage(pub Handle); +pub struct UiImage { + /// The asset handle used to display image. + pub handle: Handle, + /// Defines a portion of the image to be rendered where [`bevy_sprite::Rect::min`] is the begining and [`bevy_sprite::Rect::max`] is the inclusive end. + /// Defaults to zero sized rect, which loads the full imagem. + pub offset: bevy_sprite::Rect, +} impl Default for UiImage { fn default() -> Self { - Self(DEFAULT_IMAGE_HANDLE.typed()) + Self { + handle: DEFAULT_IMAGE_HANDLE.typed(), + offset: default(), + } } } impl From> for UiImage { fn from(handle: Handle) -> Self { - Self(handle) + Self { + handle, + offset: default(), + } } } diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 7519d4966e126..9e4b57c9a0079 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -25,7 +25,7 @@ pub fn image_node_system( mut query: Query<(&mut CalculatedSize, &UiImage), With>, ) { for (mut calculated_size, image) in &mut query { - if let Some(texture) = textures.get(image) { + if let Some(texture) = textures.get(&image.handle) { let size = Size { width: Val::Px(texture.texture_descriptor.size.width as f32), height: Val::Px(texture.texture_descriptor.size.height as f32), diff --git a/examples/README.md b/examples/README.md index b23260cdcb5e0..ddd38f0130e9e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -309,6 +309,7 @@ Example | Description --- | --- [Button](../examples/ui/button.rs) | Illustrates creating and updating a button [Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) +[Image Button](../examples/ui/image_button.rs) | Illustrates creating and updating a button using images [Text](../examples/ui/text.rs) | Illustrates creating and updating text [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI diff --git a/examples/games/game_menu.rs b/examples/games/game_menu.rs index 325c8ea4c3f04..7f44698cd1938 100644 --- a/examples/games/game_menu.rs +++ b/examples/games/game_menu.rs @@ -90,7 +90,7 @@ mod splash { size: Size::new(Val::Px(200.0), Val::Auto), ..default() }, - image: UiImage(icon), + image: icon.into(), ..default() }) .insert(OnSplashScreen); @@ -456,7 +456,7 @@ mod menu { let icon = asset_server.load("textures/Game Icons/right.png"); parent.spawn_bundle(ImageBundle { style: button_icon_style.clone(), - image: UiImage(icon), + image: icon.into(), ..default() }); parent.spawn_bundle(TextBundle::from_section( @@ -475,7 +475,7 @@ mod menu { let icon = asset_server.load("textures/Game Icons/wrench.png"); parent.spawn_bundle(ImageBundle { style: button_icon_style.clone(), - image: UiImage(icon), + image: icon.into(), ..default() }); parent.spawn_bundle(TextBundle::from_section( @@ -494,7 +494,7 @@ mod menu { let icon = asset_server.load("textures/Game Icons/exitRight.png"); parent.spawn_bundle(ImageBundle { style: button_icon_style, - image: UiImage(icon), + image: icon.into(), ..default() }); parent.spawn_bundle(TextBundle::from_section("Quit", button_text_style)); diff --git a/examples/ui/image_button.rs b/examples/ui/image_button.rs new file mode 100644 index 0000000000000..ea0734e203a40 --- /dev/null +++ b/examples/ui/image_button.rs @@ -0,0 +1,68 @@ +//! This example illustrates how to create an imagem button that changes image offset based on its +//! interaction state. + +use bevy::{prelude::*, sprite::Rect, winit::WinitSettings}; + +fn main() { + App::new() + // Change image filter to a pixel-art friendly + .insert_resource(ImageSettings::default_nearest()) + // Match the background color with base image color + .insert_resource(ClearColor(Color::rgb(0.475, 0.239, 0.306))) + .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_startup_system(setup) + .add_system(button_system) + .run(); +} + +// Image rect in pixels, inside the base image. +// Values are using to built a rect with begin (X, Y) and end (X, Y) format +const HOVERED_BUTTON_OFFSET: [f32; 4] = [23.0, 38.0, 36.0, 52.0]; +const NORMAL_BUTTON_OFFSET: [f32; 4] = [7.0, 38.0, 20.0, 52.0]; +const CLICKED_BUTTON_OFFSET: [f32; 4] = [39.0, 38.0, 52.0, 52.0]; + +fn button_system( + mut interaction_query: Query< + (&Interaction, &mut UiImage), + (Changed, With