Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UiImage offset property #5629

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
45 changes: 34 additions & 11 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
}
Expand Down
22 changes: 17 additions & 5 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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::*;
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};

Expand Down Expand Up @@ -387,19 +387,31 @@ impl From<Color> 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<Image>);
pub struct UiImage {
/// The asset handle used to display image.
pub handle: Handle<Image>,
/// 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<Handle<Image>> for UiImage {
fn from(handle: Handle<Image>) -> Self {
Self(handle)
Self {
handle,
offset: default(),
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ui/src/widget/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn image_node_system(
mut query: Query<(&mut CalculatedSize, &UiImage), With<ImageMode>>,
) {
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),
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions examples/games/game_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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));
Expand Down
68 changes: 68 additions & 0 deletions examples/ui/image_button.rs
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Change image filter to a pixel-art friendly
// Change image filter to a pixel-art friendly mode

.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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users would really benefit from some more advice on how these values were determined.

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<Interaction>, With<Button>),
>,
) {
for (interaction, mut ui_image) in &mut interaction_query {
let offset = match *interaction {
Interaction::Hovered => NORMAL_BUTTON_OFFSET,
Interaction::None => HOVERED_BUTTON_OFFSET,
Interaction::Clicked => CLICKED_BUTTON_OFFSET,
};

ui_image.offset = Rect {
min: Vec2::new(offset[0], offset[1]),
max: Vec2::new(offset[2], offset[3]),
};
}
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// ui camera
commands.spawn_bundle(Camera2dBundle::default());

// image button
commands.spawn_bundle(ButtonBundle {
style: Style {
size: Size::new(Val::Px(150.0), Val::Px(150.0)),
// center button
margin: UiRect::all(Val::Auto),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
// Default image has no offset, but that's OK since it'll be update it on button_system
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Default image has no offset, but that's OK since it'll be update it on button_system
// Default image has no offset, but that's OK since it'll be updated in button_system

image: asset_server
.load("textures/rpg/ui/generic-rpg-ui-inventario.png")
.into(),
..default()
});
}