use serde::Deserialize;
use bevy::text::{FontWeight, Justify, LineBreak};
use bevy::ui::{
AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, FocusPolicy,
GridAutoFlow, GridPlacement, GridTrack, JustifyContent, JustifyItems, JustifySelf,
OverflowAxis, PositionType, RepeatedGridTrack,
};
use super::animatable::Animatable;
use super::background_image::{BackgroundImageSpec, de_background_image};
use super::grid::{de_grid_auto_tracks, de_grid_placement, de_grid_template};
use super::keywords::*;
use super::transform::{Transform, Transform3d};
use super::units::{FontSize, Length, Rect};
use super::visual::{
BorderColorSpec, BoxShadowList, GradientList, LetterSpacingSpec, LineHeightSpec, OutlineSpec,
TextShadowSpec,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LayerCache {
#[default]
Auto,
Always,
Never,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Style {
#[serde(default, deserialize_with = "de_display")]
pub display: Option<Display>,
#[serde(default, deserialize_with = "de_box_sizing")]
pub box_sizing: Option<BoxSizing>,
#[serde(default, deserialize_with = "de_position_type")]
pub position_type: Option<PositionType>,
#[serde(default, deserialize_with = "de_overflow_axis")]
pub overflow_x: Option<OverflowAxis>,
#[serde(default, deserialize_with = "de_overflow_axis")]
pub overflow_y: Option<OverflowAxis>,
#[serde(default)]
pub scrollbar_width: Option<f32>,
#[serde(default)]
pub left: Option<Animatable<Length>>,
#[serde(default)]
pub right: Option<Animatable<Length>>,
#[serde(default)]
pub top: Option<Animatable<Length>>,
#[serde(default)]
pub bottom: Option<Animatable<Length>>,
#[serde(default)]
pub width: Option<Animatable<Length>>,
#[serde(default)]
pub height: Option<Animatable<Length>>,
#[serde(default)]
pub min_width: Option<Animatable<Length>>,
#[serde(default)]
pub min_height: Option<Animatable<Length>>,
#[serde(default)]
pub max_width: Option<Animatable<Length>>,
#[serde(default)]
pub max_height: Option<Animatable<Length>>,
#[serde(default)]
pub aspect_ratio: Option<Animatable<f32>>,
#[serde(default, deserialize_with = "de_align_items")]
pub align_items: Option<AlignItems>,
#[serde(default, deserialize_with = "de_justify_items")]
pub justify_items: Option<JustifyItems>,
#[serde(default, deserialize_with = "de_align_self")]
pub align_self: Option<AlignSelf>,
#[serde(default, deserialize_with = "de_justify_self")]
pub justify_self: Option<JustifySelf>,
#[serde(default, deserialize_with = "de_align_content")]
pub align_content: Option<AlignContent>,
#[serde(default, deserialize_with = "de_justify_content")]
pub justify_content: Option<JustifyContent>,
#[serde(default)]
pub margin: Option<Rect>,
#[serde(default)]
pub padding: Option<Rect>,
#[serde(default)]
pub border: Option<Rect>,
#[serde(default, deserialize_with = "de_flex_direction")]
pub flex_direction: Option<FlexDirection>,
#[serde(default, deserialize_with = "de_flex_wrap")]
pub flex_wrap: Option<FlexWrap>,
#[serde(default)]
pub flex_grow: Option<f32>,
#[serde(default)]
pub flex_shrink: Option<f32>,
#[serde(default)]
pub flex_basis: Option<Animatable<Length>>,
#[serde(default)]
pub gap: Option<Animatable<Length>>,
#[serde(default)]
pub row_gap: Option<Animatable<Length>>,
#[serde(default)]
pub column_gap: Option<Animatable<Length>>,
#[serde(default, deserialize_with = "de_grid_auto_flow")]
pub grid_auto_flow: Option<GridAutoFlow>,
#[serde(default, deserialize_with = "de_grid_template")]
pub grid_template_rows: Option<Vec<RepeatedGridTrack>>,
#[serde(default, deserialize_with = "de_grid_template")]
pub grid_template_columns: Option<Vec<RepeatedGridTrack>>,
#[serde(default, deserialize_with = "de_grid_auto_tracks")]
pub grid_auto_rows: Option<Vec<GridTrack>>,
#[serde(default, deserialize_with = "de_grid_auto_tracks")]
pub grid_auto_columns: Option<Vec<GridTrack>>,
#[serde(default, deserialize_with = "de_grid_placement")]
pub grid_row: Option<GridPlacement>,
#[serde(default, deserialize_with = "de_grid_placement")]
pub grid_column: Option<GridPlacement>,
#[serde(default)]
pub background_color: Option<Animatable<String>>,
#[serde(default)]
pub border_color: Option<Animatable<BorderColorSpec>>,
#[serde(default)]
pub border_radius: Option<Rect>,
#[serde(default)]
pub outline: Option<OutlineSpec>,
#[serde(default)]
pub box_shadow: Option<BoxShadowList>,
#[serde(default)]
pub filter: Option<crate::filters::FilterChain>,
#[serde(default)]
pub backdrop_filter: Option<crate::filters::FilterChain>,
#[serde(default)]
pub background_gradient: Option<GradientList>,
#[serde(default)]
pub border_gradient: Option<GradientList>,
#[serde(default, deserialize_with = "de_background_image")]
pub background_image: Option<BackgroundImageSpec>,
#[serde(default)]
pub z_index: Option<i32>,
#[serde(default)]
pub global_z_index: Option<i32>,
#[serde(default, deserialize_with = "de_focus_policy")]
pub focus_policy: Option<FocusPolicy>,
#[serde(default)]
pub cursor: Option<String>,
#[serde(default)]
pub transform: Option<Transform>,
#[serde(default)]
pub transform3d: Option<Transform3d>,
#[serde(default)]
pub opacity: Option<Animatable<f32>>,
#[serde(default)]
pub group_alpha: Option<bool>,
#[serde(default, deserialize_with = "de_layer_cache")]
pub cache: Option<LayerCache>,
#[serde(default)]
pub transition: Option<crate::transition::Transition>,
#[serde(default)]
pub scrollbar: Option<crate::scrollbar::ScrollbarSpec>,
#[serde(default)]
pub color: Option<Animatable<String>>,
#[serde(default)]
pub font_size: Option<FontSize>,
#[serde(default, deserialize_with = "de_font_weight")]
pub font_weight: Option<FontWeight>,
#[serde(default)]
pub font_family: Option<String>,
#[serde(default, deserialize_with = "de_text_align")]
pub text_align: Option<Justify>,
#[serde(default)]
pub line_height: Option<LineHeightSpec>,
#[serde(default)]
pub letter_spacing: Option<LetterSpacingSpec>,
#[serde(default)]
pub text_shadow: Option<TextShadowSpec>,
#[serde(default, deserialize_with = "de_line_break")]
pub line_break: Option<LineBreak>,
}
pub mod style_groups {
pub const LAYOUT: u32 = 1 << 0;
pub const BACKGROUND: u32 = 1 << 1;
pub const TRANSFORM: u32 = 1 << 2;
pub const BORDER_COLOR: u32 = 1 << 3;
pub const OUTLINE: u32 = 1 << 4;
pub const BOX_SHADOW: u32 = 1 << 5;
pub const BG_GRADIENT: u32 = 1 << 6;
pub const BORDER_GRADIENT: u32 = 1 << 7;
pub const TEXT_SHADOW: u32 = 1 << 8;
pub const Z_INDEX: u32 = 1 << 9;
pub const GLOBAL_Z_INDEX: u32 = 1 << 10;
pub const FOCUS_POLICY: u32 = 1 << 11;
pub const FILTER: u32 = 1 << 12;
pub const TRANSITION: u32 = 1 << 13;
pub const SCROLL_TRANSITION: u32 = 1 << 14;
pub const TEXT: u32 = 1 << 15;
pub const TEXT_LAYOUT: u32 = 1 << 16;
pub const CURSOR: u32 = 1 << 17;
pub const SCROLLBAR: u32 = 1 << 18;
pub const LAYER: u32 = 1 << 19;
pub const TRANSFORM3D: u32 = 1 << 20;
pub const BACKDROP: u32 = 1 << 21;
pub const BG_IMAGE: u32 = 1 << 22;
}
macro_rules! with_style_fields {
($cb:ident) => {
$cb! {
(display, "display", (LAYOUT), overlay),
(box_sizing, "boxSizing", (LAYOUT), overlay),
(position_type, "positionType", (LAYOUT), overlay),
(overflow_x, "overflowX", (LAYOUT), overlay),
(overflow_y, "overflowY", (LAYOUT), overlay),
(scrollbar_width, "scrollbarWidth", (LAYOUT), overlay),
(left, "left", (LAYOUT), overlay),
(right, "right", (LAYOUT), overlay),
(top, "top", (LAYOUT), overlay),
(bottom, "bottom", (LAYOUT), overlay),
(width, "width", (LAYOUT | TRANSITION), overlay),
(height, "height", (LAYOUT | TRANSITION), overlay),
(min_width, "minWidth", (LAYOUT), overlay),
(min_height, "minHeight", (LAYOUT), overlay),
(max_width, "maxWidth", (LAYOUT | TRANSITION), overlay),
(max_height, "maxHeight", (LAYOUT | TRANSITION), overlay),
(aspect_ratio, "aspectRatio", (LAYOUT), overlay),
(align_items, "alignItems", (LAYOUT), overlay),
(justify_items, "justifyItems", (LAYOUT), overlay),
(align_self, "alignSelf", (LAYOUT), overlay),
(justify_self, "justifySelf", (LAYOUT), overlay),
(align_content, "alignContent", (LAYOUT), overlay),
(justify_content, "justifyContent", (LAYOUT), overlay),
(margin, "margin", (LAYOUT), overlay),
(padding, "padding", (LAYOUT), overlay),
(border, "border", (LAYOUT), overlay),
(flex_direction, "flexDirection", (LAYOUT), overlay),
(flex_wrap, "flexWrap", (LAYOUT), overlay),
(flex_grow, "flexGrow", (LAYOUT), overlay),
(flex_shrink, "flexShrink", (LAYOUT), overlay),
(flex_basis, "flexBasis", (LAYOUT), overlay),
(gap, "gap", (LAYOUT), overlay),
(row_gap, "rowGap", (LAYOUT), overlay),
(column_gap, "columnGap", (LAYOUT), overlay),
(grid_auto_flow, "gridAutoFlow", (LAYOUT), overlay),
(grid_template_rows, "gridTemplateRows", (LAYOUT), overlay),
(grid_template_columns, "gridTemplateColumns", (LAYOUT), overlay),
(grid_auto_rows, "gridAutoRows", (LAYOUT), overlay),
(grid_auto_columns, "gridAutoColumns", (LAYOUT), overlay),
(grid_row, "gridRow", (LAYOUT), overlay),
(grid_column, "gridColumn", (LAYOUT), overlay),
(background_color, "backgroundColor", (BACKGROUND | TRANSITION), overlay),
(border_color, "borderColor", (BORDER_COLOR), overlay),
(border_radius, "borderRadius", (LAYOUT), overlay),
(outline, "outline", (OUTLINE), overlay),
(box_shadow, "boxShadow", (BOX_SHADOW), overlay),
(filter, "filter", (FILTER | LAYER), overlay),
(backdrop_filter, "backdropFilter", (BACKDROP | LAYER), overlay),
(background_gradient, "backgroundGradient", (BG_GRADIENT), overlay),
(border_gradient, "borderGradient", (BORDER_GRADIENT), overlay),
(background_image, "backgroundImage", (BG_IMAGE), overlay),
(z_index, "zIndex", (Z_INDEX), overlay),
(global_z_index, "globalZIndex", (GLOBAL_Z_INDEX), overlay),
(focus_policy, "focusPolicy", (FOCUS_POLICY), no_overlay),
(cursor, "cursor", (CURSOR), overlay),
(scrollbar, "scrollbar", (SCROLLBAR | LAYOUT), overlay),
(
transform,
"transform",
(TRANSFORM | TRANSITION),
overlay
),
(
transform3d,
"transform3d",
(TRANSFORM3D | LAYER | TRANSITION),
overlay
),
(
opacity,
"opacity",
(BACKGROUND | BG_GRADIENT | BORDER_GRADIENT | BG_IMAGE | TEXT_SHADOW
| TRANSITION | TEXT | LAYER),
overlay
),
(group_alpha, "groupAlpha", (LAYER), no_overlay),
(cache, "cache", (LAYER), no_overlay),
(
transition,
"transition",
(TRANSITION | SCROLL_TRANSITION),
overlay
),
(color, "color", (TEXT), overlay),
(font_size, "fontSize", (TEXT), overlay),
(font_weight, "fontWeight", (TEXT), overlay),
(font_family, "fontFamily", (TEXT), overlay),
(text_align, "textAlign", (TEXT_LAYOUT), overlay),
(line_height, "lineHeight", (TEXT), overlay),
(letter_spacing, "letterSpacing", (TEXT), overlay),
(text_shadow, "textShadow", (TEXT_SHADOW), overlay),
(line_break, "lineBreak", (TEXT_LAYOUT), overlay),
}
};
}
pub(crate) use with_style_fields;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct StyleDirty(pub u32);
impl StyleDirty {
pub const NONE: Self = Self(0);
pub const ALL: Self = Self(u32::MAX);
pub fn intersects(self, groups: u32) -> bool {
self.0 & groups != 0
}
pub fn any(self) -> bool {
self.0 != 0
}
}
impl Style {
pub(crate) fn overlay_delta(&mut self, delta: &Style) -> u32 {
let mut groups = 0u32;
macro_rules! merge_field {
($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
$(
if delta.$f.is_some() {
self.$f = delta.$f.clone();
groups |= {
use style_groups::*;
$g
};
}
)*
};
}
with_style_fields!(merge_field);
groups
}
pub(crate) fn unset_field(&mut self, wire_name: &str) -> Option<u32> {
macro_rules! unset_match {
($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
match wire_name {
$(
$name => {
self.$f = None;
Some({
use style_groups::*;
$g
})
}
)*
_ => {
tracing::warn!(
target: "bevy_react",
"unknown style field {wire_name:?} in styleUnset; ignoring"
);
None
}
}
};
}
with_style_fields!(unset_match)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::animatable::AnimatableField;
use crate::protocol::props::{Props, props_from_json as props};
#[test]
fn group_alpha_decodes_and_dirties_layer() {
let s: Style = serde_json::from_str(r#"{ "groupAlpha": false }"#).expect("style decodes");
assert_eq!(s.group_alpha, Some(false));
let s: Style = serde_json::from_str("{}").expect("style decodes");
assert_eq!(s.group_alpha, None);
let mut cached = Props::default();
let (dirty, _) = cached.merge_delta(
props(serde_json::json!({ "style": { "groupAlpha": false } })),
&[],
&[],
);
assert!(dirty.style.intersects(style_groups::LAYER));
let (dirty, _) = cached.merge_delta(
props(serde_json::json!({ "style": { "opacity": 0.5 } })),
&[],
&[],
);
assert!(dirty.style.intersects(style_groups::LAYER));
let style = cached.style.as_ref().expect("style retained");
assert_eq!(style.group_alpha, Some(false));
assert_eq!(style.opacity.static_val(), Some(0.5));
}
#[test]
fn cache_keyword_decodes_and_dirties_layer() {
let s: Style = serde_json::from_str(r#"{ "cache": "always" }"#).expect("style decodes");
assert_eq!(s.cache, Some(LayerCache::Always));
let s: Style = serde_json::from_str(r#"{ "cache": "auto" }"#).expect("style decodes");
assert_eq!(s.cache, Some(LayerCache::Auto));
let s: Style = serde_json::from_str(r#"{ "cache": "never" }"#).expect("style decodes");
assert_eq!(s.cache, Some(LayerCache::Never));
let s: Style = serde_json::from_str("{}").expect("style decodes");
assert_eq!(s.cache, None);
let s: Style = serde_json::from_str(r#"{ "cache": "sometimes" }"#).expect("style decodes");
assert_eq!(s.cache, Some(LayerCache::Auto));
let mut cached = Props::default();
let (dirty, _) = cached.merge_delta(
props(serde_json::json!({ "style": { "cache": "always" } })),
&[],
&[],
);
assert!(dirty.style.intersects(style_groups::LAYER));
assert_eq!(
cached.style.as_ref().and_then(|s| s.cache),
Some(LayerCache::Always)
);
}
#[test]
fn deserializes_filter_chain() {
use crate::filters::FilterChain;
let s: Style =
serde_json::from_str(r#"{ "filter": { "name": "blur", "params": { "radius": 4 } } }"#)
.expect("filter decodes");
let chain = s.filter.expect("filter present");
assert_eq!(chain.0.len(), 1);
assert_eq!(chain.0[0].name, "blur");
assert_eq!(chain.0[0].params["radius"], serde_json::json!(4));
let s: Style =
serde_json::from_str(r#"{ "filter": [{ "name": "blur" }, { "name": "grayscale" }] }"#)
.expect("filter decodes");
let names: Vec<&str> = s
.filter
.as_ref()
.expect("filter present")
.0
.iter()
.map(|u| u.name.as_str())
.collect();
assert_eq!(names, ["blur", "grayscale"]);
let s: Style =
serde_json::from_str(r#"{ "filter": [{ "name": "blur" }, 3], "opacity": 0.5 }"#)
.expect("a bad filter entry must not abort the style");
assert_eq!(s.filter, Some(FilterChain::default()));
assert_eq!(s.opacity.static_val(), Some(0.5));
}
#[test]
fn filter_delta_dirties_filter_and_layer() {
let mut cached = Props::default();
let (dirty, _) = cached.merge_delta(
props(serde_json::json!({ "style": { "filter": { "name": "blur" } } })),
&[],
&[],
);
assert!(dirty.style.intersects(style_groups::FILTER));
assert!(dirty.style.intersects(style_groups::LAYER));
let (dirty, _) = cached.merge_delta(
props(serde_json::json!({ "hoverStyle": { "filter": { "name": "blur" } } })),
&[],
&[],
);
assert!(dirty.hover_style);
let hover = cached.hover_style.as_ref().expect("variant retained");
assert!(hover.filter.is_some(), "variant carries the chain");
}
#[test]
fn backdrop_filter_delta_dirties_backdrop_and_layer() {
let mut cached = Props::default();
let (dirty, _) = cached.merge_delta(
props(serde_json::json!({ "style": { "backdropFilter": { "name": "blur" } } })),
&[],
&[],
);
assert!(dirty.style.intersects(style_groups::BACKDROP));
assert!(dirty.style.intersects(style_groups::LAYER));
assert!(!dirty.style.intersects(style_groups::FILTER));
assert!(
cached
.style
.as_ref()
.is_some_and(|s| s.backdrop_filter.is_some())
);
let (dirty, _) = cached.merge_delta(Props::default(), &[], &["backdropFilter".into()]);
assert!(dirty.style.intersects(style_groups::BACKDROP));
assert!(dirty.style.intersects(style_groups::LAYER));
assert!(
cached
.style
.as_ref()
.is_some_and(|s| s.backdrop_filter.is_none())
);
}
#[test]
fn style_field_table_is_complete() {
macro_rules! build_full {
($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
Style { $($f: None,)* }
};
}
let _style: Style = with_style_fields!(build_full);
}
#[test]
fn style_wire_names_match_serde_rename() {
fn camel(s: &str) -> String {
let mut out = String::new();
let mut up = false;
for c in s.chars() {
if c == '_' {
up = true;
} else if up {
out.extend(c.to_uppercase());
up = false;
} else {
out.push(c);
}
}
out
}
macro_rules! check {
($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
$( assert_eq!(camel(stringify!($f)), $name, "table wire name for `{}`", stringify!($f)); )*
};
}
with_style_fields!(check);
}
}