Introduction
Bevy ECSS is a crate which allows the usage of a subset of CSS
to interact with bevy_ecs
. It's mainly aimed to apply styling on bevy_ui
but it can be used by any component by implementing custom properties.
Why the name?
Just because Bevy ECS + CSS is a perfect fit!
Usage
To use Bevy ECSS just add a StyleSheet
with a loaded css
file to any entity and all style sheet rules will be applied to the entity and all its descendants
(children of children of children and so on).
use bevy::prelude::*;
use bevy_ecss::prelude::*;
fn setup_awesome_ui(root: Entity, mut commands: Commands, asset_server: Res<AssetServer>) {
commands
.entity(root)
.insert(StyleSheet::new(asset_server.load("sheets/awesome.css")));
}
That's it, now your UI will indeed look awesome!
Bevy support table
bevy | bevy_ecss |
---|---|
0.8 | 0.1 |
0.9 | 0.2 |
0.10 | 0.3 |
0.11 | 0.4 |
0.12 | 0.5 |
Contributing
Got some idea, feedback, question or found any bug? Feel free to open an issue at any time!
License
Bevy ECSS is dual-licensed under either:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both.
Changelog
[0.7.0]
Added
- Added support for applying multiple style sheets to an entity #45
- Added quality of life methods for modifying class lists on existing class components. #48
- Added support for
row-gap
andcolumn-gap
properties. #49 - Added support for
Any
(*) selector. #55
Changed
- Fixed wrong properties assignments #42
- Fixed bug with style sheets being applied in a non-deterministic order #51
[0.6.0]
Added
- Added watch for changes on entities with components affected by css.
Changed
- Fixed CSS precedence order, more broad rules are applied first.
[0.5.1]
Added
- Added support for
:hover
pseudo-class. - Added support for Pseudo-classes.
[0.5.1]
Added
- New selectors for setting up BorderColor and Image texture.
- New documentation available on: link.
Changed
- Updated
cssparser
to0.33
. - Use color function from
cssparser
.
[0.5.0]
Added
- Support for auto value
Changed
- Upgraded to Bevy 0.12.
[0.4.0]
Changed
- Upgraded to Bevy 0.11.
[0.3.0]
Added
TextAlignProperty
Changed
- Upgraded to Bevy 0.10.
Removed
VerticalAlignProperty
andHorizontalAlignProperty
[0.2.0]
Changed
- Upgraded to Bevy 0.9.
[0.1.0]
Added
- First version
CSS Subset
Bevy ECSS only supports a subset of CSS at moment, since many properties and features requires more advanced selectors, components and properties which currently isn't implemented.
Here you can find a list of all currently supported selectors and properties:
Selectors
Type | Details | Example |
---|---|---|
Name | Selects by using bevy built-int Name component. | #inventory { ... } |
Class | Selects by using Class component, which is provided by Bevy ECSS. | .enabled { ... } |
Component | Selects by using any component, but it has to be registered before usage. You can find more details bellow. | button { ... } |
PseudoClass | Selects by using pseudo-classes. A list of supported pseudo-classes are listed at the end of the page. | :hover { ... } |
You may combine any of the above selector types to create a complex selector, if you like so. For instance, window.enabled.pop-up
select all window
s, which are enabled
and are of pop-up
type. The same rules of CSS Class selectors
applies here.
This assumes that window
is a bevy_ecs
component and was registered before usage. Also assumes the entities has the Class
component with at least enabled pop-up
class name.
Aditionally, Bevy ECSS also supports descendant combinator
which selects all entities that are descendant the given selector tree.
#quest-window text {
color: red;
}
The above rule will match all entities which has a Text
component and is descendant of any entity which as a Name
component which the value of quest-window
.
So it's possible to combine complex composed selectors with descendant combinator.
#main-menu button.enabled .border {
background-color: #ff03ab;
}
This rule will match all components which has a Class
with the value of border
and are descendant of any entity which has a button
component and a Class
component with the value of enabled
and also are descendant of any entity which has a Name
component with value main-menu
.
Supported pseudo-classes
Pseudo-Class | Description |
---|---|
:hover | Matches any entity which has Interaction component with Interaction::Hovered variant. |
Properties
Here is a list of all currently supported properties. Note that these are properties which are provived by Bevy ECSS but you can also add your own properties at anytime.
Before reading properties description, we'll use this notation to describe accepted values:
Notation | Description |
---|---|
00.00% | Any percent value, like 93% or 4.45% |
00.00px | Any dimensional value, like 11px or 0.99px |
00.00 | Any number value, like 0 or 14.2 |
<ident> | <ident> | Only one of the identifiers are allowed, without quotes, like none or hidden |
<area-short-hand > | Allows the short hand area constructor by using either dimensions or percentage, like 10px or 5% 10px 3% auto . No global values are supported yet |
Style
properties
Property | Values | Description |
---|---|---|
display | flex | none | Applies the display property on display field of all sections on matched Style components. |
position-type | absolute | relative | Applies the position-type property on position_type field of all sections on matched Style components. |
direction | inherit | left-to-right | right-to-left | Applies the direction property on direction field of all sections on matched Style components. |
flex-direction | row | column | row-reverse | column-reverse | Applies the flex-direction property on flex_direction field of all sections on matched Style components. |
flex-wrap | no-wrap | wrap | wrap-reverse | Applies the flex-wrap property on flex_wrap field of all sections on matched Style components. |
align-items | flex-start | flex-end | center | baseline | stretch | Applies the align-items property on align_items field of all sections on matched Style components. |
align-self | auto | flex-start | flex-end | center | baseline | stretch | Applies the align-self property on align_self field of all sections on matched Style components. |
align-content | flex-start | flex-end | center | stretch | space-between | space-around | Applies the align-content property on align_content field of all sections on matched Style components. |
justify-content | flex-start | flex-end | center | space-between | space-around | space-evenly | Applies the justify-content property on justify_content field of all sections on matched Style components. |
overflow-x | visible | hidden | Applies the overflow-x property on overflow.x field of all sections on matched Style components. |
overflow-y | visible | hidden | Applies the overflow-y property on overflow.y field of all sections on matched Style components. |
left | 00.00% | 00.00px | Applies the property on left field of all matched components. |
right | 00.00% | 00.00px | Applies the property on right field of all matched components. |
top | 00.00% | 00.00px | Applies the property on top field of all matched components. |
bottom | 00.00% | 00.00px | Applies the property on bottom field of all matched components. |
width | 00.00% | 00.00px | Applies the property on width field of all matched components. |
height | 00.00% | 00.00px | Applies the property on height field of all matched components. |
min-width | 00.00% | 00.00px | Applies the property on min_width field of all matched components. |
min-height | 00.00% | 00.00px | Applies the property on min_height field of all matched components. |
max-width | 00.00% | 00.00px | Applies the property on max_width field of all matched components. |
max-height | 00.00% | 00.00px | Applies the property on max_height field of all matched components. |
flex-basis | 00.00% | 00.00px | Applies the property on flex_basis field of all matched components. |
flex-grow | 0 | 1 | 2 | Applies the property on flex_grow field of all matched components. |
flex-shrink | 0 | 1 | 2 | Applies the property on flex_shrink field of all matched components. |
aspect-ratio | 00.00 | none | Applies the property on aspect_ratio field of all matched components. |
margin | <area-short-hand > | Applies the property on margin field of all matched components. |
padding | <area-short-hand > | Applies the property on padding field of all matched components. |
border | <area-short-hand > | Applies the property on border field of all matched components. |
row-gap | 00.00% | 00.00px | Applies the property on row_gap field of all matched components. |
column-gap | 00.00% | 00.00px | Applies the property on column_gap field of all matched components. |
Text
properties
Property | Values | Description |
---|---|---|
color | named-colors | hex_colors | Applies the property on style.color for all sections of matched components. |
font | "path/to/font.ttf" | Applies the property on style.font for all sections of matched components. |
font-size | 00.00 | Applies the property on style.font_size for all sections of matched components. |
text-content | "Some text value" | Applies the property on value for all sections of matched components. |
text-align | left | center | right | Applies the property on alignment of all matched components. |
Components properties
Property | Values | Description |
---|---|---|
background-color | named-colors | hex_colors | Applies the property on BackgroundColor of all matched components. |
border-color | named-colors | hex_colors | Applies the property on BorderColor of all matched components. |
Image properties
Property | Values | Description |
---|---|---|
image-path | "path/to/image.png" | Applies the property on image.texture for all images of matched components. |
Component Selector Builtin
Bevy ECSS provites the following components selector:
Selector | Component |
---|---|
background-color | BackgroundColor |
text | Text |
button | Button |
node | Node |
style | Style |
ui-image | UiImage |
interaction | Interaction |
This list will be expanded to match bevy_ui
and other bevy
core components.
Custom Component Selector
You may also register your own components or alias/overwrite builtin components selector.
use bevy::prelude::*;
use bevy_ecss::prelude::*;
#[derive(Component)]
struct MyFancyComponentSelector;
#[derive(Component)]
struct FancyColor;
fn some_main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins).add_plugins(EcssPlugin::default());
// You may use it as selector now, like
// fancy-pants {
// background-color: pink;
// }
app.register_component_selector::<MyFancyComponentSelector>("fancy-pants");
// Or you can overwrite a component selector.
app.register_component_selector::<FancyColor>("background-color");
}
Custom property
It is possible to implement your own properties, be it part of CSS
standard or not.
Let's implement a custom alpha
property with will set the alpha channel of any BackgroundColor
.
use bevy::{ecs::query::QueryItem, prelude::*};
use bevy_ecss::{prelude::*, EcssError, Property, PropertyValues};
#[derive(Default)]
pub(crate) struct AlphaProperty;
impl Property for AlphaProperty {
// This is the cached value to be used when applying the property value.
// It is evaluated only on the first time and futures runs are cached for performance reasons.
type Cache = f32;
// Which components the property needs when applying the cached value.
// It is the same as using bevy_ecs Query<C, F>.
type Components = &'static mut BackgroundColor;
// If this property can be set only when there is another property, it's possible to filter here.
// It's not recommended to use only With<> and Without<>.
type Filters = ();
fn name() -> &'static str {
// The name of property. prefer kebab-case for consistency.
"alpha"
}
fn parse<'a>(values: &PropertyValues) -> Result<Self::Cache, EcssError> {
// PropertyValues::f32 tries to parse property value into a numeric value
if let Some(value) = values.f32() {
Ok(value)
} else {
Err(EcssError::InvalidPropertyValue(Self::name().to_string()))
}
}
// This function will be called for every entity matched on every rule selector.
fn apply<'w>(
cache: &Self::Cache,
mut components: QueryItem<Self::Components>,
_asset_server: &AssetServer,
_commands: &mut Commands,
) {
components.0.set_a(*cache);
}
}
Now just register the property on App
:
app.register_property::<AlphaProperty>();
Done! Whenever an alpha
property is found on any css
file, the AlphaProperty
will be applied. You can find this full example here
.
Simple example
CSS
#ui-root {
background-color: #3b3a3a
}
#left-border {
margin: 0px 0px 0px 50px;
border: 0px;
padding: 5px;
background-color: #000000;
}
#ui-root #left-bg {
display: flex;
}
#right-list {
height: 50%;
}
.big-text {
font-size: 15;
width: 100%;
color: violet;
}
.container .blue-bg {
background-color: #ff00FF10;
}
* {
color: cyan;
}
Code
use bevy::prelude::*;
use bevy_ecss::prelude::{Class, EcssPlugin, StyleSheet};
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
canvas: Some("#bevy".to_string()),
..default()
}),
..default()
}))
.add_plugins(EcssPlugin::default())
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Camera
commands.spawn(Camera2dBundle::default());
// root node
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
..default()
},
background_color: Color::NONE.into(),
..default()
})
.insert(StyleSheet::new(asset_server.load("sheets/simple_ui.css")))
.insert(Name::new("ui-root"))
.with_children(|parent| {
// left vertical fill (border)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(200.0),
height: Val::Percent(100.0),
border: UiRect::all(Val::Px(2.0)),
..default()
},
background_color: Color::rgb(0.65, 0.65, 0.65).into(),
..default()
})
.insert(Name::new("left-border"))
.with_children(|parent| {
// left vertical fill (content)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::FlexEnd,
..default()
},
background_color: Color::rgb(0.15, 0.15, 0.15).into(),
..default()
})
.insert(Name::new("left-bg"))
.with_children(|parent| {
// text
parent
.spawn(
TextBundle::from_section(
"Text Example",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::WHITE,
},
)
.with_style(Style {
margin: UiRect::all(Val::Px(5.0)),
..default()
}),
)
.insert(Name::new("left-text"));
});
});
// right vertical fill
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::ColumnReverse,
justify_content: JustifyContent::Center,
width: Val::Px(200.0),
height: Val::Percent(100.0),
..default()
},
background_color: Color::rgb(0.15, 0.15, 0.15).into(),
..default()
})
.insert(Name::new("right-border"))
.with_children(|parent| {
// Title
parent
.spawn(
TextBundle::from_section(
"Scrolling list",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 25.,
color: Color::WHITE,
},
)
.with_style(Style {
height: Val::Px(25.0),
margin: UiRect {
left: Val::Auto,
right: Val::Auto,
..default()
},
..default()
}),
)
.insert(Name::new("right-bg"));
// List with hidden overflow
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::ColumnReverse,
align_self: AlignSelf::Center,
width: Val::Percent(100.0),
height: Val::Percent(50.0),
overflow: Overflow::clip(),
..default()
},
background_color: Color::rgb(0.10, 0.10, 0.10).into(),
..default()
})
.insert(Name::new("right-list"))
.with_children(|parent| {
// Moving panel
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::ColumnReverse,
flex_grow: 1.0,
..default()
},
background_color: Color::NONE.into(),
..default()
})
.insert(Name::new("right-moving-panel"))
.with_children(|parent| {
// List items
for i in 0..30 {
parent
.spawn(
TextBundle::from_section(
format!("Item {i}"),
TextStyle {
font: asset_server
.load("fonts/FiraSans-Bold.ttf"),
font_size: 20.,
color: Color::WHITE,
},
)
.with_style(Style {
flex_shrink: 0.,
height: Val::Px(20.),
margin: UiRect {
left: Val::Auto,
right: Val::Auto,
..default()
},
..default()
}),
)
.insert(Class::new("big-text"))
.insert(Name::new(format!("right-item-{}", i)));
}
});
});
});
// absolute positioning
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(200.0),
height: Val::Px(200.0),
position_type: PositionType::Absolute,
left: Val::Px(210.0),
bottom: Val::Px(10.0),
border: UiRect::all(Val::Px(20.0)),
..default()
},
background_color: Color::rgb(0.4, 0.4, 1.0).into(),
..default()
})
.insert(Name::new("mid-blue-border"))
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
background_color: Color::rgb(0.8, 0.8, 1.0).into(),
..default()
})
.insert(Name::new("mid-navy-blue-content"));
});
// render order test: reddest in the back, whitest in the front (flex center)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
background_color: Color::NONE.into(),
..default()
})
.insert(Name::new("mid-red-last"))
.insert(Class::new("blue-bg container"))
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
..default()
},
background_color: Color::rgb(1.0, 0.0, 0.0).into(),
..default()
})
.insert(Name::new("mid-red-last-but-one"))
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(20.0),
bottom: Val::Px(20.0),
..default()
},
background_color: Color::rgb(1.0, 0.3, 0.3).into(),
..default()
})
.insert(Name::new("mid-red-center"));
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(40.0),
bottom: Val::Px(40.0),
..default()
},
background_color: Color::rgb(1.0, 0.5, 0.5).into(),
..default()
})
.insert(Class::new("blue-bg"))
.insert(Name::new("mid-red-top-but-one"));
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(60.0),
bottom: Val::Px(60.0),
..default()
},
background_color: Color::rgb(1.0, 0.7, 0.7).into(),
..default()
})
.insert(Name::new("mid-red-top"));
// alpha test
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(80.0),
bottom: Val::Px(80.0),
..default()
},
background_color: Color::rgba(1.0, 0.9, 0.9, 0.4).into(),
..default()
})
.insert(Class::new("blue-bg"))
.insert(Name::new("mid-red-alpha"));
});
});
// bevy logo (flex center)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd,
..default()
},
background_color: Color::NONE.into(),
..default()
})
.insert(Name::new("mid-bevy-logo-bg"))
.with_children(|parent| {
// bevy logo (image)
parent
.spawn(ImageBundle {
style: Style {
width: Val::Px(500.0),
..default()
},
image: asset_server.load("branding/bevy_logo_dark_big.png").into(),
..default()
})
.insert(Name::new("mid-bevy-logo-image"));
});
});
}
Custom property
CSS
ui-image {
alpha: 0.3;
image-path: "branding/bevy_logo_dark_big.png";
width: 100%;
height: auto;
}
Code
use bevy::{ecs::query::QueryItem, prelude::*};
use bevy_ecss::{prelude::*, EcssError, Property, PropertyValues};
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
canvas: Some("#bevy".to_string()),
..default()
}),
..default()
}))
.add_plugins(EcssPlugin::default())
.add_systems(Startup, setup);
app.register_property::<AlphaProperty>();
app.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
..default()
},
background_color: Color::NONE.into(),
..default()
})
.insert(StyleSheet::new(asset_server.load("sheets/alpha.css")))
.with_children(|parent| {
// bevy logo (image)
parent.spawn(ImageBundle::default());
});
}
#[derive(Default)]
pub(crate) struct AlphaProperty;
impl Property for AlphaProperty {
// This is the cached value to be used when applying the property value.
// It is evaluated only on the first time and futures values are cached for performance reasons.
type Cache = f32;
// Which components we need when applying the cache. It is the same as using bevy ecs Query.
type Components = &'static mut BackgroundColor;
// If this property can be set only when there is another property, we may filter there.
// It's not recommended to use only With<> and Without<>.
type Filters = ();
fn name() -> &'static str {
// The name of property. prefer kebab-case for consistency.
"alpha"
}
fn parse<'a>(values: &PropertyValues) -> Result<Self::Cache, EcssError> {
// PropertyValues::f32 tries to parse property value into a numeric value
if let Some(value) = values.f32() {
Ok(value)
} else {
Err(EcssError::InvalidPropertyValue(Self::name().to_string()))
}
}
fn apply<'w>(
cache: &Self::Cache,
mut components: QueryItem<Self::Components>,
_asset_server: &AssetServer,
_commands: &mut Commands,
) {
components.0.set_a(*cache);
}
}
Theme example
CSS
Dark theme
#ui-root {
background-color: #181414
}
#left-border {
margin: 0px;
border: 0px;
padding: 5px;
background-color: #353535;
}
#ui-root #left-bg {
display: flex;
background-color: #242424;
}
#right-border {
background-color: #242424;
}
#left-bg text {
color: #b3b3b3
}
#right-list {
height: 50%;
background-color: #3b3b3b;
}
.big-text {
font-size: 15;
width: 100%;
color: #7c7c7c
}
.big-text-red {
font-size: 15;
width: 100%;
color: red;
}
text {
color: blue;
}
#mid-blue-border {
background-color: #0a0a24;
}
#mid-blue-border node {
background-color: #383838;
}
title {
font-size: 22;
vertical-align: bottom;
color: #b3b3b3;
}
.container .blue-bg {
background-color: #ff00FF10;
}
#mid-bevy-logo-image {
background-color: #aaaaaa;
}
button {
align-items: center;
align-content: center;
}
button text {
width: 100%;
height: 25px;
left: 10px;
font-size: 20;
color: azure;
text-content: "Darkness!";
}
button:hover text {
text-content: "Switch to Lightness?";
}
Light theme
#ui-root {
background-color: #bbb6b6
}
#left-border {
margin: 0px;
border: 0px;
padding: 5px;
background-color: #dddddd;
}
#ui-root #left-bg {
display: flex;
background-color: darkgray;
}
#right-border {
background-color: #a5a1a1;
}
#left-bg text {
color: #252525
}
#right-list {
height: 50%;
background-color: #ebebeb;
}
.big-text {
font-size: 15;
width: 100%;
color: #252525
}
#mid-blue-border {
background-color: #28288d;
}
#mid-blue-border node {
background-color: #a5a1a1;
}
title {
font-size: 22;
vertical-align: bottom;
color: #3f3f3f;
}
.container .blue-bg {
background-color: #44004410;
}
#mid-bevy-logo-image {
background-color: #424242;
}
button {
align-items: center;
align-content: center;
}
button text {
width: 100%;
height: 25px;
left: 10px;
font-size: 20;
color: #3b3f3f;
text-content: "Lightness!!!!";
}
button:hover text {
text-content: "Switch to Darkness?";
}
Code
use bevy::{prelude::*, ui::FocusPolicy};
use bevy_ecss::prelude::{
Class, EcssPlugin, RegisterComponentSelector, StyleSheet, StyleSheetAsset,
};
#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
struct Title;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
canvas: Some("#bevy".to_string()),
..default()
}),
..default()
}))
.add_plugins(EcssPlugin::with_hot_reload())
.add_systems(Startup, setup)
.add_systems(Update, change_theme)
.register_component_selector::<Title>("title");
app.run();
}
#[derive(Resource)]
struct Themes {
pub root: Entity,
pub dark: Handle<StyleSheetAsset>,
pub light: Handle<StyleSheetAsset>,
}
fn change_theme(
themes: Res<Themes>,
mut styles_query: Query<&mut StyleSheet>,
interaction_query: Query<&Interaction, (Changed<Interaction>, With<Button>)>,
) {
for interaction in &interaction_query {
if let Interaction::Pressed = *interaction {
if let Ok(mut sheet) = styles_query.get_mut(themes.root) {
if sheet.handles().first() == Some(&themes.dark) {
sheet.set_handles(vec![themes.light.clone()]);
} else {
sheet.set_handles(vec![themes.dark.clone()]);
}
}
}
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let dark = asset_server.load("sheets/dark_theme.css");
let light = asset_server.load("sheets/light_theme.css");
// Camera
commands.spawn(Camera2dBundle::default());
// root node
let root = commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
..default()
},
focus_policy: FocusPolicy::Pass,
background_color: Color::NONE.into(),
..default()
})
.insert(Name::new("ui-root"))
.insert(StyleSheet::new(dark.clone()))
.with_children(|parent| {
// left vertical fill (border)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(200.0),
height: Val::Percent(100.0),
border: UiRect::all(Val::Px(2.0)),
..default()
},
background_color: Color::rgb(0.65, 0.65, 0.65).into(),
..default()
})
.insert(Name::new("left-border"))
.with_children(|parent| {
// left vertical fill (content)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::FlexEnd,
..default()
},
background_color: Color::rgb(0.15, 0.15, 0.15).into(),
..default()
})
.insert(Name::new("left-bg"))
.with_children(|parent| {
// text
parent
.spawn(
TextBundle::from_section(
"Text Example",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::WHITE,
},
)
.with_style(Style {
margin: UiRect::all(Val::Px(5.0)),
..default()
}),
)
.insert(Name::new("left-text"));
});
});
// right vertical fill
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::ColumnReverse,
justify_content: JustifyContent::Center,
width: Val::Px(200.0),
height: Val::Percent(100.0),
..default()
},
background_color: Color::rgb(0.15, 0.15, 0.15).into(),
..default()
})
.insert(Name::new("right-border"))
.with_children(|parent| {
// Title
parent
.spawn(
TextBundle::from_section(
"Scrolling list",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 25.,
color: Color::WHITE,
},
)
.with_style(Style {
width: Val::Auto,
height: Val::Px(25.),
margin: UiRect {
left: Val::Auto,
right: Val::Auto,
..default()
},
..default()
}),
)
.insert(Title)
.insert(Name::new("right-bg"));
// List with hidden overflow
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::ColumnReverse,
align_self: AlignSelf::Center,
width: Val::Percent(100.0),
height: Val::Percent(50.0),
overflow: Overflow::clip(),
..default()
},
background_color: Color::rgb(0.10, 0.10, 0.10).into(),
..default()
})
.insert(Name::new("right-list"))
.with_children(|parent| {
// Moving panel
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::ColumnReverse,
flex_grow: 1.0,
..default()
},
background_color: Color::NONE.into(),
..default()
})
.insert(Name::new("right-moving-panel"))
.with_children(|parent| {
// List items
for i in 0..30 {
parent
.spawn(
TextBundle::from_section(
format!("Item {i}"),
TextStyle {
font: asset_server
.load("fonts/FiraSans-Bold.ttf"),
font_size: 20.,
color: Color::WHITE,
},
)
.with_style(Style {
flex_shrink: 0.,
height: Val::Px(20.0),
margin: UiRect {
left: Val::Auto,
right: Val::Auto,
..default()
},
..default()
}),
)
.insert(Class::new("big-text"))
.insert(Name::new(format!("right-item-{}", i)));
}
});
});
});
// render order test: reddest in the back, whitest in the front (flex center)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
background_color: Color::NONE.into(),
..default()
})
.insert(Name::new("mid-red-last"))
.insert(Class::new("blue-bg container"))
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
..default()
},
background_color: Color::rgb(1.0, 0.0, 0.0).into(),
..default()
})
.insert(Name::new("mid-red-last-but-one"))
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(20.0),
bottom: Val::Px(20.0),
..default()
},
background_color: Color::rgb(1.0, 0.3, 0.3).into(),
..default()
})
.insert(Name::new("mid-red-center"));
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(40.0),
bottom: Val::Px(40.0),
..default()
},
background_color: Color::rgb(1.0, 0.5, 0.5).into(),
..default()
})
.insert(Class::new("blue-bg"))
.insert(Name::new("mid-red-top-but-one"));
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(60.0),
bottom: Val::Px(60.0),
..default()
},
background_color: Color::rgb(1.0, 0.7, 0.7).into(),
..default()
})
.insert(Name::new("mid-red-top"));
// alpha test
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
left: Val::Px(80.0),
bottom: Val::Px(80.0),
..default()
},
background_color: Color::rgba(1.0, 0.9, 0.9, 0.4).into(),
..default()
})
.insert(Class::new("blue-bg"))
.insert(Name::new("mid-red-alpha"));
});
});
// bevy logo (flex center)
parent
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd,
..default()
},
focus_policy: FocusPolicy::Pass,
background_color: Color::NONE.into(),
..default()
})
.insert(Name::new("mid-bevy-logo-bg"))
.with_children(|parent| {
// bevy logo (image)
parent
.spawn(ImageBundle {
style: Style {
width: Val::Px(500.0),
..default()
},
image: asset_server.load("branding/bevy_logo_dark_big.png").into(),
..default()
})
.insert(Name::new("mid-bevy-logo-image"));
});
// absolute positioning
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(200.0),
height: Val::Px(200.0),
position_type: PositionType::Absolute,
left: Val::Px(210.0),
bottom: Val::Px(10.0),
border: UiRect::all(Val::Px(20.0)),
..default()
},
background_color: Color::rgb(0.4, 0.4, 1.0).into(),
..default()
})
.insert(Name::new("mid-blue-border"))
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
..default()
},
focus_policy: FocusPolicy::Pass,
background_color: Color::rgb(0.8, 0.8, 1.0).into(),
..default()
})
.insert(Name::new("mid-navy-blue-content"))
.with_children(|parent| {
parent
.spawn(ButtonBundle {
style: Style {
width: Val::Px(100.0),
height: Val::Px(100.0),
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Change Theme",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
},
));
});
});
});
})
.id();
commands.insert_resource(Themes { root, dark, light })
}
Interactive example
CSS
#ui-root {
width: 30%;
height: 100%;
justfy-content: space-between;
}
#list {
background-color: #050505;
width: 100%;
height: 100%;
flex-direction: column-reverse;
align-self: center;
overflow: clip;
}
#panel {
flex-direction: column-reverse;
flex-grow: 1.0;
}
text {
font: "fonts/FiraSans-Bold.ttf";
font-size: 20;
color: white;
flex-shrink: 0;
height: 20px;
margin: auto;
}
text:hover {
color: red;
font-size: 25;
}
Code
use bevy::{prelude::*, ui::FocusPolicy};
use bevy_ecss::prelude::{EcssPlugin, StyleSheet};
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
canvas: Some("#bevy".to_string()),
..default()
}),
..default()
}))
.add_plugins(EcssPlugin::with_hot_reload())
.add_systems(Startup, setup);
app.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Camera
commands.spawn(Camera2dBundle::default());
// root node
commands
.spawn(NodeBundle {
focus_policy: FocusPolicy::Pass,
..default()
})
.insert(Name::new("ui-root"))
.insert(StyleSheet::new(asset_server.load("sheets/interactive.css")))
.with_children(|parent| {
parent
.spawn(NodeBundle {
focus_policy: FocusPolicy::Pass,
..default()
})
.insert(Name::new("list"))
.with_children(|parent| {
// Moving panel
parent
.spawn(NodeBundle {
focus_policy: FocusPolicy::Pass,
..default()
})
.insert(Name::new("panel"))
.with_children(|parent| {
// List items
for i in 0..30 {
parent
.spawn(TextBundle::from_section(
format!("Item {i}"),
TextStyle::default(),
))
.insert(Interaction::default());
}
});
});
});
}