bevy_ui: Ability to display an image without scaling.
inodentry opened this issue · 8 comments
What problem does this solve or what need does it fill?
Having images in UI that you want to be displayed exactly without any scaling. No resizing allowed.
For example, I want to create a toolbar button with an icon in it. The image assets for the button's background and the icons are intended to be fixed size and displayed as is, exactly that size.
What solution would you like?
Currently we have an ImageMode
enum component, which seems to have been created for this exact purpose (controlling the behavior of image scaling). It only has one variant: KeepAspect
. It seems like the sensible/appropriate place for this new addition. We could add a new variant: Exact
, that preserves image dimensions exactly / allows no resizing.
While we are at it, we could also add a ImageMode::Stretch
that allows arbitrary resizing without regard for aspect ratio. Then, the ImageMode
feature set would feel "complete" and cover the most common use cases.
What alternative(s) have you considered?
I could use the Style properties to force the node to have exactly the size of the image, but this is boilerplatey and error-prone. It would be nice if the layout system could take care of it automatically.
#6674 makes sense given that it was useless in its old form. Only one enum variant is equivalent to a unit type, i.e a no-op.
I think this issue is worth keeping open, for how we could make it (or something like it) useful, so it can be worth having / bringing back. :)
I'd like to add that currently it seems impossible to display an image in UI that should not be scaled.
I have a use case where I want to display a minimap in UI. The minimap image is rendered by the game, so it can be generated at any size. I want to display it without any respect to UiScale or the window's scale factor, so it appears crisp.
Further, the fields of UiImageSize
are private (ugh), meaning I can't even hack around it easily.
At the moment you have to write your own image widget.
Something like this (for 0.11 but only needs trivial changes to work with main) :
use bevy::prelude::*;
use bevy::render::Extract;
use bevy::render::RenderApp;
use bevy::ui::ContentSize;
use bevy::ui::ExtractedUiNode;
use bevy::ui::ExtractedUiNodes;
use bevy::ui::FixedMeasure;
use bevy::ui::FocusPolicy;
use bevy::ui::RenderUiSystem;
use bevy::ui::UiStack;
use bevy::ui::UiSystem;
use bevy::window::PrimaryWindow;
/// A UI node that is an image
#[derive(Bundle, Debug, Default)]
pub struct ExactImageBundle {
/// Describes the logical size of the node
///
/// This field is automatically managed by the UI layout system.
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
pub node: Node,
/// Styles which control the layout (size and position) of the node and it's children
/// In some cases these styles also affect how the node drawn/painted.
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: ExactImage,
/// The size of the image in pixels
///
/// This field is set automatically
pub image_size: ExactImageSize,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The transform of the node
///
/// This field is automatically managed by the UI layout system.
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
pub transform: Transform,
/// The global transform of the node
///
/// This field is automatically managed by the UI layout system.
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
pub global_transform: GlobalTransform,
/// Describes the visibility properties of the node
pub visibility: Visibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
pub computed_visibility: ComputedVisibility,
/// Indicates the depth at which the node should appear in the UI
pub z_index: ZIndex,
}
/// The 2D texture displayed for this UI node
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default)]
pub struct ExactImage {
/// Handle to the 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,
/// Scaling to apply to image size
pub scale: Vec2,
}
impl Default for ExactImage {
fn default() -> Self {
Self {
texture: Default::default(),
flip_x: Default::default(),
flip_y: Default::default(),
scale: Vec2::ONE,
}
}
}
impl ExactImage {
pub fn new(texture: Handle<Image>) -> Self {
Self {
texture,
..Default::default()
}
}
/// flip the image along its x-axis
#[must_use]
pub const fn with_flip_x(mut self) -> Self {
self.flip_x = true;
self
}
/// flip the image along its y-axis
#[must_use]
pub const fn with_flip_y(mut self) -> Self {
self.flip_y = true;
self
}
}
impl From<Handle<Image>> for ExactImage {
fn from(texture: Handle<Image>) -> Self {
Self::new(texture)
}
}
/// The size of the image's texture
///
/// This component is updated automatically by [`update_image_content_size_system`]
#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
#[reflect(Component, Default)]
pub struct ExactImageSize {
/// The size of the image's texture
///
/// This field is updated automatically by [`update_image_content_size_system`]
pub(crate) size: Vec2,
}
impl ExactImageSize {
/// The size of the image's texture
pub fn size(&self) -> Vec2 {
self.size
}
}
/// Updates content size of the node based on the image provided
pub fn update_exact_image_content_size_system(
mut previous_combined_scale_factor: Local<f64>,
ui_scale: Res<UiScale>,
windows: Query<&Window, With<PrimaryWindow>>,
textures: Res<Assets<Image>>,
mut query: Query<(&mut ContentSize, &ExactImage, &mut ExactImageSize)>,
) {
let combined_scale_factor = windows
.get_single()
.map(|window| window.resolution.scale_factor())
.unwrap_or(1.)
* ui_scale.scale;
for (mut content_size, image, mut image_size) in &mut query {
if let Some(texture) = textures.get(&image.texture) {
let size = image.scale
* Vec2::new(
texture.texture_descriptor.size.width as f32,
texture.texture_descriptor.size.height as f32,
);
if size != image_size.size {
image_size.size = size;
content_size.set(FixedMeasure {
size: size * image.scale,
});
}
}
}
*previous_combined_scale_factor = combined_scale_factor;
}
pub fn extract_exact_sized_image(
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
images: Extract<Res<Assets<Image>>>,
ui_stack: Extract<Res<UiStack>>,
uinode_query: Extract<
Query<
(
&Node,
&GlobalTransform,
&BackgroundColor,
&ExactImage,
&ExactImageSize,
&ComputedVisibility,
Option<&CalculatedClip>,
),
Without<UiTextureAtlasImage>,
>,
>,
) {
for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
if let Ok((node, transform, color, image, image_size, visibility, clip)) =
uinode_query.get(*entity)
{
// Skip invisible and completely transparent nodes
if !visibility.is_visible() || color.0.a() == 0.0 {
continue;
}
if !images.contains(&image.texture) {
continue;
}
let transform = transform.compute_matrix()
* Mat4::from_translation(0.5 * (image_size.size - node.size()).extend(0.));
extracted_uinodes.uinodes.push(ExtractedUiNode {
stack_index,
transform,
color: color.0,
rect: Rect {
min: Vec2::ZERO,
max: image_size.size,
},
clip: clip.map(|clip| clip.clip),
image: image.texture.clone(),
atlas_size: None,
flip_x: image.flip_x,
flip_y: image.flip_y,
});
};
}
}
pub struct ExactImagePlugin;
impl Plugin for ExactImagePlugin {
fn build(&self, app: &mut bevy::app::App) {
app.add_systems(
PostUpdate,
update_exact_image_content_size_system.before(UiSystem::Layout),
);
let render_app = match app.get_sub_app_mut(RenderApp) {
Ok(render_app) => render_app,
Err(_) => return,
};
render_app.add_systems(
ExtractSchedule,
(extract_exact_sized_image
.after(RenderUiSystem::ExtractNode)
.before(RenderUiSystem::ExtractAtlasNode),),
);
}
}
There's some bug with the scale and scale_factor (like always), the images display correctly but the size of the containing UI node is off. If you don't care about fitting the image to the layout you don't need the widget system or ContentSize
stuff and can just retrieve the size of the image from assets in the extraction system.
I implemented a more advanced image widget for Bevy a while ago but I haven't upstreamed it yet because the API was a bit too complicated and I couldn't decide about some of the behaviours or whether to support things like letterboxing etc (and I also have way too many open PRs already).
Another thing to be aware of with images in Bevy UI is that because of layout coordinate rounding sometimes the size of a node might be adjusted by a pixel to close gaps in the layout which will ruin a lot of pixel art unless you use a custom widget like the one above.
May I give it a try?
Wanted to implement a custom cursor using UI (because sprites are always drawn below UI), but got blocked by this.