Skip to content

Instantly share code, notes, and snippets.

@tigregalis
Created April 25, 2023 15:10
Show Gist options
  • Save tigregalis/51d0b08426da5c53779bcbb944a8e071 to your computer and use it in GitHub Desktop.
Save tigregalis/51d0b08426da5c53779bcbb944a8e071 to your computer and use it in GitHub Desktop.
bevy-style-stack
[package]
name = "bevy-style-stack"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies.bevy]
git = "https://github.com/bevyengine/bevy"
rev = "6f291a0"
use bevy::prelude::*;
// -- application --
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(StyleStackPlugin)
.add_systems(Startup, setup)
.add_systems(PreUpdate, toggle_styles)
.run();
}
#[derive(Resource)]
struct MyStyles {
base_style: StyleLayerEntity,
hidden_style: StyleLayerEntity,
bg_style: StyleLayerEntity,
}
#[derive(Component)]
struct Index(usize);
#[derive(Component)]
struct SelectedIndexText;
#[derive(Component)]
struct ToggleVisibility;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// set up camera
commands.spawn(Camera2dBundle::default());
// set up styles
let base_style = commands
.spawn(StyleLayer {
display: Some(Display::Flex),
background_color: Some(BackgroundColor(Color::BLACK)),
..default()
})
.id();
let hidden_style = commands
.spawn(StyleLayer {
display: Some(Display::None),
..default()
})
.id();
let bg_style = commands
.spawn(StyleLayer {
background_color: Some(BackgroundColor(Color::GREEN)),
..default()
})
.id();
commands.insert_resource(MyStyles {
base_style,
hidden_style,
bg_style,
});
// set up text style
let text_style = TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::WHITE,
};
// spawn a bunch of entities
commands
.spawn(NodeBundle {
style: Style {
display: Display::Flex,
flex_direction: FlexDirection::Column,
justify_items: JustifyItems::Center,
align_items: AlignItems::Center,
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn((TextBundle {
text: Text {
sections: vec![
TextSection::new(
"Use arrows or numbers to make a selection.\nPress space or enter to toggle visibility.\nCurrent selection: ",
text_style.clone(),
),
TextSection::new("0", text_style.clone()),
],
..default()
},
..default()
}, SelectedIndexText));
for i in 0..10 {
parent
.spawn((
Index(i),
StyleStack(if i == 0 {
vec![base_style, bg_style]
} else {
vec![base_style]
}),
NodeBundle {
style: Style {
size: Size::new(Val::Px(100.0), Val::Percent(100.)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
..default()
},
ToggleVisibility,
))
.with_children(|parent| {
parent.spawn(TextBundle {
text: Text::from_section(
format!("{i}"),
text_style.clone(),
),
..default()
});
});
}
});
}
fn toggle_styles(
mut selected: Local<usize>,
keys: Res<Input<KeyCode>>,
mut query: Query<(&Index, &mut StyleStack), With<ToggleVisibility>>,
mut selected_index_text: Query<&mut Text, With<SelectedIndexText>>,
styles: Res<MyStyles>,
) {
for key in keys.get_just_pressed() {
let should_update_selection = match key {
// use space or enter to toggle Display::None
// (base style with Display::Flex still applies regardless)
KeyCode::Space | KeyCode::NumpadEnter | KeyCode::Return => {
if let Some((_, mut style_stack)) =
query.iter_mut().find(|(index, _)| index.0 == *selected)
{
style_stack.toggle(styles.hidden_style);
bevy::log::info!("toggled hidden style");
}
None
}
// use numbers or arrow keys to select
KeyCode::Key0 | KeyCode::Numpad0 => Some(0),
KeyCode::Key1 | KeyCode::Numpad1 => Some(1),
KeyCode::Key2 | KeyCode::Numpad2 => Some(2),
KeyCode::Key3 | KeyCode::Numpad3 => Some(3),
KeyCode::Key4 | KeyCode::Numpad4 => Some(4),
KeyCode::Key5 | KeyCode::Numpad5 => Some(5),
KeyCode::Key6 | KeyCode::Numpad6 => Some(6),
KeyCode::Key7 | KeyCode::Numpad7 => Some(7),
KeyCode::Key8 | KeyCode::Numpad8 => Some(8),
KeyCode::Key9 | KeyCode::Numpad9 => Some(9),
KeyCode::Up => Some(selected.max(1) - 1),
KeyCode::Down => Some(selected.min(8) + 1),
_ => None,
};
if let Some(new_selection) = should_update_selection {
*selected = new_selection;
for (index, mut style_stack) in query.iter_mut() {
if index.0 == *selected {
style_stack.add(styles.bg_style);
bevy::log::info!("added background style");
} else {
style_stack.remove(styles.bg_style);
}
}
selected_index_text.single_mut().sections[1].value = format!("{}", *selected);
}
}
}
// -- plugin --
struct StyleStackPlugin;
impl Plugin for StyleStackPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, update_styles);
}
}
fn update_styles(
mut query: Query<(&mut Style, &mut BackgroundColor, Ref<StyleStack>), Without<StyleLayer>>,
styles: Query<&StyleLayer>,
) {
for (mut style, mut bg_color, style_stack) in query.iter_mut() {
if style_stack.is_changed() {
// TODO: handle when the reference style layer itself is changed
*bg_color = style_stack
.0
.iter()
.rev()
.find_map(|style_entity| {
styles
.get(*style_entity)
.ok()
.and_then(|style| style.background_color)
})
.unwrap_or(BackgroundColor::DEFAULT);
style.display = style_stack
.0
.iter()
.rev()
.find_map(|style_entity| {
styles
.get(*style_entity)
.ok()
.and_then(|style| style.display)
})
.unwrap_or(Style::DEFAULT.display);
// TODO: handle other style properties
}
}
}
type StyleLayerEntity = Entity;
/// A stack of [`StyleLayerEntity`]s.
///
/// Similar to the browser [Element: classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) API.
#[derive(Component, Default, Debug)]
struct StyleStack(Vec<StyleLayerEntity>);
impl StyleStack {
/// add a [`StyleLayerEntity`] on top of the stack
///
/// first removes it from the stack if already present, so that the newly added entity is on top
fn add(&mut self, entity: StyleLayerEntity) {
self.remove(entity);
self.0.push(entity);
}
/// remove a [`StyleLayerEntity`] if present
fn remove(&mut self, entity: StyleLayerEntity) {
self.0.retain(|e| e != &entity);
}
/// replace a [`StyleLayerEntity`] if present
///
/// does not replace if not already present
fn replace(&mut self, old_entity: StyleLayerEntity, new_entity: StyleLayerEntity) {
let Some(pos) = self.0
.iter()
.position(|e| *e == old_entity) else {
return;
};
self.0[pos] = new_entity;
}
/// toggle a [`StyleLayerEntity`]
fn toggle(&mut self, entity: StyleLayerEntity) {
if let Some(pos) = self.0.iter().position(|e| *e == entity) {
self.0.remove(pos);
} else {
self.add(entity);
}
}
/// clear the stack
fn clear(&mut self) {
self.0.clear();
}
}
#[derive(Component, Clone, Debug, Default)]
pub struct StyleLayer {
/// The background color of the node
///
/// This serves as the "fill" color.
/// When combined with [`UiImage`], tints the provided texture.
pub background_color: Option<BackgroundColor>,
/// Which layout algorithm to use when laying out this node's contents:
/// - [`Display::Flex`]: Use the Flexbox layout algorithm
/// - [`Display::Grid`]: Use the CSS Grid layout algorithm
/// - [`Display::None`]: Hide this node and perform layout as if it does not exist.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/display>
pub display: Option<Display>,
/// Whether a node should be laid out in-flow with, or independently of it's siblings:
/// - [`PositionType::Relative`]: Layout this node in-flow with other nodes using the usual (flexbox/grid) layout algorithm.
/// - [`PositionType::Absolute`]: Layout this node on top and independently of other nodes.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/position>
pub position_type: Option<PositionType>,
/// Whether overflowing content should be displayed or clipped.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
pub overflow: Option<Overflow>,
/// Defines the text direction. For example English is written LTR (left-to-right) while Arabic is written RTL (right-to-left).
///
/// Note: the corresponding CSS property also affects box layout order, but this isn't yet implemented in bevy.
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/direction>
pub direction: Option<Direction>,
/// The horizontal position of the left edge of the node.
/// - For relatively positioned nodes, this is relative to the node's position as computed during regular layout.
/// - For absolutely positioned nodes, this is relative to the *parent* node's bounding box.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/left>
pub left: Option<Val>,
/// The horizontal position of the right edge of the node.
/// - For relatively positioned nodes, this is relative to the node's position as computed during regular layout.
/// - For absolutely positioned nodes, this is relative to the *parent* node's bounding box.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/right>
pub right: Option<Val>,
/// The vertical position of the top edge of the node.
/// - For relatively positioned nodes, this is relative to the node's position as computed during regular layout.
/// - For absolutely positioned nodes, this is relative to the *parent* node's bounding box.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/top>
pub top: Option<Val>,
/// The vertical position of the bottom edge of the node.
/// - For relatively positioned nodes, this is relative to the node's position as computed during regular layout.
/// - For absolutely positioned nodes, this is relative to the *parent* node's bounding box.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/bottom>
pub bottom: Option<Val>,
/// The ideal size of the node
///
/// `size.width` is used when it is within the bounds defined by `min_size.width` and `max_size.width`.
/// `size.height` is used when it is within the bounds defined by `min_size.height` and `max_size.height`.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/width> <br />
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/height>
pub size: Option<Size>,
/// The minimum size of the node
///
/// `min_size.width` is used if it is greater than either `size.width` or `max_size.width`, or both.
/// `min_size.height` is used if it is greater than either `size.height` or `max_size.height`, or both.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/min-width> <br />
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/min-height>
pub min_size: Option<Size>,
/// The maximum size of the node
///
/// `max_size.width` is used if it is within the bounds defined by `min_size.width` and `size.width`.
/// `max_size.height` is used if it is within the bounds defined by `min_size.height` and `size.height.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/max-width> <br />
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/max-height>
pub max_size: Option<Size>,
/// The aspect ratio of the node (defined as `width / height`)
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio>
pub aspect_ratio: Option<Option<f32>>,
/// For Flexbox containers:
/// - Sets default cross-axis alignment of the child items.
/// For CSS Grid containers:
/// - Controls block (vertical) axis alignment of children of this grid container within their grid areas
///
/// This value is overriden [`JustifySelf`] on the child node is set.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/align-items>
pub align_items: Option<AlignItems>,
/// For Flexbox containers:
/// - This property has no effect. See `justify_content` for main-axis alignment of flex items.
/// For CSS Grid containers:
/// - Sets default inline (horizontal) axis alignment of child items within their grid areas
///
/// This value is overriden [`JustifySelf`] on the child node is set.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items>
pub justify_items: Option<JustifyItems>,
/// For Flexbox items:
/// - Controls cross-axis alignment of the item.
/// For CSS Grid items:
/// - Controls block (vertical) axis alignment of a grid item within it's grid area
///
/// If set to `Auto`, alignment is inherited from the value of [`AlignItems`] set on the parent node.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/align-self>
pub align_self: Option<AlignSelf>,
/// For Flexbox items:
/// - This property has no effect. See `justify_content` for main-axis alignment of flex items.
/// For CSS Grid items:
/// - Controls inline (horizontal) axis alignment of a grid item within it's grid area.
///
/// If set to `Auto`, alignment is inherited from the value of [`JustifyItems`] set on the parent node.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items>
pub justify_self: Option<JustifySelf>,
/// For Flexbox containers:
/// - Controls alignment of lines if flex_wrap is set to [`FlexWrap::Wrap`] and there are multiple lines of items
/// For CSS Grid container:
/// - Controls alignment of grid rows
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/align-content>
pub align_content: Option<AlignContent>,
/// For Flexbox containers:
/// - Controls alignment of items in the main axis
/// For CSS Grid containers:
/// - Controls alignment of grid columns
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content>
pub justify_content: Option<JustifyContent>,
/// The amount of space around a node outside its border.
///
/// If a percentage value is used, the percentage is calculated based on the width of the parent node.
///
/// # Example
/// ```
/// # use bevy_ui::{Style, UiRect, Val};
/// let style = Style {
/// margin: UiRect {
/// left: Val::Percent(10.),
/// right: Val::Percent(10.),
/// top: Val::Percent(15.),
/// bottom: Val::Percent(15.)
/// },
/// ..Default::default()
/// };
/// ```
/// A node with this style and a parent with dimensions of 100px by 300px, will have calculated margins of 10px on both left and right edges, and 15px on both top and bottom edges.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/margin>
pub margin: Option<UiRect>,
/// The amount of space between the edges of a node and its contents.
///
/// If a percentage value is used, the percentage is calculated based on the width of the parent node.
///
/// # Example
/// ```
/// # use bevy_ui::{Style, UiRect, Val};
/// let style = Style {
/// padding: UiRect {
/// left: Val::Percent(1.),
/// right: Val::Percent(2.),
/// top: Val::Percent(3.),
/// bottom: Val::Percent(4.)
/// },
/// ..Default::default()
/// };
/// ```
/// A node with this style and a parent with dimensions of 300px by 100px, will have calculated padding of 3px on the left, 6px on the right, 9px on the top and 12px on the bottom.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/padding>
pub padding: Option<UiRect>,
/// The amount of space between the margins of a node and its padding.
///
/// If a percentage value is used, the percentage is calculated based on the width of the parent node.
///
/// The size of the node will be expanded if there are constraints that prevent the layout algorithm from placing the border within the existing node boundary.
///
/// Rendering for borders is not yet implemented.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
pub border: Option<UiRect>,
/// Whether a Flexbox container should be a row or a column. This property has no effect of Grid nodes.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction>
pub flex_direction: Option<FlexDirection>,
/// Whether a Flexbox container should wrap it's contents onto multiple line wrap if they overflow. This property has no effect of Grid nodes.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap>
pub flex_wrap: Option<FlexWrap>,
/// Defines how much a flexbox item should grow if there's space available. Defaults to 0 (don't grow at all).
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/flex-grow>
pub flex_grow: Option<f32>,
/// Defines how much a flexbox item should shrink if there's not enough space available. Defaults to 1.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/flex-shrink>
pub flex_shrink: Option<f32>,
/// The initial length of a flexbox in the main axis, before flex growing/shrinking properties are applied.
///
/// `flex_basis` overrides `size` on the main axis if both are set, but it obeys the bounds defined by `min_size` and `max_size`.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis>
pub flex_basis: Option<Val>,
/// The size of the gutters between items in flexbox layout or rows/columns in a grid layout
///
/// Note: Values of `Val::Auto` are not valid and are treated as zero.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gap>
pub gap: Option<Size>,
/// Controls whether automatically placed grid items are placed row-wise or column-wise. And whether the sparse or dense packing algorithm is used.
/// Only affect Grid layouts
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-flow>
pub grid_auto_flow: Option<GridAutoFlow>,
/// Defines the number of rows a grid has and the sizes of those rows. If grid items are given explicit placements then more rows may
/// be implicitly generated by items that are placed out of bounds. The sizes of those rows are controlled by `grid_auto_rows` property.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-rows>
pub grid_template_rows: Option<Vec<RepeatedGridTrack>>,
/// Defines the number of columns a grid has and the sizes of those columns. If grid items are given explicit placements then more columns may
/// be implicitly generated by items that are placed out of bounds. The sizes of those columns are controlled by `grid_auto_columns` property.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns>
pub grid_template_columns: Option<Vec<RepeatedGridTrack>>,
/// Defines the size of implicitly created rows. Rows are created implicitly when grid items are given explicit placements that are out of bounds
/// of the rows explicitly created using `grid_template_rows`.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-rows>
pub grid_auto_rows: Option<Vec<GridTrack>>,
/// Defines the size of implicitly created columns. Columns are created implicitly when grid items are given explicit placements that are out of bounds
/// of the columns explicitly created using `grid_template_columms`.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns>
pub grid_auto_columns: Option<Vec<GridTrack>>,
/// The row in which a grid item starts and how many rows it spans.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row>
pub grid_row: Option<GridPlacement>,
/// The column in which a grid item starts and how many columns it spans.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column>
pub grid_column: Option<GridPlacement>,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment