use std::fmt;
use bevy::ui::{GridPlacement, GridTrack, RepeatedGridTrack};
use serde::de::{self, Deserializer, Visitor};
use super::decode_warn;
fn split_tracks(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut depth = 0usize;
let mut cur = String::new();
for ch in s.chars() {
match ch {
'(' => {
depth += 1;
cur.push(ch);
}
')' => {
depth = depth.saturating_sub(1);
cur.push(ch);
}
c if c.is_whitespace() && depth == 0 => {
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
}
c => cur.push(c),
}
}
if !cur.is_empty() {
out.push(cur);
}
out
}
fn single_track(token: &str) -> Option<GridTrack> {
let t = token.trim();
match t {
"auto" => return Some(GridTrack::auto()),
"min-content" => return Some(GridTrack::min_content()),
"max-content" => return Some(GridTrack::max_content()),
_ => {}
}
let parse = |num: &str| num.trim().parse::<f32>().ok();
if let Some(v) = t.strip_suffix("fr").and_then(parse) {
Some(GridTrack::fr(v))
} else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
Some(GridTrack::flex(v))
} else if let Some(v) = t.strip_suffix("px").and_then(parse) {
Some(GridTrack::px(v))
} else {
t.strip_suffix('%').and_then(parse).map(GridTrack::percent)
}
}
fn repeated_track(count: u16, token: &str) -> Option<RepeatedGridTrack> {
let t = token.trim();
match t {
"auto" => return Some(RepeatedGridTrack::auto(count)),
"min-content" => return Some(RepeatedGridTrack::min_content(count)),
"max-content" => return Some(RepeatedGridTrack::max_content(count)),
_ => {}
}
let parse = |num: &str| num.trim().parse::<f32>().ok();
if let Some(v) = t.strip_suffix("fr").and_then(parse) {
Some(RepeatedGridTrack::fr(count, v))
} else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
Some(RepeatedGridTrack::flex(count, v))
} else if let Some(v) = t.strip_suffix("px").and_then(parse) {
Some(RepeatedGridTrack::px(count as usize, v))
} else {
t.strip_suffix('%')
.and_then(parse)
.map(|v| RepeatedGridTrack::percent(count as usize, v))
}
}
fn parse_template(s: &str) -> Vec<RepeatedGridTrack> {
split_tracks(s)
.into_iter()
.filter_map(|tok| {
let parse_one = || {
if let Some(inner) = tok
.strip_prefix("repeat(")
.and_then(|t| t.strip_suffix(')'))
{
let (count, track) = inner.split_once(',')?;
repeated_track(count.trim().parse().ok()?, track)
} else {
single_track(&tok).map(Into::into)
}
};
let parsed = parse_one();
if parsed.is_none() {
decode_warn(
"gridTrack",
&tok,
&format!("ignoring unparsable grid track {tok:?}"),
);
}
parsed
})
.collect()
}
fn parse_auto_tracks(s: &str) -> Vec<GridTrack> {
split_tracks(s)
.iter()
.filter_map(|t| {
let parsed = single_track(t);
if parsed.is_none() {
decode_warn(
"gridTrack",
t,
&format!("ignoring unparsable grid track {t:?}"),
);
}
parsed
})
.collect()
}
fn try_grid_placement(s: &str) -> Option<GridPlacement> {
enum Token {
Num(i16), Span(u16), Auto,
Invalid, }
fn token(t: &str) -> Token {
let t = t.trim();
if t == "auto" {
return Token::Auto;
}
if let Some(n) = t.strip_prefix("span") {
return match n.trim().parse::<u16>() {
Ok(0) | Err(_) => Token::Invalid,
Ok(n) => Token::Span(n),
};
}
match t.parse::<i16>() {
Ok(0) | Err(_) => Token::Invalid,
Ok(n) => Token::Num(n),
}
}
use Token::*;
if let Some((a, b)) = s.split_once('/') {
return Some(match (token(a), token(b)) {
(Num(start), Span(span)) => GridPlacement::start_span(start, span),
(Auto, Span(span)) => GridPlacement::span(span),
(Num(start), Num(end)) => GridPlacement::start_end(start, end),
(Num(start), Auto) => GridPlacement::start(start),
(Auto, Num(end)) => GridPlacement::end(end),
(Auto, Auto) => GridPlacement::auto(),
_ => return None,
});
}
match token(s) {
Auto => Some(GridPlacement::auto()),
Span(span) => Some(GridPlacement::span(span)),
Num(line) => Some(GridPlacement::start(line)),
Invalid => None,
}
}
macro_rules! grid_fields {
( $(
$(#[$meta:meta])*
fn $fn_name:ident($expect:literal) -> $ty:ty { $parse:expr }
)+ ) => { $(
$(#[$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($expect)
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
let parse: fn(&str) -> $ty = $parse;
Ok(Some(parse(s)))
}
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)
}
)+ };
}
grid_fields! {
fn de_grid_template("a CSS grid template string") -> Vec<RepeatedGridTrack> {
parse_template
}
fn de_grid_auto_tracks("a grid auto-track list string") -> Vec<GridTrack> {
parse_auto_tracks
}
fn de_grid_placement("a grid line placement string") -> GridPlacement {
|s| {
try_grid_placement(s).unwrap_or_else(|| {
decode_warn(
"gridPlacement",
s,
&format!("unrecognized grid placement {s:?}"),
);
GridPlacement::default()
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::style::Style;
#[test]
fn grid_templates_and_placement_decode() {
let s: Style = serde_json::from_value(serde_json::json!({
"gridTemplateColumns": "1fr 2fr 100px",
"gridTemplateRows": "repeat(3, 1fr)",
"gridAutoRows": "auto 40px",
}))
.expect("grid template decodes");
assert_eq!(s.grid_template_columns.map(|t| t.len()), Some(3));
assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(1));
assert_eq!(s.grid_auto_rows.map(|t| t.len()), Some(2));
let s: Style =
serde_json::from_value(serde_json::json!({ "gridTemplateRows": "1fr bogus 2fr" }))
.expect("bad track must not abort");
assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(2));
let placed = |v: &str| {
let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
.expect("grid placement decodes");
format!("{:?}", s.grid_row.unwrap())
};
let expect = |p: GridPlacement| format!("{p:?}");
assert_eq!(placed("1 / 3"), expect(GridPlacement::start_end(1, 3)));
assert_eq!(placed("span 2"), expect(GridPlacement::span(2)));
assert_eq!(
placed("2 / span 3"),
expect(GridPlacement::start_span(2, 3))
);
assert_eq!(placed("2 / 2"), expect(GridPlacement::start_end(2, 2)));
assert_eq!(placed("-1"), expect(GridPlacement::start(-1)));
assert_eq!(placed("2 / auto"), expect(GridPlacement::start(2)));
assert_eq!(placed("auto / 3"), expect(GridPlacement::end(3)));
}
#[test]
fn grid_placement_zero_falls_back_to_auto() {
let placed = |v: &str| {
let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
.expect("zero placement must not abort");
format!("{:?}", s.grid_row.unwrap())
};
let auto = format!("{:?}", GridPlacement::auto());
for s in ["0", "span 0", "0 / 2", "2 / 0", "0 / span 2", "2 / span 0"] {
assert_eq!(placed(s), auto, "input {s:?}");
}
assert_eq!(placed("garbage"), auto);
}
}