Add CSS Grid support to bevy_ui (#8026)

# Objective

An easy way to create 2D grid layouts

## Solution

Enable the `grid` feature in Taffy and add new style types for defining
grids.

## Notes

- ~I'm having a bit of trouble getting `#[derive(Reflect)]` to work
properly. Help with that would be appreciated (EDIT: got it to compile
by ignoring the problematic fields, but this presumably can't be
merged).~ This is now fixed
- ~The alignment types now have a `Normal` variant because I couldn't
get reflect to work with `Option`.~ I've decided to stick with the
flattened variant, as it saves a level of wrapping when authoring
styles. But I've renamed the variants from `Normal` to `Default`.
- ~This currently exposes a simplified API on top of grid. In particular
the following is not currently supported:~
   - ~Negative grid indices~ Now supported.
- ~Custom `end` values for grid placement (you can only use `start` and
`span`)~ Now supported
- ~`minmax()` track sizing functions~ minmax is now support through a
`GridTrack::minmax()` constructor
   - ~`repeat()`~ repeat is now implemented as `RepeatedGridTrack`

- ~Documentation still needs to be improved.~ An initial pass over the
documentation has been completed.

## Screenshot

<img width="846" alt="Screenshot 2023-03-10 at 17 56 21"
src="https://user-images.githubusercontent.com/1007307/224435332-69aa9eac-123d-4856-b75d-5449d3f1d426.png">

---

## Changelog

- Support for CSS Grid layout added to `bevy_ui`

---------

Co-authored-by: Rob Parrett <robparrett@gmail.com>
Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com>
This commit is contained in:
Nico Burns 2023-04-17 17:21:38 +01:00 committed by GitHub
parent cfa750a741
commit 363d0f0c7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1415 additions and 171 deletions

View file

@ -1806,7 +1806,17 @@ path = "examples/ui/flex_layout.rs"
name = "Flex Layout"
description = "Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text"
category = "UI (User Interface)"
wasm = false
wasm = true
[[example]]
name = "grid"
path = "examples/ui/grid.rs"
[package.metadata.example.grid]
name = "CSS Grid"
description = "An example for CSS Grid layout"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "transparency_ui"

View file

@ -31,7 +31,7 @@ bevy_window = { path = "../bevy_window", version = "0.11.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
# other
taffy = { version = "0.3.10", default-features = false, features = ["std"] }
taffy = { version = "0.3.10" }
serde = { version = "1", features = ["derive"] }
smallvec = { version = "1.6", features = ["union", "const_generics"] }
bytemuck = { version = "1.5", features = ["derive"] }

View file

@ -1,6 +1,10 @@
use taffy::style_helpers;
use crate::{
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
PositionType, Size, Style, UiRect, Val,
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, GridAutoFlow,
GridPlacement, GridTrack, GridTrackRepetition, JustifyContent, JustifyItems, JustifySelf,
MaxTrackSizingFunction, MinTrackSizingFunction, PositionType, RepeatedGridTrack, Size, Style,
UiRect, Val,
};
use super::LayoutContext;
@ -74,10 +78,12 @@ pub fn from_style(context: &LayoutContext, style: &Style) -> taffy::style::Style
position: style.position_type.into(),
flex_direction: style.flex_direction.into(),
flex_wrap: style.flex_wrap.into(),
align_items: Some(style.align_items.into()),
align_items: style.align_items.into(),
justify_items: style.justify_items.into(),
align_self: style.align_self.into(),
align_content: Some(style.align_content.into()),
justify_content: Some(style.justify_content.into()),
justify_self: style.justify_self.into(),
align_content: style.align_content.into(),
justify_content: style.justify_content.into(),
inset: taffy::prelude::Rect {
left: style.left.into_length_percentage_auto(context),
right: style.right.into_length_percentage_auto(context),
@ -107,20 +113,56 @@ pub fn from_style(context: &LayoutContext, style: &Style) -> taffy::style::Style
gap: style
.gap
.map_to_taffy_size(|s| s.into_length_percentage(context)),
justify_self: None,
grid_auto_flow: style.grid_auto_flow.into(),
grid_template_rows: style
.grid_template_rows
.iter()
.map(|track| track.clone_into_repeated_taffy_track(context))
.collect::<Vec<_>>(),
grid_template_columns: style
.grid_template_columns
.iter()
.map(|track| track.clone_into_repeated_taffy_track(context))
.collect::<Vec<_>>(),
grid_auto_rows: style
.grid_auto_rows
.iter()
.map(|track| track.into_taffy_track(context))
.collect::<Vec<_>>(),
grid_auto_columns: style
.grid_auto_columns
.iter()
.map(|track| track.into_taffy_track(context))
.collect::<Vec<_>>(),
grid_row: style.grid_row.into(),
grid_column: style.grid_column.into(),
}
}
impl From<AlignItems> for taffy::style::AlignItems {
impl From<AlignItems> for Option<taffy::style::AlignItems> {
fn from(value: AlignItems) -> Self {
match value {
AlignItems::Start => taffy::style::AlignItems::Start,
AlignItems::End => taffy::style::AlignItems::End,
AlignItems::FlexStart => taffy::style::AlignItems::FlexStart,
AlignItems::FlexEnd => taffy::style::AlignItems::FlexEnd,
AlignItems::Center => taffy::style::AlignItems::Center,
AlignItems::Baseline => taffy::style::AlignItems::Baseline,
AlignItems::Stretch => taffy::style::AlignItems::Stretch,
AlignItems::Default => None,
AlignItems::Start => taffy::style::AlignItems::Start.into(),
AlignItems::End => taffy::style::AlignItems::End.into(),
AlignItems::FlexStart => taffy::style::AlignItems::FlexStart.into(),
AlignItems::FlexEnd => taffy::style::AlignItems::FlexEnd.into(),
AlignItems::Center => taffy::style::AlignItems::Center.into(),
AlignItems::Baseline => taffy::style::AlignItems::Baseline.into(),
AlignItems::Stretch => taffy::style::AlignItems::Stretch.into(),
}
}
}
impl From<JustifyItems> for Option<taffy::style::JustifyItems> {
fn from(value: JustifyItems) -> Self {
match value {
JustifyItems::Default => None,
JustifyItems::Start => taffy::style::JustifyItems::Start.into(),
JustifyItems::End => taffy::style::JustifyItems::End.into(),
JustifyItems::Center => taffy::style::JustifyItems::Center.into(),
JustifyItems::Baseline => taffy::style::JustifyItems::Baseline.into(),
JustifyItems::Stretch => taffy::style::JustifyItems::Stretch.into(),
}
}
}
@ -140,18 +182,48 @@ impl From<AlignSelf> for Option<taffy::style::AlignSelf> {
}
}
impl From<AlignContent> for taffy::style::AlignContent {
impl From<JustifySelf> for Option<taffy::style::JustifySelf> {
fn from(value: JustifySelf) -> Self {
match value {
JustifySelf::Auto => None,
JustifySelf::Start => taffy::style::JustifySelf::Start.into(),
JustifySelf::End => taffy::style::JustifySelf::End.into(),
JustifySelf::Center => taffy::style::JustifySelf::Center.into(),
JustifySelf::Baseline => taffy::style::JustifySelf::Baseline.into(),
JustifySelf::Stretch => taffy::style::JustifySelf::Stretch.into(),
}
}
}
impl From<AlignContent> for Option<taffy::style::AlignContent> {
fn from(value: AlignContent) -> Self {
match value {
AlignContent::Start => taffy::style::AlignContent::Start,
AlignContent::End => taffy::style::AlignContent::End,
AlignContent::FlexStart => taffy::style::AlignContent::FlexStart,
AlignContent::FlexEnd => taffy::style::AlignContent::FlexEnd,
AlignContent::Center => taffy::style::AlignContent::Center,
AlignContent::Stretch => taffy::style::AlignContent::Stretch,
AlignContent::SpaceBetween => taffy::style::AlignContent::SpaceBetween,
AlignContent::SpaceAround => taffy::style::AlignContent::SpaceAround,
AlignContent::SpaceEvenly => taffy::style::AlignContent::SpaceEvenly,
AlignContent::Default => None,
AlignContent::Start => taffy::style::AlignContent::Start.into(),
AlignContent::End => taffy::style::AlignContent::End.into(),
AlignContent::FlexStart => taffy::style::AlignContent::FlexStart.into(),
AlignContent::FlexEnd => taffy::style::AlignContent::FlexEnd.into(),
AlignContent::Center => taffy::style::AlignContent::Center.into(),
AlignContent::Stretch => taffy::style::AlignContent::Stretch.into(),
AlignContent::SpaceBetween => taffy::style::AlignContent::SpaceBetween.into(),
AlignContent::SpaceAround => taffy::style::AlignContent::SpaceAround.into(),
AlignContent::SpaceEvenly => taffy::style::AlignContent::SpaceEvenly.into(),
}
}
}
impl From<JustifyContent> for Option<taffy::style::JustifyContent> {
fn from(value: JustifyContent) -> Self {
match value {
JustifyContent::Default => None,
JustifyContent::Start => taffy::style::JustifyContent::Start.into(),
JustifyContent::End => taffy::style::JustifyContent::End.into(),
JustifyContent::FlexStart => taffy::style::JustifyContent::FlexStart.into(),
JustifyContent::FlexEnd => taffy::style::JustifyContent::FlexEnd.into(),
JustifyContent::Center => taffy::style::JustifyContent::Center.into(),
JustifyContent::SpaceBetween => taffy::style::JustifyContent::SpaceBetween.into(),
JustifyContent::SpaceAround => taffy::style::JustifyContent::SpaceAround.into(),
JustifyContent::SpaceEvenly => taffy::style::JustifyContent::SpaceEvenly.into(),
}
}
}
@ -160,6 +232,7 @@ impl From<Display> for taffy::style::Display {
fn from(value: Display) -> Self {
match value {
Display::Flex => taffy::style::Display::Flex,
Display::Grid => taffy::style::Display::Grid,
Display::None => taffy::style::Display::None,
}
}
@ -176,21 +249,6 @@ impl From<FlexDirection> for taffy::style::FlexDirection {
}
}
impl From<JustifyContent> for taffy::style::JustifyContent {
fn from(value: JustifyContent) -> Self {
match value {
JustifyContent::Start => taffy::style::JustifyContent::Start,
JustifyContent::End => taffy::style::JustifyContent::End,
JustifyContent::FlexStart => taffy::style::JustifyContent::FlexStart,
JustifyContent::FlexEnd => taffy::style::JustifyContent::FlexEnd,
JustifyContent::Center => taffy::style::JustifyContent::Center,
JustifyContent::SpaceBetween => taffy::style::JustifyContent::SpaceBetween,
JustifyContent::SpaceAround => taffy::style::JustifyContent::SpaceAround,
JustifyContent::SpaceEvenly => taffy::style::JustifyContent::SpaceEvenly,
}
}
}
impl From<PositionType> for taffy::style::Position {
fn from(value: PositionType) -> Self {
match value {
@ -210,12 +268,140 @@ impl From<FlexWrap> for taffy::style::FlexWrap {
}
}
impl From<GridAutoFlow> for taffy::style::GridAutoFlow {
fn from(value: GridAutoFlow) -> Self {
match value {
GridAutoFlow::Row => taffy::style::GridAutoFlow::Row,
GridAutoFlow::RowDense => taffy::style::GridAutoFlow::RowDense,
GridAutoFlow::Column => taffy::style::GridAutoFlow::Column,
GridAutoFlow::ColumnDense => taffy::style::GridAutoFlow::ColumnDense,
}
}
}
impl From<GridPlacement> for taffy::geometry::Line<taffy::style::GridPlacement> {
fn from(value: GridPlacement) -> Self {
let span = value.span.unwrap_or(1).max(1);
match (value.start, value.end) {
(Some(start), Some(end)) => taffy::geometry::Line {
start: style_helpers::line(start),
end: style_helpers::line(end),
},
(Some(start), None) => taffy::geometry::Line {
start: style_helpers::line(start),
end: style_helpers::span(span),
},
(None, Some(end)) => taffy::geometry::Line {
start: style_helpers::span(span),
end: style_helpers::line(end),
},
(None, None) => style_helpers::span(span),
}
}
}
impl MinTrackSizingFunction {
fn into_taffy(self, context: &LayoutContext) -> taffy::style::MinTrackSizingFunction {
match self {
MinTrackSizingFunction::Px(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Px(val).into_length_percentage(context),
),
MinTrackSizingFunction::Percent(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Percent(val).into_length_percentage(context),
),
MinTrackSizingFunction::Auto => taffy::style::MinTrackSizingFunction::Auto,
MinTrackSizingFunction::MinContent => taffy::style::MinTrackSizingFunction::MinContent,
MinTrackSizingFunction::MaxContent => taffy::style::MinTrackSizingFunction::MaxContent,
}
}
}
impl MaxTrackSizingFunction {
fn into_taffy(self, context: &LayoutContext) -> taffy::style::MaxTrackSizingFunction {
match self {
MaxTrackSizingFunction::Px(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Px(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Percent(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Percent(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Auto => taffy::style::MaxTrackSizingFunction::Auto,
MaxTrackSizingFunction::MinContent => taffy::style::MaxTrackSizingFunction::MinContent,
MaxTrackSizingFunction::MaxContent => taffy::style::MaxTrackSizingFunction::MaxContent,
MaxTrackSizingFunction::FitContentPx(val) => {
taffy::style::MaxTrackSizingFunction::FitContent(
Val::Px(val).into_length_percentage(context),
)
}
MaxTrackSizingFunction::FitContentPercent(val) => {
taffy::style::MaxTrackSizingFunction::FitContent(
Val::Percent(val).into_length_percentage(context),
)
}
MaxTrackSizingFunction::Fraction(fraction) => {
taffy::style::MaxTrackSizingFunction::Fraction(fraction)
}
}
}
}
impl GridTrack {
fn into_taffy_track(
self,
context: &LayoutContext,
) -> taffy::style::NonRepeatedTrackSizingFunction {
let min = self.min_sizing_function.into_taffy(context);
let max = self.max_sizing_function.into_taffy(context);
taffy::style_helpers::minmax(min, max)
}
}
impl RepeatedGridTrack {
fn clone_into_repeated_taffy_track(
&self,
context: &LayoutContext,
) -> taffy::style::TrackSizingFunction {
if self.tracks.len() == 1 && self.repetition == GridTrackRepetition::Count(1) {
let min = self.tracks[0].min_sizing_function.into_taffy(context);
let max = self.tracks[0].max_sizing_function.into_taffy(context);
let taffy_track = taffy::style_helpers::minmax(min, max);
taffy::style::TrackSizingFunction::Single(taffy_track)
} else {
let taffy_tracks: Vec<_> = self
.tracks
.iter()
.map(|track| {
let min = track.min_sizing_function.into_taffy(context);
let max = track.max_sizing_function.into_taffy(context);
taffy::style_helpers::minmax(min, max)
})
.collect();
match self.repetition {
GridTrackRepetition::Count(count) => {
taffy::style_helpers::repeat(count, taffy_tracks)
}
GridTrackRepetition::AutoFit => taffy::style_helpers::repeat(
taffy::style::GridTrackRepetition::AutoFit,
taffy_tracks,
),
GridTrackRepetition::AutoFill => taffy::style_helpers::repeat(
taffy::style::GridTrackRepetition::AutoFill,
taffy_tracks,
),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_from() {
use taffy::style_helpers as sh;
let bevy_style = crate::Style {
display: Display::Flex,
position_type: PositionType::Absolute,
@ -229,6 +415,8 @@ mod tests {
align_items: AlignItems::Baseline,
align_self: AlignSelf::Start,
align_content: AlignContent::SpaceAround,
justify_items: JustifyItems::Default,
justify_self: JustifySelf::Center,
justify_content: JustifyContent::SpaceEvenly,
margin: UiRect {
left: Val::Percent(0.),
@ -269,6 +457,25 @@ mod tests {
width: Val::Px(0.),
height: Val::Percent(0.),
},
grid_auto_flow: GridAutoFlow::ColumnDense,
grid_template_rows: vec![
GridTrack::px(10.0),
GridTrack::percent(50.0),
GridTrack::fr(1.0),
],
grid_template_columns: RepeatedGridTrack::px(5, 10.0),
grid_auto_rows: vec![
GridTrack::fit_content_px(10.0),
GridTrack::fit_content_percent(25.0),
GridTrack::flex(2.0),
],
grid_auto_columns: vec![
GridTrack::auto(),
GridTrack::min_content(),
GridTrack::max_content(),
],
grid_column: GridPlacement::start(4),
grid_row: GridPlacement::span(3),
};
let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
let taffy_style = from_style(&viewport_values, &bevy_style);
@ -308,6 +515,11 @@ mod tests {
taffy_style.justify_content,
Some(taffy::style::JustifyContent::SpaceEvenly)
);
assert_eq!(taffy_style.justify_items, None);
assert_eq!(
taffy_style.justify_self,
Some(taffy::style::JustifySelf::Center)
);
assert!(matches!(
taffy_style.margin.left,
taffy::style::LengthPercentageAuto::Percent(_)
@ -395,6 +607,38 @@ mod tests {
taffy_style.gap.height,
taffy::style::LengthPercentage::Percent(0.)
);
assert_eq!(
taffy_style.grid_auto_flow,
taffy::style::GridAutoFlow::ColumnDense
);
assert_eq!(
taffy_style.grid_template_rows,
vec![sh::points(10.0), sh::percent(0.5), sh::fr(1.0)]
);
assert_eq!(
taffy_style.grid_template_columns,
vec![sh::repeat(5, vec![sh::points(10.0)])]
);
assert_eq!(
taffy_style.grid_auto_rows,
vec![
sh::fit_content(taffy::style::LengthPercentage::Points(10.0)),
sh::fit_content(taffy::style::LengthPercentage::Percent(0.25)),
sh::minmax(sh::points(0.0), sh::fr(2.0)),
]
);
assert_eq!(
taffy_style.grid_auto_columns,
vec![sh::auto(), sh::min_content(), sh::max_content()]
);
assert_eq!(
taffy_style.grid_column,
taffy::geometry::Line {
start: sh::line(4),
end: sh::span(1)
}
);
assert_eq!(taffy_style.grid_row, sh::span(3));
}
#[test]

View file

@ -42,32 +42,29 @@ impl LayoutContext {
}
#[derive(Resource)]
pub struct FlexSurface {
pub struct UiSurface {
entity_to_taffy: HashMap<Entity, taffy::node::Node>,
window_nodes: HashMap<Entity, taffy::node::Node>,
taffy: Taffy,
}
// SAFETY: as long as MeasureFunc is Send + Sync. https://github.com/DioxusLabs/taffy/issues/146
unsafe impl Send for FlexSurface {}
unsafe impl Sync for FlexSurface {}
fn _assert_send_sync_flex_surface_impl_safe() {
fn _assert_send_sync_ui_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<HashMap<Entity, taffy::node::Node>>();
_assert_send_sync::<Taffy>();
_assert_send_sync::<UiSurface>();
}
impl fmt::Debug for FlexSurface {
impl fmt::Debug for UiSurface {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("FlexSurface")
f.debug_struct("UiSurface")
.field("entity_to_taffy", &self.entity_to_taffy)
.field("window_nodes", &self.window_nodes)
.finish()
}
}
impl Default for FlexSurface {
impl Default for UiSurface {
fn default() -> Self {
Self {
entity_to_taffy: Default::default(),
@ -77,7 +74,7 @@ impl Default for FlexSurface {
}
}
impl FlexSurface {
impl UiSurface {
pub fn upsert_node(&mut self, entity: Entity, style: &Style, context: &LayoutContext) {
let mut added = false;
let taffy = &mut self.taffy;
@ -217,35 +214,35 @@ without UI components as a child of an entity with UI components, results may be
}
}
pub fn get_layout(&self, entity: Entity) -> Result<&taffy::layout::Layout, FlexError> {
pub fn get_layout(&self, entity: Entity) -> Result<&taffy::layout::Layout, LayoutError> {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy
.layout(*taffy_node)
.map_err(FlexError::TaffyError)
.map_err(LayoutError::TaffyError)
} else {
warn!(
"Styled child in a non-UI entity hierarchy. You are using an entity \
with UI components as a child of an entity without UI components, results may be unexpected."
);
Err(FlexError::InvalidHierarchy)
Err(LayoutError::InvalidHierarchy)
}
}
}
#[derive(Debug)]
pub enum FlexError {
pub enum LayoutError {
InvalidHierarchy,
TaffyError(taffy::error::TaffyError),
}
#[allow(clippy::too_many_arguments)]
pub fn flex_node_system(
pub fn ui_layout_system(
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
windows: Query<(Entity, &Window)>,
ui_scale: Res<UiScale>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut resize_events: EventReader<bevy_window::WindowResized>,
mut flex_surface: ResMut<FlexSurface>,
mut ui_surface: ResMut<UiSurface>,
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With<Node>, Changed<Style>)>,
full_node_query: Query<(Entity, &Style, Option<&CalculatedSize>), With<Node>>,
@ -281,7 +278,7 @@ pub fn flex_node_system(
// update window root nodes
for (entity, window) in windows.iter() {
flex_surface.update_window(entity, &window.resolution);
ui_surface.update_window(entity, &window.resolution);
}
let scale_factor = logical_to_physical_factor * ui_scale.scale;
@ -289,7 +286,7 @@ pub fn flex_node_system(
let viewport_values = LayoutContext::new(scale_factor, physical_size);
fn update_changed<F: ReadOnlyWorldQuery>(
flex_surface: &mut FlexSurface,
ui_surface: &mut UiSurface,
viewport_values: &LayoutContext,
query: Query<(Entity, &Style, Option<&CalculatedSize>), F>,
) {
@ -297,45 +294,45 @@ pub fn flex_node_system(
for (entity, style, calculated_size) in &query {
// TODO: remove node from old hierarchy if its root has changed
if let Some(calculated_size) = calculated_size {
flex_surface.upsert_leaf(entity, style, calculated_size, viewport_values);
ui_surface.upsert_leaf(entity, style, calculated_size, viewport_values);
} else {
flex_surface.upsert_node(entity, style, viewport_values);
ui_surface.upsert_node(entity, style, viewport_values);
}
}
}
if !scale_factor_events.is_empty() || ui_scale.is_changed() || resized {
scale_factor_events.clear();
update_changed(&mut flex_surface, &viewport_values, full_node_query);
update_changed(&mut ui_surface, &viewport_values, full_node_query);
} else {
update_changed(&mut flex_surface, &viewport_values, node_query);
update_changed(&mut ui_surface, &viewport_values, node_query);
}
for (entity, style, calculated_size) in &changed_size_query {
flex_surface.upsert_leaf(entity, style, calculated_size, &viewport_values);
ui_surface.upsert_leaf(entity, style, calculated_size, &viewport_values);
}
// clean up removed nodes
flex_surface.remove_entities(removed_nodes.iter());
ui_surface.remove_entities(removed_nodes.iter());
// When a `CalculatedSize` component is removed from an entity, we need to remove the measure from the corresponding taffy node.
for entity in removed_calculated_sizes.iter() {
flex_surface.try_remove_measure(entity);
ui_surface.try_remove_measure(entity);
}
// update window children (for now assuming all Nodes live in the primary window)
flex_surface.set_window_children(primary_window_entity, root_node_query.iter());
ui_surface.set_window_children(primary_window_entity, root_node_query.iter());
// update and remove children
for entity in removed_children.iter() {
flex_surface.try_remove_children(entity);
ui_surface.try_remove_children(entity);
}
for (entity, children) in &children_query {
flex_surface.update_children(entity, children);
ui_surface.update_children(entity, children);
}
// compute layouts
flex_surface.compute_window_layouts();
ui_surface.compute_window_layouts();
let physical_to_logical_factor = 1. / logical_to_physical_factor;
@ -343,7 +340,7 @@ pub fn flex_node_system(
// PERF: try doing this incrementally
for (entity, mut node, mut transform, parent) in &mut node_transform_query {
let layout = flex_surface.get_layout(entity).unwrap();
let layout = ui_surface.get_layout(entity).unwrap();
let new_size = Vec2::new(
to_logical(layout.size.width),
to_logical(layout.size.height),
@ -356,7 +353,7 @@ pub fn flex_node_system(
new_position.x = to_logical(layout.location.x + layout.size.width / 2.0);
new_position.y = to_logical(layout.location.y + layout.size.height / 2.0);
if let Some(parent) = parent {
if let Ok(parent_layout) = flex_surface.get_layout(**parent) {
if let Ok(parent_layout) = ui_surface.get_layout(**parent) {
new_position.x -= to_logical(parent_layout.size.width / 2.0);
new_position.y -= to_logical(parent_layout.size.height / 2.0);
}

View file

@ -3,10 +3,10 @@
//! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games
//! # Basic usage
//! Spawn UI elements with [`node_bundles::ButtonBundle`], [`node_bundles::ImageBundle`], [`node_bundles::TextBundle`] and [`node_bundles::NodeBundle`]
//! This UI is laid out with the Flexbox layout model (see <https://cssreference.io/flexbox/>)
mod flex;
//! This UI is laid out with the Flexbox and CSS Grid layout models (see <https://cssreference.io/flexbox/>)
mod focus;
mod geometry;
mod layout;
mod render;
mod stack;
mod ui_node;
@ -22,9 +22,9 @@ pub mod widget;
#[cfg(feature = "bevy_text")]
use bevy_render::camera::CameraUpdateSystem;
use bevy_render::extract_component::ExtractComponentPlugin;
pub use flex::*;
pub use focus::*;
pub use geometry::*;
pub use layout::*;
pub use measurement::*;
pub use render::*;
pub use ui_node::*;
@ -54,8 +54,8 @@ pub struct UiPlugin;
/// The label enum labeling the types of systems in the Bevy UI
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum UiSystem {
/// After this label, the ui flex state has been updated
Flex,
/// After this label, the ui layout state has been updated
Layout,
/// After this label, input interactions with UI entities have been updated for this frame
Focus,
/// After this label, the [`UiStack`] resource has been updated
@ -81,7 +81,7 @@ impl Default for UiScale {
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
.init_resource::<FlexSurface>()
.init_resource::<UiSurface>()
.init_resource::<UiScale>()
.init_resource::<UiStack>()
.register_type::<AlignContent>()
@ -92,9 +92,15 @@ impl Plugin for UiPlugin {
.register_type::<Display>()
.register_type::<FlexDirection>()
.register_type::<FlexWrap>()
.register_type::<GridAutoFlow>()
.register_type::<GridPlacement>()
.register_type::<GridTrack>()
.register_type::<RepeatedGridTrack>()
.register_type::<FocusPolicy>()
.register_type::<Interaction>()
.register_type::<JustifyContent>()
.register_type::<JustifyItems>()
.register_type::<JustifySelf>()
.register_type::<Node>()
// NOTE: used by Style::aspect_ratio
.register_type::<Option<f32>>()
@ -116,8 +122,8 @@ impl Plugin for UiPlugin {
#[cfg(feature = "bevy_text")]
app.add_systems(
PostUpdate,
widget::measure_text_system
.before(UiSystem::Flex)
widget::text_system
.before(UiSystem::Layout)
// Potential conflict: `Assets<Image>`
// In practice, they run independently since `bevy_render::camera_update_system`
// will only ever observe its own render target, and `widget::text_system`
@ -131,7 +137,7 @@ impl Plugin for UiPlugin {
#[cfg(feature = "bevy_text")]
app.add_plugin(accessibility::AccessibilityPlugin);
app.add_systems(PostUpdate, {
let system = widget::update_image_calculated_size_system.before(UiSystem::Flex);
let system = widget::update_image_calculated_size_system.before(UiSystem::Layout);
// Potential conflicts: `Assets<Image>`
// They run independently since `widget::image_node_system` will only ever observe
// its own UiImage, and `widget::text_system` & `bevy_text::update_text2d_layout`
@ -146,12 +152,11 @@ impl Plugin for UiPlugin {
.add_systems(
PostUpdate,
(
flex_node_system
.in_set(UiSystem::Flex)
ui_layout_system
.in_set(UiSystem::Layout)
.before(TransformSystem::TransformPropagate),
ui_stack_system.in_set(UiSystem::Stack),
update_clipping_system.after(TransformSystem::TransformPropagate),
widget::text_system.after(UiSystem::Flex),
),
);

View file

@ -20,7 +20,8 @@ use bevy_transform::prelude::{GlobalTransform, Transform};
pub struct NodeBundle {
/// Describes the logical size of the node
pub node: Node,
/// Describes the style including flexbox settings
/// 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 background color, which serves as a "fill" for this node
pub background_color: BackgroundColor,
@ -65,8 +66,12 @@ impl Default for NodeBundle {
#[derive(Bundle, Clone, Debug, Default)]
pub struct ImageBundle {
/// 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,
/// Describes the style including flexbox settings
/// 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: CalculatedSize,
@ -106,7 +111,8 @@ pub struct ImageBundle {
pub struct TextBundle {
/// Describes the logical size of the node
pub node: Node,
/// Describes the style including flexbox settings
/// 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,
/// Contains the text of the node
pub text: Text,
@ -186,7 +192,7 @@ impl TextBundle {
}
/// Returns this [`TextBundle`] with a new [`Style`].
pub const fn with_style(mut self, style: Style) -> Self {
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
@ -205,7 +211,8 @@ pub struct ButtonBundle {
pub node: Node,
/// Marker component that signals this node is a button
pub button: Button,
/// Describes the style including flexbox settings
/// 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,
/// Describes whether and how the button has been interacted with by the input
pub interaction: Interaction,

File diff suppressed because it is too large Load diff

View file

@ -331,6 +331,7 @@ Example | Description
Example | Description
--- | ---
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
[CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout
[Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior

218
examples/ui/grid.rs Normal file
View file

@ -0,0 +1,218 @@
//! Demonstrates how CSS Grid layout can be used to lay items out in a 2D grid
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: [800., 600.].into(),
title: "Bevy CSS Grid Layout Example".to_string(),
..default()
}),
..default()
}))
.add_systems(Startup, spawn_layout)
.run();
}
fn spawn_layout(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
commands.spawn(Camera2dBundle::default());
// Top-level grid (app frame)
commands
.spawn(NodeBundle {
style: Style {
/// Use the CSS Grid algorithm for laying out this node
display: Display::Grid,
/// Make node fill the entirety it's parent (in this case the window)
size: Size::all(Val::Percent(100.)),
/// Set the grid to have 2 columns with sizes [min-content, minmax(0, 1fr)]
/// - The first column will size to the size of it's contents
/// - The second column will take up the remaining available space
grid_template_columns: vec![GridTrack::min_content(), GridTrack::flex(1.0)],
/// Set the grid to have 3 rows with sizes [auto, minmax(0, 1fr), 20px]
/// - The first row will size to the size of it's contents
/// - The second row take up remaining available space (after rows 1 and 3 have both been sized)
/// - The third row will be exactly 20px high
grid_template_rows: vec![
GridTrack::auto(),
GridTrack::flex(1.0),
GridTrack::px(20.),
],
..default()
},
background_color: BackgroundColor(Color::WHITE),
..default()
})
.with_children(|builder| {
// Header
builder
.spawn(NodeBundle {
style: Style {
display: Display::Grid,
/// Make this node span two grid columns so that it takes up the entire top tow
grid_column: GridPlacement::span(2),
padding: UiRect::all(Val::Px(6.0)),
..default()
},
..default()
})
.with_children(|builder| {
spawn_nested_text_bundle(builder, font.clone(), "Bevy CSS Grid Layout Example");
});
// Main content grid (auto placed in row 2, column 1)
builder
.spawn(NodeBundle {
style: Style {
/// Make the height of the node fill its parent
size: Size::height(Val::Percent(100.0)),
/// Make the grid have a 1:1 aspect ratio meaning it will scale as an exact square
/// As the height is set explicitly, this means the width will adjust to match the height
aspect_ratio: Some(1.0),
/// Use grid layout for this node
display: Display::Grid,
// Add 24px of padding around the grid
padding: UiRect::all(Val::Px(24.0)),
/// Set the grid to have 4 columns all with sizes minmax(0, 1fr)
/// This creates 4 exactly evenly sized columns
grid_template_columns: RepeatedGridTrack::flex(4, 1.0),
/// Set the grid to have 4 rows all with sizes minmax(0, 1fr)
/// This creates 4 exactly evenly sized rows
grid_template_rows: RepeatedGridTrack::flex(4, 1.0),
/// Set a 12px gap/gutter between rows and columns
gap: Size::all(Val::Px(12.0)),
..default()
},
background_color: BackgroundColor(Color::DARK_GRAY),
..default()
})
.with_children(|builder| {
// Note there is no need to specify the position for each grid item. Grid items that are
// not given an explicit position will be automatically positioned into the next available
// grid cell. The order in which this is performed can be controlled using the grid_auto_flow
// style property.
item_rect(builder, Color::ORANGE);
item_rect(builder, Color::BISQUE);
item_rect(builder, Color::BLUE);
item_rect(builder, Color::CRIMSON);
item_rect(builder, Color::CYAN);
item_rect(builder, Color::ORANGE_RED);
item_rect(builder, Color::DARK_GREEN);
item_rect(builder, Color::FUCHSIA);
item_rect(builder, Color::TEAL);
item_rect(builder, Color::ALICE_BLUE);
item_rect(builder, Color::CRIMSON);
item_rect(builder, Color::ANTIQUE_WHITE);
item_rect(builder, Color::YELLOW);
item_rect(builder, Color::PINK);
item_rect(builder, Color::YELLOW_GREEN);
item_rect(builder, Color::SALMON);
});
// Right side bar (auto placed in row 2, column 2)
builder
.spawn(NodeBundle {
style: Style {
display: Display::Grid,
// Align content towards the start (top) in the vertical axis
align_items: AlignItems::Start,
// Align content towards the center in the horizontal axis
justify_items: JustifyItems::Center,
// Add 20px padding to the top
padding: UiRect::top(Val::Px(20.)),
..default()
},
background_color: BackgroundColor(Color::BLACK),
..default()
})
.with_children(|builder| {
builder.spawn(TextBundle::from_section(
"Sidebar",
TextStyle {
font,
font_size: 24.0,
color: Color::WHITE,
},
));
});
// Footer / status bar
builder.spawn(NodeBundle {
style: Style {
// Make this node span two grid column so that it takes up the entire bottom row
grid_column: GridPlacement::span(2),
..default()
},
background_color: BackgroundColor(Color::WHITE),
..default()
});
// Modal (absolutely positioned on top of content - uncomment to view)
// builder.spawn(NodeBundle {
// style: Style {
// position_type: PositionType::Absolute,
// margin: UiRect {
// top: Val::Px(100.),
// bottom: Val::Auto,
// left: Val::Auto,
// right: Val::Auto,
// },
// size: Size {
// width: Val::Percent(60.),
// height: Val::Px(300.),
// },
// max_size: Size {
// width: Val::Px(600.),
// height: Val::Auto,
// },
// ..default()
// },
// background_color: BackgroundColor(Color::Rgba {
// red: 255.0,
// green: 255.0,
// blue: 255.0,
// alpha: 0.8,
// }),
// ..default()
// });
});
}
/// Create a coloured rectangle node. The node has size as it is assumed that it will be
/// spawned as a child of a Grid container with `AlignItems::Stretch` and `JustifyItems::Stretch`
/// which will allow it to take it's size from the size of the grid area it occupies.
fn item_rect(builder: &mut ChildBuilder, color: Color) {
builder
.spawn(NodeBundle {
style: Style {
display: Display::Grid,
padding: UiRect::all(Val::Px(3.0)),
..default()
},
background_color: BackgroundColor(Color::BLACK),
..default()
})
.with_children(|builder| {
builder.spawn(NodeBundle {
background_color: BackgroundColor(color),
..default()
});
});
}
fn spawn_nested_text_bundle(builder: &mut ChildBuilder, font: Handle<Font>, text: &str) {
builder.spawn(TextBundle::from_section(
text,
TextStyle {
font,
font_size: 24.0,
color: Color::BLACK,
},
));
}