From 336fddb1019cad526398e0ccdb28b9b0317635bb Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 25 Jun 2024 17:50:41 -0400 Subject: [PATCH] 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` 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. --- crates/bevy_render/src/texture/image.rs | 32 +++++++++++++ crates/bevy_render/src/texture/mod.rs | 16 +++++-- crates/bevy_ui/src/node_bundles.rs | 60 +++++-------------------- crates/bevy_ui/src/ui_node.rs | 54 +++++++++++++++++++--- examples/3d/color_grading.rs | 2 +- examples/3d/split_screen.rs | 2 +- examples/games/game_menu.rs | 16 +++---- examples/mobile/src/lib.rs | 1 - examples/state/computed_states.rs | 7 +-- examples/state/custom_transitions.rs | 2 +- examples/state/states.rs | 2 +- examples/state/sub_states.rs | 2 +- examples/stress_tests/many_buttons.rs | 2 +- examples/ui/button.rs | 2 +- examples/ui/display_and_visibility.rs | 2 +- examples/ui/size_constraints.rs | 5 ++- examples/ui/transparency_ui.rs | 4 +- examples/ui/ui_texture_atlas.rs | 2 +- 18 files changed, 131 insertions(+), 82 deletions(-) diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index b11f7937d9..d1a63fc817 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -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. /// diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index ac5d74cdc7..9b1a592773 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -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::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 = + 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::() .register_asset_reflect::(); - app.world_mut() - .resource_mut::>() - .insert(&Handle::default(), Image::default()); + let mut image_assets = app.world_mut().resource_mut::>(); + + 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() diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 89d4ecfced..42e7517487 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -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(), diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 75852e78ee..95166fed1c 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -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> From 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, /// 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`]'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) -> 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 { diff --git a/examples/3d/color_grading.rs b/examples/3d/color_grading.rs index aa4ceea8cd..42cb0ae163 100644 --- a/examples/3d/color_grading.rs +++ b/examples/3d/color_grading.rs @@ -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 { diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index df2b109db1..a5e93fc14e 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -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() }, )) diff --git a/examples/games/game_menu.rs b/examples/games/game_menu.rs index 60d8d5e349..f93c258a62 100644 --- a/examples/games/game_menu.rs +++ b/examples/games/game_menu.rs @@ -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, diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index 122710733c..14e929cab0 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -125,7 +125,6 @@ fn setup_scene( bottom: Val::Px(50.0), ..default() }, - image: UiImage::default().with_color(Color::NONE), ..default() }, BackgroundColor(Color::WHITE), diff --git a/examples/state/computed_states.rs b/examples/state/computed_states.rs index 38b0659d1c..9179cb884a 100644 --- a/examples/state/computed_states.rs +++ b/examples/state/computed_states.rs @@ -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, diff --git a/examples/state/custom_transitions.rs b/examples/state/custom_transitions.rs index 0d578ea052..f87d617b2e 100644 --- a/examples/state/custom_transitions.rs +++ b/examples/state/custom_transitions.rs @@ -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| { diff --git a/examples/state/states.rs b/examples/state/states.rs index ef329b0baa..4f029d8f36 100644 --- a/examples/state/states.rs +++ b/examples/state/states.rs @@ -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| { diff --git a/examples/state/sub_states.rs b/examples/state/sub_states.rs index 1190315967..64219d893a 100644 --- a/examples/state/sub_states.rs +++ b/examples/state/sub_states.rs @@ -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| { diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 03fad2bc86..63855c1733 100644 --- a/examples/stress_tests/many_buttons.rs +++ b/examples/stress_tests/many_buttons.rs @@ -246,7 +246,7 @@ fn spawn_button( border, ..default() }, - image: UiImage::default().with_color(background_color), + background_color: background_color.into(), border_color, ..default() }, diff --git a/examples/ui/button.rs b/examples/ui/button.rs index c00242fc88..e13eb024cd 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -75,7 +75,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, 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| { diff --git a/examples/ui/display_and_visibility.rs b/examples/ui/display_and_visibility.rs index 2f07d9b6da..b72f016320 100644 --- a/examples/ui/display_and_visibility.rs +++ b/examples/ui/display_and_visibility.rs @@ -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::::new(target), diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index d32fe9734c..dc2890bed8 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -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, diff --git a/examples/ui/transparency_ui.rs b/examples/ui/transparency_ui.rs index 92c27059d5..f83d2ba215 100644 --- a/examples/ui/transparency_ui.rs +++ b/examples/ui/transparency_ui.rs @@ -37,7 +37,7 @@ fn setup(mut commands: Commands, asset_server: Res) { 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) { 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| { diff --git a/examples/ui/ui_texture_atlas.rs b/examples/ui/ui_texture_atlas.rs index 947cdecf90..3caa9d6616 100644 --- a/examples/ui/ui_texture_atlas.rs +++ b/examples/ui/ui_texture_atlas.rs @@ -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([