Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bevy_ui: Ability to display an image without scaling. #7349

Open
inodentry opened this issue Jan 24, 2023 · 8 comments
Open

bevy_ui: Ability to display an image without scaling. #7349

inodentry opened this issue Jan 24, 2023 · 8 comments
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Enhancement A new feature D-Good-First-Issue Nice and easy! A great choice to get started with Bevy

Comments

@inodentry
Copy link
Contributor

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.

@inodentry inodentry added C-Enhancement A new feature D-Good-First-Issue Nice and easy! A great choice to get started with Bevy A-UI Graphical user interfaces, styles, layouts, and widgets labels Jan 24, 2023
@mockersf
Copy link
Member

Currently we have an ImageMode enum component

Not anymore...
#6674

@inodentry
Copy link
Contributor Author

#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. :)

@inodentry inodentry changed the title bevy_ui: ImageMode::Exact bevy_ui: Ability to display an image without scaling. Sep 25, 2023
@inodentry
Copy link
Contributor Author

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.

@inodentry
Copy link
Contributor Author

Further, the fields of UiImageSize are private (ugh), meaning I can't even hack around it easily.

@ickshonpe
Copy link
Contributor

ickshonpe commented Sep 26, 2023

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).

@ickshonpe
Copy link
Contributor

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.

@mghildiy
Copy link
Contributor

May I give it a try?

@musjj
Copy link

musjj commented Apr 2, 2024

Wanted to implement a custom cursor using UI (because sprites are always drawn below UI), but got blocked by this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Enhancement A new feature D-Good-First-Issue Nice and easy! A great choice to get started with Bevy
Projects
None yet
Development

No branches or pull requests

5 participants