mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
Make default behavior for BackgroundColor
and BorderColor
more intuitive (#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.
This commit is contained in:
parent
dbffb41e50
commit
336fddb101
18 changed files with 131 additions and 82 deletions
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -1819,12 +1821,17 @@ 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,
|
||||
|
@ -1832,14 +1839,49 @@ pub struct UiImage {
|
|||
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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
))
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
|
|
@ -125,7 +125,6 @@ fn setup_scene(
|
|||
bottom: Val::Px(50.0),
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(Color::NONE),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::WHITE),
|
||||
|
|
|
@ -363,7 +363,7 @@ mod ui {
|
|||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
},
|
||||
MenuButton::Play,
|
||||
|
@ -391,10 +391,11 @@ mod ui {
|
|||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(match tutorial_state.get() {
|
||||
background_color: match tutorial_state.get() {
|
||||
TutorialState::Active => ACTIVE_BUTTON,
|
||||
TutorialState::Inactive => NORMAL_BUTTON,
|
||||
}),
|
||||
}
|
||||
.into(),
|
||||
..default()
|
||||
},
|
||||
MenuButton::Tutorial,
|
||||
|
|
|
@ -270,7 +270,7 @@ fn setup_menu(mut commands: Commands) {
|
|||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
|
|
|
@ -74,7 +74,7 @@ fn setup_menu(mut commands: Commands) {
|
|||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
|
|
|
@ -180,7 +180,7 @@ mod ui {
|
|||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
|
|
|
@ -246,7 +246,7 @@ fn spawn_button(
|
|||
border,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(background_color),
|
||||
background_color: background_color.into(),
|
||||
border_color,
|
||||
..default()
|
||||
},
|
||||
|
|
|
@ -75,7 +75,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
},
|
||||
border_color: BorderColor(Color::BLACK),
|
||||
border_radius: BorderRadius::MAX,
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
|
|
|
@ -415,7 +415,7 @@ where
|
|||
padding: UiRect::axes(Val::Px(5.), Val::Px(1.)),
|
||||
..Default::default()
|
||||
},
|
||||
image: UiImage::default().with_color(Color::BLACK.with_alpha(0.5)),
|
||||
background_color: Color::BLACK.with_alpha(0.5).into(),
|
||||
..Default::default()
|
||||
},
|
||||
Target::<T>::new(target),
|
||||
|
|
|
@ -245,11 +245,12 @@ fn spawn_button(
|
|||
margin: UiRect::horizontal(Val::Px(2.)),
|
||||
..Default::default()
|
||||
},
|
||||
image: UiImage::default().with_color(if active {
|
||||
background_color: if active {
|
||||
ACTIVE_BORDER_COLOR
|
||||
} else {
|
||||
INACTIVE_BORDER_COLOR
|
||||
}),
|
||||
}
|
||||
.into(),
|
||||
..Default::default()
|
||||
},
|
||||
constraint,
|
||||
|
|
|
@ -37,7 +37,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(Color::srgb(0.1, 0.5, 0.1)),
|
||||
background_color: Color::srgb(0.1, 0.5, 0.1).into(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
|
@ -63,7 +63,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(Color::srgb(0.5, 0.1, 0.5)),
|
||||
background_color: Color::srgb(0.5, 0.1, 0.5).into(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
|
|
|
@ -54,10 +54,10 @@ fn setup(
|
|||
..default()
|
||||
},
|
||||
image: UiImage::new(texture_handle),
|
||||
background_color: BackgroundColor(ANTIQUE_WHITE.into()),
|
||||
..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([
|
||||
|
|
Loading…
Reference in a new issue