
# 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:  this pr:  </details> <details> <summary>Text example</summary> m  ain branch: this pr:  </details> <details> <summary>Text debug example</summary> main branch:  this pr:  </details> <details> <summary>Transparency UI example</summary> main branch:  this pr:  </details> <details> <summary>UI example</summary> **ui example** main branch:  this pr:  </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
325 lines
14 KiB
Rust
325 lines
14 KiB
Rust
//! This example illustrates the various features of Bevy UI.
|
|
|
|
use bevy::{
|
|
input::mouse::{MouseScrollUnit, MouseWheel},
|
|
prelude::*,
|
|
winit::WinitSettings,
|
|
};
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
|
.insert_resource(WinitSettings::desktop_app())
|
|
.add_startup_system(setup)
|
|
.add_system(mouse_scroll)
|
|
.run();
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
// Camera
|
|
commands.spawn(Camera2dBundle::default());
|
|
|
|
// root node
|
|
commands
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
|
|
justify_content: JustifyContent::SpaceBetween,
|
|
..default()
|
|
},
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
// left vertical fill (border)
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(200.0), Val::Percent(100.0)),
|
|
border: UiRect::all(Val::Px(2.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(0.65, 0.65, 0.65).into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
// left vertical fill (content)
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(0.15, 0.15, 0.15).into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
// text
|
|
parent.spawn(
|
|
TextBundle::from_section(
|
|
"Text Example",
|
|
TextStyle {
|
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
|
font_size: 30.0,
|
|
color: Color::WHITE,
|
|
},
|
|
)
|
|
.with_style(Style {
|
|
margin: UiRect::all(Val::Px(5.0)),
|
|
..default()
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
// right vertical fill
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
flex_direction: FlexDirection::Column,
|
|
justify_content: JustifyContent::Center,
|
|
size: Size::new(Val::Px(200.0), Val::Percent(100.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(0.15, 0.15, 0.15).into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
// Title
|
|
parent.spawn(
|
|
TextBundle::from_section(
|
|
"Scrolling list",
|
|
TextStyle {
|
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
|
font_size: 25.,
|
|
color: Color::WHITE,
|
|
},
|
|
)
|
|
.with_style(Style {
|
|
size: Size::new(Val::Undefined, Val::Px(25.)),
|
|
margin: UiRect {
|
|
left: Val::Auto,
|
|
right: Val::Auto,
|
|
..default()
|
|
},
|
|
..default()
|
|
}),
|
|
);
|
|
// List with hidden overflow
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
flex_direction: FlexDirection::Column,
|
|
align_self: AlignSelf::Center,
|
|
size: Size::new(Val::Percent(100.0), Val::Percent(50.0)),
|
|
overflow: Overflow::Hidden,
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(0.10, 0.10, 0.10).into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
// Moving panel
|
|
parent
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
flex_direction: FlexDirection::Column,
|
|
flex_grow: 1.0,
|
|
max_size: Size::UNDEFINED,
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
ScrollingList::default(),
|
|
))
|
|
.with_children(|parent| {
|
|
// List items
|
|
for i in 0..30 {
|
|
parent.spawn(
|
|
TextBundle::from_section(
|
|
format!("Item {i}"),
|
|
TextStyle {
|
|
font: asset_server
|
|
.load("fonts/FiraSans-Bold.ttf"),
|
|
font_size: 20.,
|
|
color: Color::WHITE,
|
|
},
|
|
)
|
|
.with_style(Style {
|
|
flex_shrink: 0.,
|
|
size: Size::new(Val::Undefined, Val::Px(20.)),
|
|
margin: UiRect {
|
|
left: Val::Auto,
|
|
right: Val::Auto,
|
|
..default()
|
|
},
|
|
..default()
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(200.0), Val::Px(200.0)),
|
|
position_type: PositionType::Absolute,
|
|
position: UiRect {
|
|
left: Val::Px(210.0),
|
|
bottom: Val::Px(10.0),
|
|
..default()
|
|
},
|
|
border: UiRect::all(Val::Px(20.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(0.4, 0.4, 1.0).into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(0.8, 0.8, 1.0).into(),
|
|
..default()
|
|
});
|
|
});
|
|
// render order test: reddest in the back, whitest in the front (flex center)
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
|
|
position_type: PositionType::Absolute,
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(1.0, 0.0, 0.0).into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
|
|
position_type: PositionType::Absolute,
|
|
position: UiRect {
|
|
left: Val::Px(20.0),
|
|
bottom: Val::Px(20.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(1.0, 0.3, 0.3).into(),
|
|
..default()
|
|
});
|
|
parent.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
|
|
position_type: PositionType::Absolute,
|
|
position: UiRect {
|
|
left: Val::Px(40.0),
|
|
bottom: Val::Px(40.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(1.0, 0.5, 0.5).into(),
|
|
..default()
|
|
});
|
|
parent.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
|
|
position_type: PositionType::Absolute,
|
|
position: UiRect {
|
|
left: Val::Px(60.0),
|
|
bottom: Val::Px(60.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
background_color: Color::rgb(1.0, 0.7, 0.7).into(),
|
|
..default()
|
|
});
|
|
// alpha test
|
|
parent.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
|
|
position_type: PositionType::Absolute,
|
|
position: UiRect {
|
|
left: Val::Px(80.0),
|
|
bottom: Val::Px(80.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
background_color: Color::rgba(1.0, 0.9, 0.9, 0.4).into(),
|
|
..default()
|
|
});
|
|
});
|
|
});
|
|
// bevy logo (flex center)
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
|
|
position_type: PositionType::Absolute,
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::FlexStart,
|
|
..default()
|
|
},
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
// bevy logo (image)
|
|
parent.spawn(ImageBundle {
|
|
style: Style {
|
|
size: Size::new(Val::Px(500.0), Val::Auto),
|
|
..default()
|
|
},
|
|
image: asset_server.load("branding/bevy_logo_dark_big.png").into(),
|
|
..default()
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
#[derive(Component, Default)]
|
|
struct ScrollingList {
|
|
position: f32,
|
|
}
|
|
|
|
fn mouse_scroll(
|
|
mut mouse_wheel_events: EventReader<MouseWheel>,
|
|
mut query_list: Query<(&mut ScrollingList, &mut Style, &Children, &Node)>,
|
|
query_item: Query<&Node>,
|
|
) {
|
|
for mouse_wheel_event in mouse_wheel_events.iter() {
|
|
for (mut scrolling_list, mut style, children, uinode) in &mut query_list {
|
|
let items_height: f32 = children
|
|
.iter()
|
|
.map(|entity| query_item.get(*entity).unwrap().size.y)
|
|
.sum();
|
|
let panel_height = uinode.size.y;
|
|
let max_scroll = (items_height - panel_height).max(0.);
|
|
let dy = match mouse_wheel_event.unit {
|
|
MouseScrollUnit::Line => mouse_wheel_event.y * 20.,
|
|
MouseScrollUnit::Pixel => mouse_wheel_event.y,
|
|
};
|
|
scrolling_list.position -= dy;
|
|
scrolling_list.position = scrolling_list.position.clamp(0., max_scroll);
|
|
style.position.top = Val::Px(scrolling_list.position);
|
|
}
|
|
}
|
|
}
|