diff --git a/Cargo.toml b/Cargo.toml index 3cd87c291c26a..bc674525fc409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3418,6 +3418,17 @@ description = "An example for CSS Grid layout" category = "UI (User Interface)" wasm = true +[[example]] +name = "image_node_rotation" +path = "examples/ui/image_node_rotation.rs" +doc-scrape-examples = true + +[package.metadata.example.image_node_rotation] +name = "Image Node Rotation" +description = "Show the use of `ImageNode::rotation`" +category = "UI (User Interface)" +wasm = true + [[example]] name = "gradients" path = "examples/ui/gradients.rs" diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index 79001f3ba1982..403d64c4b7729 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -102,8 +102,8 @@ pub fn extract_debug_overlay( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform.compute_matrix(), + rotation: 0., flip_x: false, - flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), border_radius: uinode.border_radius(), node_type: NodeType::Border, diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index b908e148e9554..4b7e1de72bd81 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -411,8 +411,8 @@ pub fn extract_gradients( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, + rotation: 0., flip_x: false, - flip_y: false, border_radius: uinode.border_radius, border: uinode.border, node_type, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 8b0d6bad879fe..10a4eb0838f4d 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -235,8 +235,8 @@ pub enum NodeType { pub enum ExtractedUiItem { Node { atlas_scaling: Option, + rotation: f32, flip_x: bool, - flip_y: bool, /// Border radius of the UI node. /// Ordering: top left, top right, bottom right, bottom left. border_radius: ResolvedBorderRadius, @@ -385,8 +385,8 @@ pub fn extract_uinode_background_colors( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform.compute_matrix(), + rotation: 0., flip_x: false, - flip_y: false, border: uinode.border(), border_radius: uinode.border_radius(), node_type: NodeType::Rect, @@ -469,8 +469,8 @@ pub fn extract_uinode_images( item: ExtractedUiItem::Node { atlas_scaling, transform: transform.compute_matrix(), + rotation: image.rotation, flip_x: image.flip_x, - flip_y: image.flip_y, border: uinode.border, border_radius: uinode.border_radius, node_type: NodeType::Rect, @@ -537,8 +537,8 @@ pub fn extract_uinode_borders( item: ExtractedUiItem::Node { atlas_scaling: None, transform: global_transform.compute_matrix(), + rotation: 0., flip_x: false, - flip_y: false, border: computed_node.border(), border_radius: computed_node.border_radius(), node_type: NodeType::Border, @@ -570,8 +570,8 @@ pub fn extract_uinode_borders( item: ExtractedUiItem::Node { transform: global_transform.compute_matrix(), atlas_scaling: None, + rotation: 0., flip_x: false, - flip_y: false, border: BorderRect::all(computed_node.outline_width()), border_radius: computed_node.outline_radius(), node_type: NodeType::Border, @@ -760,8 +760,8 @@ pub fn extract_viewport_nodes( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform.compute_matrix(), + rotation: 0., flip_x: false, - flip_y: false, border: uinode.border(), border_radius: uinode.border_radius(), node_type: NodeType::Rect, @@ -1010,8 +1010,8 @@ pub fn extract_text_background_colors( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform * Mat4::from_translation(rect.center().extend(0.)), + rotation: 0., flip_x: false, - flip_y: false, border: uinode.border(), border_radius: uinode.border_radius(), node_type: NodeType::Rect, @@ -1269,8 +1269,8 @@ pub fn prepare_uinodes( match &extracted_uinode.item { ExtractedUiItem::Node { atlas_scaling, + rotation, flip_x, - flip_y, border_radius, border, node_type, @@ -1287,8 +1287,12 @@ pub fn prepare_uinodes( let rect_size = uinode_rect.size().extend(1.0); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (*transform + * Mat4::from_rotation_z(*rotation) + * (pos * rect_size).extend(1.)) + .xyz() + }); let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); // Calculate the effect of clipping @@ -1365,13 +1369,6 @@ pub fn prepare_uinodes( positions_diff[2].x *= -1.; positions_diff[3].x *= -1.; } - if *flip_y { - core::mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); - positions_diff[0].y *= -1.; - positions_diff[1].y *= -1.; - positions_diff[2].y *= -1.; - positions_diff[3].y *= -1.; - } [ Vec2::new( uinode_rect.min.x + positions_diff[0].x, diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 7d0fdb6a42f0c..93d1d07539da7 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -232,8 +232,8 @@ pub struct ExtractedUiTextureSlice { pub extracted_camera_entity: Entity, pub color: LinearRgba, pub image_scale_mode: SpriteImageMode, + pub rotation: f32, pub flip_x: bool, - pub flip_y: bool, pub inverse_scale_factor: f32, pub main_entity: MainEntity, pub render_entity: Entity, @@ -323,8 +323,8 @@ pub fn extract_ui_texture_slices( extracted_camera_entity, image_scale_mode, atlas_rect, + rotation: image.rotation, flip_x: image.flip_x, - flip_y: image.flip_y, inverse_scale_factor: uinode.inverse_scale_factor, main_entity: entity.into(), }); @@ -506,8 +506,12 @@ pub fn prepare_ui_slices( let rect_size = uinode_rect.size().extend(1.0); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform + * Mat4::from_rotation_z(texture_slices.rotation) + * (pos * rect_size).extend(1.)) + .xyz() + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -609,10 +613,6 @@ pub fn prepare_ui_slices( atlas.swap(0, 2); } - if texture_slices.flip_y { - atlas.swap(1, 3); - } - let [slices, border, repeat] = compute_texture_slices( image_size, uinode_rect.size() * texture_slices.inverse_scale_factor, diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index c65c4df354881..c2ee138977f34 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -25,10 +25,12 @@ pub struct ImageNode { pub image: Handle, /// The (optional) texture atlas used to render the image. pub texture_atlas: Option, + /// Clockwise rotation of the image in radians. + /// Rotations done through this field do not update the node's size and might cause clipping or overflow, + /// to rotate updating the node's size use [`Transform`](bevy_transform::prelude::Transform). + pub rotation: f32, /// 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, /// An optional rectangle representing the region of the image to render, instead of rendering /// the full image. This is an easy one-off alternative to using a [`TextureAtlas`]. /// @@ -55,8 +57,8 @@ impl Default for ImageNode { texture_atlas: None, // This texture needs to be transparent by default, to avoid covering the background color image: TRANSPARENT_IMAGE_HANDLE, + rotation: 0., flip_x: false, - flip_y: false, rect: None, image_mode: NodeImageMode::Auto, } @@ -80,8 +82,8 @@ impl ImageNode { Self { image: Handle::default(), color, + rotation: 0., flip_x: false, - flip_y: false, texture_atlas: None, rect: None, image_mode: NodeImageMode::Auto, @@ -104,17 +106,17 @@ impl ImageNode { self } - /// Flip the image along its x-axis + /// Rotates the image #[must_use] - pub const fn with_flip_x(mut self) -> Self { - self.flip_x = true; + pub const fn with_rotation(mut self, rotation: f32) -> Self { + self.rotation = rotation; self } - /// Flip the image along its y-axis + /// Flip the image along its x-axis #[must_use] - pub const fn with_flip_y(mut self) -> Self { - self.flip_y = true; + pub const fn with_flip_x(mut self) -> Self { + self.flip_x = true; self } diff --git a/examples/README.md b/examples/README.md index a4ff3474dd89b..b5d5d8150971f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -548,6 +548,7 @@ Example | Description [Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) [Ghost Nodes](../examples/ui/ghost_nodes.rs) | Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy [Gradients](../examples/ui/gradients.rs) | An example demonstrating gradients +[Image Node Rotation](../examples/ui/image_node_rotation.rs) | Show the use of `ImageNode::rotation` [Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior [Overflow Clip Margin](../examples/ui/overflow_clip_margin.rs) | Simple example demonstrating the OverflowClipMargin style property [Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index 551785ca0fbad..0c710934b1491 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -1,6 +1,6 @@ //! This example illustrates the various features of Bevy UI. -use std::f32::consts::PI; +use std::f32::consts::{FRAC_PI_2, PI}; use accesskit::{Node as Accessible, Role}; use bevy::{ @@ -375,14 +375,21 @@ fn setup(mut commands: Commands, asset_server: Res) { }) .insert(Pickable::IGNORE) .with_children(|parent| { - for (flip_x, flip_y) in - [(false, false), (false, true), (true, true), (true, false)] - { + for (rotation, flip_x) in [ + (0., false), + (FRAC_PI_2, false), + (PI, false), + (FRAC_PI_2 + PI, false), + (0., true), + (FRAC_PI_2, true), + (PI, true), + (FRAC_PI_2 + PI, true), + ] { parent.spawn(( ImageNode { image: asset_server.load("branding/icon.png"), + rotation, flip_x, - flip_y, ..default() }, Node { diff --git a/examples/ui/image_node_rotation.rs b/examples/ui/image_node_rotation.rs new file mode 100644 index 0000000000000..079a46e30abaa --- /dev/null +++ b/examples/ui/image_node_rotation.rs @@ -0,0 +1,247 @@ +//! Shows usage of `ImageNode::rotation` + +use bevy::{ + app::{App, Startup, Update}, + asset::AssetServer, + color::Color, + core_pipeline::core_2d::Camera2d, + ecs::{ + children, + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut, Single}, + }, + image::{ImageLoaderSettings, ImageSampler}, + input::{keyboard::KeyCode, ButtonInput}, + math::ops, + prelude::SpawnRelated, + sprite::{BorderRect, SliceScaleMode, TextureSlicer}, + state::{ + app::AppExtStates, + state::{NextState, OnEnter, OnExit, State, States}, + }, + time::Time, + ui::{ + widget::{ImageNode, NodeImageMode}, + FlexDirection, JustifyContent, Node, PositionType, Val, + }, + utils::default, + DefaultPlugins, +}; + +const IMAGE_NODE_SIZE: f32 = 128.; +const UI_GAPS: f32 = 16.; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .init_state::() + .add_systems(Startup, setup) + .add_systems(Update, (switch_modes, rotate_image_nodes)) + .add_systems(OnEnter(Mode::Image), image_mode) + .add_systems(OnExit(Mode::Image), drop_previous_ui) + .add_systems(OnEnter(Mode::Slices), slices_mode) + .add_systems(OnExit(Mode::Slices), drop_previous_ui) + .run(); +} + +#[derive(Component)] +struct UiMarker; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, States)] +/// Selection of modes +enum Mode { + /// Uses [`ImageNode`] with [`NodeImageMode::Auto`] + #[default] + Image, + /// Uses [`ImageNode`] with [`NodeImageMode::Sliced`] + Slices, +} + +impl Mode { + fn next(&self) -> Self { + match self { + Self::Image => Self::Slices, + Self::Slices => Self::Image, + } + } +} + +/// Spawns a camera +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); +} + +/// Switching between modes on the press of the Q key +fn switch_modes( + keys: Res>, + current_state: Res>, + mut next_state: ResMut>, +) { + if keys.just_pressed(KeyCode::KeyQ) { + next_state.set(current_state.next()); + } +} + +/// Rock the images back and forth +fn rotate_image_nodes(mut image_nodes: Query<&mut ImageNode>, time: Res