use std::fmt;
use bevy::text::{FontWeight, Justify, LineBreak};
use bevy::ui::{
AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, FocusPolicy,
GridAutoFlow, JustifyContent, JustifyItems, JustifySelf, OverflowAxis,
};
use serde::de::{self, Deserializer, Visitor};
use super::background_image::BackgroundImageMode;
use super::decode_warn;
use crate::image_rendering::ImageRendering;
use crate::scrollbar::{HorizontalEdge, ScrollbarPosition, VerticalEdge};
use crate::style::KeywordTable;
use crate::style::props::{LayerCache, PositionMode};
macro_rules! keyword_table {
(; $($rest:tt)*) => {};
($table:ident; $fn_name:ident, $kind:literal, $ty:ty, [$($kw:literal),+]) => {
pub(crate) static $table: KeywordTable<$ty> = KeywordTable {
kind: $kind,
keywords: &[$($kw),+],
decode: {
fn decode(
d: &mut dyn erased_serde::Deserializer<'_>,
) -> Result<Option<$ty>, erased_serde::Error> {
$fn_name(d)
}
decode
},
};
};
}
macro_rules! keyword_fields {
( $(
$(#[$meta:meta])*
fn $fn_name:ident($kind:literal) -> $ty:ty $(as $table:ident)? {
$( $($kw:literal)|+ => $variant:ident ),+ $(,)?
}
)+ ) => { $(
$(#[$meta])*
pub(crate) fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<$ty>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(concat!("a `", $kind, "` keyword string"))
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok(Some(match s {
$( $($kw)|+ => <$ty>::$variant, )+
_ => {
decode_warn(
$kind,
s,
&format!("unrecognized {} {s:?}", $kind),
);
<$ty>::default()
}
}))
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
keyword_table! { $($table)?; $fn_name, $kind, $ty, [$( $($kw),+ ),+] }
)+ };
}
keyword_fields! {
fn de_display("display") -> Display as DISPLAY_KEYWORDS {
"flex" => Flex, "grid" => Grid, "block" => Block, "none" => None,
}
fn de_layer_cache("cache") -> LayerCache as LAYER_CACHE_KEYWORDS {
"auto" => Auto, "always" => Always, "never" => Never,
}
fn de_box_sizing("boxSizing") -> BoxSizing as BOX_SIZING_KEYWORDS {
"borderBox" | "border-box" => BorderBox,
"contentBox" | "content-box" => ContentBox,
}
fn de_position_type("positionType") -> PositionMode as POSITION_TYPE_KEYWORDS {
"absolute" => Absolute, "relative" => Relative, "fixed" => Fixed,
}
fn de_overflow_axis("overflow") -> OverflowAxis as OVERFLOW_KEYWORDS {
"visible" => Visible, "clip" => Clip, "hidden" => Hidden, "scroll" => Scroll,
}
fn de_align_items("alignItems") -> AlignItems as ALIGN_ITEMS_KEYWORDS {
"start" => Start, "end" => End,
"flexStart" => FlexStart, "flexEnd" => FlexEnd,
"center" => Center, "baseline" => Baseline, "stretch" => Stretch,
"safeStart" => StartSafe, "safeEnd" => EndSafe,
"safeFlexStart" => FlexStartSafe, "safeFlexEnd" => FlexEndSafe,
"safeCenter" => CenterSafe,
}
fn de_justify_items("justifyItems") -> JustifyItems as JUSTIFY_ITEMS_KEYWORDS {
"start" => Start, "end" => End,
"center" => Center, "baseline" => Baseline, "stretch" => Stretch,
"safeStart" => StartSafe, "safeEnd" => EndSafe, "safeCenter" => CenterSafe,
}
fn de_align_self("alignSelf") -> AlignSelf as ALIGN_SELF_KEYWORDS {
"auto" => Auto, "start" => Start, "end" => End,
"flexStart" => FlexStart, "flexEnd" => FlexEnd,
"center" => Center, "baseline" => Baseline, "stretch" => Stretch,
"safeStart" => StartSafe, "safeEnd" => EndSafe,
"safeFlexStart" => FlexStartSafe, "safeFlexEnd" => FlexEndSafe,
"safeCenter" => CenterSafe,
}
fn de_justify_self("justifySelf") -> JustifySelf as JUSTIFY_SELF_KEYWORDS {
"auto" => Auto, "start" => Start, "end" => End,
"center" => Center, "baseline" => Baseline, "stretch" => Stretch,
"safeStart" => StartSafe, "safeEnd" => EndSafe, "safeCenter" => CenterSafe,
}
fn de_align_content("alignContent") -> AlignContent as ALIGN_CONTENT_KEYWORDS {
"start" => Start, "end" => End,
"flexStart" => FlexStart, "flexEnd" => FlexEnd,
"center" => Center, "stretch" => Stretch,
"spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
"spaceAround" => SpaceAround,
"safeStart" => StartSafe, "safeEnd" => EndSafe,
"safeFlexStart" => FlexStartSafe, "safeFlexEnd" => FlexEndSafe,
"safeCenter" => CenterSafe,
}
fn de_justify_content("justifyContent") -> JustifyContent as JUSTIFY_CONTENT_KEYWORDS {
"start" => Start, "end" => End,
"flexStart" => FlexStart, "flexEnd" => FlexEnd,
"center" => Center, "stretch" => Stretch,
"spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
"spaceAround" => SpaceAround,
"safeStart" => StartSafe, "safeEnd" => EndSafe,
"safeFlexStart" => FlexStartSafe, "safeFlexEnd" => FlexEndSafe,
"safeCenter" => CenterSafe,
}
fn de_flex_direction("flexDirection") -> FlexDirection as FLEX_DIRECTION_KEYWORDS {
"row" => Row, "column" => Column,
"rowReverse" => RowReverse, "columnReverse" => ColumnReverse,
}
fn de_flex_wrap("flexWrap") -> FlexWrap as FLEX_WRAP_KEYWORDS {
"nowrap" | "noWrap" => NoWrap, "wrap" => Wrap, "wrapReverse" => WrapReverse,
}
fn de_grid_auto_flow("gridAutoFlow") -> GridAutoFlow as GRID_AUTO_FLOW_KEYWORDS {
"row" => Row, "column" => Column,
"rowDense" => RowDense, "columnDense" => ColumnDense,
}
fn de_focus_policy("focusPolicy") -> FocusPolicy as FOCUS_POLICY_KEYWORDS {
"block" => Block, "pass" => Pass,
}
fn de_text_align("textAlign") -> Justify as TEXT_ALIGN_KEYWORDS {
"left" => Left, "center" => Center, "right" => Right,
"justify" => Justified, "start" => Start, "end" => End,
}
fn de_line_break("lineBreak") -> LineBreak as LINE_BREAK_KEYWORDS {
"wordBoundary" => WordBoundary, "anyCharacter" => AnyCharacter,
"wordOrCharacter" => WordOrCharacter, "noWrap" => NoWrap,
}
fn de_bg_image_mode("backgroundImage") -> BackgroundImageMode {
"stretch" => Stretch, "repeat" => Repeat,
"repeatX" => RepeatX, "repeatY" => RepeatY,
}
fn de_scrollbar_position("scrollbar") -> ScrollbarPosition {
"gutter" => Gutter, "float" => Float,
}
fn de_scrollbar_vertical_side("scrollbar") -> HorizontalEdge {
"left" => Left, "right" => Right,
}
fn de_scrollbar_horizontal_side("scrollbar") -> VerticalEdge {
"top" => Top, "bottom" => Bottom,
}
fn de_image_rendering("imageRendering") -> ImageRendering as IMAGE_RENDERING_KEYWORDS {
"auto" => Auto, "bilinear" => Bilinear,
"trilinear" => Trilinear, "nearest" => Nearest,
}
}
pub(crate) fn de_font_weight<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<FontWeight>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<FontWeight>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a `fontWeight` keyword or numeric weight string")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok(Some(match s {
"thin" => FontWeight::THIN,
"light" => FontWeight(300),
"normal" => FontWeight::NORMAL,
"medium" => FontWeight(500),
"semibold" => FontWeight(600),
"bold" => FontWeight::BOLD,
"black" => FontWeight::BLACK,
other => other.parse::<u16>().map(FontWeight).unwrap_or_else(|_| {
decode_warn(
"fontWeight",
other,
&format!("unrecognized fontWeight {other:?}"),
);
FontWeight::NORMAL
}),
}))
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Style;
use crate::style::props::{
ALIGN_CONTENT, ALIGN_ITEMS, ALIGN_SELF, BOX_SIZING, DISPLAY, FLEX_DIRECTION, FLEX_WRAP,
FOCUS_POLICY, FONT_WEIGHT, JUSTIFY_CONTENT, JUSTIFY_ITEMS, JUSTIFY_SELF, LINE_BREAK,
TEXT_ALIGN,
};
#[test]
fn keyword_fields_decode_to_bevy_enums() {
let s: Style = serde_json::from_value(serde_json::json!({
"display": "grid",
"alignItems": "start",
"alignSelf": "flexStart",
"alignContent": "spaceBetween",
"justifyContent": "flexEnd",
"flexWrap": "nowrap",
"focusPolicy": "block",
"textAlign": "justify",
"lineBreak": "anyCharacter",
}))
.expect("keyword style decodes");
assert_eq!(s.get(&DISPLAY).copied(), Some(Display::Grid));
assert_eq!(s.get(&ALIGN_ITEMS).copied(), Some(AlignItems::Start));
assert_eq!(s.get(&ALIGN_SELF).copied(), Some(AlignSelf::FlexStart));
assert_eq!(
s.get(&ALIGN_CONTENT).copied(),
Some(AlignContent::SpaceBetween)
);
assert_eq!(
s.get(&JUSTIFY_CONTENT).copied(),
Some(JustifyContent::FlexEnd)
);
assert_eq!(s.get(&FLEX_WRAP).copied(), Some(FlexWrap::NoWrap));
assert_eq!(s.get(&FOCUS_POLICY).copied(), Some(FocusPolicy::Block));
assert_eq!(s.get(&TEXT_ALIGN).copied(), Some(Justify::Justified));
assert_eq!(s.get(&LINE_BREAK).copied(), Some(LineBreak::AnyCharacter));
let s: Style = serde_json::from_value(serde_json::json!({
"alignItems": "flexStart",
"justifyContent": "start",
"boxSizing": "border-box",
"flexWrap": "noWrap",
}))
.expect("alias keywords decode");
assert_eq!(s.get(&ALIGN_ITEMS).copied(), Some(AlignItems::FlexStart));
assert_eq!(
s.get(&JUSTIFY_CONTENT).copied(),
Some(JustifyContent::Start)
);
assert_eq!(s.get(&BOX_SIZING).copied(), Some(BoxSizing::BorderBox));
assert_eq!(s.get(&FLEX_WRAP).copied(), Some(FlexWrap::NoWrap));
}
#[test]
fn safe_alignment_keywords_decode_to_the_safe_variants() {
let s: Style = serde_json::from_value(serde_json::json!({
"alignItems": "safeCenter",
"alignSelf": "safeFlexEnd",
"alignContent": "safeEnd",
"justifyContent": "safeFlexStart",
"justifyItems": "safeStart",
"justifySelf": "safeCenter",
}))
.expect("keyword style decodes");
assert_eq!(s.get(&ALIGN_ITEMS).copied(), Some(AlignItems::CenterSafe));
assert_eq!(s.get(&ALIGN_SELF).copied(), Some(AlignSelf::FlexEndSafe));
assert_eq!(s.get(&ALIGN_CONTENT).copied(), Some(AlignContent::EndSafe));
assert_eq!(
s.get(&JUSTIFY_CONTENT).copied(),
Some(JustifyContent::FlexStartSafe)
);
assert_eq!(
s.get(&JUSTIFY_ITEMS).copied(),
Some(JustifyItems::StartSafe)
);
assert_eq!(s.get(&JUSTIFY_SELF).copied(), Some(JustifySelf::CenterSafe));
for table in [
ALIGN_ITEMS_KEYWORDS.keywords,
JUSTIFY_CONTENT_KEYWORDS.keywords,
] {
let safe = table.iter().filter(|k| k.starts_with("safe")).count();
assert_eq!(safe, 5, "{table:?}");
}
let s: Style =
serde_json::from_value(serde_json::json!({ "justifyItems": "safeFlexStart" }))
.expect("decodes");
assert_eq!(s.get(&JUSTIFY_ITEMS).copied(), Some(JustifyItems::Default));
}
#[test]
fn safe_center_falls_back_to_start_on_overflow() {
use bevy::prelude::*;
let mut app = crate::transition::layout_tests::layout_app();
let root = app
.world_mut()
.spawn(Node {
width: Val::Vw(100.0),
height: Val::Vh(100.0),
flex_direction: FlexDirection::Column,
..Default::default()
})
.id();
let mut row = |justify: &str, width: f32| {
let style: Style =
serde_json::from_value(serde_json::json!({ "justifyContent": justify }))
.expect("decodes");
let row = Node {
width: Val::Px(100.0),
height: Val::Px(20.0),
justify_content: style.get(&JUSTIFY_CONTENT).copied().unwrap(),
..Default::default()
};
let row = app.world_mut().spawn((row, ChildOf(root))).id();
let item = Node {
width: Val::Px(width),
flex_shrink: 0.0,
..Default::default()
};
let first = app.world_mut().spawn((item.clone(), ChildOf(row))).id();
app.world_mut().spawn((item, ChildOf(row)));
(row, first)
};
let rows = [
(row("center", 80.0), -30.0),
(row("safeCenter", 80.0), 0.0),
(row("center", 20.0), 30.0),
(row("safeCenter", 20.0), 30.0),
];
app.update();
let left = |e: Entity| {
let node = app.world().entity(e).get::<ComputedNode>().unwrap();
let global = app.world().entity(e).get::<UiGlobalTransform>().unwrap();
global.translation.x - 0.5 * node.size.x
};
for ((row, first), offset) in rows {
assert_eq!(left(first) - left(row), offset);
}
}
#[test]
fn unknown_enum_keywords_fall_back_to_default() {
let s: Style = serde_json::from_value(serde_json::json!({
"display": "flx",
"alignItems": "centre",
"flexDirection": "sideways",
"textAlign": "middle",
"fontWeight": "heavyish",
"focusPolicy": "weird",
"lineBreak": "wordBoundary",
}))
.expect("bad keywords must not abort deserialization");
assert_eq!(s.get(&DISPLAY).copied(), Some(Display::default()));
assert_eq!(s.get(&ALIGN_ITEMS).copied(), Some(AlignItems::default()));
assert_eq!(
s.get(&FLEX_DIRECTION).copied(),
Some(FlexDirection::default())
);
assert_eq!(s.get(&TEXT_ALIGN).copied(), Some(Justify::default()));
assert_eq!(s.get(&FONT_WEIGHT).copied(), Some(FontWeight::NORMAL));
assert_eq!(s.get(&FOCUS_POLICY).copied(), Some(FocusPolicy::Pass));
assert_eq!(s.get(&LINE_BREAK).copied(), Some(LineBreak::WordBoundary));
}
#[test]
fn font_weight_keywords_and_numeric() {
let fw = |v: serde_json::Value| {
serde_json::from_value::<Style>(serde_json::json!({ "fontWeight": v }))
.expect("fontWeight decodes")
.get(&FONT_WEIGHT)
.copied()
};
assert_eq!(fw("bold".into()), Some(FontWeight::BOLD));
assert_eq!(fw("600".into()), Some(FontWeight(600)));
assert_eq!(fw("thin".into()), Some(FontWeight::THIN));
}
}