use crate::color::parse_color;
use crate::element::{Element, ElementId};
use crate::page::Page;
use bevy::math::{Rect, Vec2};
use bevy::prelude::{FontWidth, NodeImageMode, OverflowClipMargin, UiRect};
use bevy::sprite::TextureSlicer;
use bevy::text::{FontSize, FontStyle, FontWeight};
use bevy::ui;
use bevy::ui::{
AlignContent, AlignItems, AlignSelf, BorderRadius, BoxSizing, Display, FlexDirection, FlexWrap,
GridAutoFlow, GridPlacement, GridTrack, InlineDirection, JustifyContent, JustifyItems,
JustifySelf, Overflow, OverflowAxis, PositionType, RepeatedGridTrack, Val, VisualBox,
};
use roxmltree::{Document, Node};
pub(crate) fn parse_page(doc: Document) -> Result<Page, String> {
let root_xml = doc
.root()
.first_child()
.expect("Failed to get root element");
assert!(
root_xml.has_tag_name("Page"),
"Root element must be a <Page> element",
);
let root = parse_node(&root_xml, true)?;
let elements = parse_children_elements(&root_xml)?;
Ok(Page::new(root, elements))
}
fn parse_element(node: &Node) -> Result<Element, String> {
if !node.is_element() {
return Err("Expected an XML element".into());
}
match node.tag_name().name() {
"Node" => parse_element_node(node),
"Text" => parse_element_text(node),
"Button" => parse_element_button(node),
"Image" => parse_element_image(node),
tag => Err(format!("Unknown element <{}>", tag)),
}
}
fn parse_element_node(node: &Node) -> Result<Element, String> {
Ok(Element::Node {
node: parse_node(node, false)?,
id: node.attribute("id").map(ElementId::new),
children: parse_children_elements(node)?,
})
}
fn parse_element_text(node: &Node) -> Result<Element, String> {
Ok(Element::Text {
node: parse_node(node, false)?,
id: node.attribute("id").map(ElementId::new),
content: node
.text()
.map(str::trim)
.unwrap_or_else(|| node.attribute("content").unwrap_or(""))
.to_string(),
font: node.attribute("font").map(str::to_string),
font_weight: if let Some(s) = node.attribute("font-weight") {
Some(FontWeight(parse_int(s)? as u16))
} else {
None
},
font_width: if let Some(s) = node.attribute("font-width") {
Some(FontWidth(parse_float(s)?))
} else {
None
},
font_size: if let Some(s) = node.attribute("font-size") {
Some(parse_font_size(s)?)
} else {
None
},
font_style: if let Some(s) = node.attribute("font-style") {
Some(parse_matches(
s,
&[
("normal", |_| Ok(FontStyle::Normal)),
("italic", |_| Ok(FontStyle::Italic)),
("oblique", |_| Ok(FontStyle::Oblique(None))),
],
)?)
} else {
None
},
color: if let Some(s) = node.attribute("color") {
Some(parse_color(s)?)
} else {
None
},
})
}
fn parse_element_button(node: &Node) -> Result<Element, String> {
Ok(Element::Button {
node: parse_node(node, false)?,
id: node.attribute("id").map(ElementId::new),
children: parse_children_elements(node)?,
})
}
fn parse_element_image(node: &Node) -> Result<Element, String> {
Ok(Element::Image {
node: parse_node(node, false)?,
id: node.attribute("id").map(ElementId::new),
src: node
.attribute("src")
.ok_or_else(|| "Missing 'src' attribute inside <Image> node".to_string())?
.to_string(),
color: if let Some(s) = node.attribute("color") {
Some(parse_color(s)?)
} else {
None
},
flip_x: if let Some(s) = node.attribute("flip-x") {
Some(parse_bool(s)?)
} else {
None
},
flip_y: if let Some(s) = node.attribute("flip-y") {
Some(parse_bool(s)?)
} else {
None
},
rect: if let Some(s) = node.attribute("rect") {
Some(parse_rect(s)?)
} else {
None
},
mode: if let Some(s) = node.attribute("mode") {
Some(parse_matches(
s,
&[
("auto", |_| Ok(NodeImageMode::Auto)),
("stretch", |_| Ok(NodeImageMode::Stretch)),
("sliced", |_| {
Ok(NodeImageMode::Sliced(TextureSlicer::default()))
}),
("tiled_x", |_| {
Ok(NodeImageMode::Tiled {
tile_x: true,
tile_y: false,
stretch_value: 0.0,
})
}),
("tiled_y", |_| {
Ok(NodeImageMode::Tiled {
tile_x: false,
tile_y: true,
stretch_value: 0.0,
})
}),
("tiled_xy", |_| {
Ok(NodeImageMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 0.0,
})
}),
],
)?)
} else {
None
},
visual_box: if let Some(s) = node.attribute("visual-box") {
Some(parse_matches(
s,
&[
("padding", |_| Ok(VisualBox::PaddingBox)),
("content", |_| Ok(VisualBox::ContentBox)),
("border", |_| Ok(VisualBox::BorderBox)),
],
)?)
} else {
None
},
})
}
fn parse_children_elements(node: &Node) -> Result<Vec<Element>, String> {
let mut children = Vec::with_capacity(if node.has_children() { 1 } else { 0 });
for child in node.children().filter(|n| n.is_element()) {
children.push(parse_element(&child)?);
}
Ok(children)
}
fn parse_node(node: &Node, root: bool) -> Result<ui::Node, String> {
Ok(ui::Node {
display: parse_matches(
node.attribute("display").unwrap_or("flex"),
&[
("flex", |_| Ok(Display::Flex)),
("grid", |_| Ok(Display::Grid)),
("block", |_| Ok(Display::Block)),
("none", |_| Ok(Display::None)),
],
)?,
box_sizing: parse_matches(
node.attribute("box-sizing").unwrap_or("border"),
&[
("border", |_| Ok(BoxSizing::BorderBox)),
("content", |_| Ok(BoxSizing::ContentBox)),
],
)?,
position_type: parse_matches(
node.attribute("position").unwrap_or("relative"),
&[
("absolute", |_| Ok(PositionType::Absolute)),
("relative", |_| Ok(PositionType::Relative)),
],
)?,
overflow: Overflow {
x: parse_matches(
node.attribute("overflow-x").unwrap_or("visible"),
&[
("visible", |_| Ok(OverflowAxis::Visible)),
("clip", |_| Ok(OverflowAxis::Clip)),
("hidden", |_| Ok(OverflowAxis::Hidden)),
("scroll", |_| Ok(OverflowAxis::Scroll)),
],
)?,
y: parse_matches(
node.attribute("overflow-y").unwrap_or("visible"),
&[
("visible", |_| Ok(OverflowAxis::Visible)),
("clip", |_| Ok(OverflowAxis::Clip)),
("hidden", |_| Ok(OverflowAxis::Hidden)),
("scroll", |_| Ok(OverflowAxis::Scroll)),
],
)?,
},
scrollbar_width: parse_float(node.attribute("scrollbar-width").unwrap_or("1.0"))?,
overflow_clip_margin: OverflowClipMargin {
visual_box: parse_matches(
node.attribute("overflow_clip_visual_box")
.unwrap_or("padding"),
&[
("padding", |_| Ok(VisualBox::PaddingBox)),
("content", |_| Ok(VisualBox::ContentBox)),
("border", |_| Ok(VisualBox::BorderBox)),
],
)?,
margin: parse_float(node.attribute("overflow_clip_margin").unwrap_or("0.0"))?,
},
left: parse_val(node.attribute("left").unwrap_or("auto"))?,
right: parse_val(node.attribute("right").unwrap_or("auto"))?,
top: parse_val(node.attribute("top").unwrap_or("auto"))?,
bottom: parse_val(node.attribute("bottom").unwrap_or("auto"))?,
width: parse_val(
node.attribute("width")
.unwrap_or(if root { "100%" } else { "auto" }),
)?,
height: parse_val(
node.attribute("height")
.unwrap_or(if root { "100%" } else { "auto" }),
)?,
min_width: parse_val(node.attribute("min-width").unwrap_or("auto"))?,
min_height: parse_val(node.attribute("min-height").unwrap_or("auto"))?,
max_width: parse_val(node.attribute("max-width").unwrap_or("auto"))?,
max_height: parse_val(node.attribute("max-height").unwrap_or("auto"))?,
aspect_ratio: match node.attribute("aspect-ratio") {
Some(v) => Some(parse_float(v)?),
None => None,
},
align_items: parse_matches(
node.attribute("align-items").unwrap_or("default"),
&[
("default", |_| Ok(AlignItems::Default)),
("start", |_| Ok(AlignItems::Start)),
("end", |_| Ok(AlignItems::End)),
("center", |_| Ok(AlignItems::Center)),
("baseline", |_| Ok(AlignItems::Baseline)),
("stretch", |_| Ok(AlignItems::Stretch)),
],
)?,
justify_items: parse_matches(
node.attribute("justify-items").unwrap_or("default"),
&[
("default", |_| Ok(JustifyItems::Default)),
("start", |_| Ok(JustifyItems::Start)),
("end", |_| Ok(JustifyItems::End)),
("center", |_| Ok(JustifyItems::Center)),
("stretch", |_| Ok(JustifyItems::Stretch)),
],
)?,
align_self: parse_matches(
node.attribute("align-self").unwrap_or("auto"),
&[
("auto", |_| Ok(AlignSelf::Auto)),
("start", |_| Ok(AlignSelf::Start)),
("end", |_| Ok(AlignSelf::End)),
("center", |_| Ok(AlignSelf::Center)),
("stretch", |_| Ok(AlignSelf::Stretch)),
],
)?,
justify_self: parse_matches(
node.attribute("justify-self").unwrap_or("auto"),
&[
("auto", |_| Ok(JustifySelf::Auto)),
("start", |_| Ok(JustifySelf::Start)),
("end", |_| Ok(JustifySelf::End)),
("center", |_| Ok(JustifySelf::Center)),
("stretch", |_| Ok(JustifySelf::Stretch)),
],
)?,
align_content: parse_matches(
node.attribute("align-content").unwrap_or("default"),
&[
("default", |_| Ok(AlignContent::Default)),
("start", |_| Ok(AlignContent::Start)),
("end", |_| Ok(AlignContent::End)),
("center", |_| Ok(AlignContent::Center)),
("stretch", |_| Ok(AlignContent::Stretch)),
],
)?,
justify_content: parse_matches(
node.attribute("justify-content").unwrap_or("default"),
&[
("default", |_| Ok(JustifyContent::Default)),
("start", |_| Ok(JustifyContent::Start)),
("end", |_| Ok(JustifyContent::End)),
("center", |_| Ok(JustifyContent::Center)),
("space-between", |_| Ok(JustifyContent::SpaceBetween)),
("space-around", |_| Ok(JustifyContent::SpaceAround)),
("space-evenly", |_| Ok(JustifyContent::SpaceEvenly)),
],
)?,
direction: parse_matches(
node.attribute("direction").unwrap_or("ltr"),
&[
("ltr", |_| Ok(InlineDirection::Ltr)),
("rtl", |_| Ok(InlineDirection::Rtl)),
],
)?,
margin: parse_ui_rect(node.attribute("margin").unwrap_or("0"))?,
padding: parse_ui_rect(node.attribute("padding").unwrap_or("0"))?,
border: parse_ui_rect(node.attribute("border").unwrap_or("0"))?,
border_radius: parse_border_radius(node.attribute("border-radius").unwrap_or("0"))?,
flex_direction: parse_matches(
node.attribute("flex-direction").unwrap_or("row"),
&[
("row", |_| Ok(FlexDirection::Row)),
("column", |_| Ok(FlexDirection::Column)),
("row-reverse", |_| Ok(FlexDirection::RowReverse)),
("column-reverse", |_| Ok(FlexDirection::ColumnReverse)),
],
)?,
flex_wrap: parse_matches(
node.attribute("flex-wrap").unwrap_or("no-wrap"),
&[
("no-wrap", |_| Ok(FlexWrap::NoWrap)),
("wrap", |_| Ok(FlexWrap::Wrap)),
("wrap-reverse", |_| Ok(FlexWrap::WrapReverse)),
],
)?,
flex_grow: parse_float(node.attribute("flex-grow").unwrap_or("0"))?,
flex_shrink: parse_float(node.attribute("flex-shrink").unwrap_or("1"))?,
flex_basis: parse_val(node.attribute("flex-basis").unwrap_or("auto"))?,
row_gap: parse_val(node.attribute("row-gap").unwrap_or("0"))?,
column_gap: parse_val(node.attribute("column-gap").unwrap_or("0"))?,
grid_auto_flow: parse_matches(
node.attribute("grid-auto-flow").unwrap_or("row"),
&[
("row", |_| Ok(GridAutoFlow::Row)),
("column", |_| Ok(GridAutoFlow::Column)),
("row-dense", |_| Ok(GridAutoFlow::RowDense)),
("column-dense", |_| Ok(GridAutoFlow::ColumnDense)),
],
)?,
grid_template_rows: parse_grid_track(
node.attribute("grid-template-rows").unwrap_or("auto"),
)?,
grid_template_columns: parse_grid_track(
node.attribute("grid-template-columns").unwrap_or("auto"),
)?,
grid_auto_rows: parse_grid_template(node.attribute("grid-auto-rows").unwrap_or(""))?,
grid_auto_columns: parse_grid_template(node.attribute("grid-auto-columns").unwrap_or(""))?,
grid_row: parse_grid_placement(node.attribute("grid-row").unwrap_or("auto"))?,
grid_column: parse_grid_placement(node.attribute("grid-column").unwrap_or("auto"))?,
})
}
fn parse_rect(i: &str) -> Result<Rect, String> {
let values: Vec<f32> = i
.split_whitespace()
.map(parse_float)
.collect::<Result<Vec<_>, _>>()?;
match values.as_slice() {
[] => Ok(Rect::default()),
[all] => Ok(Rect {
min: Vec2::new(-*all, -*all),
max: Vec2::new(*all, *all),
}),
[width, height] => Ok(Rect {
min: Vec2::new(-*width / 2.0, -*height / 2.0),
max: Vec2::new(*width / 2.0, *height / 2.0),
}),
[left, top, right, bottom] => Ok(Rect {
min: Vec2::new(*left, *top),
max: Vec2::new(*right, *bottom),
}),
_ => Err(format!("Invalid rect '{i}'. Expected 1, 2 or 4 values.")),
}
}
fn parse_ui_rect(i: &str) -> Result<UiRect, String> {
let values: Vec<Val> = i
.split_whitespace()
.map(parse_val)
.collect::<Result<Vec<_>, _>>()?;
match values.as_slice() {
[] => Ok(UiRect::ZERO),
[all] => Ok(UiRect::all(*all)),
[vertical, horizontal] => Ok(UiRect {
left: *horizontal,
right: *horizontal,
top: *vertical,
bottom: *vertical,
}),
[top, horizontal, bottom] => Ok(UiRect {
left: *horizontal,
right: *horizontal,
top: *top,
bottom: *bottom,
}),
[top, right, bottom, left] => Ok(UiRect {
left: *left,
right: *right,
top: *top,
bottom: *bottom,
}),
_ => Err(format!("Invalid rect '{i}'. Expected 1-4 values.")),
}
}
fn parse_border_radius(i: &str) -> Result<BorderRadius, String> {
let values: Vec<Val> = i
.split_whitespace()
.map(parse_val)
.collect::<Result<Vec<_>, _>>()?;
match values.as_slice() {
[] => Ok(BorderRadius::ZERO),
[all] => Ok(BorderRadius::all(*all)),
[top_left, top_right, bottom_right, bottom_left] => Ok(BorderRadius {
top_left: *top_left,
top_right: *top_right,
bottom_right: *bottom_right,
bottom_left: *bottom_left,
}),
_ => Err(format!(
"Invalid border radius '{i}'. Expected 1 or 4 values."
)),
}
}
fn parse_grid_template(i: &str) -> Result<Vec<GridTrack>, String> {
if i.trim().is_empty() {
return Ok(Vec::new());
}
i.split_whitespace()
.map(|value| {
Ok(GridTrack::px(parse_float(
value.strip_suffix("px").unwrap_or(value),
)?))
})
.collect()
}
fn parse_grid_track(i: &str) -> Result<Vec<RepeatedGridTrack>, String> {
let track = if let Some(i) = i.strip_suffix("px") {
GridTrack::px(parse_float(i)?)
} else if let Some(i) = i.strip_suffix('%') {
GridTrack::percent(parse_float(i)?)
} else if let Some(i) = i.strip_suffix("fr") {
GridTrack::flex(parse_float(i)?)
} else if i == "auto" {
GridTrack::auto()
} else {
return Err(format!(
"Invalid grid track '{i}'. Expected px, %, fr or auto."
));
};
Ok(vec![track])
}
fn parse_grid_placement(i: &str) -> Result<GridPlacement, String> {
let i = i.trim().to_lowercase();
if i == "auto" {
return Ok(GridPlacement::auto());
}
let line = i
.parse::<i16>()
.map_err(|err| format!("Invalid grid placement '{i}': {err}"))?;
Ok(GridPlacement::start(line))
}
fn parse_val(i: &str) -> Result<Val, String> {
let i = i.trim().to_lowercase();
if let Ok(i) = i.parse::<f32>() {
return Ok(Val::Px(i));
}
if i == "auto" {
return Ok(Val::Auto);
}
if let Some(i) = i.strip_suffix("px") {
return Ok(Val::Px(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("%") {
return Ok(Val::Percent(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vw") {
return Ok(Val::Vw(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vh") {
return Ok(Val::Vh(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vmin") {
return Ok(Val::VMin(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vmax") {
return Ok(Val::VMax(parse_float(i)?));
}
Err(format!(
"Failed to parse value '{i}'. Expected 'auto', <float> or <float><unit> where <unit> is one of: px, %, vw, vh, vmin, vmax"
))
}
fn parse_font_size(i: &str) -> Result<FontSize, String> {
let i = i.trim().to_lowercase();
if let Ok(i) = i.parse::<f32>() {
return Ok(FontSize::Px(i));
}
if let Some(i) = i.strip_suffix("px") {
return Ok(FontSize::Px(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("rem") {
return Ok(FontSize::Rem(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vw") {
return Ok(FontSize::Vw(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vh") {
return Ok(FontSize::Vh(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vmin") {
return Ok(FontSize::VMin(parse_float(i)?));
}
if let Some(i) = i.strip_suffix("vmax") {
return Ok(FontSize::VMax(parse_float(i)?));
}
Err(format!(
"Failed to parse value '{i}'. Expected <float> or <float><unit> where <unit> is one of: px, rem, vw, vh, vmin, vmax"
))
}
fn parse_matches<'a, T>(
i: &str,
cases: &[(&'a str, fn(i: &str) -> Result<T, String>)],
) -> Result<T, String> {
let i = i.trim().to_lowercase();
for (case, f) in cases {
if *case == i {
return f(&i);
}
}
let possible = cases.iter().map(|(case, _)| *case).collect::<Vec<_>>();
Err(format!(
"Failed to parse value '{i}'. Expected one of: {}",
possible.join(", ")
))
}
fn parse_float(i: &str) -> Result<f32, String> {
i.to_lowercase()
.trim()
.parse::<f32>()
.map_err(|err| format!("Failed to parse float '{i}': {err}"))
}
fn parse_int(i: &str) -> Result<i32, String> {
i.to_lowercase()
.trim()
.parse::<i32>()
.map_err(|err| format!("Failed to parse int '{i}': {err}"))
}
fn parse_bool(i: &str) -> Result<bool, String> {
i.to_lowercase()
.trim()
.parse::<bool>()
.map_err(|err| format!("Failed to parse int '{i}': {err}"))
}