Layout language AST building now complete with recursive parsing referenced XML files

This commit is contained in:
Keavon Chambers 2020-05-31 22:02:41 -07:00
parent 0c7e6bc883
commit 559833be8e
17 changed files with 429 additions and 288 deletions

View File

@ -1,3 +1,3 @@
<box content="INNER_XML: (None) = none" :fill="FILL: (Color | None) = none" :round="ROUND: (AbsolutePx) = 0px" :border-thickness="BORDER_THICKNESS: (AbsolutePx) = 0px" :border-color="BORDER_COLOR: (Color | None) = none">
<box content="INNER_XML: (None) = none" :fill="FILL: (Color | None) = none" :round="ROUND: (AbsolutePx | AbsolutePx, AbsolutePx, AbsolutePx, AbsolutePx) = 0px" :border-thickness="BORDER_THICKNESS: (AbsolutePx) = 0px" :border-color="BORDER_COLOR: (Color | None) = none">
{{INNER_XML}}
</box>

View File

@ -1,16 +1,16 @@
<header:window-buttons :maximized="IS_MAXIMIZED: (Bool) = false">
<box height="100%" y-align="50%" x-padding="18px">
<icon :src="window_minimize.svg" />
<icon :svg="`window_minimize.svg`" />
</box>
<box height="100%" y-align="50%" x-padding="18px">
<if :a="{{IS_MAXIMIZED}}">
<icon :src="window_restore_down.svg" />
<icon :svg="`window_restore_down.svg`" />
</if>
<if :a="{{IS_MAXIMIZED}}" b="false">
<icon :src="maximize.svg" />
<icon :svg="`maximize.svg`" />
</if>
</box>
<box height="100%" y-align="50%" x-padding="18px">
<icon :src="window_close.svg" />
<icon :svg="`window_close.svg`" />
</box>
</header:window-buttons>

View File

@ -1,3 +1,3 @@
<icon content="SVG_DATA: (None) = none" :src="SVG_SOURCE: (TemplateString | None) = none">
<icon content="INNER_XML: (None) = none" :svg="SVG_SOURCE: (TemplateString) = ``" :style="SVG_STYLE: (TemplateString) = ``">
{{INNER_XML}}
</icon>

3
gui/if.xml Normal file
View File

@ -0,0 +1,3 @@
<if content="INNER_XML: (None) = none" :a="CONDITION_A: (Integer | Decimal | AbsolutePx | Percent | PercentRemainder | Inner | Width | Height | TemplateString | Color | Bool | None) = true" :b="CONDITION_B: (Integer | Decimal | AbsolutePx | Percent | PercentRemainder | Inner | Width | Height | TemplateString | Color | Bool | None) = true">
{{RESULT}}
</if>

View File

@ -1,7 +1,7 @@
<input:checkbox-with-dropdown content="OPTION_LIST: (None) = none" :disabled="DISABLED: (Bool) = false" :checked="CHECKED: (Bool) = false" :selected-index="SELECTED_INDEX: (Integer) = 0">
<!-- Checkbox -->
<col width="height" height="100%">
<box width="100%" height="100%" x-align="50%" y-align="50%" :round="4px 0px 0px 4px" :fill="['accent']">
<box width="100%" height="100%" x-align="50%" y-align="50%" :round="4px, 0px, 0px, 4px" :fill="['accent']">
<input:checkbox :checked="{{CHECKED}}" :disabled="{{DISABLED}}" :inverted="true" />
</box>
</col>

View File

@ -2,18 +2,10 @@
<box width="14px" height="14px" :fill="{{BOX_COLOR}}" :round="2px">
<if :a="{{CHECKED}}">
<if :a="{{INVERTED}}" :b="false">
<icon>
<svg width="12.84px" height="11.59px" viewBox="0 0 12.84 11.59">
<polygon points="5.53,11.59 0,6.67 1.66,4.8 5.13,7.88 10.82,0 12.84,1.46" fill="#ffffff" />
</svg>
</icon>
<icon :svg="`checkmark_white.svg`" />
</if>
<if :a="{{INVERTED}}">
<icon>
<svg width="12.84px" height="11.59px" viewBox="0 0 12.84 11.59">
<polygon points="5.53,11.59 0,6.67 1.66,4.8 5.13,7.88 10.82,0 12.84,1.46" fill="#4b79a7" />
</svg>
</icon>
<icon :svg="`checkmark_accent.svg`" />
</if>
</if>
</box>

View File

@ -6,11 +6,7 @@
</col>
<!-- Dropdown arrow icon -->
<col width="8px" height="100%">
<icon width="8px" height="100%" x-align="50%" y-align="50%">
<svg width="7" height="5" viewBox="0 0 7 5">
<path d="M0,.5l3.5,3L7,.5v2l-3.5,3L0,2.5Z" transform="translate(0 -0.5)" fill="#ddd" />
</svg>
</icon>
<icon width="8px" height="100%" x-align="50%" y-align="50%" :svg="`dropdown_arrow.svg`" />
</col>
</box>
</input:dropdown>

