mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 14:08:32 +00:00
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:
parent
cfa750a741
commit
363d0f0c7c
9 changed files with 1415 additions and 171 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -1806,7 +1806,17 @@ path = "examples/ui/flex_layout.rs"
|
||||||
name = "Flex Layout"
|
name = "Flex Layout"
|
||||||
description = "Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text"
|
description = "Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text"
|
||||||
category = "UI (User Interface)"
|
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]]
|
[[example]]
|
||||||
name = "transparency_ui"
|
name = "transparency_ui"
|
||||||
|
|
|
@ -31,7 +31,7 @@ bevy_window = { path = "../bevy_window", version = "0.11.0-dev" }
|
||||||
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
|
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
|
||||||
|
|
||||||
# other
|
# other
|
||||||
taffy = { version = "0.3.10", default-features = false, features = ["std"] }
|
taffy = { version = "0.3.10" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
smallvec = { version = "1.6", features = ["union", "const_generics"] }
|
smallvec = { version = "1.6", features = ["union", "const_generics"] }
|
||||||
bytemuck = { version = "1.5", features = ["derive"] }
|
bytemuck = { version = "1.5", features = ["derive"] }
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
use taffy::style_helpers;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
|
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, GridAutoFlow,
|
||||||
PositionType, Size, Style, UiRect, Val,
|
GridPlacement, GridTrack, GridTrackRepetition, JustifyContent, JustifyItems, JustifySelf,
|
||||||
|
MaxTrackSizingFunction, MinTrackSizingFunction, PositionType, RepeatedGridTrack, Size, Style,
|
||||||
|
UiRect, Val,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::LayoutContext;
|
use super::LayoutContext;
|
||||||
|
@ -74,10 +78,12 @@ pub fn from_style(context: &LayoutContext, style: &Style) -> taffy::style::Style
|
||||||
position: style.position_type.into(),
|
position: style.position_type.into(),
|
||||||
flex_direction: style.flex_direction.into(),
|
flex_direction: style.flex_direction.into(),
|
||||||
flex_wrap: style.flex_wrap.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_self: style.align_self.into(),
|
||||||
align_content: Some(style.align_content.into()),
|
justify_self: style.justify_self.into(),
|
||||||
justify_content: Some(style.justify_content.into()),
|
align_content: style.align_content.into(),
|
||||||
|
justify_content: style.justify_content.into(),
|
||||||
inset: taffy::prelude::Rect {
|
inset: taffy::prelude::Rect {
|
||||||
left: style.left.into_length_percentage_auto(context),
|
left: style.left.into_length_percentage_auto(context),
|
||||||
right: style.right.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: style
|
||||||
.gap
|
.gap
|
||||||
.map_to_taffy_size(|s| s.into_length_percentage(context)),
|
.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 {
|
fn from(value: AlignItems) -> Self {
|
||||||
match value {
|
match value {
|
||||||
AlignItems::Start => taffy::style::AlignItems::Start,
|
AlignItems::Default => None,
|
||||||
AlignItems::End => taffy::style::AlignItems::End,
|
AlignItems::Start => taffy::style::AlignItems::Start.into(),
|
||||||
AlignItems::FlexStart => taffy::style::AlignItems::FlexStart,
|
AlignItems::End => taffy::style::AlignItems::End.into(),
|
||||||
AlignItems::FlexEnd => taffy::style::AlignItems::FlexEnd,
|
AlignItems::FlexStart => taffy::style::AlignItems::FlexStart.into(),
|
||||||
AlignItems::Center => taffy::style::AlignItems::Center,
|
AlignItems::FlexEnd => taffy::style::AlignItems::FlexEnd.into(),
|
||||||
AlignItems::Baseline => taffy::style::AlignItems::Baseline,
|
AlignItems::Center => taffy::style::AlignItems::Center.into(),
|
||||||
AlignItems::Stretch => taffy::style::AlignItems::Stretch,
|
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 {
|
fn from(value: AlignContent) -> Self {
|
||||||
match value {
|
match value {
|
||||||
AlignContent::Start => taffy::style::AlignContent::Start,
|
AlignContent::Default => None,
|
||||||
AlignContent::End => taffy::style::AlignContent::End,
|
AlignContent::Start => taffy::style::AlignContent::Start.into(),
|
||||||
AlignContent::FlexStart => taffy::style::AlignContent::FlexStart,
|
AlignContent::End => taffy::style::AlignContent::End.into(),
|
||||||
AlignContent::FlexEnd => taffy::style::AlignContent::FlexEnd,
|
AlignContent::FlexStart => taffy::style::AlignContent::FlexStart.into(),
|
||||||
AlignContent::Center => taffy::style::AlignContent::Center,
|
AlignContent::FlexEnd => taffy::style::AlignContent::FlexEnd.into(),
|
||||||
AlignContent::Stretch => taffy::style::AlignContent::Stretch,
|
AlignContent::Center => taffy::style::AlignContent::Center.into(),
|
||||||
AlignContent::SpaceBetween => taffy::style::AlignContent::SpaceBetween,
|
AlignContent::Stretch => taffy::style::AlignContent::Stretch.into(),
|
||||||
AlignContent::SpaceAround => taffy::style::AlignContent::SpaceAround,
|
AlignContent::SpaceBetween => taffy::style::AlignContent::SpaceBetween.into(),
|
||||||
AlignContent::SpaceEvenly => taffy::style::AlignContent::SpaceEvenly,
|
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 {
|
fn from(value: Display) -> Self {
|
||||||
match value {
|
match value {
|
||||||
Display::Flex => taffy::style::Display::Flex,
|
Display::Flex => taffy::style::Display::Flex,
|
||||||
|
Display::Grid => taffy::style::Display::Grid,
|
||||||
Display::None => taffy::style::Display::None,
|
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 {
|
impl From<PositionType> for taffy::style::Position {
|
||||||
fn from(value: PositionType) -> Self {
|
fn from(value: PositionType) -> Self {
|
||||||
match value {
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_from() {
|
fn test_convert_from() {
|
||||||
|
use taffy::style_helpers as sh;
|
||||||
|
|
||||||
let bevy_style = crate::Style {
|
let bevy_style = crate::Style {
|
||||||
display: Display::Flex,
|
display: Display::Flex,
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
|
@ -229,6 +415,8 @@ mod tests {
|
||||||
align_items: AlignItems::Baseline,
|
align_items: AlignItems::Baseline,
|
||||||
align_self: AlignSelf::Start,
|
align_self: AlignSelf::Start,
|
||||||
align_content: AlignContent::SpaceAround,
|
align_content: AlignContent::SpaceAround,
|
||||||
|
justify_items: JustifyItems::Default,
|
||||||
|
justify_self: JustifySelf::Center,
|
||||||
justify_content: JustifyContent::SpaceEvenly,
|
justify_content: JustifyContent::SpaceEvenly,
|
||||||
margin: UiRect {
|
margin: UiRect {
|
||||||
left: Val::Percent(0.),
|
left: Val::Percent(0.),
|
||||||
|
@ -269,6 +457,25 @@ mod tests {
|
||||||
width: Val::Px(0.),
|
width: Val::Px(0.),
|
||||||
height: Val::Percent(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 viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
|
||||||
let taffy_style = from_style(&viewport_values, &bevy_style);
|
let taffy_style = from_style(&viewport_values, &bevy_style);
|
||||||
|
@ -308,6 +515,11 @@ mod tests {
|
||||||
taffy_style.justify_content,
|
taffy_style.justify_content,
|
||||||
Some(taffy::style::JustifyContent::SpaceEvenly)
|
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!(
|
assert!(matches!(
|
||||||
taffy_style.margin.left,
|
taffy_style.margin.left,
|
||||||
taffy::style::LengthPercentageAuto::Percent(_)
|
taffy::style::LengthPercentageAuto::Percent(_)
|
||||||
|
@ -395,6 +607,38 @@ mod tests {
|
||||||
taffy_style.gap.height,
|
taffy_style.gap.height,
|
||||||
taffy::style::LengthPercentage::Percent(0.)
|
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]
|
#[test]
|
|
@ -42,32 +42,29 @@ impl LayoutContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct FlexSurface {
|
pub struct UiSurface {
|
||||||
entity_to_taffy: HashMap<Entity, taffy::node::Node>,
|
entity_to_taffy: HashMap<Entity, taffy::node::Node>,
|
||||||
window_nodes: HashMap<Entity, taffy::node::Node>,
|
window_nodes: HashMap<Entity, taffy::node::Node>,
|
||||||
taffy: Taffy,
|
taffy: Taffy,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: as long as MeasureFunc is Send + Sync. https://github.com/DioxusLabs/taffy/issues/146
|
fn _assert_send_sync_ui_surface_impl_safe() {
|
||||||
unsafe impl Send for FlexSurface {}
|
|
||||||
unsafe impl Sync for FlexSurface {}
|
|
||||||
|
|
||||||
fn _assert_send_sync_flex_surface_impl_safe() {
|
|
||||||
fn _assert_send_sync<T: Send + Sync>() {}
|
fn _assert_send_sync<T: Send + Sync>() {}
|
||||||
_assert_send_sync::<HashMap<Entity, taffy::node::Node>>();
|
_assert_send_sync::<HashMap<Entity, taffy::node::Node>>();
|
||||||
_assert_send_sync::<Taffy>();
|
_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 {
|
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("entity_to_taffy", &self.entity_to_taffy)
|
||||||
.field("window_nodes", &self.window_nodes)
|
.field("window_nodes", &self.window_nodes)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FlexSurface {
|
impl Default for UiSurface {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
entity_to_taffy: Default::default(),
|
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) {
|
pub fn upsert_node(&mut self, entity: Entity, style: &Style, context: &LayoutContext) {
|
||||||
let mut added = false;
|
let mut added = false;
|
||||||
let taffy = &mut self.taffy;
|
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) {
|
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
|
||||||
self.taffy
|
self.taffy
|
||||||
.layout(*taffy_node)
|
.layout(*taffy_node)
|
||||||
.map_err(FlexError::TaffyError)
|
.map_err(LayoutError::TaffyError)
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
"Styled child in a non-UI entity hierarchy. You are using an entity \
|
"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."
|
with UI components as a child of an entity without UI components, results may be unexpected."
|
||||||
);
|
);
|
||||||
Err(FlexError::InvalidHierarchy)
|
Err(LayoutError::InvalidHierarchy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FlexError {
|
pub enum LayoutError {
|
||||||
InvalidHierarchy,
|
InvalidHierarchy,
|
||||||
TaffyError(taffy::error::TaffyError),
|
TaffyError(taffy::error::TaffyError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn flex_node_system(
|
pub fn ui_layout_system(
|
||||||
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
|
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
|
||||||
windows: Query<(Entity, &Window)>,
|
windows: Query<(Entity, &Window)>,
|
||||||
ui_scale: Res<UiScale>,
|
ui_scale: Res<UiScale>,
|
||||||
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
|
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
|
||||||
mut resize_events: EventReader<bevy_window::WindowResized>,
|
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>)>,
|
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
|
||||||
node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With<Node>, Changed<Style>)>,
|
node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With<Node>, Changed<Style>)>,
|
||||||
full_node_query: Query<(Entity, &Style, Option<&CalculatedSize>), With<Node>>,
|
full_node_query: Query<(Entity, &Style, Option<&CalculatedSize>), With<Node>>,
|
||||||
|
@ -281,7 +278,7 @@ pub fn flex_node_system(
|
||||||
|
|
||||||
// update window root nodes
|
// update window root nodes
|
||||||
for (entity, window) in windows.iter() {
|
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;
|
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);
|
let viewport_values = LayoutContext::new(scale_factor, physical_size);
|
||||||
|
|
||||||
fn update_changed<F: ReadOnlyWorldQuery>(
|
fn update_changed<F: ReadOnlyWorldQuery>(
|
||||||
flex_surface: &mut FlexSurface,
|
ui_surface: &mut UiSurface,
|
||||||
viewport_values: &LayoutContext,
|
viewport_values: &LayoutContext,
|
||||||
query: Query<(Entity, &Style, Option<&CalculatedSize>), F>,
|
query: Query<(Entity, &Style, Option<&CalculatedSize>), F>,
|
||||||
) {
|
) {
|
||||||
|
@ -297,45 +294,45 @@ pub fn flex_node_system(
|
||||||
for (entity, style, calculated_size) in &query {
|
for (entity, style, calculated_size) in &query {
|
||||||
// TODO: remove node from old hierarchy if its root has changed
|
// TODO: remove node from old hierarchy if its root has changed
|
||||||
if let Some(calculated_size) = calculated_size {
|
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 {
|
} 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 {
|
if !scale_factor_events.is_empty() || ui_scale.is_changed() || resized {
|
||||||
scale_factor_events.clear();
|
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 {
|
} 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 {
|
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
|
// 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.
|
// 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() {
|
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)
|
// 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
|
// update and remove children
|
||||||
for entity in removed_children.iter() {
|
for entity in removed_children.iter() {
|
||||||
flex_surface.try_remove_children(entity);
|
ui_surface.try_remove_children(entity);
|
||||||
}
|
}
|
||||||
for (entity, children) in &children_query {
|
for (entity, children) in &children_query {
|
||||||
flex_surface.update_children(entity, children);
|
ui_surface.update_children(entity, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute layouts
|
// compute layouts
|
||||||
flex_surface.compute_window_layouts();
|
ui_surface.compute_window_layouts();
|
||||||
|
|
||||||
let physical_to_logical_factor = 1. / logical_to_physical_factor;
|
let physical_to_logical_factor = 1. / logical_to_physical_factor;
|
||||||
|
|
||||||
|
@ -343,7 +340,7 @@ pub fn flex_node_system(
|
||||||
|
|
||||||
// PERF: try doing this incrementally
|
// PERF: try doing this incrementally
|
||||||
for (entity, mut node, mut transform, parent) in &mut node_transform_query {
|
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(
|
let new_size = Vec2::new(
|
||||||
to_logical(layout.size.width),
|
to_logical(layout.size.width),
|
||||||
to_logical(layout.size.height),
|
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.x = to_logical(layout.location.x + layout.size.width / 2.0);
|
||||||
new_position.y = to_logical(layout.location.y + layout.size.height / 2.0);
|
new_position.y = to_logical(layout.location.y + layout.size.height / 2.0);
|
||||||
if let Some(parent) = parent {
|
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.x -= to_logical(parent_layout.size.width / 2.0);
|
||||||
new_position.y -= to_logical(parent_layout.size.height / 2.0);
|
new_position.y -= to_logical(parent_layout.size.height / 2.0);
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@
|
||||||
//! 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 [`node_bundles::ButtonBundle`], [`node_bundles::ImageBundle`], [`node_bundles::TextBundle`] and [`node_bundles::NodeBundle`]
|
//! 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/>)
|
//! This UI is laid out with the Flexbox and CSS Grid layout models (see <https://cssreference.io/flexbox/>)
|
||||||
mod flex;
|
|
||||||
mod focus;
|
mod focus;
|
||||||
mod geometry;
|
mod geometry;
|
||||||
|
mod layout;
|
||||||
mod render;
|
mod render;
|
||||||
mod stack;
|
mod stack;
|
||||||
mod ui_node;
|
mod ui_node;
|
||||||
|
@ -22,9 +22,9 @@ pub mod widget;
|
||||||
#[cfg(feature = "bevy_text")]
|
#[cfg(feature = "bevy_text")]
|
||||||
use bevy_render::camera::CameraUpdateSystem;
|
use bevy_render::camera::CameraUpdateSystem;
|
||||||
use bevy_render::extract_component::ExtractComponentPlugin;
|
use bevy_render::extract_component::ExtractComponentPlugin;
|
||||||
pub use flex::*;
|
|
||||||
pub use focus::*;
|
pub use focus::*;
|
||||||
pub use geometry::*;
|
pub use geometry::*;
|
||||||
|
pub use layout::*;
|
||||||
pub use measurement::*;
|
pub use measurement::*;
|
||||||
pub use render::*;
|
pub use render::*;
|
||||||
pub use ui_node::*;
|
pub use ui_node::*;
|
||||||
|
@ -54,8 +54,8 @@ pub struct UiPlugin;
|
||||||
/// The label enum labeling the types of systems in the Bevy UI
|
/// The label enum labeling the types of systems in the Bevy UI
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||||
pub enum UiSystem {
|
pub enum UiSystem {
|
||||||
/// After this label, the ui flex state has been updated
|
/// After this label, the ui layout state has been updated
|
||||||
Flex,
|
Layout,
|
||||||
/// After this label, input interactions with UI entities have been updated for this frame
|
/// After this label, input interactions with UI entities have been updated for this frame
|
||||||
Focus,
|
Focus,
|
||||||
/// After this label, the [`UiStack`] resource has been updated
|
/// After this label, the [`UiStack`] resource has been updated
|
||||||
|
@ -81,7 +81,7 @@ impl Default for UiScale {
|
||||||
impl Plugin for UiPlugin {
|
impl Plugin for UiPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
|
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
|
||||||
.init_resource::<FlexSurface>()
|
.init_resource::<UiSurface>()
|
||||||
.init_resource::<UiScale>()
|
.init_resource::<UiScale>()
|
||||||
.init_resource::<UiStack>()
|
.init_resource::<UiStack>()
|
||||||
.register_type::<AlignContent>()
|
.register_type::<AlignContent>()
|
||||||
|
@ -92,9 +92,15 @@ impl Plugin for UiPlugin {
|
||||||
.register_type::<Display>()
|
.register_type::<Display>()
|
||||||
.register_type::<FlexDirection>()
|
.register_type::<FlexDirection>()
|
||||||
.register_type::<FlexWrap>()
|
.register_type::<FlexWrap>()
|
||||||
|
.register_type::<GridAutoFlow>()
|
||||||
|
.register_type::<GridPlacement>()
|
||||||
|
.register_type::<GridTrack>()
|
||||||
|
.register_type::<RepeatedGridTrack>()
|
||||||
.register_type::<FocusPolicy>()
|
.register_type::<FocusPolicy>()
|
||||||
.register_type::<Interaction>()
|
.register_type::<Interaction>()
|
||||||
.register_type::<JustifyContent>()
|
.register_type::<JustifyContent>()
|
||||||
|
.register_type::<JustifyItems>()
|
||||||
|
.register_type::<JustifySelf>()
|
||||||
.register_type::<Node>()
|
.register_type::<Node>()
|
||||||
// NOTE: used by Style::aspect_ratio
|
// NOTE: used by Style::aspect_ratio
|
||||||
.register_type::<Option<f32>>()
|
.register_type::<Option<f32>>()
|
||||||
|
@ -116,8 +122,8 @@ impl Plugin for UiPlugin {
|
||||||
#[cfg(feature = "bevy_text")]
|
#[cfg(feature = "bevy_text")]
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
widget::measure_text_system
|
widget::text_system
|
||||||
.before(UiSystem::Flex)
|
.before(UiSystem::Layout)
|
||||||
// Potential conflict: `Assets<Image>`
|
// Potential conflict: `Assets<Image>`
|
||||||
// In practice, they run independently since `bevy_render::camera_update_system`
|
// In practice, they run independently since `bevy_render::camera_update_system`
|
||||||
// will only ever observe its own render target, and `widget::text_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")]
|
#[cfg(feature = "bevy_text")]
|
||||||
app.add_plugin(accessibility::AccessibilityPlugin);
|
app.add_plugin(accessibility::AccessibilityPlugin);
|
||||||
app.add_systems(PostUpdate, {
|
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>`
|
// Potential conflicts: `Assets<Image>`
|
||||||
// They run independently since `widget::image_node_system` will only ever observe
|
// They run independently since `widget::image_node_system` will only ever observe
|
||||||
// its own UiImage, and `widget::text_system` & `bevy_text::update_text2d_layout`
|
// its own UiImage, and `widget::text_system` & `bevy_text::update_text2d_layout`
|
||||||
|
@ -146,12 +152,11 @@ impl Plugin for UiPlugin {
|
||||||
.add_systems(
|
.add_systems(
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
(
|
(
|
||||||
flex_node_system
|
ui_layout_system
|
||||||
.in_set(UiSystem::Flex)
|
.in_set(UiSystem::Layout)
|
||||||
.before(TransformSystem::TransformPropagate),
|
.before(TransformSystem::TransformPropagate),
|
||||||
ui_stack_system.in_set(UiSystem::Stack),
|
ui_stack_system.in_set(UiSystem::Stack),
|
||||||
update_clipping_system.after(TransformSystem::TransformPropagate),
|
update_clipping_system.after(TransformSystem::TransformPropagate),
|
||||||
widget::text_system.after(UiSystem::Flex),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,8 @@ use bevy_transform::prelude::{GlobalTransform, Transform};
|
||||||
pub struct NodeBundle {
|
pub struct NodeBundle {
|
||||||
/// Describes the logical size of the node
|
/// Describes the logical size of the node
|
||||||
pub node: 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,
|
pub style: Style,
|
||||||
/// The background color, which serves as a "fill" for this node
|
/// The background color, which serves as a "fill" for this node
|
||||||
pub background_color: BackgroundColor,
|
pub background_color: BackgroundColor,
|
||||||
|
@ -65,8 +66,12 @@ impl Default for NodeBundle {
|
||||||
#[derive(Bundle, Clone, Debug, Default)]
|
#[derive(Bundle, Clone, Debug, Default)]
|
||||||
pub struct ImageBundle {
|
pub struct ImageBundle {
|
||||||
/// Describes the logical size of the node
|
/// 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,
|
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,
|
pub style: Style,
|
||||||
/// The calculated size based on the given image
|
/// The calculated size based on the given image
|
||||||
pub calculated_size: CalculatedSize,
|
pub calculated_size: CalculatedSize,
|
||||||
|
@ -106,7 +111,8 @@ pub struct ImageBundle {
|
||||||
pub struct TextBundle {
|
pub struct TextBundle {
|
||||||
/// Describes the logical size of the node
|
/// Describes the logical size of the node
|
||||||
pub node: 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,
|
pub style: Style,
|
||||||
/// Contains the text of the node
|
/// Contains the text of the node
|
||||||
pub text: Text,
|
pub text: Text,
|
||||||
|
@ -186,7 +192,7 @@ impl TextBundle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns this [`TextBundle`] with a new [`Style`].
|
/// 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.style = style;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -205,7 +211,8 @@ pub struct ButtonBundle {
|
||||||
pub node: Node,
|
pub node: Node,
|
||||||
/// Marker component that signals this node is a button
|
/// Marker component that signals this node is a button
|
||||||
pub button: 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,
|
pub style: Style,
|
||||||
/// Describes whether and how the button has been interacted with by the input
|
/// Describes whether and how the button has been interacted with by the input
|
||||||
pub interaction: Interaction,
|
pub interaction: Interaction,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -331,6 +331,7 @@ Example | Description
|
||||||
Example | Description
|
Example | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
|
[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
|
[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)
|
[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
|
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
|
||||||
|
|
218
examples/ui/grid.rs
Normal file
218
examples/ui/grid.rs
Normal 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,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue