diff --git a/gui/box.xml b/gui/box.xml index c788c674..9d313b70 100644 --- a/gui/box.xml +++ b/gui/box.xml @@ -1,3 +1,3 @@ - + {{INNER_XML}} \ No newline at end of file diff --git a/gui/header/window-buttons.xml b/gui/header/window-buttons.xml index 1838d2d5..623578f1 100644 --- a/gui/header/window-buttons.xml +++ b/gui/header/window-buttons.xml @@ -1,16 +1,16 @@ - + - + - + - + \ No newline at end of file diff --git a/gui/icon.xml b/gui/icon.xml index d4dc814d..4a7b173d 100644 --- a/gui/icon.xml +++ b/gui/icon.xml @@ -1,3 +1,3 @@ - - + + {{INNER_XML}} \ No newline at end of file diff --git a/gui/if.xml b/gui/if.xml new file mode 100644 index 00000000..daa93141 --- /dev/null +++ b/gui/if.xml @@ -0,0 +1,3 @@ + + {{RESULT}} + \ No newline at end of file diff --git a/gui/input/checkbox-with-dropdown.xml b/gui/input/checkbox-with-dropdown.xml index cd10d885..7ac4112d 100644 --- a/gui/input/checkbox-with-dropdown.xml +++ b/gui/input/checkbox-with-dropdown.xml @@ -1,7 +1,7 @@ - + diff --git a/gui/input/checkbox.xml b/gui/input/checkbox.xml index 9fcb4850..6395b190 100644 --- a/gui/input/checkbox.xml +++ b/gui/input/checkbox.xml @@ -2,18 +2,10 @@ - - - - - + - - - - - + diff --git a/gui/input/dropdown.xml b/gui/input/dropdown.xml index 862c9992..6d17827f 100644 --- a/gui/input/dropdown.xml +++ b/gui/input/dropdown.xml @@ -6,11 +6,7 @@ - - - - - + diff --git a/gui/text.xml b/gui/text.xml index 5e4464ce..5630e0aa 100644 --- a/gui/text.xml +++ b/gui/text.xml @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/gui/viewport/panels.xml b/gui/viewport/panels.xml index e9276036..a7ce1f62 100644 --- a/gui/viewport/panels.xml +++ b/gui/viewport/panels.xml @@ -1,7 +1,7 @@ - + Option A Option B Option C - + \ No newline at end of file diff --git a/src/application.rs b/src/application.rs index fc667a43..6cbc02a7 100644 --- a/src/application.rs +++ b/src/application.rs @@ -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(&mut self, event: Event<'_, T>, control_flow: &mut ControlFlow, window: &Window) { + fn main_event_loop(&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"); diff --git a/src/gui_layout.rs b/src/gui_layout.rs index e5fd057e..e44d69bf 100644 --- a/src/gui_layout.rs +++ b/src/gui_layout.rs @@ -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>, + 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> { + + 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, already_loaded_layouts: &mut HashSet) { + 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> { let source = fs::read_to_string(path)?; let parsed = xmlparser::Tokenizer::from(&source[..]); - let mut stack: Vec> = Vec::new(); - let mut current: Option> = None; - let mut result: Option> = None; + let mut stack: Vec> = Vec::new(); + let mut current: Option> = None; + let mut result: Option> = 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) { - 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!(); - } - } } \ No newline at end of file diff --git a/src/layout_abstract_attributes.rs b/src/layout_abstract_attributes.rs deleted file mode 100644 index eab3a5a7..00000000 --- a/src/layout_abstract_attributes.rs +++ /dev/null @@ -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::>() - ); - - // 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::>(); - - 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::().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::().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::().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::().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::().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::::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::(); - 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), - } -} diff --git a/src/layout_abstract_syntax.rs b/src/layout_abstract_syntax.rs index 41520641..5e35ce67 100644 --- a/src/layout_abstract_syntax.rs +++ b/src/layout_abstract_syntax.rs @@ -1,23 +1,53 @@ -use crate::layout_abstract_attributes::*; + +use crate::layout_abstract_types::*; #[derive(Debug)] -pub struct LayoutAbstractSyntaxNode { - pub namespace: Option, +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, } -impl LayoutAbstractSyntaxNode { - pub fn new(namespace: Option, 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() } } -} \ No newline at end of file + + 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), +} diff --git a/src/layout_abstract_types.rs b/src/layout_abstract_types.rs index d1625143..ad47edb2 100644 --- a/src/layout_abstract_types.rs +++ b/src/layout_abstract_types.rs @@ -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, - pub default: TypeValue, - // pub value: TypeValue, + pub type_sequence_options: Vec>, + pub type_sequence_default: Vec, +} + +impl VariableParameter { + pub fn new(name: String, valid_types: Vec>, default: Vec) -> 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), } diff --git a/src/layout_attribute_parser.rs b/src/layout_attribute_parser.rs new file mode 100644 index 00000000..8f8f2ea0 --- /dev/null +++ b/src/layout_attribute_parser.rs @@ -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::>(); + let list = attribute_types.iter().map(|attribute_type| self.parse_attribute_type(attribute_type)).collect::>(); + 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::>() + ); + + // 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::().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::().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::().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::().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::().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::::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::(); + 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::>() + ); + + // 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::>() + }).collect::>>(); + + // 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::>(); + + // 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), + } + } +} diff --git a/src/layout_parsed_node.rs b/src/layout_parsed_node.rs deleted file mode 100644 index 2bd251fe..00000000 --- a/src/layout_parsed_node.rs +++ /dev/null @@ -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, - 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); - } -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index eab7994e..7c26aab7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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;