From 363d0f0c7c67adff37a86727b29fd36ddf064a37 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 17 Apr 2023 17:21:38 +0100 Subject: [PATCH] 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 Screenshot 2023-03-10 at 17 56 21 --- ## Changelog - Support for CSS Grid layout added to `bevy_ui` --------- Co-authored-by: Rob Parrett Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com> --- Cargo.toml | 12 +- crates/bevy_ui/Cargo.toml | 2 +- .../bevy_ui/src/{flex => layout}/convert.rs | 322 +++++- crates/bevy_ui/src/{flex => layout}/mod.rs | 59 +- crates/bevy_ui/src/lib.rs | 29 +- crates/bevy_ui/src/node_bundles.rs | 17 +- crates/bevy_ui/src/ui_node.rs | 926 ++++++++++++++++-- examples/README.md | 1 + examples/ui/grid.rs | 218 +++++ 9 files changed, 1415 insertions(+), 171 deletions(-) rename crates/bevy_ui/src/{flex => layout}/convert.rs (57%) rename crates/bevy_ui/src/{flex => layout}/mod.rs (88%) create mode 100644 examples/ui/grid.rs diff --git a/Cargo.toml b/Cargo.toml index cec3985f61..7df77f3ffd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index b927f278fe..0f3bf3320f 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -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"] } diff --git a/crates/bevy_ui/src/flex/convert.rs b/crates/bevy_ui/src/layout/convert.rs similarity index 57% rename from crates/bevy_ui/src/flex/convert.rs rename to crates/bevy_ui/src/layout/convert.rs index a2bb1af48c..62114d5c28 100644 --- a/crates/bevy_ui/src/flex/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -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::>(), + grid_template_columns: style + .grid_template_columns + .iter() + .map(|track| track.clone_into_repeated_taffy_track(context)) + .collect::>(), + grid_auto_rows: style + .grid_auto_rows + .iter() + .map(|track| track.into_taffy_track(context)) + .collect::>(), + grid_auto_columns: style + .grid_auto_columns + .iter() + .map(|track| track.into_taffy_track(context)) + .collect::>(), + grid_row: style.grid_row.into(), + grid_column: style.grid_column.into(), } } -impl From for taffy::style::AlignItems { +impl From for Option { 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 for Option { + 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 for Option { } } -impl From for taffy::style::AlignContent { +impl From for Option { + 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 for Option { 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 for Option { + 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 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 for taffy::style::FlexDirection { } } -impl From 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 for taffy::style::Position { fn from(value: PositionType) -> Self { match value { @@ -210,12 +268,140 @@ impl From for taffy::style::FlexWrap { } } +impl From 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 for taffy::geometry::Line { + 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] diff --git a/crates/bevy_ui/src/flex/mod.rs b/crates/bevy_ui/src/layout/mod.rs similarity index 88% rename from crates/bevy_ui/src/flex/mod.rs rename to crates/bevy_ui/src/layout/mod.rs index 748b0450e6..077f8a8987 100644 --- a/crates/bevy_ui/src/flex/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -42,32 +42,29 @@ impl LayoutContext { } #[derive(Resource)] -pub struct FlexSurface { +pub struct UiSurface { entity_to_taffy: HashMap, window_nodes: HashMap, 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() {} _assert_send_sync::>(); _assert_send_sync::(); + _assert_send_sync::(); } -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>, windows: Query<(Entity, &Window)>, ui_scale: Res, mut scale_factor_events: EventReader, mut resize_events: EventReader, - mut flex_surface: ResMut, + mut ui_surface: ResMut, root_node_query: Query, Without)>, node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With, Changed