Decouple BackgroundColor from UiImage (#11165)

# Objective

Fixes https://github.com/bevyengine/bevy/issues/11157.

## Solution

Stop using `BackgroundColor` as a color tint for `UiImage`. Add a
`UiImage::color` field for color tint instead. Allow a UI node to
simultaneously include a solid-color background and an image, with the
image rendered on top of the background (this is already how it works
for e.g. text).


![2024-02-29_1709239666_563x520](https://github.com/bevyengine/bevy/assets/12173779/ec50c9ef-4c7f-4ab8-a457-d086ce5b3425)

---

## Changelog

- The `BackgroundColor` component now renders a solid-color background
behind `UiImage` instead of tinting its color.
- Removed `BackgroundColor` from `ImageBundle`, `AtlasImageBundle`, and
`ButtonBundle`.
- Added `UiImage::color`.
- Expanded `RenderUiSystem` variants.
- Renamed `bevy_ui::extract_text_uinodes` to `extract_uinodes_text` for
consistency.

## Migration Guide

- `BackgroundColor` no longer tints the color of UI images. Use
`UiImage::color` for that instead.
- For solid color buttons, replace `ButtonBundle { background_color:
my_color.into(), ... }` with `ButtonBundle { image:
UiImage::default().with_color(my_color), ... }`, and update button
interaction systems to use `UiImage::color` instead of `BackgroundColor`
as well.
- `bevy_ui::RenderUiSystem::ExtractNode` has been split into
`ExtractBackgrounds`, `ExtractImages`, `ExtractBorders`, and
`ExtractText`.
- `bevy_ui::extract_uinodes` has been split into
`bevy_ui::extract_uinode_background_colors` and
`bevy_ui::extract_uinode_images`.
- `bevy_ui::extract_text_uinodes` has been renamed to
`extract_uinode_text`.
This commit is contained in:
Ben Frankel 2024-03-03 13:35:50 -08:00 committed by GitHub
parent 6e83439a06
commit e8ae0d6c49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 241 additions and 214 deletions

View file

@ -93,10 +93,6 @@ pub struct ImageBundle {
pub style: Style,
/// The calculated size based on the given image
pub calculated_size: ContentSize,
/// The background color, which serves as a "fill" for this node
///
/// Combines with `UiImage` to tint the provided image.
pub background_color: BackgroundColor,
/// The image of the node
pub image: UiImage,
/// The size of the image in pixels
@ -140,10 +136,6 @@ pub struct AtlasImageBundle {
pub style: Style,
/// The calculated size based on the given image
pub calculated_size: ContentSize,
/// The background color, which serves as a "fill" for this node
///
/// Combines with `UiImage` to tint the provided image.
pub background_color: BackgroundColor,
/// The image of the node
pub image: UiImage,
/// A handle to the texture atlas to use for this Ui Node
@ -319,10 +311,6 @@ pub struct ButtonBundle {
pub interaction: Interaction,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The background color, which serves as a "fill" for this node
///
/// When combined with `UiImage`, tints the provided image.
pub background_color: BackgroundColor,
/// The color of the Node's border
pub border_color: BorderColor,
/// The image of the node
@ -349,13 +337,12 @@ pub struct ButtonBundle {
impl Default for ButtonBundle {
fn default() -> Self {
Self {
focus_policy: FocusPolicy::Block,
node: Default::default(),
button: Default::default(),
style: Default::default(),
border_color: BorderColor(Color::NONE),
interaction: Default::default(),
background_color: Default::default(),
focus_policy: FocusPolicy::Block,
border_color: BorderColor(Color::NONE),
image: Default::default(),
transform: Default::default(),
global_transform: Default::default(),

View file

@ -59,7 +59,10 @@ pub const UI_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(130128470471
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum RenderUiSystem {
ExtractNode,
ExtractBackgrounds,
ExtractImages,
ExtractBorders,
ExtractText,
}
pub fn build_ui_render(app: &mut App) {
@ -77,16 +80,27 @@ pub fn build_ui_render(app: &mut App) {
.allow_ambiguous_resource::<ExtractedUiNodes>()
.init_resource::<DrawFunctions<TransparentUi>>()
.add_render_command::<TransparentUi, DrawUi>()
.configure_sets(
ExtractSchedule,
(
RenderUiSystem::ExtractBackgrounds,
RenderUiSystem::ExtractImages,
RenderUiSystem::ExtractBorders,
RenderUiSystem::ExtractText,
)
.chain(),
)
.add_systems(
ExtractSchedule,
(
extract_default_ui_camera_view::<Camera2d>,
extract_default_ui_camera_view::<Camera3d>,
extract_uinodes.in_set(RenderUiSystem::ExtractNode),
extract_uinode_borders,
extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds),
extract_uinode_images.in_set(RenderUiSystem::ExtractImages),
extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders),
extract_uinode_outlines.in_set(RenderUiSystem::ExtractBorders),
#[cfg(feature = "bevy_text")]
extract_text_uinodes,
extract_uinode_outlines,
extract_uinode_text.in_set(RenderUiSystem::ExtractText),
),
)
.add_systems(
@ -148,6 +162,134 @@ pub struct ExtractedUiNodes {
pub uinodes: EntityHashMap<ExtractedUiNode>,
}
pub fn extract_uinode_background_colors(
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
default_ui_camera: Extract<DefaultUiCamera>,
uinode_query: Extract<
Query<(
Entity,
&Node,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&BackgroundColor,
)>,
>,
) {
for (entity, uinode, transform, view_visibility, clip, camera, background_color) in
&uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
};
// Skip invisible backgrounds
if !view_visibility.get() || background_color.0.is_fully_transparent() {
continue;
}
extracted_uinodes.uinodes.insert(
entity,
ExtractedUiNode {
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
color: background_color.0.into(),
rect: Rect {
min: Vec2::ZERO,
max: uinode.calculated_size,
},
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
atlas_size: None,
flip_x: false,
flip_y: false,
camera_entity,
},
);
}
}
pub fn extract_uinode_images(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
default_ui_camera: Extract<DefaultUiCamera>,
uinode_query: Extract<
Query<(
&Node,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
Option<&TextureAtlas>,
Option<&ComputedTextureSlices>,
)>,
>,
) {
for (uinode, transform, view_visibility, clip, camera, image, atlas, slices) in &uinode_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
};
// Skip invisible images
if !view_visibility.get() || image.color.is_fully_transparent() {
continue;
}
if let Some(slices) = slices {
extracted_uinodes.uinodes.extend(
slices
.extract_ui_nodes(transform, uinode, image, clip, camera_entity)
.map(|e| (commands.spawn_empty().id(), e)),
);
continue;
}
let (rect, atlas_size) = match atlas {
Some(atlas) => {
let Some(layout) = texture_atlases.get(&atlas.layout) else {
// Atlas not present in assets resource (should this warn the user?)
continue;
};
let mut atlas_rect = layout.textures[atlas.index].as_rect();
let mut atlas_size = layout.size.as_vec2();
let scale = uinode.size() / atlas_rect.size();
atlas_rect.min *= scale;
atlas_rect.max *= scale;
atlas_size *= scale;
(atlas_rect, Some(atlas_size))
}
None => (
Rect {
min: Vec2::ZERO,
max: uinode.calculated_size,
},
None,
),
};
extracted_uinodes.uinodes.insert(
commands.spawn_empty().id(),
ExtractedUiNode {
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
color: image.color.into(),
rect,
clip: clip.map(|clip| clip.clip),
image: image.texture.id(),
atlas_size,
flip_x: image.flip_x,
flip_y: image.flip_y,
camera_entity,
},
);
}
}
pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 {
match value {
Val::Auto => 0.,
@ -171,12 +313,12 @@ pub fn extract_uinode_borders(
(
&Node,
&GlobalTransform,
&Style,
&BorderColor,
Option<&Parent>,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
Option<&Parent>,
&Style,
&BorderColor,
),
Without<ContentSize>,
>,
@ -185,13 +327,14 @@ pub fn extract_uinode_borders(
) {
let image = AssetId::<Image>::default();
for (node, global_transform, style, border_color, parent, view_visibility, clip, camera) in
for (node, global_transform, view_visibility, clip, camera, parent, style, border_color) in
&uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
};
// Skip invisible borders
if !view_visibility.get()
|| border_color.0.is_fully_transparent()
@ -290,19 +433,20 @@ pub fn extract_uinode_outlines(
Query<(
&Node,
&GlobalTransform,
&Outline,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&Outline,
)>,
>,
) {
let image = AssetId::<Image>::default();
for (node, global_transform, outline, view_visibility, maybe_clip, camera) in &uinode_query {
for (node, global_transform, view_visibility, maybe_clip, camera, outline) in &uinode_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
};
// Skip invisible outlines
if !view_visibility.get()
|| outline.color.is_fully_transparent()
@ -373,104 +517,6 @@ 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>,
uinode_query: Extract<
Query<(
Entity,
&Node,
&GlobalTransform,
&BackgroundColor,
Option<&UiImage>,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TextureAtlas>,
Option<&TargetCamera>,
Option<&ComputedTextureSlices>,
)>,
>,
) {
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 {
continue;
};
// Skip invisible and completely transparent nodes
if !view_visibility.get() || color.0.is_fully_transparent() {
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 {
(AssetId::default(), false, false)
};
let (rect, atlas_size) = match atlas {
Some(atlas) => {
let Some(layout) = texture_atlases.get(&atlas.layout) else {
// Atlas not present in assets resource (should this warn the user?)
continue;
};
let mut atlas_rect = layout.textures[atlas.index].as_rect();
let mut atlas_size = layout.size.as_vec2();
let scale = uinode.size() / atlas_rect.size();
atlas_rect.min *= scale;
atlas_rect.max *= scale;
atlas_size *= scale;
(atlas_rect, Some(atlas_size))
}
None => (
Rect {
min: Vec2::ZERO,
max: uinode.calculated_size,
},
None,
),
};
extracted_uinodes.uinodes.insert(
entity,
ExtractedUiNode {
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
color: color.0.into(),
rect,
clip: clip.map(|clip| clip.clip),
image,
atlas_size,
flip_x,
flip_y,
camera_entity,
},
);
}
}
/// The UI camera is "moved back" by this many units (plus the [`UI_CAMERA_TRANSFORM_OFFSET`]) and also has a view
/// distance of this many units. This ensures that with a left-handed projection,
/// as ui elements are "stacked on top of each other", they are within the camera's view
@ -546,7 +592,7 @@ pub fn extract_default_ui_camera_view<T: Component>(
}
#[cfg(feature = "bevy_text")]
pub fn extract_text_uinodes(
pub fn extract_uinode_text(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
camera_query: Extract<Query<(Entity, &Camera)>>,
@ -557,21 +603,22 @@ pub fn extract_text_uinodes(
Query<(
&Node,
&GlobalTransform,
&Text,
&TextLayoutInfo,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&Text,
&TextLayoutInfo,
)>,
>,
) {
for (uinode, global_transform, text, text_layout_info, view_visibility, clip, camera) in
uinode_query.iter()
for (uinode, global_transform, view_visibility, clip, camera, text, text_layout_info) in
&uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
};
// Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`)
if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. {
continue;

View file

@ -74,7 +74,7 @@ where
ExtractSchedule,
(
extract_ui_materials::<M>,
extract_ui_material_nodes::<M>.in_set(RenderUiSystem::ExtractNode),
extract_ui_material_nodes::<M>.in_set(RenderUiSystem::ExtractBackgrounds),
),
)
.add_systems(

View file

@ -10,7 +10,7 @@ use bevy_sprite::{ImageScaleMode, TextureSlice};
use bevy_transform::prelude::*;
use bevy_utils::HashSet;
use crate::{BackgroundColor, CalculatedClip, ExtractedUiNode, Node, UiImage};
use crate::{CalculatedClip, ExtractedUiNode, Node, UiImage};
/// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`]
///
@ -35,7 +35,6 @@ impl ComputedTextureSlices {
&'a self,
transform: &'a GlobalTransform,
node: &'a Node,
background_color: &'a BackgroundColor,
image: &'a UiImage,
clip: Option<&'a CalculatedClip>,
camera_entity: Entity,
@ -60,7 +59,7 @@ impl ComputedTextureSlices {
let atlas_size = Some(self.image_size * scale);
ExtractedUiNode {
stack_index: node.stack_index,
color: background_color.0.into(),
color: image.color.into(),
transform: transform.compute_matrix(),
rect,
flip_x,

View file

@ -1589,7 +1589,6 @@ pub enum GridPlacementError {
/// The background color of the node
///
/// This serves as the "fill" color.
/// When combined with [`UiImage`], tints the provided texture.
#[derive(Component, Copy, Clone, Debug, Reflect)]
#[reflect(Component, Default)]
#[cfg_attr(
@ -1729,6 +1728,8 @@ impl Outline {
#[derive(Component, Clone, Debug, Reflect, Default)]
#[reflect(Component, Default)]
pub struct UiImage {
/// The tint color used to draw the image
pub color: Color,
/// Handle to the texture
pub texture: Handle<Image>,
/// Whether the image should be flipped along its x-axis
@ -1745,6 +1746,13 @@ impl UiImage {
}
}
/// Set the color tint
#[must_use]
pub const fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
/// Flip the image along its x-axis
#[must_use]
pub const fn with_flip_x(mut self) -> Self {

View file

@ -147,7 +147,7 @@ fn setup(
..default()
},
border_color: Color::WHITE.into(),
background_color: DARK_GRAY.into(),
image: UiImage::default().with_color(DARK_GRAY.into()),
..default()
},
))

View file

@ -74,7 +74,7 @@ fn setup_menu(mut commands: Commands) {
align_items: AlignItems::Center,
..default()
},
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
})
.with_children(|parent| {
@ -95,21 +95,21 @@ fn setup_menu(mut commands: Commands) {
fn menu(
mut next_state: ResMut<NextState<AppState>>,
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor),
(&Interaction, &mut UiImage),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, mut color) in &mut interaction_query {
for (interaction, mut image) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
*color = PRESSED_BUTTON.into();
image.color = PRESSED_BUTTON;
next_state.set(AppState::InGame);
}
Interaction::Hovered => {
*color = HOVERED_BUTTON.into();
image.color = HOVERED_BUTTON;
}
Interaction::None => {
*color = NORMAL_BUTTON.into();
image.color = NORMAL_BUTTON;
}
}
}

View file

@ -345,16 +345,16 @@ mod menu {
// This system handles changing all buttons color based on mouse interaction
fn button_system(
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor, Option<&SelectedOption>),
(&Interaction, &mut UiImage, Option<&SelectedOption>),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, mut color, selected) in &mut interaction_query {
*color = match (*interaction, selected) {
(Interaction::Pressed, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON.into(),
(Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON.into(),
(Interaction::Hovered, None) => HOVERED_BUTTON.into(),
(Interaction::None, None) => NORMAL_BUTTON.into(),
for (interaction, mut image, selected) in &mut interaction_query {
image.color = match (*interaction, selected) {
(Interaction::Pressed, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON,
(Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON,
(Interaction::Hovered, None) => HOVERED_BUTTON,
(Interaction::None, None) => NORMAL_BUTTON,
}
}
}
@ -363,14 +363,14 @@ mod menu {
// the button as the one currently selected
fn setting_button<T: Resource + Component + PartialEq + Copy>(
interaction_query: Query<(&Interaction, &T, Entity), (Changed<Interaction>, With<Button>)>,
mut selected_query: Query<(Entity, &mut BackgroundColor), With<SelectedOption>>,
mut selected_query: Query<(Entity, &mut UiImage), With<SelectedOption>>,
mut commands: Commands,
mut setting: ResMut<T>,
) {
for (interaction, button_setting, entity) in &interaction_query {
if *interaction == Interaction::Pressed && *setting != *button_setting {
let (previous_button, mut previous_color) = selected_query.single_mut();
*previous_color = NORMAL_BUTTON.into();
let (previous_button, mut previous_image) = selected_query.single_mut();
previous_image.color = NORMAL_BUTTON;
commands.entity(previous_button).remove::<SelectedOption>();
commands.entity(entity).insert(SelectedOption);
*setting = *button_setting;
@ -456,7 +456,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style.clone(),
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
MenuButtonAction::Play,
@ -477,7 +477,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style.clone(),
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
MenuButtonAction::Settings,
@ -498,7 +498,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style,
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
MenuButtonAction::Quit,
@ -567,7 +567,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style.clone(),
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
action,
@ -654,7 +654,7 @@ mod menu {
height: Val::Px(65.0),
..button_style.clone()
},
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
quality_setting,
@ -675,7 +675,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style,
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
MenuButtonAction::BackToSettings,
@ -750,7 +750,7 @@ mod menu {
height: Val::Px(65.0),
..button_style.clone()
},
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
Volume(volume_setting),
@ -764,7 +764,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style,
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
},
MenuButtonAction::BackToSettings,

View file

@ -97,18 +97,15 @@ fn main() {
}
#[derive(Component)]
struct IdleColor(BackgroundColor);
struct IdleColor(Color);
fn button_system(
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor, &IdleColor),
Changed<Interaction>,
>,
mut interaction_query: Query<(&Interaction, &mut UiImage, &IdleColor), Changed<Interaction>>,
) {
for (interaction, mut button_color, IdleColor(idle_color)) in interaction_query.iter_mut() {
*button_color = match interaction {
for (interaction, mut image, &IdleColor(idle_color)) in interaction_query.iter_mut() {
image.color = match interaction {
Interaction::Hovered => ORANGE_RED.into(),
_ => *idle_color,
_ => idle_color,
};
}
}
@ -148,7 +145,7 @@ fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<
.spawn(NodeBundle::default())
.with_children(|commands| {
for row in 0..args.buttons {
let color = as_rainbow(row % column.max(1)).into();
let color = as_rainbow(row % column.max(1));
let border_color = Color::WHITE.with_alpha(0.5).into();
spawn_button(
commands,
@ -202,7 +199,7 @@ fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<
.with_children(|commands| {
for column in 0..args.buttons {
for row in 0..args.buttons {
let color = as_rainbow(row % column.max(1)).into();
let color = as_rainbow(row % column.max(1));
let border_color = Color::WHITE.with_alpha(0.5).into();
spawn_button(
commands,
@ -226,7 +223,7 @@ fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<
#[allow(clippy::too_many_arguments)]
fn spawn_button(
commands: &mut ChildBuilder,
background_color: BackgroundColor,
background_color: Color,
buttons: f32,
column: usize,
row: usize,
@ -249,7 +246,7 @@ fn spawn_button(
border,
..default()
},
background_color,
image: UiImage::default().with_color(background_color),
border_color,
..default()
},

View file

@ -19,33 +19,28 @@ const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
fn button_system(
mut interaction_query: Query<
(
&Interaction,
&mut BackgroundColor,
&mut BorderColor,
&Children,
),
(&Interaction, &mut UiImage, &mut BorderColor, &Children),
(Changed<Interaction>, With<Button>),
>,
mut text_query: Query<&mut Text>,
) {
for (interaction, mut color, mut border_color, children) in &mut interaction_query {
for (interaction, mut image, mut border_color, 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();
*color = PRESSED_BUTTON.into();
image.color = PRESSED_BUTTON;
border_color.0 = RED.into();
}
Interaction::Hovered => {
text.sections[0].value = "Hover".to_string();
*color = HOVERED_BUTTON.into();
border_color.0 = WHITE.into();
image.color = HOVERED_BUTTON;
border_color.0 = Color::WHITE;
}
Interaction::None => {
text.sections[0].value = "Button".to_string();
*color = NORMAL_BUTTON.into();
border_color.0 = BLACK.into();
image.color = NORMAL_BUTTON;
border_color.0 = Color::BLACK;
}
}
}
@ -79,7 +74,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
border_color: BorderColor(Color::BLACK),
background_color: NORMAL_BUTTON.into(),
image: UiImage::default().with_color(NORMAL_BUTTON),
..default()
})
.with_children(|parent| {

View file

@ -417,7 +417,7 @@ where
padding: UiRect::axes(Val::Px(5.), Val::Px(1.)),
..Default::default()
},
background_color: BackgroundColor(Color::BLACK.with_alpha(0.5)),
image: UiImage::default().with_color(Color::BLACK.with_alpha(0.5)),
..Default::default()
},
Target::<T>::new(target),
@ -461,13 +461,13 @@ fn buttons_handler<T>(
}
fn text_hover(
mut button_query: Query<(&Interaction, &mut BackgroundColor, &Children), Changed<Interaction>>,
mut button_query: Query<(&Interaction, &mut UiImage, &Children), Changed<Interaction>>,
mut text_query: Query<&mut Text>,
) {
for (interaction, mut background_color, children) in button_query.iter_mut() {
for (interaction, mut image, children) in button_query.iter_mut() {
match interaction {
Interaction::Hovered => {
*background_color = BackgroundColor(Color::BLACK.with_alpha(0.6));
image.color = Color::BLACK.with_alpha(0.6);
for &child in children {
if let Ok(mut text) = text_query.get_mut(child) {
// Bypass change detection to avoid recomputation of the text when only changing the color
@ -476,7 +476,7 @@ fn text_hover(
}
}
_ => {
*background_color = BackgroundColor(Color::BLACK.with_alpha(0.5));
image.color = Color::BLACK.with_alpha(0.5);
for &child in children {
if let Ok(mut text) = text_query.get_mut(child) {
text.bypass_change_detection().sections[0].style.color =

View file

@ -95,8 +95,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
min_height: Val::Px(100.),
..Default::default()
},
background_color: Color::WHITE.into(),
..Default::default()
},
Interaction::default(),

View file

@ -245,12 +245,11 @@ fn spawn_button(
margin: UiRect::horizontal(Val::Px(2.)),
..Default::default()
},
background_color: if active {
image: UiImage::default().with_color(if active {
ACTIVE_BORDER_COLOR
} else {
INACTIVE_BORDER_COLOR
}
.into(),
}),
..Default::default()
},
constraint,
@ -359,6 +358,7 @@ fn update_buttons(
fn update_radio_buttons_colors(
mut event_reader: EventReader<ButtonActivatedEvent>,
button_query: Query<(Entity, &Constraint, &Interaction)>,
mut image_query: Query<&mut UiImage>,
mut color_query: Query<&mut BackgroundColor>,
mut text_query: Query<&mut Text>,
children_query: Query<&Children>,
@ -381,16 +381,12 @@ fn update_radio_buttons_colors(
)
};
color_query.get_mut(id).unwrap().0 = border_color;
if let Ok(children) = children_query.get(id) {
for &child in children {
color_query.get_mut(child).unwrap().0 = inner_color;
if let Ok(grand_children) = children_query.get(child) {
for &grandchild in grand_children {
if let Ok(mut text) = text_query.get_mut(grandchild) {
text.sections[0].style.color = text_color;
}
}
image_query.get_mut(id).unwrap().color = border_color;
for &child in children_query.get(id).into_iter().flatten() {
color_query.get_mut(child).unwrap().0 = inner_color;
for &grandchild in children_query.get(child).into_iter().flatten() {
if let Ok(mut text) = text_query.get_mut(grandchild) {
text.sections[0].style.color = text_color;
}
}
}

View file

@ -37,7 +37,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
align_items: AlignItems::Center,
..default()
},
background_color: Color::srgb(0.1, 0.5, 0.1).into(),
image: UiImage::default().with_color(Color::srgb(0.1, 0.5, 0.1)),
..default()
})
.with_children(|parent| {
@ -63,7 +63,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
align_items: AlignItems::Center,
..default()
},
background_color: Color::srgb(0.5, 0.1, 0.5).into(),
image: UiImage::default().with_color(Color::srgb(0.5, 0.1, 0.5)),
..default()
})
.with_children(|parent| {

View file

@ -1,6 +1,6 @@
//! This example illustrates how to use `TextureAtlases` within ui
use bevy::{color::palettes::basic::YELLOW, prelude::*, winit::WinitSettings};
use bevy::{color::palettes::css::*, prelude::*, winit::WinitSettings};
fn main() {
App::new()
@ -60,6 +60,8 @@ fn setup(
..default()
},
TextureAtlas::from(texture_atlas_handle),
BackgroundColor(ANTIQUE_WHITE.into()),
Outline::new(Val::Px(8.0), Val::ZERO, CRIMSON.into()),
));
parent.spawn(TextBundle::from_sections([
TextSection::new("press ".to_string(), text_style.clone()),

View file

@ -19,25 +19,25 @@ fn main() {
fn button_system(
mut interaction_query: Query<
(&Interaction, &Children, &mut BackgroundColor),
(&Interaction, &Children, &mut UiImage),
(Changed<Interaction>, With<Button>),
>,
mut text_query: Query<&mut Text>,
) {
for (interaction, children, mut color) in &mut interaction_query {
for (interaction, children, mut image) 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();
color.0 = GOLD.into();
image.color = GOLD.into();
}
Interaction::Hovered => {
text.sections[0].value = "Hover".to_string();
color.0 = ORANGE.into();
image.color = ORANGE.into();
}
Interaction::None => {
text.sections[0].value = "Button".to_string();
color.0 = Color::WHITE;
image.color = Color::WHITE;
}
}
}
@ -81,8 +81,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
image: image.clone().into(),
// When combined with an image, this tints the image.
background_color: Color::WHITE.into(),
..default()
},
ImageScaleMode::Sliced(slicer.clone()),