Skip to content

Commit 82f0156

Browse files
alice-i-cecilemockersf
authored andcommitted
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.
1 parent 65daab8 commit 82f0156

File tree

18 files changed

+131
-82
lines changed

18 files changed

+131
-82
lines changed

crates/bevy_render/src/texture/image.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,38 @@ impl Image {
531531
image
532532
}
533533

534+
/// A transparent white 1x1x1 image.
535+
///
536+
/// Contrast to [`Image::default`], which is opaque.
537+
pub fn transparent() -> Image {
538+
// We rely on the default texture format being RGBA8UnormSrgb
539+
// when constructing a transparent color from bytes.
540+
// If this changes, this function will need to be updated.
541+
let format = TextureFormat::bevy_default();
542+
debug_assert!(format.pixel_size() == 4);
543+
let data = vec![255, 255, 255, 0];
544+
Image {
545+
data,
546+
texture_descriptor: wgpu::TextureDescriptor {
547+
size: Extent3d {
548+
width: 1,
549+
height: 1,
550+
depth_or_array_layers: 1,
551+
},
552+
format,
553+
dimension: TextureDimension::D2,
554+
label: None,
555+
mip_level_count: 1,
556+
sample_count: 1,
557+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
558+
view_formats: &[],
559+
},
560+
sampler: ImageSampler::Default,
561+
texture_view_descriptor: None,
562+
asset_usage: RenderAssetUsages::default(),
563+
}
564+
}
565+
534566
/// Creates a new image from raw binary data and the corresponding metadata, by filling
535567
/// the image data with the `pixel` data repeated multiple times.
536568
///

crates/bevy_render/src/texture/mod.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ use bevy_app::{App, Plugin};
4343
use bevy_asset::{AssetApp, Assets, Handle};
4444
use bevy_ecs::prelude::*;
4545

