Change UI coordinate system to have origin at top left corner (#6000)

# Objective
Fixes #5572

## Solution

Approach is to invert the Y-axis of the UI Camera by changing the UI projection matrix to render the UI upside down.

After that I'm trying to fix all issues, that pop up:
- interaction expected the "old" position
- images and text were displayed upside-down
- baseline of text was based on the top of the glyph instead of bottom

... probably a lot more.

---

Result when running examples:
<details>
    <summary>Button example</summary>

main branch:
![button main](https://user-images.githubusercontent.com/4232644/190856087-61dd1d98-42b5-4238-bd97-149744ddfeba.png)
this pr:
![button pr](https://user-images.githubusercontent.com/4232644/190856097-3f4bc97a-ed15-4e97-b7f1-2b2dd6bb8b14.png)

</details>

<details>
    <summary>Text example</summary>

m
![text main](https://user-images.githubusercontent.com/4232644/192142831-4cf19aa1-f49a-485e-af7b-374d6f5c396c.png)
ain branch: 


this pr:
![text pr fixed](https://user-images.githubusercontent.com/4232644/192142829-c433db3b-32e1-4ee8-b493-0b4a4d9c8e70.png)


</details>

<details>
    <summary>Text debug example</summary>

main branch:
![text_debug main](https://user-images.githubusercontent.com/4232644/192142822-940aefa6-e502-410b-8da4-5570f77b5df2.png)

this pr:
![text_debug pr fixed](https://user-images.githubusercontent.com/4232644/194547010-8c968f5c-5a71-4ffc-871d-790c06d48016.png)

</details>

<details>
    <summary>Transparency UI example</summary>

main branch:
![transparency_ui main](https://user-images.githubusercontent.com/4232644/190856172-328c60fe-3622-4598-97d5-2f1595db13b3.png)


this pr:
![transperency_ui pr](https://user-images.githubusercontent.com/4232644/190856179-a2dafb99-41ea-45a9-9dd6-400fa3ef24b9.png)

</details>

<details>
    <summary>UI example</summary>

**ui example**
main branch:
![ui main](https://user-images.githubusercontent.com/4232644/192142812-e20ba31a-6841-46d9-a785-4198cf22dc99.png)

this pr:
![ui pr fixed](https://user-images.githubusercontent.com/4232644/192142788-cc0b74e0-7710-4faa-b5a2-60270a5da77c.png)

</details>

## Changelog
UI coordinate system and cursor position was changed from bottom left origin, y+ up to top left origin, y+ down.

## Migration Guide
All flex layout should be inverted (ColumnReverse => Column, FlexStart => FlexEnd, WrapReverse => Wrap)
System where dealing with cursor position should be changed to account for cursor position being based on the top left instead of bottom left
This commit is contained in:
Michel van der Hulst 2022-10-11 12:51:44 +00:00
parent 13dcdba05f
commit 6ce7ce208e
11 changed files with 62 additions and 70 deletions

View file

@ -73,16 +73,17 @@ impl GlyphBrush {
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let mut max_y = std::f32::MIN;
let mut min_x = std::f32::MAX; let mut min_x = std::f32::MAX;
let mut min_y = std::f32::MAX;
for sg in &glyphs { for sg in &glyphs {
let glyph = &sg.glyph; let glyph = &sg.glyph;
let scaled_font = sections_data[sg.section_index].3; let scaled_font = sections_data[sg.section_index].3;
max_y = max_y.max(glyph.position.y - scaled_font.descent());
min_x = min_x.min(glyph.position.x); min_x = min_x.min(glyph.position.x);
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
} }
max_y = max_y.floor();
min_x = min_x.floor(); min_x = min_x.floor();
min_y = min_y.floor();
let mut positioned_glyphs = Vec::new(); let mut positioned_glyphs = Vec::new();
for sg in glyphs { for sg in glyphs {
@ -119,7 +120,7 @@ impl GlyphBrush {
let size = Vec2::new(glyph_rect.width(), glyph_rect.height()); let size = Vec2::new(glyph_rect.width(), glyph_rect.height());
let x = bounds.min.x + size.x / 2.0 - min_x; let x = bounds.min.x + size.x / 2.0 - min_x;
let y = max_y - bounds.max.y + size.y / 2.0; let y = bounds.min.y + size.y / 2.0 - min_y;
let position = adjust.position(Vec2::new(x, y)); let position = adjust.position(Vec2::new(x, y));
positioned_glyphs.push(PositionedGlyph { positioned_glyphs.push(PositionedGlyph {

View file

@ -10,9 +10,8 @@ pub fn from_rect(
taffy::geometry::Rect { taffy::geometry::Rect {
start: from_val(scale_factor, rect.left), start: from_val(scale_factor, rect.left),
end: from_val(scale_factor, rect.right), end: from_val(scale_factor, rect.right),
// NOTE: top and bottom are intentionally flipped. stretch has a flipped y-axis top: from_val(scale_factor, rect.top),
top: from_val(scale_factor, rect.bottom), bottom: from_val(scale_factor, rect.bottom),
bottom: from_val(scale_factor, rect.top),
} }
} }

View file

@ -1,7 +1,7 @@
//! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games //! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games
//! # Basic usage //! # Basic usage
//! Spawn UI elements with [`entity::ButtonBundle`], [`entity::ImageBundle`], [`entity::TextBundle`] and [`entity::NodeBundle`] //! Spawn UI elements with [`entity::ButtonBundle`], [`entity::ImageBundle`], [`entity::TextBundle`] and [`entity::NodeBundle`]
//! This UI is laid out with the Flexbox paradigm (see <https://cssreference.io/flexbox/> ) except the vertical axis is inverted //! This UI is laid out with the Flexbox paradigm (see <https://cssreference.io/flexbox/>)
mod flex; mod flex;
mod focus; mod focus;
mod geometry; mod geometry;

View file

@ -12,7 +12,7 @@ use bevy_ecs::prelude::*;
use bevy_math::{Mat4, Rect, UVec4, Vec2, Vec3, Vec4Swizzles}; use bevy_math::{Mat4, Rect, UVec4, Vec2, Vec3, Vec4Swizzles};
use bevy_reflect::TypeUuid; use bevy_reflect::TypeUuid;
use bevy_render::{ use bevy_render::{
camera::{Camera, CameraProjection, OrthographicProjection, WindowOrigin}, camera::Camera,
color::Color, color::Color,
render_asset::RenderAssets, render_asset::RenderAssets,
render_graph::{RenderGraph, RunGraphOnViewNode, SlotInfo, SlotType}, render_graph::{RenderGraph, RunGraphOnViewNode, SlotInfo, SlotType},
@ -243,15 +243,12 @@ pub fn extract_default_ui_camera_view<T: Component>(
camera.physical_viewport_rect(), camera.physical_viewport_rect(),
camera.physical_viewport_size(), camera.physical_viewport_size(),
) { ) {
let mut projection = OrthographicProjection { // use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection
far: UI_CAMERA_FAR, let projection_matrix =
window_origin: WindowOrigin::BottomLeft, Mat4::orthographic_rh(0.0, logical_size.x, logical_size.y, 0.0, 0.0, UI_CAMERA_FAR);
..Default::default()
};
projection.update(logical_size.x, logical_size.y);
let default_camera_view = commands let default_camera_view = commands
.spawn(ExtractedView { .spawn(ExtractedView {
projection: projection.get_projection_matrix(), projection: projection_matrix,
transform: GlobalTransform::from_xyz( transform: GlobalTransform::from_xyz(
0.0, 0.0,
0.0, 0.0,
@ -464,24 +461,23 @@ pub fn prepare_uinodes(
} }
} }
// Clip UVs (Note: y is reversed in UV space)
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
let uvs = [ let uvs = [
Vec2::new( Vec2::new(
uinode_rect.min.x + positions_diff[0].x, uinode_rect.min.x + positions_diff[3].x,
uinode_rect.max.y - positions_diff[0].y, uinode_rect.min.y - positions_diff[3].y,
),
Vec2::new(
uinode_rect.max.x + positions_diff[1].x,
uinode_rect.max.y - positions_diff[1].y,
), ),
Vec2::new( Vec2::new(
uinode_rect.max.x + positions_diff[2].x, uinode_rect.max.x + positions_diff[2].x,
uinode_rect.min.y - positions_diff[2].y, uinode_rect.min.y - positions_diff[2].y,
), ),
Vec2::new( Vec2::new(
uinode_rect.min.x + positions_diff[3].x, uinode_rect.max.x + positions_diff[1].x,
uinode_rect.min.y - positions_diff[3].y, uinode_rect.max.y - positions_diff[1].y,
),
Vec2::new(
uinode_rect.min.x + positions_diff[0].x,
uinode_rect.max.y - positions_diff[0].y,
), ),
] ]
.map(|pos| pos / atlas_extent); .map(|pos| pos / atlas_extent);

View file

@ -303,11 +303,11 @@ pub enum FlexDirection {
/// Same way as text direction along the main axis /// Same way as text direction along the main axis
#[default] #[default]
Row, Row,
/// Flex from bottom to top /// Flex from top to bottom
Column, Column,
/// Opposite way as text direction along the main axis /// Opposite way as text direction along the main axis
RowReverse, RowReverse,
/// Flex from top to bottom /// Flex from bottom to top
ColumnReverse, ColumnReverse,
} }

View file

@ -149,12 +149,9 @@ fn change_window(
} }
bevy_window::WindowCommand::SetCursorPosition { position } => { bevy_window::WindowCommand::SetCursorPosition { position } => {
let window = winit_windows.get_window(id).unwrap(); let window = winit_windows.get_window(id).unwrap();
let inner_size = window.inner_size().to_logical::<f32>(window.scale_factor());
window window
.set_cursor_position(LogicalPosition::new( .set_cursor_position(LogicalPosition::new(position.x, position.y))
position.x,
inner_size.height - position.y,
))
.unwrap_or_else(|e| error!("Unable to set cursor position: {}", e)); .unwrap_or_else(|e| error!("Unable to set cursor position: {}", e));
} }
bevy_window::WindowCommand::SetMaximized { maximized } => { bevy_window::WindowCommand::SetMaximized { maximized } => {
@ -431,13 +428,8 @@ pub fn winit_runner_with(mut app: App) {
} }
WindowEvent::CursorMoved { position, .. } => { WindowEvent::CursorMoved { position, .. } => {
let mut cursor_moved_events = world.resource_mut::<Events<CursorMoved>>(); let mut cursor_moved_events = world.resource_mut::<Events<CursorMoved>>();
let winit_window = winit_windows.get_window(window_id).unwrap();
let inner_size = winit_window.inner_size();
// move origin to bottom left let physical_position = DVec2::new(position.x, position.y);
let y_position = inner_size.height as f64 - position.y;
let physical_position = DVec2::new(position.x, y_position);
window window
.update_cursor_physical_position_from_backend(Some(physical_position)); .update_cursor_physical_position_from_backend(Some(physical_position));

View file

@ -152,9 +152,8 @@ mod game {
style: Style { style: Style {
// This will center the current node // This will center the current node
margin: UiRect::all(Val::Auto), margin: UiRect::all(Val::Auto),
// This will display its children in a column, from top to bottom. Unlike // This will display its children in a column, from top to bottom
// in Flexbox, Bevy origin is on bottom left, so the vertical axis is reversed flex_direction: FlexDirection::Column,
flex_direction: FlexDirection::ColumnReverse,
// `align_items` will align children on the cross axis. Here the main axis is // `align_items` will align children on the cross axis. Here the main axis is
// vertical (column), so the cross axis is horizontal. This will center the // vertical (column), so the cross axis is horizontal. This will center the
// children // children
@ -420,7 +419,7 @@ mod menu {
NodeBundle { NodeBundle {
style: Style { style: Style {
margin: UiRect::all(Val::Auto), margin: UiRect::all(Val::Auto),
flex_direction: FlexDirection::ColumnReverse, flex_direction: FlexDirection::Column,
align_items: AlignItems::Center, align_items: AlignItems::Center,
..default() ..default()
}, },
@ -533,7 +532,7 @@ mod menu {
NodeBundle { NodeBundle {
style: Style { style: Style {
margin: UiRect::all(Val::Auto), margin: UiRect::all(Val::Auto),
flex_direction: FlexDirection::ColumnReverse, flex_direction: FlexDirection::Column,
align_items: AlignItems::Center, align_items: AlignItems::Center,
..default() ..default()
}, },
@ -587,7 +586,7 @@ mod menu {
NodeBundle { NodeBundle {
style: Style { style: Style {
margin: UiRect::all(Val::Auto), margin: UiRect::all(Val::Auto),
flex_direction: FlexDirection::ColumnReverse, flex_direction: FlexDirection::Column,
align_items: AlignItems::Center, align_items: AlignItems::Center,
..default() ..default()
}, },
@ -678,7 +677,7 @@ mod menu {
NodeBundle { NodeBundle {
style: Style { style: Style {
margin: UiRect::all(Val::Auto), margin: UiRect::all(Val::Auto),
flex_direction: FlexDirection::ColumnReverse, flex_direction: FlexDirection::Column,
align_items: AlignItems::Center, align_items: AlignItems::Center,
..default() ..default()
}, },

View file

@ -82,12 +82,27 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut state: ResM
let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf"); let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");
state.handle = font_handle.clone(); state.handle = font_handle.clone();
commands.spawn(Camera2dBundle::default()); commands.spawn(Camera2dBundle::default());
commands.spawn(TextBundle::from_section( commands
"a", .spawn(NodeBundle {
TextStyle { background_color: Color::NONE.into(),
font: font_handle, style: Style {
font_size: 60.0, position_type: PositionType::Absolute,
color: Color::YELLOW, position: UiRect {
}, bottom: Val::Px(0.0),
)); ..default()
},
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"a",
TextStyle {
font: font_handle,
font_size: 60.0,
color: Color::YELLOW,
},
));
});
} }

View file

@ -44,7 +44,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.with_text_alignment(TextAlignment::TOP_CENTER) .with_text_alignment(TextAlignment::TOP_CENTER)
// Set the style of the TextBundle itself. // Set the style of the TextBundle itself.
.with_style(Style { .with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
position: UiRect { position: UiRect {
bottom: Val::Px(5.0), bottom: Val::Px(5.0),
@ -72,11 +71,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
font_size: 60.0, font_size: 60.0,
color: Color::GOLD, color: Color::GOLD,
}), }),
]) ]),
.with_style(Style {
align_self: AlignSelf::FlexEnd,
..default()
}),
FpsText, FpsText,
)); ));
} }

View file

@ -35,7 +35,6 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
}, },
) )
.with_style(Style { .with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
position: UiRect { position: UiRect {
top: Val::Px(5.0), top: Val::Px(5.0),
@ -55,7 +54,6 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
) )
.with_text_alignment(TextAlignment::CENTER) .with_text_alignment(TextAlignment::CENTER)
.with_style(Style { .with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
position: UiRect { position: UiRect {
top: Val::Px(5.0), top: Val::Px(5.0),
@ -115,7 +113,6 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
), ),
]) ])
.with_style(Style { .with_style(Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
position: UiRect { position: UiRect {
bottom: Val::Px(5.0), bottom: Val::Px(5.0),

View file

@ -48,7 +48,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn(NodeBundle { .spawn(NodeBundle {
style: Style { style: Style {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
align_items: AlignItems::FlexEnd,
..default() ..default()
}, },
background_color: Color::rgb(0.15, 0.15, 0.15).into(), background_color: Color::rgb(0.15, 0.15, 0.15).into(),
@ -76,7 +75,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
parent parent
.spawn(NodeBundle { .spawn(NodeBundle {
style: Style { style: Style {
flex_direction: FlexDirection::ColumnReverse, flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
size: Size::new(Val::Px(200.0), Val::Percent(100.0)), size: Size::new(Val::Px(200.0), Val::Percent(100.0)),
..default() ..default()
@ -109,7 +108,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
parent parent
.spawn(NodeBundle { .spawn(NodeBundle {
style: Style { style: Style {
flex_direction: FlexDirection::ColumnReverse, flex_direction: FlexDirection::Column,
align_self: AlignSelf::Center, align_self: AlignSelf::Center,
size: Size::new(Val::Percent(100.0), Val::Percent(50.0)), size: Size::new(Val::Percent(100.0), Val::Percent(50.0)),
overflow: Overflow::Hidden, overflow: Overflow::Hidden,
@ -124,7 +123,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn(( .spawn((
NodeBundle { NodeBundle {
style: Style { style: Style {
flex_direction: FlexDirection::ColumnReverse, flex_direction: FlexDirection::Column,
flex_grow: 1.0, flex_grow: 1.0,
max_size: Size::UNDEFINED, max_size: Size::UNDEFINED,
..default() ..default()
@ -161,7 +160,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
}); });
}); });
}); });
// absolute positioning
parent parent
.spawn(NodeBundle { .spawn(NodeBundle {
style: Style { style: Style {
@ -277,7 +275,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd, align_items: AlignItems::FlexStart,
..default() ..default()
}, },
..default() ..default()
@ -318,8 +316,8 @@ fn mouse_scroll(
MouseScrollUnit::Line => mouse_wheel_event.y * 20., MouseScrollUnit::Line => mouse_wheel_event.y * 20.,
MouseScrollUnit::Pixel => mouse_wheel_event.y, MouseScrollUnit::Pixel => mouse_wheel_event.y,
}; };
scrolling_list.position += dy; scrolling_list.position -= dy;
scrolling_list.position = scrolling_list.position.clamp(-max_scroll, 0.); scrolling_list.position = scrolling_list.position.clamp(0., max_scroll);
style.position.top = Val::Px(scrolling_list.position); style.position.top = Val::Px(scrolling_list.position);
} }
} }