Skip to content

Commit

Permalink
Make default behavior for BackgroundColor and BorderColor more in…
Browse files Browse the repository at this point in the history
…tuitive (#14017)

# Objective

In Bevy 0.13, `BackgroundColor` simply tinted the image of any
`UiImage`. This was confusing: in every other case (e.g. Text), this
added a solid square behind the element. #11165 changed this, but
removed `BackgroundColor` from `ImageBundle` to avoid confusion, since
the semantic meaning had changed.

However, this resulted in a serious UX downgrade / inconsistency, as
this behavior was no longer part of the bundle (unlike for `TextBundle`
or `NodeBundle`), leaving users with a relatively frustrating upgrade
path.

Additionally, adding both `BackgroundColor` and `UiImage` resulted in a
bizarre effect, where the background color was seemingly ignored as it
was covered by a solid white placeholder image.

Fixes #13969.

## Solution

Per @viridia's design:

> - if you don't specify a background color, it's transparent.
> - if you don't specify an image color, it's white (because it's a
multiplier).
> - if you don't specify an image, no image is drawn.
> - if you specify both a background color and an image color, they are
independent.
> - the background color is drawn behind the image (in whatever pixels
are transparent)

As laid out by @benfrankel, this involves:

1. Changing the default `UiImage` to use a transparent texture but a
pure white tint.
2. Adding `UiImage::solid_color` to quickly set placeholder images.
3. Changing the default `BorderColor` and `BackgroundColor` to
transparent.
4. Removing the default overrides for these values in the other assorted
UI bundles.
5. Adding `BackgroundColor` back to `ImageBundle` and `ButtonBundle`.
6. Adding a 1x1 `Image::transparent`, which can be accessed from
`Assets<Image>` via the `TRANSPARENT_IMAGE_HANDLE` constant.

Huge thanks to everyone who helped out with the design in the linked
issue and [the Discord
thread](https://discord.com/channels/691052431525675048/1255209923890118697/1255209999278280844):
this was very much a joint design.

@cart helped me figure out how to set the UiImage's default texture to a
transparent 1x1 image, which is a much nicer fix.

## Testing

I've checked the examples modified by this PR, and the `ui` example as
well just to be sure.

## Migration Guide

- `BackgroundColor` no longer tints the color of images in `ImageBundle`
or `ButtonBundle`. Set `UiImage::color` to tint images instead.
- The default texture for `UiImage` is now a transparent white square.
Use `UiImage::solid_color` to quickly draw debug images.
- The default value for `BackgroundColor` and `BorderColor` is now
transparent. Set the color to white manually to return to previous
behavior.
  • Loading branch information
alice-i-cecile authored Jun 25, 2024
1 parent dbffb41 commit 336fddb
Show file tree
Hide file tree
Showing 18 changed files with 131 additions and 82 deletions.
32 changes: 32 additions & 0 deletions crates/bevy_render/src/texture/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,38 @@ impl Image {
image
}

/// A transparent white 1x1x1 image.
///
/// Contrast to [`Image::default`], which is opaque.
pub fn transparent() -> Image {
// We rely on the default texture format being RGBA8UnormSrgb
// when constructing a transparent color from bytes.
// If this changes, this function will need to be updated.
let format = TextureFormat::bevy_default();
debug_assert!(format.pixel_size() == 4);
let data = vec![255, 255, 255, 0];
Image {
data,
texture_descriptor: wgpu::TextureDescriptor {
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
format,
dimension: TextureDimension::D2,
label: None,
mip_level_count: 1,
sample_count: 1,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
sampler: ImageSampler::Default,
texture_view_descriptor: None,
asset_usage: RenderAssetUsages::default(),
}
}

/// Creates a new image from raw binary data and the corresponding metadata, by filling
/// the image data with the `pixel` data repeated multiple times.
///
Expand Down
16 changes: 13 additions & 3 deletions crates/bevy_render/src/texture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ use bevy_app::{App, Plugin};
use bevy_asset::{AssetApp, Assets, Handle};
use bevy_ecs::prelude::*;

/// A handle to a 1 x 1 transparent white image.
///
/// Like [`Handle<Image>::default`], this is a handle to a fallback image asset.
/// While that handle points to an opaque white 1 x 1 image, this handle points to a transparent 1 x 1 white image.
// Number randomly selected by fair WolframAlpha query. Totally arbitrary.
pub const TRANSPARENT_IMAGE_HANDLE: Handle<Image> =
Handle::weak_from_u128(154728948001857810431816125397303024160);

// TODO: replace Texture names with Image names?
/// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU.
pub struct ImagePlugin {
Expand Down Expand Up @@ -89,9 +97,11 @@ impl Plugin for ImagePlugin {
.init_asset::<Image>()
.register_asset_reflect::<Image>();

app.world_mut()
.resource_mut::<Assets<Image>>()
.insert(&Handle::default(), Image::default());
let mut image_assets = app.world_mut().resource_mut::<Assets<Image>>();

image_assets.insert(&Handle::default(), Image::default());
image_assets.insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent());

#[cfg(feature = "basis-universal")]
if let Some(processor) = app
.world()
Expand Down
60 changes: 12 additions & 48 deletions crates/bevy_ui/src/node_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use bevy_transform::prelude::{GlobalTransform, Transform};
/// Contains the [`Node`] component and other components required to make a container.
///
/// See [`node_bundles`](crate::node_bundles) for more specialized bundles like [`TextBundle`].
#[derive(Bundle, Clone, Debug)]
#[derive(Bundle, Clone, Debug, Default)]
pub struct NodeBundle {
/// Describes the logical size of the node
pub node: Node,
Expand Down Expand Up @@ -58,26 +58,6 @@ pub struct NodeBundle {
pub z_index: ZIndex,
}

impl Default for NodeBundle {
fn default() -> Self {
NodeBundle {
// Transparent background
background_color: Color::NONE.into(),
border_color: Color::NONE.into(),
border_radius: BorderRadius::default(),
node: Default::default(),
style: Default::default(),
focus_policy: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
visibility: Default::default(),
inherited_visibility: Default::default(),
view_visibility: Default::default(),
z_index: Default::default(),
}
}
}

/// A UI node that is an image
///
/// # Extra behaviours
Expand All @@ -94,8 +74,12 @@ pub struct ImageBundle {
pub style: Style,
/// The calculated size based on the given image
pub calculated_size: ContentSize,
/// The image of the node
/// The image of the node.
///
/// To tint the image, change the `color` field of this component.
pub image: UiImage,
/// The color of the background that will fill the containing node.
pub background_color: BackgroundColor,
/// The size of the image in pixels
///
/// This component is set automatically
Expand Down Expand Up @@ -176,7 +160,7 @@ pub struct AtlasImageBundle {
///
/// The positioning of this node is controlled by the UI layout system. If you need manual control,
/// use [`Text2dBundle`](bevy_text::Text2dBundle).
#[derive(Bundle, Debug)]
#[derive(Bundle, Debug, Default)]
pub struct TextBundle {
/// Describes the logical size of the node
pub node: Node,
Expand Down Expand Up @@ -214,29 +198,6 @@ pub struct TextBundle {
pub background_color: BackgroundColor,
}

#[cfg(feature = "bevy_text")]
impl Default for TextBundle {
fn default() -> Self {
Self {
text: Default::default(),
text_layout_info: Default::default(),
text_flags: Default::default(),
calculated_size: Default::default(),
node: Default::default(),
style: Default::default(),
focus_policy: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
visibility: Default::default(),
inherited_visibility: Default::default(),
view_visibility: Default::default(),
z_index: Default::default(),
// Transparent background
background_color: BackgroundColor(Color::NONE),
}
}
}

#[cfg(feature = "bevy_text")]
impl TextBundle {
/// Create a [`TextBundle`] from a single section.
Expand Down Expand Up @@ -321,6 +282,8 @@ pub struct ButtonBundle {
pub border_radius: BorderRadius,
/// The image of the node
pub image: UiImage,
/// The background color that will fill the containing node
pub background_color: BackgroundColor,
/// The transform of the node
///
/// This component is automatically managed by the UI layout system.
Expand Down Expand Up @@ -348,9 +311,10 @@ impl Default for ButtonBundle {
style: Default::default(),
interaction: Default::default(),
focus_policy: FocusPolicy::Block,
border_color: BorderColor(Color::NONE),
border_radius: BorderRadius::default(),
border_color: Default::default(),
border_radius: Default::default(),
image: Default::default(),
background_color: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
visibility: Default::default(),
Expand Down
54 changes: 48 additions & 6 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use bevy_math::{Rect, Vec2};
use bevy_reflect::prelude::*;
use bevy_render::{
camera::{Camera, RenderTarget},
texture::Image,
texture::{Image, TRANSPARENT_IMAGE_HANDLE},
};
use bevy_transform::prelude::GlobalTransform;
use bevy_utils::warn_once;
Expand Down Expand Up @@ -1693,7 +1693,8 @@ pub enum GridPlacementError {
pub struct BackgroundColor(pub Color);

impl BackgroundColor {
pub const DEFAULT: Self = Self(Color::WHITE);
/// Background color is transparent by default.
pub const DEFAULT: Self = Self(Color::NONE);
}

impl Default for BackgroundColor {
Expand Down Expand Up @@ -1725,7 +1726,8 @@ impl<T: Into<Color>> From<T> for BorderColor {
}

impl BorderColor {
pub const DEFAULT: Self = BorderColor(Color::WHITE);
/// Border color is transparent by default.
pub const DEFAULT: Self = BorderColor(Color::NONE);
}

impl Default for BorderColor {
Expand Down Expand Up @@ -1819,27 +1821,67 @@ impl Outline {
}

/// The 2D texture displayed for this UI node
#[derive(Component, Clone, Debug, Reflect, Default)]
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default)]
pub struct UiImage {
/// The tint color used to draw the image
/// The tint color used to draw the image.
///
/// This is multiplied by the color of each pixel in the image.
/// The field value defaults to solid white, which will pass the image through unmodified.
pub color: Color,
/// Handle to the texture
/// Handle to the texture.
///
/// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
pub texture: Handle<Image>,
/// Whether the image should be flipped along its x-axis
pub flip_x: bool,
/// Whether the image should be flipped along its y-axis
pub flip_y: bool,
}

impl Default for UiImage {
/// A transparent 1x1 image with a solid white tint.
///
/// # Warning
///
/// This will be invisible by default.
/// To set this to a visible image, you need to set the `texture` field to a valid image handle,
/// or use [`Handle<Image>`]'s default 1x1 solid white texture (as is done in [`UiImage::solid_color`]).
fn default() -> Self {
UiImage {
// This should be white because the tint is multiplied with the image,
// so if you set an actual image with default tint you'd want its original colors
color: Color::WHITE,
// This texture needs to be transparent by default, to avoid covering the background color
texture: TRANSPARENT_IMAGE_HANDLE,
flip_x: false,
flip_y: false,
}
}
}

impl UiImage {
/// Create a new [`UiImage`] with the given texture.
pub fn new(texture: Handle<Image>) -> Self {
Self {
texture,
color: Color::WHITE,
..Default::default()
}
}

/// Create a solid color [`UiImage`].
///
/// This is primarily useful for debugging / mocking the extents of your image.
pub fn solid_color(color: Color) -> Self {
Self {
texture: Handle::default(),
color,
flip_x: false,
flip_y: false,
}
}

/// Set the color tint
#[must_use]
pub const fn with_color(mut self, color: Color) -> Self {
Expand Down
2 changes: 1 addition & 1 deletion examples/3d/color_grading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ fn add_button_for_value(
},
border_color: BorderColor(Color::WHITE),
border_radius: BorderRadius::MAX,
image: UiImage::default().with_color(Color::BLACK),
background_color: Color::BLACK.into(),
..default()
})
.insert(ColorGradingOptionWidget {
Expand Down
2 changes: 1 addition & 1 deletion examples/3d/split_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn setup(
..default()
},
border_color: Color::WHITE.into(),
image: UiImage::default().with_color(Color::srgb(0.25, 0.25, 0.25)),
background_color: Color::srgb(0.25, 0.25, 0.25).into(),
..default()
},
))
Expand Down
16 changes: 8 additions & 8 deletions examples/games/game_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style.clone(),
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
MenuButtonAction::Play,
Expand All @@ -477,7 +477,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style.clone(),
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
MenuButtonAction::Settings,
Expand All @@ -498,7 +498,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style,
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
MenuButtonAction::Quit,
Expand Down Expand Up @@ -567,7 +567,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style.clone(),
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
action,
Expand Down Expand Up @@ -654,7 +654,7 @@ mod menu {
height: Val::Px(65.0),
..button_style.clone()
},
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
quality_setting,
Expand All @@ -675,7 +675,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style,
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
MenuButtonAction::BackToSettings,
Expand Down Expand Up @@ -750,7 +750,7 @@ mod menu {
height: Val::Px(65.0),
..button_style.clone()
},
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
Volume(volume_setting),
Expand All @@ -764,7 +764,7 @@ mod menu {
.spawn((
ButtonBundle {
style: button_style,
image: UiImage::default().with_color(NORMAL_BUTTON),
background_color: NORMAL_BUTTON.into(),
..default()
},
MenuButtonAction::BackToSettings,
Expand Down
1 change: 0 additions & 1 deletion examples/mobile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ fn setup_scene(
bottom: Val::Px(50.0),
..default()
},
image: UiImage::default().with_color(Color::NONE),
..default()
},
BackgroundColor(Color::WHITE),
Expand Down
Loading

0 comments on commit 336fddb

Please sign in to comment.