46+
/// A handle to a 1 x 1 transparent white image.
47+
///
48+
/// Like [`Handle<Image>::default`], this is a handle to a fallback image asset.
49+
/// While that handle points to an opaque white 1 x 1 image, this handle points to a transparent 1 x 1 white image.
50+
// Number randomly selected by fair WolframAlpha query. Totally arbitrary.
51+
pub const TRANSPARENT_IMAGE_HANDLE: Handle<Image> =
52+
Handle::weak_from_u128(154728948001857810431816125397303024160);
53+
4654
// TODO: replace Texture names with Image names?
4755
/// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU.
4856
pub struct ImagePlugin {
@@ -89,9 +97,11 @@ impl Plugin for ImagePlugin {
8997
.init_asset::<Image>()
9098
.register_asset_reflect::<Image>();
9199

92-
app.world_mut()
93-
.resource_mut::<Assets<Image>>()
94-
.insert(&Handle::default(), Image::default());
100+
let mut image_assets = app.world_mut().resource_mut::<Assets<Image>>();
101+
102+
image_assets.insert(&Handle::default(), Image::default());
103+
image_assets.insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent());
104+
95105
#[cfg(feature = "basis-universal")]
96106
if let Some(processor) = app
97107
.world()

crates/bevy_ui/src/node_bundles.rs

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use bevy_transform::prelude::{GlobalTransform, Transform};
2323
/// Contains the [`Node`] component and other components required to make a container.
2424
///
2525
/// See [`node_bundles`](crate::node_bundles) for more specialized bundles like [`TextBundle`].
26-
#[derive(Bundle, Clone, Debug)]
26+
#[derive(Bundle, Clone, Debug, Default)]
2727
pub struct NodeBundle {
2828
/// Describes the logical size of the node
2929
pub node: Node,
@@ -58,26 +58,6 @@ pub struct NodeBundle {
5858
pub z_index: ZIndex,
5959
}
6060

61-
impl Default for NodeBundle {
62-
fn default() -> Self {
63-
NodeBundle {
64-
// Transparent background
65-
background_color: Color::NONE.into(),
66-
border_color: Color::NONE.into(),
67-
border_radius: BorderRadius::default(),
68-
node: Default::default(),
69-
style: Default::default(),
70-
focus_policy: Default::default(),
71-
transform: Default::default(),
72-
global_transform: Default::default(),
73-
visibility: Default::default(),
74-
inherited_visibility: Default::default(),
75-
view_visibility: Default::default(),
76-
z_index: Default::default(),
77-
}
78-
}
79-
}
80-
8161
/// A UI node that is an image
8262
///
8363
/// # Extra behaviours
@@ -94,8 +74,12 @@ pub struct ImageBundle {
9474
pub style: Style,
9575
/// The calculated size based on the given image
9676
pub calculated_size: ContentSize,
97-
/// The image of the node
77+
/// The image of the node.
78+
///
79+
/// To tint the image, change the `color` field of this component.
9880
pub image: UiImage,
81+
/// The color of the background that will fill the containing node.
82+
pub background_color: BackgroundColor,
9983
/// The size of the image in pixels
10084
///
10185
/// This component is set automatically
@@ -176,7 +160,7 @@ pub struct AtlasImageBundle {
176160
///
177161
/// The positioning of this node is controlled by the UI layout system. If you need manual control,
178162
/// use [`Text2dBundle`](bevy_text::Text2dBundle).
179-
#[derive(Bundle, Debug)]
163+
#[derive(Bundle, Debug, Default)]
180164
pub struct TextBundle {
181165
/// Describes the logical size of the node
182166
pub node: Node,
@@ -214,29 +198,6 @@ pub struct TextBundle {
214198
pub background_color: BackgroundColor,
215199
}
216200

217-
#[cfg(feature = "bevy_text")]
218-
impl Default for TextBundle {
219-
fn default() -> Self {
220-
Self {
221-
text: Default::default(),
222-
text_layout_info: Default::default(),
223-
text_flags: Default::default(),
224-
calculated_size: Default::default(),
225-
node: Default::default(),
226-
style: Default::default(),
227-
focus_policy: Default::default(),
228-
transform: Default::default(),
229-
global_transform: Default::default(),
230-
visibility: Default::default(),
231-
inherited_visibility: Default::default(),
232-
view_visibility: Default::default(),
233-
z_index: Default::default(),
234-
// Transparent background
235-
background_color: BackgroundColor(Color::NONE),
236-
}
237-
}
238-
}
239-
240201
#[cfg(feature = "bevy_text")]
241202
impl TextBundle {
242203
/// Create a [`TextBundle`] from a single section.
@@ -321,6 +282,8 @@ pub struct ButtonBundle {
321282
pub border_radius: BorderRadius,
322283
/// The image of the node
323284
pub image: UiImage,
285+
/// The background color that will fill the containing node
286+
pub background_color: BackgroundColor,
324287
/// The transform of the node
325288
///
326289
/// This component is automatically managed by the UI layout system.
@@ -348,9 +311,10 @@ impl Default for ButtonBundle {
348311
style: Default::default(),
349312
interaction: Default::default(),
350313
focus_policy: FocusPolicy::Block,
351-
border_color: BorderColor(Color::NONE),
352-
border_radius: BorderRadius::default(),
314+
border_color: Default::default(),
315+
border_radius: Default::default(),
353316
image: Default::default(),
317+
background_color: Default::default(),
354318
transform: Default::default(),
355319
global_transform: Default::default(),
356320
visibility: Default::default(),

crates/bevy_ui/src/ui_node.rs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bevy_math::{Rect, Vec2};
66
use bevy_reflect::prelude::*;
77
use bevy_render::{
88
camera::{Camera, RenderTarget},
9-
texture::Image,
9+
texture::{Image, TRANSPARENT_IMAGE_HANDLE},
1010
};
1111
use bevy_transform::prelude::GlobalTransform;
1212
use bevy_utils::warn_once;
@@ -1693,7 +1693,8 @@ pub enum GridPlacementError {
16931693
pub struct BackgroundColor(pub Color);
16941694

16951695
impl BackgroundColor {
1696-
pub const DEFAULT: Self = Self(Color::WHITE);
1696+
/// Background color is transparent by default.
1697+
pub const DEFAULT: Self = Self(Color::NONE);
16971698
}
16981699

16991700
impl Default for BackgroundColor {
@@ -1725,7 +1726,8 @@ impl<T: Into<Color>> From<T> for BorderColor {
17251726
}
17261727

17271728
impl BorderColor {
1728-
pub const DEFAULT: Self = BorderColor(Color::WHITE);
1729+
/// Border color is transparent by default.
1730+
pub const DEFAULT: Self = BorderColor(Color::NONE);
17291731
}
17301732

17311733
impl Default for BorderColor {
@@ -1819,27 +1821,67 @@ impl Outline {
18191821
}
18201822

18211823
/// The 2D texture displayed for this UI node
1822-
#[derive(Component, Clone, Debug, Reflect, Default)]
1824+
#[derive(Component, Clone, Debug, Reflect)]
18231825
#[reflect(Component, Default)]
18241826
pub struct UiImage {
1825-
/// The tint color used to draw the image
1827+
/// The tint color used to draw the image.
1828+
///
1829+
/// This is multiplied by the color of each pixel in the image.
1830+
/// The field value defaults to solid white, which will pass the image through unmodified.
18261831
pub color: Color,
1827-
/// Handle to the texture
1832+
/// Handle to the texture.
1833+
///
1834+
/// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
18281835
pub texture: Handle<Image>,
18291836
/// Whether the image should be flipped along its x-axis
18301837
pub flip_x: bool,
18311838
/// Whether the image should be flipped along its y-axis
18321839
pub flip_y: bool,
18331840
}
18341841

1842+
impl Default for UiImage {
1843+
/// A transparent 1x1 image with a solid white tint.
1844+
///
1845+
/// # Warning
1846+
///
1847+
/// This will be invisible by default.
1848+
/// To set this to a visible image, you need to set the `texture` field to a valid image handle,
1849+
/// or use [`Handle<Image>`]'s default 1x1 solid white texture (as is done in [`UiImage::solid_color`]).
1850+
fn default() -> Self {
1851+
UiImage {
1852+
// This should be white because the tint is multiplied with the image,
1853+
// so if you set an actual image with default tint you'd want its original colors
1854+
color: Color::WHITE,
1855+
// This texture needs to be transparent by default, to avoid covering the background color
1856+
texture: TRANSPARENT_IMAGE_HANDLE,
1857+
flip_x: false,
1858+
flip_y: false,
1859+
}
1860+
}
1861+
}
1862+
18351863
impl UiImage {
1864+
/// Create a new [`UiImage`] with the given texture.
18361865
pub fn new(texture: Handle<Image>) -> Self {
18371866
Self {
18381867
texture,
1868+
color: Color::WHITE,
18391869
..Default::default()
18401870
}
18411871
}
18421872

1873+
/// Create a solid color [`UiImage`].
1874+
///
1875+
/// This is primarily useful for debugging / mocking the extents of your image.
1876+
pub fn solid_color(color: Color) -> Self {
1877+
Self {
1878+
texture: Handle::default(),
1879+
color,
1880+
flip_x: false,
1881+
flip_y: false,
1882+
}
1883+
}
1884+
18431885
/// Set the color tint
18441886
#[must_use]
18451887
pub const fn with_color(mut self, color: Color) -> Self {

examples/3d/color_grading.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ fn add_button_for_value(
267267
},
268268
border_color: BorderColor(Color::WHITE),
269269
border_radius: BorderRadius::MAX,
270-
image: UiImage::default().with_color(Color::BLACK),
270+
background_color: Color::BLACK.into(),
271271
..default()
272272
})
273273
.insert(ColorGradingOptionWidget {

examples/3d/split_screen.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ fn setup(
140140
..default()
141141
},
142142
border_color: Color::WHITE.into(),
143-
image: UiImage::default().with_color(Color::srgb(0.25, 0.25, 0.25)),
143+
background_color: Color::srgb(0.25, 0.25, 0.25).into(),
144144
..default()
145145
},
146146
))

examples/games/game_menu.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ mod menu {
456456
.spawn((
457457
ButtonBundle {
458458
style: button_style.clone(),
459-
image: UiImage::default().with_color(NORMAL_BUTTON),
459+
background_color: NORMAL_BUTTON.into(),
460460
..default()
461461
},
462462
MenuButtonAction::Play,
@@ -477,7 +477,7 @@ mod menu {
477477
.spawn((
478478
ButtonBundle {
479479
style: button_style.clone(),
480-
image: UiImage::default().with_color(NORMAL_BUTTON),
480+
background_color: NORMAL_BUTTON.into(),
481481
..default()
482482
},
483483
MenuButtonAction::Settings,
@@ -498,7 +498,7 @@ mod menu {
498498
.spawn((
499499
ButtonBundle {
500500
style: button_style,
501-
image: UiImage::default().with_color(NORMAL_BUTTON),
501+
background_color: NORMAL_BUTTON.into(),
502502
..default()
503503
},
504504
MenuButtonAction::Quit,
@@ -567,7 +567,7 @@ mod menu {
567567
.spawn((
568568
ButtonBundle {
569569
style: button_style.clone(),
570-
image: UiImage::default().with_color(NORMAL_BUTTON),
570+
background_color: NORMAL_BUTTON.into(),
571571
..default()
572572
},
573573
action,
@@ -654,7 +654,7 @@ mod menu {
654654
height: Val::Px(65.0),
655655
..button_style.clone()
656656
},
657-
image: UiImage::default().with_color(NORMAL_BUTTON),
657+
background_color: NORMAL_BUTTON.into(),
658658
..default()
659659
},
660660
quality_setting,
@@ -675,7 +675,7 @@ mod menu {
675675
.spawn((
676676
ButtonBundle {
677677
style: button_style,
678-
image: UiImage::default().with_color(NORMAL_BUTTON),
678+
background_color: NORMAL_BUTTON.into(),
679679
..default()
680680
},
681681
MenuButtonAction::BackToSettings,
@@ -750,7 +750,7 @@ mod menu {
750750
height: Val::Px(65.0),
751751
..button_style.clone()
752752
},
753-
image: UiImage::default().with_color(NORMAL_BUTTON),
753+
background_color: NORMAL_BUTTON.into(),
754754
..default()
755755
},
756756
Volume(volume_setting),
@@ -764,7 +764,7 @@ mod menu {
764764
.spawn((
765765
ButtonBundle {
766766
style: button_style,
767-
image: UiImage::default().with_color(NORMAL_BUTTON),
767+
background_color: NORMAL_BUTTON.into(),
768768
..default()
769769
},
770770
MenuButtonAction::BackToSettings,

examples/mobile/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ fn setup_scene(
125125
bottom: Val::Px(50.0),
126126
..default()
127127
},
128-
image: UiImage::default().with_color(Color::NONE),
129128
..default()
130129
},
131130
BackgroundColor(Color::WHITE),

0 commit comments

Comments
 (0)