diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 2b180fa7c2dd2..7837d19ac0bfa 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,4 +1,4 @@ -use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack}; +use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiScale, UiStack}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChangesMut, @@ -138,6 +138,7 @@ pub fn ui_focus_system( windows: Query<&Window>, mouse_button_input: Res>, touches_input: Res, + ui_scale: Res, ui_stack: Res, mut node_query: Query, primary_window: Query>, @@ -187,7 +188,10 @@ pub fn ui_focus_system( .ok() .and_then(|window| window.cursor_position()) }) - .or_else(|| touches_input.first_pressed_position()); + .or_else(|| touches_input.first_pressed_position()) + // The cursor position returned by `Window` only takes into account the window scale factor and not `UiScale`. + // To convert the cursor position to logical UI viewport coordinates we have to divide it by `UiScale`. + .map(|cursor_position| cursor_position / ui_scale.scale as f32); // prepare an iterator that contains all the nodes that have the cursor in their rect, // from the top node to the bottom one. this will also reset the interaction to `None` diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index ca38a415003b8..cc4cc9301db91 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -301,7 +301,7 @@ pub fn ui_layout_system( // compute layouts ui_surface.compute_window_layouts(); - let physical_to_logical_factor = 1. / logical_to_physical_factor; + let physical_to_logical_factor = 1. / scale_factor; let to_logical = |v| (physical_to_logical_factor * v as f64) as f32; diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 6a480721ca1eb..4eb26df186957 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -8,11 +8,11 @@ use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; -use crate::UiTextureAtlasImage; use crate::{ - prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack, + prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, ContentSize, Node, + Style, UiImage, UiScale, UiStack, UiTextureAtlasImage, Val, }; -use crate::{ContentSize, Style, Val}; + use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped}; use bevy_ecs::prelude::*; @@ -170,7 +170,6 @@ pub fn extract_atlas_uinodes( mut extracted_uinodes: ResMut, images: Extract>>, texture_atlases: Extract>>, - ui_stack: Extract>, uinode_query: Extract< Query< @@ -258,6 +257,7 @@ fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) pub fn extract_uinode_borders( mut extracted_uinodes: ResMut, windows: Extract>>, + ui_scale: Extract>, ui_stack: Extract>, uinode_query: Extract< Query< @@ -277,10 +277,13 @@ pub fn extract_uinode_borders( ) { let image = bevy_render::texture::DEFAULT_IMAGE_HANDLE.typed(); - let viewport_size = windows + let ui_logical_viewport_size = windows .get_single() .map(|window| Vec2::new(window.resolution.width(), window.resolution.height())) - .unwrap_or(Vec2::ZERO); + .unwrap_or(Vec2::ZERO) + // The logical window resolutin returned by `Window` only takes into account the window scale factor and not `UiScale`, + // so we have to divide by `UiScale` to get the size of the UI viewport. + / ui_scale.scale as f32; for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { if let Ok((node, global_transform, style, border_color, parent, visibility, clip)) = @@ -300,11 +303,21 @@ pub fn extract_uinode_borders( let parent_width = parent .and_then(|parent| parent_node_query.get(parent.get()).ok()) .map(|parent_node| parent_node.size().x) - .unwrap_or(viewport_size.x); - let left = resolve_border_thickness(style.border.left, parent_width, viewport_size); - let right = resolve_border_thickness(style.border.right, parent_width, viewport_size); - let top = resolve_border_thickness(style.border.top, parent_width, viewport_size); - let bottom = resolve_border_thickness(style.border.bottom, parent_width, viewport_size); + .unwrap_or(ui_logical_viewport_size.x); + let left = + resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); + let right = resolve_border_thickness( + style.border.right, + parent_width, + ui_logical_viewport_size, + ); + let top = + resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); + let bottom = resolve_border_thickness( + style.border.bottom, + parent_width, + ui_logical_viewport_size, + ); // Calculate the border rects, ensuring no overlap. // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. @@ -433,8 +446,10 @@ pub struct DefaultCameraView(pub Entity); pub fn extract_default_ui_camera_view( mut commands: Commands, + ui_scale: Extract>, query: Extract), With>>, ) { + let scale = (ui_scale.scale as f32).recip(); for (entity, camera, camera_ui) in &query { // ignore cameras with disabled ui if matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. })) { @@ -446,8 +461,14 @@ pub fn extract_default_ui_camera_view( camera.physical_viewport_size(), ) { // use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection - let projection_matrix = - Mat4::orthographic_rh(0.0, logical_size.x, logical_size.y, 0.0, 0.0, UI_CAMERA_FAR); + let projection_matrix = Mat4::orthographic_rh( + 0.0, + logical_size.x * scale, + logical_size.y * scale, + 0.0, + 0.0, + UI_CAMERA_FAR, + ); let default_camera_view = commands .spawn(ExtractedView { projection: projection_matrix, @@ -481,6 +502,7 @@ pub fn extract_text_uinodes( texture_atlases: Extract>>, windows: Extract>>, ui_stack: Extract>, + ui_scale: Extract>, uinode_query: Extract< Query<( &Node, @@ -495,10 +517,11 @@ pub fn extract_text_uinodes( // TODO: Support window-independent UI scale: https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows .get_single() - .map(|window| window.resolution.scale_factor() as f32) - .unwrap_or(1.0); + .map(|window| window.resolution.scale_factor()) + .unwrap_or(1.0) + * ui_scale.scale; - let inverse_scale_factor = scale_factor.recip(); + let inverse_scale_factor = (scale_factor as f32).recip(); for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { if let Ok((uinode, global_transform, text, text_layout_info, visibility, clip)) = diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 532007ed3b752..c4c128198e840 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -29,12 +29,12 @@ impl Node { self.calculated_size } - /// Returns the size of the node in physical pixels based on the given scale factor. + /// Returns the size of the node in physical pixels based on the given scale factor and `UiScale`. #[inline] - pub fn physical_size(&self, scale_factor: f64) -> Vec2 { + pub fn physical_size(&self, scale_factor: f64, ui_scale: f64) -> Vec2 { Vec2::new( - (self.calculated_size.x as f64 * scale_factor) as f32, - (self.calculated_size.y as f64 * scale_factor) as f32, + (self.calculated_size.x as f64 * scale_factor * ui_scale) as f32, + (self.calculated_size.y as f64 * scale_factor * ui_scale) as f32, ) } @@ -46,16 +46,21 @@ impl Node { /// Returns the physical pixel coordinates of the UI node, based on its [`GlobalTransform`] and the scale factor. #[inline] - pub fn physical_rect(&self, transform: &GlobalTransform, scale_factor: f64) -> Rect { + pub fn physical_rect( + &self, + transform: &GlobalTransform, + scale_factor: f64, + ui_scale: f64, + ) -> Rect { let rect = self.logical_rect(transform); Rect { min: Vec2::new( - (rect.min.x as f64 * scale_factor) as f32, - (rect.min.y as f64 * scale_factor) as f32, + (rect.min.x as f64 * scale_factor * ui_scale) as f32, + (rect.min.y as f64 * scale_factor * ui_scale) as f32, ), max: Vec2::new( - (rect.max.x as f64 * scale_factor) as f32, - (rect.max.y as f64 * scale_factor) as f32, + (rect.max.x as f64 * scale_factor * ui_scale) as f32, + (rect.max.y as f64 * scale_factor * ui_scale) as f32, ), } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 06d876eb7a684..82b47ceb5bd89 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -184,7 +184,8 @@ fn queue_text( // With `NoWrap` set, no constraints are placed on the width of the text. Vec2::splat(f32::INFINITY) } else { - node.physical_size(scale_factor) + // `scale_factor` is already multiplied by `UiScale` + node.physical_size(scale_factor, 1.) }; match text_pipeline.queue_text( diff --git a/examples/ui/viewport_debug.rs b/examples/ui/viewport_debug.rs index 691d70ee6a3d8..bec9809b33ae4 100644 --- a/examples/ui/viewport_debug.rs +++ b/examples/ui/viewport_debug.rs @@ -1,10 +1,14 @@ -//! An example for debugging viewport coordinates - +//! A simple example for debugging viewport coordinates +//! +//! This example creates two uinode trees, one using viewport coordinates and one using pixel coordinates, +//! and then switches between them once per second using the `Display` style property. +//! If there are no problems both layouts should be identical, except for the color of the margin changing which is used to signal that the displayed uinode tree has changed +//! (red for viewport, yellow for pixel). use bevy::prelude::*; const PALETTE: [Color; 10] = [ - Color::ORANGE, - Color::BLUE, + Color::RED, + Color::YELLOW, Color::WHITE, Color::BEIGE, Color::CYAN, @@ -15,7 +19,7 @@ const PALETTE: [Color; 10] = [ Color::BLACK, ]; -#[derive(Default, Debug, Hash, Eq, PartialEq, Clone, States)] +#[derive(Component, Default, PartialEq)] enum Coords { #[default] Viewport, @@ -24,66 +28,66 @@ enum Coords { fn main() { App::new() + .insert_resource(UiScale { scale: 2.0 }) .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { - resolution: [800., 600.].into(), + resolution: [1600., 1200.].into(), title: "Viewport Coordinates Debug".to_string(), resizable: false, ..Default::default() }), ..Default::default() })) - .add_state::() .add_systems(Startup, setup) - .add_systems(OnEnter(Coords::Viewport), spawn_with_viewport_coords) - .add_systems(OnEnter(Coords::Pixel), spawn_with_pixel_coords) - .add_systems(OnExit(Coords::Viewport), despawn_nodes) - .add_systems(OnExit(Coords::Pixel), despawn_nodes) .add_systems(Update, update) .run(); } -fn despawn_nodes(mut commands: Commands, query: Query>) { - for entity in query.iter() { - commands.entity(entity).despawn(); - } -} - fn update( mut timer: Local, + mut visible_tree: Local, time: Res