View File

@ -1,3 +1 @@
<text content="TEXT_STRING: (None) = none" :color="COLOR: (Color | None) = ['middlegray']" :size="SIZE: (AbsolutePx) = 12px">
</text>
<text content="TEXT_STRING: (None) = none" :color="COLOR: (Color | None) = ['middlegray']" :size="SIZE: (AbsolutePx) = 12px"></text>

View File

@ -1,7 +1,7 @@
<viewport:panels>
<checkbox-with-dropdown :checked="true" :selected-index="2">
<input:checkbox-with-dropdown :checked="true" :selected-index="2">
Option A
Option B
Option C
</checkbox-with-dropdown>
</input:checkbox-with-dropdown>
</viewport:panels>

View File

@ -69,13 +69,18 @@ impl Application {
// Temporary setup below, TODO: move to appropriate place in architecture
// Data structure maintaining the user interface
let gui_rect_pipeline = Pipeline::new(&device, swap_chain_descriptor.format, vec![], &mut shader_cache, ("shaders/shader.vert", "shaders/shader.frag"));
let gui_rect_pipeline = Pipeline::new(
&device, swap_chain_descriptor.format, Vec::new(), &mut shader_cache, ("shaders/shader.vert", "shaders/shader.frag")
);
pipeline_cache.set("gui_rect", gui_rect_pipeline);
// Render quad hierarchy
let gui_root_data = GuiNode::new(swap_chain_descriptor.width, swap_chain_descriptor.height, ColorPalette::Accent.into_color_srgb());
let gui_root = rctree::Node::new(gui_root_data);
GuiLayout::new();
// Main window in the XML layout language
let mut main_window_layout = GuiLayout::new();
main_window_layout.load_layout("window", "main");
Self {
surface,
@ -97,7 +102,7 @@ impl Application {
}
// Called every time by the event loop
pub fn main_event_loop<T>(&mut self, event: Event<'_, T>, control_flow: &mut ControlFlow, window: &Window) {
fn main_event_loop<T>(&mut self, event: Event<'_, T>, control_flow: &mut ControlFlow, window: &Window) {
// Wait for the next event to cause a subsequent event loop run, instead of looping instantly as a game would need
*control_flow = ControlFlow::Wait;
@ -122,12 +127,12 @@ impl Application {
}
}
pub fn update_gui(&mut self) {
fn update_gui(&mut self) {
}
// Render the queue of pipeline draw commands over the current window
pub fn render(&mut self) {
fn render(&mut self) {
// Get a frame buffer to render on
let frame = self.swap_chain.get_next_texture().expect("Timeout getting frame buffer texture");

View File

@ -1,60 +1,143 @@
use std::fs;
use std::io;
use crate::layout_parsed_node::*;
use std::collections::HashSet;
use crate::layout_abstract_syntax::*;
use crate::layout_attribute_parser::*;
use crate::resource_cache::ResourceCache;
pub struct GuiLayout {
pub loaded_layouts: ResourceCache<rctree::Node<LayoutAbstractNode>>,
attribute_parser: AttributeParser,
}
impl GuiLayout {
pub fn new() -> GuiLayout {
let parsed_layout_tree = Self::parse_xml_file("gui/window/main.xml").unwrap();
Self::interpret_abstract_syntax_tree(parsed_layout_tree);
Self {}
Self {
loaded_layouts: ResourceCache::new(),
attribute_parser: AttributeParser::new(),
}
}
pub fn parse_xml_file(path: &str) -> io::Result<rctree::Node<LayoutParsedNode>> {
pub fn load_layout(&mut self, namespace: &str, name: &str) {
// Load and parse the XML layout
let xml_path = self.layout_xml_path(namespace, name);
let window_main = self.parse_xml_file(&xml_path[..]).unwrap();
// Keep track of it being loaded to prevent duplicate work
let mut already_loaded_layouts = HashSet::new();
already_loaded_layouts.insert(format!("{}:{}", namespace, name));
// Load XML files recursively for all tags referenced in window:main and within those layouts
self.explore_referenced_layouts(&window_main, &mut already_loaded_layouts);
let tag_name = self.layout_name(namespace, name);
self.loaded_layouts.set(&tag_name[..], window_main);
}
fn explore_referenced_layouts(&mut self, layout_tree_root: &rctree::Node<LayoutAbstractNode>, already_loaded_layouts: &mut HashSet<String>) {
for child_tag in layout_tree_root.descendants() {
match & *child_tag.borrow() {
// Tags are references to other XML layouts that should be loaded and cached
LayoutAbstractNode::Tag(layout_abstract_tag) => {
// Cache key in form namespace:name
let key = self.layout_name(&layout_abstract_tag.namespace[..], &layout_abstract_tag.name[..]);
if !already_loaded_layouts.contains(&key[..]) {
// Check if the cache has the loaded layout and load it if not
match self.loaded_layouts.get(&key[..]) {
// Tag has not been loaded, so load it now
None => {
// Load the layout for the visited tag
let xml_path = self.layout_xml_path(&layout_abstract_tag.namespace[..], &layout_abstract_tag.name[..]);
let new_loaded_layout = self.parse_xml_file(&xml_path[..]).unwrap();
// Keep track of it being loaded to prevent duplicate work
let key_copy = key.clone();
already_loaded_layouts.insert(key);
// Recursively explore the newly loaded layout's tags
self.explore_referenced_layouts(&new_loaded_layout, already_loaded_layouts);
self.loaded_layouts.set(&key_copy[..], new_loaded_layout);
}
// Tag has already been loaded
Some(_) => {}
}
}
}
// Text nodes don't need to be loaded
LayoutAbstractNode::Text(_) => {}
};
}
}
fn layout_name(&self, namespace: &str, name: &str) -> String {
if namespace.len() > 0 {
format!("{}:{}", namespace, name)
}
else {
String::from(name)
}
}
fn layout_xml_path(&self, namespace: &str, name: &str) -> String {
if namespace.len() > 0 {
format!("gui/{}/{}.xml", namespace, name)
}
else {
format!("gui/{}.xml", name)
}
}
fn parse_xml_file(&self, path: &str) -> io::Result<rctree::Node<LayoutAbstractNode>> {
let source = fs::read_to_string(path)?;
let parsed = xmlparser::Tokenizer::from(&source[..]);
let mut stack: Vec<rctree::Node<LayoutParsedNode>> = Vec::new();
let mut current: Option<rctree::Node<LayoutParsedNode>> = None;
let mut result: Option<rctree::Node<LayoutParsedNode>> = None;
let mut stack: Vec<rctree::Node<LayoutAbstractNode>> = Vec::new();
let mut current: Option<rctree::Node<LayoutAbstractNode>> = None;
let mut result: Option<rctree::Node<LayoutAbstractNode>> = None;
let mut parsing_root_tag_with_declarations = true;
for token in parsed {
match token.unwrap() {
xmlparser::Token::ElementStart { prefix, local, .. } => {
let namespace = String::from(prefix.as_str());
let tag_name = String::from(local.as_str());
let new_parsed_layout_node = LayoutParsedNode::new_tag(namespace, tag_name);
let new_parsed_layout_node = LayoutAbstractNode::new_tag(namespace, tag_name);
let new_node = rctree::Node::new(new_parsed_layout_node);
current = Some(new_node);
}
xmlparser::Token::Attribute { prefix, local, value, .. } => {
let colon_prefixed = prefix.start() > 0 && (prefix.start() == prefix.end());
let key = if colon_prefixed {
let name = if colon_prefixed {
let slice = local.as_str();
let mut string = String::with_capacity(slice.len() + 1);
string.push(':');
string.push_str(slice);
string
} else { String::from(local.as_str()) };
let value = String::from(value.as_str());
let attribute = (key, value);
let value = value.as_str();
let attribute = if parsing_root_tag_with_declarations {
let parameter_declaration = self.attribute_parser.parse_attribute_declaration(value);
Attribute::new(name, parameter_declaration)
}
else {
let parameter_types = self.attribute_parser.parse_attribute_types(value);
Attribute::new(name, parameter_types)
};
match &mut current {
Some(current_node) => {
match &mut *current_node.borrow_mut() {
LayoutParsedNode::Tag(tag) => {
LayoutAbstractNode::Tag(tag) => {
// Add this attribute to the current node that has not yet reached its closing angle bracket
tag.add_attribute(attribute);
}
LayoutParsedNode::Text(_) => {
panic!("Error adding attribute to tag when parsing XML layout in file: {}", path);
LayoutAbstractNode::Text(text) => {
panic!("Unexpected text attribute {} attemping to be added to tag when parsing XML layout in file: {}", text, path);
}
}
}
@ -70,6 +153,7 @@ impl GuiLayout {
// After adding any attributes, we are now a layer deeper which the stack keeps track of
let node_to_push = current.take().expect(&format!("Invalid syntax when parsing XML layout in file {}", path)[..]);
stack.push(node_to_push);
parsing_root_tag_with_declarations = false;
}
// After adding any attributes, the self-closing tag ends
xmlparser::ElementEnd::Empty => {
@ -99,7 +183,7 @@ impl GuiLayout {
let text_string = String::from(text.as_str());
if !text_string.trim().is_empty() {
let text_node = LayoutParsedNode::new_text(text_string);
let text_node = LayoutAbstractNode::new_text(text_string);
let new_node = rctree::Node::new(text_node);
parent_node.append(new_node);
}
@ -113,19 +197,4 @@ impl GuiLayout {
Some(tree) => Ok(tree)
}
}
pub fn interpret_abstract_syntax_tree(root: rctree::Node<LayoutParsedNode>) {
for node in root.descendants() {
println!("{:?}", node);
match & *node.borrow() {
LayoutParsedNode::Tag(tag) => {
LayoutAbstractSyntaxNode::new(tag.namespace.clone(), tag.name.clone(), &tag.attributes);
}
LayoutParsedNode::Text(_) => {}
};
println!();
}
}
}

View File

@ -1,150 +0,0 @@
use crate::layout_abstract_types::*;
use crate::color_palette::ColorPalette;
use crate::color::Color;
#[derive(Debug)]
pub enum Attribute {
VariableValue(VariableValue),
TypeValue(TypeValue),
}
pub fn parse_attribute(input: &str) -> Attribute {
// Match variables and typed values that can be in an attribute
let regex = regex::Regex::new(
r#"(?x)
^\s*(\w*)\s*(:)\s*(\()\s*(\w*\s*(?:\|\s*\w*\s*?)*)\s*(\))\s*(=)\s*(\w*)\s*$ | # Parameter ?: (? | ... | ?) = ?
^\s*(\{\{)\s*(\w*)\s*(\}\})\s*$ | # Argument {{?}}
^\s*(-?\d+)\s*$ | # Integer ?
^\s*(-?(?:(?:\d+\.\d*)|(?:\d*\.\d+)))\s*$ | # Decimal ?
^\s*(-?(?:(?:\d+(?:\.\d*)?)|(?:\d*(?:\.\d+))))([Pp][Xx])\s*$ | # AbsolutePx ?px
^\s*(-?(?:(?:\d+(?:\.\d*)?)|(?:\d*(?:\.\d+))))(%)\s*$ | # Percent ?%
^\s*(-?(?:(?:\d+(?:\.\d*)?)|(?:\d*(?:\.\d+))))(@)\s*$ | # PercentRemainder ?@
^\s*([Ii][Nn][Nn][Ee][Rr])\s*$ | # Inner inner
^\s*([Ww][Ii][Dd][Tt][Hh])\s*$ | # Width width
^\s*([Hh][Ee][Ii][Gg][Hh][Tt])\s*$ | # Height height
^\s*`(.*)`\s*$ | # TemplateString `? ... {{?}} ...`
^\s*(\[)(.*)(\])\s*$ | # Color [?]
^\s*([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])\s*$ | # Bool true/false
^\s*([Nn][Oo][Nn][Ee])\s*$ # None none
"#
).unwrap();
// Match with the regular expression
let captures = regex.captures(input).map(|captures|
captures
.iter()
.skip(1)
.flat_map(|c| c)
.map(|c| c.as_str())
.collect::<Vec<_>>()
);
// Match against the captured values as a slice
let slices = captures.as_ref().map(|c| c.as_slice());
match slices {
Some([name, ":", "(", types, ")", "=", default_value]) => {
// TODO: Extend to support a list of N types (like (AbsolutePx) (AbsolutePx) (AbsolutePx) (AbsolutePx))
let name = String::from(*name);
let split_types = types.split("|").map(|piece| piece.trim());
let valid_types = split_types.map(|type_name|
match &type_name.to_ascii_lowercase()[..] {
"xml" => TypeName::Xml,
"integer" => TypeName::Integer,
"decimal" => TypeName::Decimal,
"absolutepx" => TypeName::AbsolutePx,
"percent" => TypeName::Percent,
"percentremainder" => TypeName::PercentRemainder,
"inner" => TypeName::Inner,
"width" => TypeName::Width,
"height" => TypeName::Height,
"templatestring" => TypeName::TemplateString,
"color" => TypeName::Color,
"bool" => TypeName::Bool,
"none" => TypeName::None,
invalid => panic!("Invalid type `{}` specified in the attribute `{}` when parsing XML layout", invalid, input),
}
).collect::<Vec<_>>();
let default = match parse_attribute(default_value) {
Attribute::TypeValue(type_value) => type_value,
Attribute::VariableValue(variable_value) => panic!("Found the variable value `{:?}` in the attribute `{}` which only allows typed values, when parsing XML layout", variable_value, input),
};
Attribute::VariableValue(VariableValue::Parameter(VariableParameter {name, valid_types, default}))
}
Some(["{{", name, "}}"]) => {
let name = String::from(*name);
Attribute::VariableValue(VariableValue::Argument(name))
}
Some([value]) if regex::Regex::new(r"^\s*(-?\d+)\s*$").unwrap().is_match(value) => {
let integer = value.parse::<i64>().expect(&format!("Invalid value `{}` specified in the attribute `{}` when parsing XML layout", value, input)[..]);
Attribute::TypeValue(TypeValue::Integer(integer))
}
Some([value]) if regex::Regex::new(r"^\s*(-?(?:(?:\d+\.\d*)|(?:\d*\.\d+)))\s*$").unwrap().is_match(value) => {
let decimal = value.parse::<f64>().expect(&format!("Invalid value `{}` specified in the attribute `{}` when parsing XML layout", value, input)[..]);
Attribute::TypeValue(TypeValue::Decimal(decimal))
}
Some([value, px]) if px.eq_ignore_ascii_case("px") => {
let pixels = value.parse::<f32>().expect(&format!("Invalid value `{}` specified in the attribute `{}` when parsing XML layout", value, input)[..]);
Attribute::TypeValue(TypeValue::AbsolutePx(pixels))
}
Some([value, "%"]) => {
let percent = value.parse::<f32>().expect(&format!("Invalid value `{}` specified in the attribute `{}` when parsing XML layout", value, input)[..]);
Attribute::TypeValue(TypeValue::Percent(percent))
}
Some([value, "@"]) => {
let percent_remainder = value.parse::<f32>().expect(&format!("Invalid value `{}` specified in the attribute `{}` when parsing XML layout", value, input)[..]);
Attribute::TypeValue(TypeValue::PercentRemainder(percent_remainder))
}
Some([inner]) if inner.eq_ignore_ascii_case("inner") => {
Attribute::TypeValue(TypeValue::Inner)
}
Some([width]) if width.eq_ignore_ascii_case("width") => {
Attribute::TypeValue(TypeValue::Width)
}
Some([height]) if height.eq_ignore_ascii_case("height") => {
Attribute::TypeValue(TypeValue::Height)
}
Some(["`", string, "`"]) => {
let mut segments = Vec::<TemplateStringSegment>::new();
let mut is_template = false;
let regex = regex::Regex::new(r"\{\{|\}\}").unwrap();
for part in regex.split(string) {
let segment = match is_template {
true => TemplateStringSegment::String(String::from(part)),
false => TemplateStringSegment::Argument(VariableArgument { name: String::from(part) }),
};
segments.push(segment);
is_template = !is_template;
}
Attribute::TypeValue(TypeValue::TemplateString(segments))
}
Some(["[", color_name, "]"]) => {
let regex = regex::Regex::new(r"\s*'(.*)'\s*").unwrap();
let color = match regex.captures(color_name) {
Some(captures) => {
let palette_color = captures.get(1).expect(&format!("Invalid palette color name `{}` specified in the attribute `{}` when parsing XML layout", color_name, input)[..]).as_str();
ColorPalette::lookup_palette_color(palette_color).into_color_srgb()
}
None => {
let parsed = color_name.parse::<css_color_parser::Color>();
let css_color = parsed.expect(&format!("Invalid CSS color name `{}` specified in the attribute `{}` when parsing XML layout", color_name, input)[..]);
Color::new(css_color.r as f32 / 255.0, css_color.g as f32 / 255.0, css_color.b as f32 / 255.0, css_color.a as f32 / 255.0)
}
};
Attribute::TypeValue(TypeValue::Color(color))
}
Some([true_or_false]) if true_or_false.eq_ignore_ascii_case("true") || true_or_false.eq_ignore_ascii_case("false") => {
let boolean = true_or_false.eq_ignore_ascii_case("true");
Attribute::TypeValue(TypeValue::Bool(boolean))
}
Some([none]) if none.eq_ignore_ascii_case("none") => {
Attribute::TypeValue(TypeValue::None)
}
_ => panic!("Invalid attribute value `{}` when parsing XML layout", input),
}
}

View File

@ -1,23 +1,53 @@
use crate::layout_abstract_attributes::*;
use crate::layout_abstract_types::*;
#[derive(Debug)]
pub struct LayoutAbstractSyntaxNode {
pub namespace: Option<String>,
pub enum LayoutAbstractNode {
Tag(LayoutAbstractTag),
Text(String),
}
impl LayoutAbstractNode {
pub fn new_tag(namespace: String, name: String) -> Self {
Self::Tag(LayoutAbstractTag::new(namespace, name))
}
pub fn new_text(text: String) -> Self {
Self::Text(text)
}
}
#[derive(Debug)]
pub struct LayoutAbstractTag {
pub namespace: String,
pub name: String,
pub attributes: Vec<Attribute>,
}
impl LayoutAbstractSyntaxNode {
pub fn new(namespace: Option<String>, tag: String, attributes: &Vec<(String, String)>) -> Self {
for attribute in attributes {
let parsed = parse_attribute(&attribute.1[..]);
println!("{} : {:?} -> {:?}", attribute.0, attribute.1, parsed);
}
Self {
namespace,
name: tag,
attributes: Vec::new(),
}
impl LayoutAbstractTag {
pub fn new(namespace: String, name: String) -> Self {
Self { namespace, name, attributes: Vec::new() }
}
}
pub fn add_attribute(&mut self, attribute: Attribute) {
self.attributes.push(attribute);
}
}
#[derive(Debug)]
pub struct Attribute {
pub name: String,
pub value: AttributeValue,
}
impl Attribute {
pub fn new(name: String, value: AttributeValue) -> Self {
Self { name, value }
}
}
#[derive(Debug)]
pub enum AttributeValue {
VariableParameter(VariableParameter),
TypeValue(Vec<TypeValueOrArgument>),
}

View File

@ -1,19 +1,16 @@
use crate::color::Color;
// Variable types
#[derive(Debug)]
pub enum VariableValue {
Parameter(VariableParameter),
Argument(String),
}
#[derive(Debug)]
pub struct VariableParameter {
pub name: String,
pub valid_types: Vec<TypeName>,
pub default: TypeValue,
// pub value: TypeValue,
pub type_sequence_options: Vec<Vec<TypeName>>,
pub type_sequence_default: Vec<TypeValue>,
}
impl VariableParameter {
pub fn new(name: String, valid_types: Vec<Vec<TypeName>>, default: Vec<TypeValue>) -> Self {
Self { name, type_sequence_options: valid_types, type_sequence_default: default }
}
}
#[derive(Debug)]
@ -21,11 +18,21 @@ pub struct VariableArgument {
pub name: String,
}
// Value types
impl VariableArgument {
pub fn new(name: String) -> Self {
Self { name }
}
}
#[derive(Debug)]
pub enum TypeValueOrArgument {
TypeValue(TypeValue),
VariableArgument(VariableArgument),
}
#[derive(Debug)]
pub enum TypeName {
Xml,
// Xml, // TODO
Integer,
Decimal,
AbsolutePx,
@ -42,7 +49,7 @@ pub enum TypeName {
#[derive(Debug)]
pub enum TypeValue {
Xml(()), // TODO
// Xml(()), // TODO
Integer(i64),
Decimal(f64),
AbsolutePx(f32),
@ -60,5 +67,5 @@ pub enum TypeValue {
#[derive(Debug)]
pub enum TemplateStringSegment {
String(String),
Argument(VariableArgument),
Argument(TypeValueOrArgument),
}

View File

@ -0,0 +1,230 @@
use crate::layout_abstract_types::*;
use crate::layout_abstract_syntax::*;
use crate::color_palette::ColorPalette;
use crate::color::Color;
pub struct AttributeParser {
capture_attribute_declaration_parameter_regex: regex::Regex,
capture_attribute_type_sequences_regex: regex::Regex,
match_integer_regex: regex::Regex,
match_decimal_regex: regex::Regex,
split_by_string_templates_regex: regex::Regex,
capture_color_name_in_palette_regex: regex::Regex,
}
impl AttributeParser {
pub fn new() -> Self {
let capture_attribute_declaration_parameter_regex: regex::Regex = regex::Regex::new(
// Parameter: ?: (?, ... | ...) = ?
r"^\s*(\w*)\s*(:)\s*(\()\s*((?:(?:\w+)(?:\s*,\s*\w+)*)(?:\s*\|\s*(?:(?:\w+)(?:\s*,\s*\w+)*))*)\s*(\))\s*(=)\s*([\s\w'\[\]@%\-.`,]*?)\s*$"
).unwrap();
let capture_attribute_type_sequences_regex: regex::Regex = regex::Regex::new(concat!(
// Argument: {{?}}
r#"^\s*(\{\{)\s*(\w*)\s*(\}\})\s*$|"#,
// Integer: ?
r#"^\s*(-?\d+)\s*$|"#,
// Decimal: ?
r#"^\s*(-?(?:(?:\d+\.\d*)|(?:\d*\.\d+)))\s*$|"#,
// AbsolutePx: px
r#"^\s*(-?(?:(?:\d+(?:\.\d*)?)|(?:\d*(?:\.\d+))))([Pp][Xx])\s*$|"#,
// Percent: ?%
r#"^\s*(-?(?:(?:\d+(?:\.\d*)?)|(?:\d*(?:\.\d+))))(%)\s*$|"#,
// PercentRemainder: ?@
r#"^\s*(-?(?:(?:\d+(?:\.\d*)?)|(?:\d*(?:\.\d+))))(@)\s*$|"#,
// Inner: inner
r#"^\s*([Ii][Nn][Nn][Ee][Rr])\s*$|"#,
// Width: width
r#"^\s*([Ww][Ii][Dd][Tt][Hh])\s*$|"#,
// Height: height
r#"^\s*([Hh][Ee][Ii][Gg][Hh][Tt])\s*$|"#,
// TemplateString: `? ... {{?}} ...`
r#"^\s*(`)(.*)(`)\s*$|"#,
// Color: [?]
r#"^\s*(\[)(.*)(\])\s*$|"#,
// Bool: true/false
r#"^\s*([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])\s*$|"#,
// None: none
r#"^\s*([Nn][Oo][Nn][Ee])\s*$"#,
)).unwrap();
let match_integer_regex = regex::Regex::new(r"^\s*(-?\d+)\s*$").unwrap();
let match_decimal_regex = regex::Regex::new(r"^\s*(-?(?:(?:\d+\.\d*)|(?:\d*\.\d+)))\s*$").unwrap();
let split_by_string_templates_regex = regex::Regex::new(r"\{\{|\}\}").unwrap();
let capture_color_name_in_palette_regex = regex::Regex::new(r"\s*'(.*)'\s*").unwrap();
Self {
capture_attribute_declaration_parameter_regex,
capture_attribute_type_sequences_regex,
match_integer_regex,
match_decimal_regex,
split_by_string_templates_regex,
capture_color_name_in_palette_regex,
}
}
pub fn parse_attribute_types(&self, input: &str) -> AttributeValue {
let attribute_types = input.split(",").map(|piece| piece.trim()).collect::<Vec<&str>>();
let list = attribute_types.iter().map(|attribute_type| self.parse_attribute_type(attribute_type)).collect::<Vec<TypeValueOrArgument>>();
AttributeValue::TypeValue(list)
}
pub fn parse_attribute_type(&self, attribute_type: &str) -> TypeValueOrArgument {
// Match with the regular expression
let captures = self.capture_attribute_type_sequences_regex.captures(attribute_type).map(|captures|
captures.iter().skip(1).flat_map(|c| c).map(|c| c.as_str()).collect::<Vec<_>>()
);
// Match against the captured values as a list of tokens
let tokens = captures.as_ref().map(|c| c.as_slice());
match tokens {
// Argument: {{?}}
Some(["{{", name, "}}"]) => {
let name = String::from(*name);
TypeValueOrArgument::VariableArgument(VariableArgument::new(name))
}
// Integer: ?
Some([value]) if self.match_integer_regex.is_match(value) => {
let integer = value.parse::<i64>().expect(&format!("Invalid value `{}` specified in the attribute type `{}` when parsing XML layout", value, attribute_type)[..]);
TypeValueOrArgument::TypeValue(TypeValue::Integer(integer))
}
// Decimal: ?
Some([value]) if self.match_decimal_regex.is_match(value) => {
let decimal = value.parse::<f64>().expect(&format!("Invalid value `{}` specified in the attribute type `{}` when parsing XML layout", value, attribute_type)[..]);
TypeValueOrArgument::TypeValue(TypeValue::Decimal(decimal))
}
// AbsolutePx: px
Some([value, px]) if px.eq_ignore_ascii_case("px") => {
let pixels = value.parse::<f32>().expect(&format!("Invalid value `{}` specified in the attribute type`{}` when parsing XML layout", value, attribute_type)[..]);
TypeValueOrArgument::TypeValue(TypeValue::AbsolutePx(pixels))
}
// Percent: ?%
Some([value, "%"]) => {
let percent = value.parse::<f32>().expect(&format!("Invalid value `{}` specified in the attribute type `{}` when parsing XML layout", value, attribute_type)[..]);
TypeValueOrArgument::TypeValue(TypeValue::Percent(percent))
}
// PercentRemainder: ?@
Some([value, "@"]) => {
let percent_remainder = value.parse::<f32>().expect(&format!("Invalid value `{}` specified in the attribute type `{}` when parsing XML layout", value, attribute_type)[..]);
TypeValueOrArgument::TypeValue(TypeValue::PercentRemainder(percent_remainder))
}
// Inner: inner
Some([inner]) if inner.eq_ignore_ascii_case("inner") => {
TypeValueOrArgument::TypeValue(TypeValue::Inner)
}
// Width: width
Some([width]) if width.eq_ignore_ascii_case("width") => {
TypeValueOrArgument::TypeValue(TypeValue::Width)
}
// Height: height
Some([height]) if height.eq_ignore_ascii_case("height") => {
TypeValueOrArgument::TypeValue(TypeValue::Height)
}
// TemplateString: `? ... {{?}} ...`
Some(["`", string, "`"]) => {
let mut segments = Vec::<TemplateStringSegment>::new();
let mut is_template = false;
for part in self.split_by_string_templates_regex.split(string) {
let segment = match is_template {
true => TemplateStringSegment::String(String::from(part)),
false => TemplateStringSegment::Argument(TypeValueOrArgument::VariableArgument(VariableArgument::new(String::from(part)))),
};
segments.push(segment);
is_template = !is_template;
}
TypeValueOrArgument::TypeValue(TypeValue::TemplateString(segments))
}
// Color: [?]
Some(["[", color_name, "]"]) => {
let color = match self.capture_color_name_in_palette_regex.captures(color_name) {
Some(captures) => {
let palette_color = captures.get(1).expect(&format!("Invalid palette color name `{}` specified in the attribute type `{}` when parsing XML layout", color_name, attribute_type)[..]).as_str();
ColorPalette::lookup_palette_color(palette_color).into_color_srgb()
}
None => {
let parsed = color_name.parse::<css_color_parser::Color>();
let css_color = parsed.expect(&format!("Invalid CSS color name `{}` specified in the attribute type `{}` when parsing XML layout", color_name, attribute_type)[..]);
Color::new(css_color.r as f32 / 255.0, css_color.g as f32 / 255.0, css_color.b as f32 / 255.0, css_color.a as f32 / 255.0)
}
};
TypeValueOrArgument::TypeValue(TypeValue::Color(color))
}
// Bool: true/false
Some([true_or_false]) if true_or_false.eq_ignore_ascii_case("true") || true_or_false.eq_ignore_ascii_case("false") => {
let boolean = true_or_false.eq_ignore_ascii_case("true");
TypeValueOrArgument::TypeValue(TypeValue::Bool(boolean))
}
// None: none
Some([none]) if none.eq_ignore_ascii_case("none") => {
TypeValueOrArgument::TypeValue(TypeValue::None)
}
// Unrecognized type pattern
_ => panic!("Invalid attribute type `{}` when parsing XML layout", attribute_type),
}
}
pub fn parse_attribute_declaration(&self, attribute_declaration: &str) -> AttributeValue {
// Match with the regular expression
let captures = self.capture_attribute_declaration_parameter_regex.captures(attribute_declaration).map(|captures|
captures.iter().skip(1).flat_map(|c| c).map(|c| c.as_str()).collect::<Vec<_>>()
);
// Match against the captured values as a list of tokens
let tokens = captures.as_ref().map(|c| c.as_slice());
match tokens {
// Parameter: ?: (?, ... | ...) = ?
Some([name, ":", "(", raw_types_list, ")", "=", default_value]) => {
// Variable name bound in the parameter
let name = String::from(*name);
// Split the type sequences up into a list of options separated by vertical bars
let type_sequence_options = String::from(*raw_types_list).split("|").map(|group| {
// Split each type sequence into individual types separated by commas
group.split(",").map(|individual_type| {
// Remove any whitespace around the type
let individual_type = individual_type.trim();
// Return the case-insensitive TypeName enum for the individual type
match &individual_type.to_ascii_lowercase()[..] {
// "xml" => TypeName::Xml, // TODO
"integer" => TypeName::Integer,
"decimal" => TypeName::Decimal,
"absolutepx" => TypeName::AbsolutePx,
"percent" => TypeName::Percent,
"percentremainder" => TypeName::PercentRemainder,
"inner" => TypeName::Inner,
"width" => TypeName::Width,
"height" => TypeName::Height,
"templatestring" => TypeName::TemplateString,
"color" => TypeName::Color,
"bool" => TypeName::Bool,
"none" => TypeName::None,
_ => panic!("Invalid type `{}` specified in the attribute type `{}` when parsing XML layout", individual_type, attribute_declaration),
}
}).collect::<Vec<TypeName>>()
}).collect::<Vec<Vec<TypeName>>>();
// Required default value for the variable parameter if not provided
let default_type_sequence = default_value.split(",").map(|individual_type|
match self.parse_attribute_type(individual_type) {
TypeValueOrArgument::TypeValue(type_value) => type_value,
TypeValueOrArgument::VariableArgument(variable_value) => {
panic!("Found the default variable value `{:?}` in the attribute declaration `{}` which only allows typed values, when parsing XML layout", variable_value, attribute_declaration);
}
}
).collect::<Vec<TypeValue>>();
// Return the parameter
AttributeValue::VariableParameter(VariableParameter::new(name, type_sequence_options, default_type_sequence))
}
// Unrecognized type pattern
_ => panic!("Invalid attribute attribute declaration `{}` when parsing XML layout", attribute_declaration),
}
}
}

View File

@ -1,38 +0,0 @@
#[derive(Debug)]
pub enum LayoutParsedNode {
Tag(LayoutParsedTag),
Text(String),
}
impl LayoutParsedNode {
pub fn new_tag(namespace: String, tag: String) -> Self {
Self::Tag(LayoutParsedTag::new(namespace, tag))
}
pub fn new_text(text: String) -> Self {
Self::Text(text)
}
}
#[derive(Debug)]
pub struct LayoutParsedTag {
pub namespace: Option<String>,
pub name: String,
pub attributes: Vec<(String, String)>,
}
impl LayoutParsedTag {
pub fn new(namespace: String, name: String) -> Self {
let namespace = if namespace.is_empty() { None } else { Some(namespace) };
Self {
namespace,
name,
attributes: Vec::new(),
}
}
pub fn add_attribute(&mut self, attribute: (String, String)) {
self.attributes.push(attribute);
}
}

View File

@ -10,10 +10,9 @@ mod gui_node;
mod gui_attributes;
mod window_events;
mod gui_layout;
mod layout_parsed_node;
mod layout_abstract_types;
mod layout_abstract_attributes;
mod layout_abstract_syntax;
mod layout_attribute_parser;
use application::Application;
use winit::event_loop::EventLoop;