Browse Source

Merge branch 'data_binding'

# Conflicts:
#	Source/Core/Context.cpp
#	Source/Core/Element.cpp
#	Source/Core/Factory.cpp
Michael Ragazzon 5 years ago
parent
commit
c09981bc03
77 changed files with 5944 additions and 422 deletions
  1. 17 0
      CMake/FileList.cmake
  2. 7 0
      CMake/SampleFileList.cmake
  3. 1 1
      CMake/gen_samplelists.sh
  4. 4 1
      CMakeLists.txt
  5. 6 0
      Include/RmlUi/Core.h
  6. 40 18
      Include/RmlUi/Core/BaseXMLParser.h
  7. 1 1
      Include/RmlUi/Core/Containers/chobo/flat_map.hpp
  8. 1 1
      Include/RmlUi/Core/Containers/chobo/flat_set.hpp
  9. 31 3
      Include/RmlUi/Core/Context.h
  10. 123 0
      Include/RmlUi/Core/DataController.h
  11. 195 0
      Include/RmlUi/Core/DataModel.h
  12. 208 0
      Include/RmlUi/Core/DataTypeRegister.h
  13. 66 0
      Include/RmlUi/Core/DataTypes.h
  14. 269 0
      Include/RmlUi/Core/DataVariable.h
  15. 135 0
      Include/RmlUi/Core/DataView.h
  16. 7 7
      Include/RmlUi/Core/Element.h
  17. 2 1
      Include/RmlUi/Core/Element.inl
  18. 11 1
      Include/RmlUi/Core/ElementUtilities.h
  19. 28 2
      Include/RmlUi/Core/Factory.h
  20. 4 0
      Include/RmlUi/Core/Math.h
  21. 9 3
      Include/RmlUi/Core/StringUtilities.h
  22. 26 0
      Include/RmlUi/Core/Traits.h
  23. 15 0
      Include/RmlUi/Core/TypeConverter.h
  24. 47 7
      Include/RmlUi/Core/TypeConverter.inl
  25. 7 0
      Include/RmlUi/Core/Types.h
  26. 6 2
      Include/RmlUi/Core/Variant.h
  27. 12 4
      Include/RmlUi/Core/Variant.inl
  28. 2 1
      Include/RmlUi/Core/XMLNodeHandler.h
  29. 8 9
      Include/RmlUi/Core/XMLParser.h
  30. 2 1
      Samples/basic/bitmapfont/src/FontEngineBitmap.cpp
  31. 1 1
      Samples/basic/bitmapfont/src/FontEngineBitmap.h
  32. 238 0
      Samples/basic/databinding/data/databinding.rml
  33. 448 0
      Samples/basic/databinding/src/main.cpp
  34. 7 0
      Samples/shell/include/x11/InputX11.h
  35. 12 6
      Source/Controls/WidgetSliderInput.cpp
  36. 2 2
      Source/Controls/WidgetTextInput.cpp
  37. 2 1
      Source/Controls/XMLNodeHandlerDataGrid.cpp
  38. 1 1
      Source/Controls/XMLNodeHandlerDataGrid.h
  39. 5 5
      Source/Controls/XMLNodeHandlerTabSet.cpp
  40. 1 1
      Source/Controls/XMLNodeHandlerTabSet.h
  41. 3 1
      Source/Controls/XMLNodeHandlerTextArea.cpp
  42. 1 1
      Source/Controls/XMLNodeHandlerTextArea.h
  43. 194 155
      Source/Core/BaseXMLParser.cpp
  44. 53 0
      Source/Core/Context.cpp
  45. 77 0
      Source/Core/DataController.cpp
  46. 160 0
      Source/Core/DataControllerDefault.cpp
  47. 90 0
      Source/Core/DataControllerDefault.h
  48. 1191 0
      Source/Core/DataExpression.cpp
  49. 85 0
      Source/Core/DataExpression.h
  50. 470 0
      Source/Core/DataModel.cpp
  51. 126 0
      Source/Core/DataTypeRegister.cpp
  52. 90 0
      Source/Core/DataVariable.cpp
  53. 162 0
      Source/Core/DataView.cpp
  54. 496 0
      Source/Core/DataViewDefault.cpp
  55. 176 0
      Source/Core/DataViewDefault.h
  56. 60 4
      Source/Core/Element.cpp
  57. 125 6
      Source/Core/ElementUtilities.cpp
  58. 160 31
      Source/Core/Factory.cpp
  59. 6 0
      Source/Core/Math.cpp
  60. 1 1
      Source/Core/Property.cpp
  61. 70 3
      Source/Core/StringUtilities.cpp
  62. 22 5
      Source/Core/Variant.cpp
  63. 2 1
      Source/Core/XMLNodeHandlerBody.cpp
  64. 1 1
      Source/Core/XMLNodeHandlerBody.h
  65. 11 2
      Source/Core/XMLNodeHandlerDefault.cpp
  66. 1 1
      Source/Core/XMLNodeHandlerDefault.h
  67. 2 1
      Source/Core/XMLNodeHandlerHead.cpp
  68. 1 1
      Source/Core/XMLNodeHandlerHead.h
  69. 2 1
      Source/Core/XMLNodeHandlerTemplate.cpp
  70. 1 1
      Source/Core/XMLNodeHandlerTemplate.h
  71. 34 0
      Source/Core/XMLParseTools.cpp
  72. 6 0
      Source/Core/XMLParseTools.h
  73. 16 16
      Source/Core/XMLParser.cpp
  74. 18 95
      Source/Debugger/ElementInfo.cpp
  75. 10 15
      Source/Debugger/ElementLog.cpp
  76. 1 0
      Source/Debugger/LogSource.h
  77. 13 0
      changelog.md

+ 17 - 0
CMake/FileList.cmake

@@ -4,6 +4,9 @@ set(Core_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/Clock.h
     ${PROJECT_SOURCE_DIR}/Source/Core/ComputeProperty.h
     ${PROJECT_SOURCE_DIR}/Source/Core/ContextInstancerDefault.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataControllerDefault.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataExpression.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataViewDefault.h
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorGradient.h
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorNinePatch.h
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorTiled.h
@@ -107,6 +110,12 @@ set(Core_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ContextInstancer.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ConvolutionFilter.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Core.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataController.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataModel.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataTypeRegister.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataTypes.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataVariable.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataView.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Debug.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Decorator.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DecoratorInstancer.h
@@ -191,6 +200,14 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/ContextInstancerDefault.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/ConvolutionFilter.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Core.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataController.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataControllerDefault.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataExpression.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataModel.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataTypeRegister.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataVariable.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataView.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataViewDefault.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Decorator.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorGradient.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorInstancer.cpp

+ 7 - 0
CMake/SampleFileList.cmake

@@ -53,6 +53,13 @@ set(customlog_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Samples/basic/customlog/src/SystemInterface.cpp
 )
 
+set(databinding_HDR_FILES
+)
+
+set(databinding_SRC_FILES
+    ${PROJECT_SOURCE_DIR}/Samples/basic/databinding/src/main.cpp
+)
+
 set(demo_HDR_FILES
 )
 

+ 1 - 1
CMake/gen_samplelists.sh

@@ -7,7 +7,7 @@ hdr='set(sample_HDR_FILES'
 srcdir='${PROJECT_SOURCE_DIR}'
 srcpath=Samples
 samples=( 'shell'
-	'basic/animation' 'basic/benchmark' 'basic/bitmapfont' 'basic/customlog' 'basic/demo' 'basic/drag' 'basic/loaddocument' 'basic/treeview' 'basic/transform'
+	'basic/animation' 'basic/benchmark' 'basic/bitmapfont' 'basic/customlog' 'basic/databinding' 'basic/demo' 'basic/drag' 'basic/loaddocument' 'basic/treeview' 'basic/transform'
 	'basic/sdl2' 'basic/sfml2'
 	'tutorial/template' 'tutorial/datagrid' 'tutorial/datagrid_tree' 'tutorial/drag'
 	'invaders' 'luainvaders'

+ 4 - 1
CMakeLists.txt

@@ -468,7 +468,7 @@ endmacro()
 if(BUILD_SAMPLES)
 	include(SampleFileList)
 
-	set(samples treeview customlog drag loaddocument transform bitmapfont animation benchmark demo)
+	set(samples treeview customlog drag loaddocument transform bitmapfont animation benchmark demo databinding)
 	set(tutorials template datagrid datagrid_tree drag)
 	
 if(NOT BUILD_FRAMEWORK)
@@ -668,6 +668,9 @@ if(BUILD_SAMPLES)
 	install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/bitmapfont/data
 			DESTINATION ${SAMPLES_DIR}/basic/bitmapfont
 	)
+	install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/databinding/data
+			DESTINATION ${SAMPLES_DIR}/basic/databinding
+	)
 	install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/demo/data
 			DESTINATION ${SAMPLES_DIR}/basic/demo
 	)

+ 6 - 0
Include/RmlUi/Core.h

@@ -39,6 +39,12 @@
 #include "Core/ComputedValues.h"
 #include "Core/Context.h"
 #include "Core/ContextInstancer.h"
+#include "Core/DataController.h"
+#include "Core/DataModel.h"
+#include "Core/DataTypeRegister.h"
+#include "Core/DataTypes.h"
+#include "Core/DataVariable.h"
+#include "Core/DataView.h"
 #include "Core/Decorator.h"
 #include "Core/DecoratorInstancer.h"
 #include "Core/Element.h"

+ 40 - 18
Include/RmlUi/Core/BaseXMLParser.h

@@ -37,6 +37,10 @@ namespace Rml {
 namespace Core {
 
 class Stream;
+class URL;
+using XMLAttributes = Dictionary;
+
+enum class XMLDataType { Text, CData, InnerXML };
 
 /**
 	@author Peter Curry
@@ -53,6 +57,14 @@ class RMLUICORE_API BaseXMLParser
 		/// @param[in] tag The tag to register as containing generic character data.
 		void RegisterCDATATag(const String& tag);
 
+		/// When an XML attribute with the given name is encountered during parsing, then all content below the current
+		/// node is treated as data.
+		/// @note While children nodes are treated as data (text), it is assumed that the content represents valid XML.
+		///         The parsing proceeds as normal except that the Handle...() functions are not called until the
+		///         starting node is closed. Then, all its contents are submitted as Data (raw text string).
+		/// @note In particular, this behavior is useful for some data-binding views.
+		void RegisterInnerXMLAttribute(const String& attribute_name);
+
 		/// Parses the given stream as an XML file, and calls the handlers when
 		/// interesting phenomena are encountered.
 		void Parse(Stream* stream);
@@ -68,20 +80,31 @@ class RMLUICORE_API BaseXMLParser
 		/// Called when the parser finds the end of an element tag.
 		virtual void HandleElementEnd(const String& name);
 		/// Called when the parser encounters data.
-		virtual void HandleData(const String& data);
+		virtual void HandleData(const String& data, XMLDataType type);
 
 	protected:
-		// The stream we're reading the XML from.
-		Stream* xml_source;
+		const URL* GetSourceURLPtr() const;
 
 	private:
+		const URL* source_url = nullptr;
+		String xml_source;
+		size_t xml_index = 0;
+
+		void Next();
+		bool AtEnd() const;
+		char Look() const;
+
+		void HandleElementStartInternal(const String& name, const XMLAttributes& attributes);
+		void HandleElementEndInternal(const String& name);
+		void HandleDataInternal(const String& data, XMLDataType type);
+
 		void ReadHeader();
 		void ReadBody();
-
 		bool ReadOpenTag();
-		bool ReadCloseTag();
-		bool ReadAttributes(XMLAttributes& attributes);
-		bool ReadCDATA(const char* terminator = nullptr);
+
+		bool ReadCloseTag(size_t xml_index_tag);
+		bool ReadAttributes(XMLAttributes& attributes, bool& parse_raw_xml_content);
+		bool ReadCDATA(const char* tag_terminator = nullptr);
 
 		// Reads from the stream until a complete word is found.
 		// @param[out] word Word thats been found
@@ -89,22 +112,20 @@ class RMLUICORE_API BaseXMLParser
 		bool FindWord(String& word, const char* terminators = nullptr);
 		// Reads from the stream until the given character set is found. All
 		// intervening characters will be returned in data.
-		bool FindString(const unsigned char* string, String& data);
+		bool FindString(const char* string, String& data, bool escape_brackets = false);
 		// Returns true if the next sequence of characters in the stream
 		// matches the given string. If consume is set and this returns true,
 		// the characters will be consumed.
-		bool PeekString(const unsigned char* string, bool consume = true);
+		bool PeekString(const char* string, bool consume = true);
 
-		// Fill the buffer as much as possible, without removing any content that is still pending
-		bool FillBuffer();
+		int line_number = 0;
+		int line_number_open_tag = 0;
+		int open_tag_depth = 0;
 
-		unsigned char* read;
-		unsigned char* buffer;
-		int buffer_size;
-		int buffer_used;
-		int line_number;
-		int line_number_open_tag;
-		int open_tag_depth;
+		// Enabled when an attribute for inner xml data is encountered (see description in Register...() above).
+		bool inner_xml_data = false;
+		int inner_xml_data_terminate_depth = 0;
+		size_t inner_xml_data_index_begin = 0;
 
 		// The element attributes being read.
 		XMLAttributes attributes;
@@ -112,6 +133,7 @@ class RMLUICORE_API BaseXMLParser
 		String data;
 
 		SmallUnorderedSet< String > cdata_tags;
+		SmallUnorderedSet< String > attributes_for_inner_xml_data;
 };
 
 }

+ 1 - 1
Include/RmlUi/Core/Containers/chobo/flat_map.hpp

@@ -254,7 +254,7 @@ public:
     template <class... Args>
     std::pair<iterator, bool> emplace(Args&&... args)
     {
-        value_type val(args...);
+        value_type val(std::forward<Args>(args)...);
         return insert(std::move(val));
     }
 

+ 1 - 1
Include/RmlUi/Core/Containers/chobo/flat_set.hpp

@@ -238,7 +238,7 @@ public:
     template <class... Args>
     std::pair<iterator, bool> emplace(Args&&... args)
     {
-        value_type val(args...);
+        value_type val(std::forward<Args>(args)...);
         return insert(std::move(val));
     }
 

+ 31 - 3
Include/RmlUi/Core/Context.h

@@ -43,6 +43,9 @@ class ContextInstancer;
 class ElementDocument;
 class EventListener;
 class RenderInterface;
+class DataModel;
+class DataModelConstructor;
+class DataTypeRegister;
 enum class EventId : uint16_t;
 
 /**
@@ -218,6 +221,25 @@ public:
 	/// @param[in] instancer The context's instancer.
 	void SetInstancer(ContextInstancer* instancer);
 
+	/// Creates a data model.
+	/// The returned constructor can be used to bind data variables. Elements can bind to the model using the attribute 'data-model="name"'.
+	/// @param[in] name The name of the data model.
+	/// @return A constructor for the data model, or empty if it could not be created.
+	DataModelConstructor CreateDataModel(const String& name);
+
+	/// Retrieves the constructor for an existing data model.
+	/// The returned constructor can be used to add additional bindings to an existing model.
+	/// @param[in] name The name of the data model.
+	/// @return A constructor for the data model, or empty if it could not be found.
+	DataModelConstructor GetDataModel(const String& name);
+
+	/// Removes the given data model.
+	/// This also removes all data views, controllers and bindings contained by the data model.
+	/// @warning Invalidates all handles and constructors pointing to the data model.
+	/// @param[in] name The name of the data model.
+	/// @return True if succesfully removed, false if no data model was found.
+	bool RemoveDataModel(const String& name);
+
 protected:
 	void Release() override;
 
@@ -286,6 +308,11 @@ private:
 	Vector2i clip_origin;
 	Vector2i clip_dimensions;
 
+	using DataModels = UnorderedMap<String, UniquePtr<DataModel>>;
+	DataModels data_models;
+
+	UniquePtr<DataTypeRegister> data_type_register;
+
 	// Internal callback for when an element is detached or removed from the hierarchy.
 	void OnElementDetach(Element* element);
 	// Internal callback for when a new element gains focus.
@@ -297,13 +324,14 @@ private:
 	// Updates the current hover elements, sending required events.
 	void UpdateHoverChain(const Dictionary& parameters, const Dictionary& drag_parameters, const Vector2i& old_mouse_position);
 
-	// Creates the drag clone from the given element. The old drag clone will be released if
-	// necessary.
-	// @param[in] element The element to clone.
+	// Creates the drag clone from the given element. The old drag clone will be released if necessary.
 	void CreateDragClone(Element* element);
 	// Releases the drag clone, if one exists.
 	void ReleaseDragClone();
 
+	// Returns the data model with the provided name, or nullptr if it does not exist.
+	DataModel* GetDataModelPtr(const String& name) const;
+
 	// Builds the parameters for a generic key event.
 	void GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier);
 	// Builds the parameters for a generic mouse event.

+ 123 - 0
Include/RmlUi/Core/DataController.h

@@ -0,0 +1,123 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATACONTROLLER_H
+#define RMLUICOREDATACONTROLLER_H
+
+#include "Header.h"
+#include "Types.h"
+#include "Traits.h"
+#include <unordered_map>
+
+namespace Rml {
+namespace Core {
+
+class Element;
+class DataModel;
+
+
+class RMLUICORE_API DataControllerInstancer : public NonCopyMoveable {
+public:
+    DataControllerInstancer() {}
+    virtual ~DataControllerInstancer() {}
+    virtual DataControllerPtr InstanceController(Element* element) = 0;
+};
+
+template<typename T>
+class DataControllerInstancerDefault final : public DataControllerInstancer {
+public:
+    DataControllerPtr InstanceController(Element* element) override {
+        return DataControllerPtr(new T(element));
+    }
+};
+
+
+/**
+    Data controller.
+
+    Data controllers are used to respond to some change in the document,
+    usually by setting data variables. Such document changes are usually
+    a result of user input.
+    A data controller is declared in the document by the element attribute:
+
+        data-[type]-[modifier]="[assignment_expression]"
+
+    This is similar to declaration of data views, except that controllers
+    instead take an assignment expression to set a variable. Note that, as
+    opposed to views, controllers only respond to certain changes in the
+    document, not to changed data variables.
+
+    The modifier may or may not be required depending on the data controller.
+
+ */
+
+class RMLUICORE_API DataController : public Releasable {
+public:
+	virtual ~DataController();
+
+    // Initialize the data controller.
+    // @param[in] model The data model the controller will be attached to.
+    // @param[in] element The element which spawned the controller.
+    // @param[in] expression The value of the element's 'data-' attribute which spawned the controller (see above).
+    // @param[in] modifier The modifier for the given controller type (see above).
+    // @return True on success.
+    virtual bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) = 0;
+
+    // Returns the attached element if it still exists.
+    Element* GetElement() const;
+
+    // Returns true if the element still exists.
+    bool IsValid() const;
+
+protected:
+	DataController(Element* element);
+
+private:
+	ObserverPtr<Element> attached_element;
+};
+
+
+class RMLUICORE_API DataControllers : NonCopyMoveable {
+public:
+    DataControllers();
+    ~DataControllers();
+
+	void Add(DataControllerPtr controller);
+
+    void OnElementRemove(Element* element);
+
+private:
+    using ElementControllersMap = std::unordered_multimap<Element*, DataControllerPtr>;
+    ElementControllersMap controllers;
+};
+
+
+}
+}
+
+#endif

+ 195 - 0
Include/RmlUi/Core/DataModel.h

@@ -0,0 +1,195 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATAMODEL_H
+#define RMLUICOREDATAMODEL_H
+
+#include "Header.h"
+#include "Types.h"
+#include "Traits.h"
+#include "DataTypes.h"
+#include "DataTypeRegister.h"
+
+namespace Rml {
+namespace Core {
+
+class DataViews;
+class DataControllers;
+class Element;
+
+
+class RMLUICORE_API DataModel : NonCopyMoveable {
+public:
+	DataModel(const TransformFuncRegister* transform_register = nullptr);
+	~DataModel();
+
+	void AddView(DataViewPtr view);
+	void AddController(DataControllerPtr controller);
+
+	bool BindVariable(const String& name, DataVariable variable);
+	bool BindFunc(const String& name, DataGetFunc get_func, DataSetFunc set_func);
+
+	bool BindEventCallback(const String& name, DataEventFunc event_func);
+
+	bool InsertAlias(Element* element, const String& alias_name, DataAddress replace_with_address);
+	bool EraseAliases(Element* element);
+
+	DataAddress ResolveAddress(const String& address_str, Element* element) const;
+	const DataEventFunc* GetEventCallback(const String& name);
+
+	DataVariable GetVariable(const DataAddress& address) const;
+	bool GetVariableInto(const DataAddress& address, Variant& out_value) const;
+
+	void DirtyVariable(const String& variable_name);
+	bool IsVariableDirty(const String& variable_name) const;
+
+	bool CallTransform(const String& name, Variant& inout_result, const VariantList& arguments) const;
+
+	// Elements declaring 'data-model' need to be attached.
+	void AttachModelRootElement(Element* element);
+	ElementList GetAttachedModelRootElements() const;
+
+	void OnElementRemove(Element* element);
+
+	bool Update();
+
+private:
+	UniquePtr<DataViews> views;
+	UniquePtr<DataControllers> controllers;
+
+	UnorderedMap<String, DataVariable> variables;
+	DirtyVariables dirty_variables;
+
+	UnorderedMap<String, UniquePtr<FuncDefinition>> function_variable_definitions;
+	UnorderedMap<String, DataEventFunc> event_callbacks;
+
+	using ScopedAliases = UnorderedMap<Element*, SmallUnorderedMap<String, DataAddress>>;
+	ScopedAliases aliases;
+
+	const TransformFuncRegister* transform_register;
+
+	SmallUnorderedSet<Element*> attached_elements;
+};
+
+
+
+class RMLUICORE_API DataModelHandle {
+public:
+	DataModelHandle(DataModel* model = nullptr) : model(model)
+	{}
+
+	void Update() {
+		model->Update();
+	}
+
+	bool IsVariableDirty(const String& variable_name) {
+		return model->IsVariableDirty(variable_name);
+	}
+	void DirtyVariable(const String& variable_name) {
+		model->DirtyVariable(variable_name);
+	}
+
+	explicit operator bool() { return model; }
+
+private:
+	DataModel* model;
+};
+
+
+class RMLUICORE_API DataModelConstructor {
+public:
+	template<typename T>
+	using DataEventMemberFunc = void(T::*)(DataModelHandle, Event&, const VariantList&);
+
+	DataModelConstructor() : model(nullptr), type_register(nullptr) {}
+	DataModelConstructor(DataModel* model, DataTypeRegister* type_register) : model(model), type_register(type_register) {
+		RMLUI_ASSERT(model && type_register);
+	}
+
+	// Return a handle to the data model being constructed, which can later be used to synchronize variables and update the model.
+	DataModelHandle GetModelHandle() const {
+		return DataModelHandle(model);
+	}
+
+	// Bind a data variable.
+	// @note For non-scalar types make sure they first have been registered with the appropriate 'Register...()' functions.
+	template<typename T> bool Bind(const String& name, T* ptr) {
+		RMLUI_ASSERTMSG(ptr, "Invalid pointer to data variable");
+		return model->BindVariable(name, DataVariable(type_register->GetOrAddScalar<T>(), ptr));
+	}
+
+	// Bind a get/set function pair.
+	bool BindFunc(const String& name, DataGetFunc get_func, DataSetFunc set_func = {}) {
+		return model->BindFunc(name, std::move(get_func), std::move(set_func));
+	}
+
+	// Bind an event callback.
+	bool BindEventCallback(const String& name, DataEventFunc event_func) {
+		return model->BindEventCallback(name, std::move(event_func));
+	}
+	// Convenience wrapper around BindEventCallback for member functions.
+	template<typename T>
+	bool BindEventCallback(const String& name, DataEventMemberFunc<T> member_func, T* object_pointer) {
+		return BindEventCallback(name, std::bind(member_func, object_pointer, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
+	}
+
+	// Register a struct type.
+	// @note The type applies to every data model associated with the current Context.
+	// @return A handle which can be used to register struct members.
+	template<typename T>
+	StructHandle<T> RegisterStruct() {
+		return type_register->RegisterStruct<T>();
+	}
+
+	// Register an array type.
+	// @note The type applies to every data model associated with the current Context.
+	// @note If 'Container::value_type' represents a non-scalar type, that type must already have been registered with the appropriate 'Register...()' functions.
+	// @note Container requires the following functions to be implemented: size() and begin(). This is satisfied by several containers such as std::vector and std::array.
+	template<typename Container>
+	bool RegisterArray() {
+		return type_register->RegisterArray<Container>();
+	}
+
+	// Register a transform function.
+	// A transform function modifies a variant with optional arguments. It can be called in data expressions using the pipe '|' operator.
+	// @note The transform function applies to every data model associated with the current Context.
+	void RegisterTransformFunc(const String& name, DataTransformFunc transform_func) {
+		type_register->GetTransformFuncRegister()->Register(name, std::move(transform_func));
+	}
+
+	explicit operator bool() { return model && type_register; }
+
+private:
+	DataModel* model;
+	DataTypeRegister* type_register;
+};
+
+}
+}
+
+#endif

+ 208 - 0
Include/RmlUi/Core/DataTypeRegister.h

@@ -0,0 +1,208 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATATYPEREGISTER_H
+#define RMLUICOREDATATYPEREGISTER_H
+
+#include "Header.h"
+#include "Types.h"
+#include "Traits.h"
+#include "Variant.h"
+#include "DataTypes.h"
+#include "DataVariable.h"
+
+
+namespace Rml {
+namespace Core {
+
+
+template<typename T>
+struct is_valid_data_scalar {
+	static constexpr bool value = std::is_arithmetic<T>::value
+		|| std::is_same<typename std::remove_cv<T>::type, String>::value;
+};
+
+
+template<typename Object>
+class StructHandle {
+public:
+	StructHandle(DataTypeRegister* type_register, StructDefinition* struct_definition) : type_register(type_register), struct_definition(struct_definition) {}
+	
+	template <typename MemberType>
+	StructHandle<Object>& RegisterMember(const String& name, MemberType Object::* member_ptr);
+
+	StructHandle<Object>& RegisterMemberFunc(const String& name, MemberGetFunc<Object> get_func, MemberSetFunc<Object> set_func = nullptr);
+
+	explicit operator bool() const {
+		return type_register && struct_definition;
+	}
+
+private:
+	DataTypeRegister* type_register;
+	StructDefinition* struct_definition;
+};
+
+
+class RMLUICORE_API TransformFuncRegister {
+public:
+	void Register(const String& name, DataTransformFunc transform_func);
+
+	bool Call(const String& name, Variant& inout_result, const VariantList& arguments) const;
+
+private:
+	UnorderedMap<String, DataTransformFunc> transform_functions;
+};
+
+
+
+class RMLUICORE_API DataTypeRegister : NonCopyMoveable {
+public:
+	DataTypeRegister();
+	~DataTypeRegister();
+
+	template<typename T>
+	StructHandle<T> RegisterStruct()
+	{
+		static_assert(std::is_class<T>::value, "Type must be a struct or class type.");
+		FamilyId id = Family<T>::Id();
+
+		auto struct_variable = std::make_unique<StructDefinition>();
+		StructDefinition* struct_variable_raw = struct_variable.get();
+
+		bool inserted = type_register.emplace(id, std::move(struct_variable)).second;
+		if (!inserted)
+		{
+			RMLUI_ERRORMSG("Type already declared");
+			return StructHandle<T>(nullptr, nullptr);
+		}
+		
+		return StructHandle<T>(this, struct_variable_raw);
+	}
+
+	template<typename Container>
+	bool RegisterArray()
+	{
+		using value_type = typename Container::value_type;
+		VariableDefinition* value_variable = GetOrAddScalar<value_type>();
+		RMLUI_ASSERTMSG(value_variable, "Underlying value type of array has not been registered.");
+		if (!value_variable)
+			return false;
+
+		FamilyId container_id = Family<Container>::Id();
+
+		auto array_variable = std::make_unique<ArrayDefinition<Container>>(value_variable);
+
+		bool inserted = type_register.emplace(container_id, std::move(array_variable)).second;
+		if (!inserted)
+		{
+			RMLUI_ERRORMSG("Array type already declared.");
+			return false;
+		}
+
+		return true;
+	}
+
+	template<typename T>
+	VariableDefinition* RegisterMemberFunc(MemberGetFunc<T> get_func, MemberSetFunc<T> set_func)
+	{
+		FamilyId id = Family<MemberGetFunc<T>>::Id();
+
+		auto result = type_register.emplace(id, nullptr);
+		auto& it = result.first;
+		bool inserted = result.second;
+
+		if (inserted)
+			it->second = std::make_unique<MemberFuncDefinition<T>>(get_func, set_func);
+
+		return it->second.get();
+	}
+
+	template<typename T, typename std::enable_if<is_valid_data_scalar<T>::value, int>::type = 0>
+	VariableDefinition* GetOrAddScalar()
+	{
+		FamilyId id = Family<T>::Id();
+
+		auto result = type_register.emplace(id, nullptr);
+		bool inserted = result.second;
+		UniquePtr<VariableDefinition>& definition = result.first->second;
+
+		if (inserted)
+			definition = std::make_unique<ScalarDefinition<T>>();
+
+		return definition.get();
+	}
+
+	template<typename T, typename std::enable_if<!is_valid_data_scalar<T>::value, int>::type = 0>
+	VariableDefinition* GetOrAddScalar()
+	{
+		return Get<T>();
+	}
+
+	template<typename T>
+	VariableDefinition* Get()
+	{
+		FamilyId id = Family<T>::Id();
+		auto it = type_register.find(id);
+		if (it == type_register.end())
+		{
+			RMLUI_ERRORMSG("Desired data type T not registered with the type register, please use the 'Register...()' functions before binding values, adding members, or registering arrays of non-scalar types.")
+			return nullptr;
+		}
+
+		return it->second.get();
+	}
+
+	TransformFuncRegister* GetTransformFuncRegister() {
+		return &transform_register;
+	}
+
+private:
+	UnorderedMap<FamilyId, UniquePtr<VariableDefinition>> type_register;
+
+	TransformFuncRegister transform_register;
+
+};
+
+template<typename Object>
+template<typename MemberType>
+inline StructHandle<Object>& StructHandle<Object>::RegisterMember(const String& name, MemberType Object::* member_ptr) {
+	VariableDefinition* member_type = type_register->GetOrAddScalar<MemberType>();
+	struct_definition->AddMember(name, std::make_unique<StructMemberObject<Object, MemberType>>(member_type, member_ptr));
+	return *this;
+}
+template<typename Object>
+inline StructHandle<Object>& StructHandle<Object>::RegisterMemberFunc(const String& name, MemberGetFunc<Object> get_func, MemberSetFunc<Object> set_func) {
+	VariableDefinition* definition = type_register->RegisterMemberFunc<Object>(get_func, set_func);
+	struct_definition->AddMember(name, std::make_unique<StructMemberFunc>(definition));
+	return *this;
+}
+
+}
+}
+
+#endif

+ 66 - 0
Include/RmlUi/Core/DataTypes.h

@@ -0,0 +1,66 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATADEFINITIONS_H
+#define RMLUICOREDATADEFINITIONS_H
+
+#include "Header.h"
+#include "Types.h"
+#include <functional>
+
+namespace Rml {
+namespace Core {
+
+class VariableDefinition;
+class DataTypeRegister;
+class TransformFuncRegister;
+class DataModelHandle;
+class DataVariable;
+
+using DataGetFunc = std::function<void(Variant&)>;
+using DataSetFunc = std::function<void(const Variant&)>;
+using DataTransformFunc = std::function<bool(Variant&, const VariantList&)>;
+using DataEventFunc = std::function<void(DataModelHandle, Event&, const VariantList&)>;
+
+template<typename T> using MemberGetFunc = void(T::*)(Variant&);
+template<typename T> using MemberSetFunc = void(T::*)(const Variant&);
+
+using DirtyVariables = SmallUnorderedSet<String>;
+
+struct DataAddressEntry {
+	DataAddressEntry(String name) : name(name), index(-1) { }
+	DataAddressEntry(int index) : index(index) { }
+	String name;
+	int index;
+};
+using DataAddress = std::vector<DataAddressEntry>;
+
+}
+}
+
+#endif

+ 269 - 0
Include/RmlUi/Core/DataVariable.h

@@ -0,0 +1,269 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATAVARIABLE_H
+#define RMLUICOREDATAVARIABLE_H
+
+#include "Header.h"
+#include "Types.h"
+#include "Traits.h"
+#include "Variant.h"
+#include "DataTypes.h"
+#include <iterator>
+
+namespace Rml {
+namespace Core {
+
+
+enum class DataVariableType { Scalar, Array, Struct, Function, MemberFunction };
+
+
+class RMLUICORE_API DataVariable {
+public:
+	DataVariable() {}
+	DataVariable(VariableDefinition* definition, void* ptr) : definition(definition), ptr(ptr) {}
+
+	explicit operator bool() const { return definition; }
+
+	bool Get(Variant& variant);
+	bool Set(const Variant& variant);
+	int Size();
+	DataVariable Child(const DataAddressEntry& address);
+	DataVariableType Type();
+
+private:
+	VariableDefinition* definition = nullptr;
+	void* ptr = nullptr;
+};
+
+
+
+class RMLUICORE_API VariableDefinition {
+public:
+	virtual ~VariableDefinition() = default;
+	DataVariableType Type() const { return type; }
+
+	virtual bool Get(void* ptr, Variant& variant);
+	virtual bool Set(void* ptr, const Variant& variant);
+
+	virtual int Size(void* ptr);
+	virtual DataVariable Child(void* ptr, const DataAddressEntry& address);
+
+protected:
+	VariableDefinition(DataVariableType type) : type(type) {}
+
+private:
+	DataVariableType type;
+};
+
+
+RMLUICORE_API DataVariable MakeLiteralIntVariable(int value);
+
+
+template<typename T>
+class ScalarDefinition final : public VariableDefinition {
+public:
+	ScalarDefinition() : VariableDefinition(DataVariableType::Scalar) {}
+
+	bool Get(void* ptr, Variant& variant) override
+	{
+		variant = *static_cast<T*>(ptr);
+		return true;
+	}
+	bool Set(void* ptr, const Variant& variant) override
+	{
+		return variant.GetInto<T>(*static_cast<T*>(ptr));
+	}
+};
+
+
+class FuncDefinition final : public VariableDefinition {
+public:
+
+	FuncDefinition(DataGetFunc get, DataSetFunc set) : VariableDefinition(DataVariableType::Function), get(std::move(get)), set(std::move(set)) {}
+
+	bool Get(void* /*ptr*/, Variant& variant) override
+	{
+		if (!get)
+			return false;
+		get(variant);
+		return true;
+	}
+	bool Set(void* /*ptr*/, const Variant& variant) override
+	{
+		if (!set)
+			return false;
+		set(variant);
+		return true;
+	}
+private:
+	DataGetFunc get;
+	DataSetFunc set;
+};
+
+
+template<typename Container>
+class ArrayDefinition final : public VariableDefinition {
+public:
+	ArrayDefinition(VariableDefinition* underlying_definition) : VariableDefinition(DataVariableType::Array), underlying_definition(underlying_definition) {}
+
+	int Size(void* ptr) override {
+		return int(static_cast<Container*>(ptr)->size());
+	}
+
+protected:
+	DataVariable Child(void* void_ptr, const DataAddressEntry& address) override
+	{
+		Container* ptr = static_cast<Container*>(void_ptr);
+		const int index = address.index;
+
+		const int container_size = int(ptr->size());
+		if (index < 0 || index >= container_size)
+		{
+			if (address.name == "size")
+				return MakeLiteralIntVariable(container_size);
+
+			Log::Message(Log::LT_WARNING, "Data array index out of bounds.");
+			return DataVariable();
+		}
+
+		auto it = ptr->begin();
+		std::advance(it, index);
+
+		void* next_ptr = &(*it);
+		return DataVariable(underlying_definition, next_ptr);
+	}
+
+private:
+	VariableDefinition* underlying_definition;
+};
+
+
+class StructMember {
+public:
+	StructMember(VariableDefinition* definition) : definition(definition) {}
+	virtual ~StructMember() = default;
+
+	VariableDefinition* GetDefinition() const { return definition; }
+
+	virtual void* GetPointer(void* base_ptr) = 0;
+
+private:
+	VariableDefinition* definition;
+};
+
+template <typename Object, typename MemberType>
+class StructMemberObject final : public StructMember {
+public:
+	StructMemberObject(VariableDefinition* definition, MemberType Object::* member_ptr) : StructMember(definition), member_ptr(member_ptr) {}
+
+	void* GetPointer(void* base_ptr) override {
+		return &(static_cast<Object*>(base_ptr)->*member_ptr);
+	}
+
+private:
+	MemberType Object::* member_ptr;
+};
+
+class StructMemberFunc final : public StructMember {
+public:
+	StructMemberFunc(VariableDefinition* definition) : StructMember(definition) {}
+	void* GetPointer(void* base_ptr) override {
+		return base_ptr;
+	}
+};
+
+
+class StructDefinition final : public VariableDefinition {
+public:
+	StructDefinition() : VariableDefinition(DataVariableType::Struct)
+	{}
+
+	DataVariable Child(void* ptr, const DataAddressEntry& address) override
+	{
+		const String& name = address.name;
+		if (name.empty())
+		{
+			Log::Message(Log::LT_WARNING, "Expected a struct member name but none given.");
+			return DataVariable();
+		}
+
+		auto it = members.find(name);
+		if (it == members.end())
+		{
+			Log::Message(Log::LT_WARNING, "Member %s not found in data struct.", name.c_str());
+			return DataVariable();
+		}
+
+		void* next_ptr = it->second->GetPointer(ptr);
+		VariableDefinition* next_definition = it->second->GetDefinition();
+
+		return DataVariable(next_definition, next_ptr);
+	}
+
+	void AddMember(const String& name, UniquePtr<StructMember> member)
+	{
+		RMLUI_ASSERT(member);
+		bool inserted = members.emplace(name, std::move(member)).second;
+		RMLUI_ASSERTMSG(inserted, "Member name already exists.");
+		(void)inserted;
+	}
+
+private:
+	SmallUnorderedMap<String, UniquePtr<StructMember>> members;
+};
+
+
+template<typename T>
+class MemberFuncDefinition final : public VariableDefinition {
+public:
+	MemberFuncDefinition(MemberGetFunc<T> get, MemberSetFunc<T> set) : VariableDefinition(DataVariableType::MemberFunction), get(get), set(set) {}
+
+	bool Get(void* ptr, Variant& variant) override
+	{
+		if (!get)
+			return false;
+		(static_cast<T*>(ptr)->*get)(variant);
+		return true;
+	}
+	bool Set(void* ptr, const Variant& variant) override
+	{
+		if (!set)
+			return false;
+		(static_cast<T*>(ptr)->*set)(variant);
+		return true;
+	}
+private:
+	MemberGetFunc<T> get;
+	MemberSetFunc<T> set;
+};
+
+}
+}
+
+#endif

+ 135 - 0
Include/RmlUi/Core/DataView.h

@@ -0,0 +1,135 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATAVIEW_H
+#define RMLUICOREDATAVIEW_H
+
+#include "Header.h"
+#include "Types.h"
+#include "Traits.h"
+#include "DataTypes.h"
+#include <unordered_map>
+
+namespace Rml {
+namespace Core {
+
+class Element;
+class DataModel;
+
+
+class RMLUICORE_API DataViewInstancer : public NonCopyMoveable {
+public:
+	DataViewInstancer() {}
+	virtual ~DataViewInstancer() {}
+	virtual DataViewPtr InstanceView(Element* element) = 0;
+};
+
+template<typename T>
+class DataViewInstancerDefault final : public DataViewInstancer {
+public:
+	DataViewPtr InstanceView(Element* element) override {
+		return DataViewPtr(new T(element));
+	}
+};
+
+/**
+	Data view.
+
+	Data views are used to present a data variable in the document by different means.
+	A data view is declared in the document by the element attribute:
+	
+	    data-[type]-[modifier]="[expression]"
+
+	The modifier may or may not be required depending on the data view.
+ */
+
+class RMLUICORE_API DataView : public Releasable {
+public:
+	virtual ~DataView();
+
+	// Initialize the data view.
+	// @param[in] model The data model the view will be attached to.
+	// @param[in] element The element which spawned the view.
+	// @param[in] expression The value of the element's 'data-' attribute which spawned the view (see above).
+	// @param[in] modifier_or_inner_rml The modifier for the given view type (see above), or the inner rml contents for structural data views.
+	// @return True on success.
+	virtual bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier_or_inner_rml) = 0;
+
+	// Update the data view.
+	// Returns true if the update resulted in a document change.
+	virtual bool Update(DataModel& model) = 0;
+
+	// Returns the list of data variable name(s) which can modify this view.
+	virtual StringList GetVariableNameList() const = 0;
+
+	// Returns the attached element if it still exists.
+	Element* GetElement() const;
+
+	// Returns the depth of the attached element in the document tree.
+	int GetElementDepth() const;
+	
+	// Returns true if the element still exists.
+	bool IsValid() const;
+	
+protected:
+	DataView(Element* element);
+
+private:
+	ObserverPtr<Element> attached_element;
+	int element_depth;
+};
+
+
+
+class RMLUICORE_API DataViews : NonCopyMoveable {
+public:
+	DataViews();
+	~DataViews();
+
+	void Add(DataViewPtr view);
+
+	void OnElementRemove(Element* element);
+
+	bool Update(DataModel& model, const DirtyVariables& dirty_variables);
+
+private:
+	using DataViewList = std::vector<DataViewPtr>;
+
+	DataViewList views;
+	
+	DataViewList views_to_add;
+	DataViewList views_to_remove;
+
+	using NameViewMap = std::unordered_multimap<String, DataView*>;
+	NameViewMap name_view_map;
+};
+
+}
+}
+
+#endif

+ 7 - 7
Include/RmlUi/Core/Element.h

@@ -44,6 +44,7 @@ namespace Rml {
 namespace Core {
 
 class Context;
+class DataModel;
 class Decorator;
 class ElementInstancer;
 class EventDispatcher;
@@ -533,23 +534,19 @@ public:
 	 */
 	//@{
 	/// Access the event dispatcher for this element.
-	/// @return The element's dispatcher.
 	EventDispatcher* GetEventDispatcher() const;
 	/// Returns event types with number of listeners for debugging.
-	/// @return Summary of attached listeners.
 	String GetEventDispatcherSummary() const;
 	/// Access the element background.
-	/// @return The element's background.
 	ElementBackground* GetElementBackground() const;
 	/// Access the element border.
-	/// @return The element's boder.
 	ElementBorder* GetElementBorder() const;
 	/// Access the element decorators.
-	/// @return The element decoration.
 	ElementDecoration* GetElementDecoration() const;
 	/// Returns the element's scrollbar functionality.
-	/// @return The element's scrolling functionality.
 	ElementScroll* GetElementScroll() const;
+	/// Returns the data model of this element.
+	DataModel* GetDataModel() const;
 	//@}
 	
 	/// Returns true if this element requires clipping
@@ -622,6 +619,8 @@ protected:
 
 private:
 	void SetParent(Element* parent);
+	
+	void SetDataModel(DataModel* new_data_model);
 
 	void DirtyOffset();
 	void UpdateOffset();
@@ -672,6 +671,8 @@ private:
 	// The owning document
 	ElementDocument* owner_document;
 
+	// Active data model for this element.
+	DataModel* data_model;
 	// Attributes on this element.
 	ElementAttributes attributes;
 
@@ -736,7 +737,6 @@ private:
 	friend class ElementStyle;
 	friend class LayoutEngine;
 	friend class LayoutInlineBox;
-	friend struct ElementDeleter;
 	friend class ElementScroll;
 };
 

+ 2 - 1
Include/RmlUi/Core/Element.inl

@@ -48,7 +48,8 @@ void Element::SetAttribute(const String& name, const T& value)
 {
 	Variant variant(value);
 	attributes[name] = variant;
-	ElementAttributes changed_attributes = { {name, variant} };
+    ElementAttributes changed_attributes;
+    changed_attributes.emplace(name, std::move(variant));
 	OnAttributeChange(changed_attributes);
 }
 

+ 11 - 1
Include/RmlUi/Core/ElementUtilities.h

@@ -131,7 +131,17 @@ public:
 	/// Note: All calls to RenderInterface::SetTransform must go through here.
 	/// @param[in] element		The element whose transform to apply.
 	/// @return true if a render interface is available to set the transform.
-	static bool ApplyTransform(Element &element);
+	static bool ApplyTransform(Element& element);
+
+	/// Creates data views and data controllers if a data model applies to the element.
+	/// Attributes such as 'data-' are used to create the views and controllers.
+	/// @return True if a data view or controller was constructed.
+	static bool ApplyDataViewsControllers(Element* element);
+
+	/// Creates data views that use a raw inner xml content string to construct child elements.
+	/// Right now, this only applies to the 'data-for' view.
+	/// @return True if a data view was constructed.
+	static bool ApplyStructuralDataViews(Element* element, const String& inner_rml);
 };
 
 }

+ 28 - 2
Include/RmlUi/Core/Factory.h

@@ -37,6 +37,8 @@ namespace Core {
 
 class Context;
 class ContextInstancer;
+class DataControllerInstancer;
+class DataViewInstancer;
 class Decorator;
 class DecoratorInstancer;
 class Element;
@@ -97,8 +99,8 @@ public:
 	/// @return The instanced element, or nullptr if the instancing failed.
 	static ElementPtr InstanceElement(Element* parent, const String& instancer, const String& tag, const XMLAttributes& attributes);
 
-	/// Instances a single text element containing a string. The string is assumed to contain no RML markup, but will
-	/// be translated and therefore may have some introduced. In this case more than one element may be instanced.
+	/// Instances a text element containing a string.
+	/// More than one element may be instanced if the string contains RML or RML is introduced during translation.
 	/// @param[in] parent The element any instanced elements will be parented to.
 	/// @param[in] text The text to instance the element (or elements) from.
 	/// @return True if the string was parsed without error, false otherwise.
@@ -173,6 +175,30 @@ public:
 	/// @return The instanced event listener.
 	static EventListener* InstanceEventListener(const String& value, Element* element);
 
+	/// Register an instancer for data views.
+	/// Structural views start a special XML parsing procedure when encountering a declaration of the view. Instead of instancing
+	/// children elements, the raw inner XML/RML contents are submitted to the initializing procedure of the view.
+	/// @param[in] instancer  The instancer to be called.
+	/// @param[in] type_name  The type name of the view, determines the element attribute that is used to initialize it.
+	/// @param[in] is_structural_view  Set true if the view should be parsed as a structural view.
+	/// @lifetime The instancer must be kept alive until after the call to Core::Shutdown.
+	static void RegisterDataViewInstancer(DataViewInstancer* instancer, const String& type_name, bool is_structural_view = false);
+
+	/// Register an instancer for data controllers.
+	/// @param[in] instancer  The instancer to be called.
+	/// @param[in] type_name  The type name of the controller, determines the element attribute that is used to initialize it.
+	/// @lifetime The instancer must be kept alive until after the call to Core::Shutdown.
+	static void RegisterDataControllerInstancer(DataControllerInstancer* instancer, const String& type_name);
+
+	/// Instance the data view with the given type name.
+	static DataViewPtr InstanceDataView(const String& type_name, Element* element, bool is_structural_view);
+
+	/// Instance the data controller with the given type name.
+	static DataControllerPtr InstanceDataController(const String& type_name, Element* element);
+
+	/// Returns the list of element attribute names with an associated structural data view instancer.
+	static const StringList& GetStructuralDataViewAttributeNames();
+
 private:
 	Factory();
 	~Factory();

+ 4 - 0
Include/RmlUi/Core/Math.h

@@ -138,6 +138,10 @@ RMLUICORE_API float SquareRoot(float value);
 RMLUICORE_API float RoundFloat(float value);
 /// Rounds a floating-point value to the nearest integer.
 /// @param[in] value The value to round.
+/// @return The rounded integer as double.
+RMLUICORE_API double RoundFloat(double value);
+/// Rounds a floating-point value to the nearest integer.
+/// @param[in] value The value to round.
 /// @return The rounded integer.
 RMLUICORE_API int RoundToInteger(float value);
 /// Rounds a floating-point value up to the nearest integer.

+ 9 - 3
Include/RmlUi/Core/StringUtilities.h

@@ -73,6 +73,8 @@ namespace StringUtilities
 
 	/// Converts upper-case characters in string to lower-case.
 	RMLUICORE_API String ToLower(const String& string);
+	/// Converts lower-case characters in string to upper-case.
+	RMLUICORE_API String ToUpper(const String& string);
 
 	/// Encode RML characters, eg. '<' to '&lt;'
 	RMLUICORE_API String EncodeRml(const String& string);
@@ -94,6 +96,10 @@ namespace StringUtilities
 	/// Strip whitespace characters from the beginning and end of a string.
 	RMLUICORE_API String StripWhitespace(StringView string);
 
+	/// Trim trailing zeros and the dot from a string-representation of a number with a decimal point.
+	/// @warning If the string does not represent a number _with_ a decimal point, the result will probably not be as desired.
+	RMLUICORE_API void TrimTrailingDotZeros(String& string);
+
 	/// Case insensitive string comparison. Returns true if they compare equal.
 	RMLUICORE_API bool StringCompareCaseInsensitive(StringView lhs, StringView rhs);
 
@@ -155,7 +161,7 @@ public:
 	inline const char* begin() const { return p_begin; }
 	inline const char* end() const { return p_end; }
 
-	inline size_t size() const { return p_end - p_begin; }
+	inline size_t size() const { return size_t(p_end - p_begin); }
 
 	explicit inline operator String() const {
 		return String(p_begin, p_end);
@@ -197,10 +203,10 @@ public:
 	bool operator!=(const StringIteratorU8& other) const { return !(*this == other); }
 
 	// Return a pointer to the current position.
-	inline const char* Get() const { return p; }
+	inline const char* get() const { return p; }
 
 	// Return offset from the beginning of string. Note: Can return negative if decremented.
-	std::ptrdiff_t Offset() const { return p - view.begin(); }
+	std::ptrdiff_t offset() const { return p - view.begin(); }
 
 private:
 	StringView view;

+ 26 - 0
Include/RmlUi/Core/Traits.h

@@ -72,6 +72,32 @@ public:
 	}
 };
 
+
+enum class FamilyId : int {};
+
+class RMLUICORE_API FamilyBase {
+protected:
+	static int GetNewId() {
+		static int id = 0;
+		return id++;
+	}
+	template<typename T>
+	static FamilyId GetId() {
+		static int id = GetNewId();
+		return static_cast<FamilyId>(id);
+	}
+};
+
+template<typename T>
+class Family : FamilyBase {
+public:
+	// Get a unique ID for a given type.
+	// Note: IDs will be unique across DLL-boundaries even for the same type.
+	static FamilyId Id() {
+		return GetId< typename std::remove_cv< typename std::remove_reference< T >::type >::type >();
+	}
+};
+
 }
 }
 

+ 15 - 0
Include/RmlUi/Core/TypeConverter.h

@@ -35,6 +35,7 @@
 #include "StringUtilities.h"
 #include <stdlib.h>
 #include <stdio.h>
+#include <cinttypes>
 
 namespace Rml {
 namespace Core {
@@ -55,6 +56,20 @@ public:
 	static bool Convert(const SourceType& src, DestType& dest);
 };
 
+template<typename T>
+inline String ToString(const T& value, String default_value = String()) {
+	String result = default_value;
+    TypeConverter<T, String>::Convert(value, result);
+	return result;
+}
+
+template<typename T>
+inline T FromString(const String& string, T default_value = T()) {
+    T result = default_value;
+    TypeConverter<String, T>::Convert(string, result);
+    return result;
+}
+
 
 // Some more complex types are defined in cpp-file
 

+ 47 - 7
Include/RmlUi/Core/TypeConverter.inl

@@ -72,7 +72,9 @@ public: \
 /////////////////////////////////////////////////
 PASS_THROUGH(int);
 PASS_THROUGH(unsigned int);
+PASS_THROUGH(int64_t);
 PASS_THROUGH(float);
+PASS_THROUGH(double);
 PASS_THROUGH(bool);
 PASS_THROUGH(char);
 PASS_THROUGH(Character);
@@ -98,16 +100,34 @@ PASS_THROUGH(voidPtr);
 /////////////////////////////////////////////////
 BASIC_CONVERTER(bool, int);
 BASIC_CONVERTER(bool, unsigned int);
+BASIC_CONVERTER(bool, int64_t);
 BASIC_CONVERTER(bool, float);
+BASIC_CONVERTER(bool, double);
 
-BASIC_CONVERTER(int, unsigned int);
 BASIC_CONVERTER_BOOL(int, bool);
+BASIC_CONVERTER(int, unsigned int);
+BASIC_CONVERTER(int, int64_t);
 BASIC_CONVERTER(int, float);
+BASIC_CONVERTER(int, double);
+
+BASIC_CONVERTER_BOOL(int64_t, bool);
+BASIC_CONVERTER(int64_t, int);
+BASIC_CONVERTER(int64_t, float);
+BASIC_CONVERTER(int64_t, double);
+BASIC_CONVERTER(int64_t, unsigned int);
 
 BASIC_CONVERTER_BOOL(float, bool);
 BASIC_CONVERTER(float, int);
+BASIC_CONVERTER(float, int64_t);
+BASIC_CONVERTER(float, double);
 BASIC_CONVERTER(float, unsigned int);
 
+BASIC_CONVERTER_BOOL(double, bool);
+BASIC_CONVERTER(double, int);
+BASIC_CONVERTER(double, int64_t);
+BASIC_CONVERTER(double, float);
+BASIC_CONVERTER(double, unsigned int);
+
 BASIC_CONVERTER(char, Character);
 
 /////////////////////////////////////////////////
@@ -148,16 +168,23 @@ public:
 	}
 };
 
+template<>
+class TypeConverter< String, int64_t >
+{
+public:
+	static bool Convert(const String& src, int64_t& dest)
+	{
+		return sscanf(src.c_str(), "%" SCNd64, &dest) == 1;
+	}
+};
+
 template<>
 class TypeConverter< String, byte >
 {
 public:
 	static bool Convert(const String& src, byte& dest)
 	{
-		int value;
-		bool ret = sscanf(src.c_str(), "%d", &value) == 1;
-		dest = (byte) value;
-		return ret && (value <= 255);
+		return sscanf(src.c_str(), "%hhu", &dest) == 1;
 	}
 };
 
@@ -232,7 +259,10 @@ class TypeConverter< type, String > \
 public: \
 	static bool Convert(const type& src, String& dest) \
 	{ \
-		return FormatString(dest, 32, "%.4f", src) > 0; \
+		if(FormatString(dest, 32, "%.3f", src) == 0) \
+			return false; \
+		StringUtilities::TrimTrailingDotZeros(dest); \
+		return true; \
 	} \
 }
 FLOAT_STRING_CONVERTER(float);
@@ -258,13 +288,23 @@ public:
 	}
 };
 
+template<>
+class TypeConverter< int64_t, String >
+{
+public:
+	static bool Convert(const int64_t& src, String& dest)
+	{
+		return FormatString(dest, 32, "%" PRId64, src) > 0;
+	}
+};
+
 template<>
 class TypeConverter< byte, String >
 {
 public:
 	static bool Convert(const byte& src, String& dest)
 	{
-		return FormatString(dest, 32, "%u", src) > 0;
+		return FormatString(dest, 32, "%hhu", src) > 0;
 	}
 };
 

+ 7 - 0
Include/RmlUi/Core/Types.h

@@ -161,6 +161,7 @@ using SmallOrderedSet = chobo::flat_set< T >;
 // Container types for common classes
 using ElementList = std::vector< Element* >;
 using OwnedElementList = std::vector< ElementPtr >;
+using VariantList = std::vector< Variant >;
 using ElementAnimationList = std::vector< ElementAnimation >;
 
 using PseudoClassList = SmallUnorderedSet< String >;
@@ -189,6 +190,12 @@ using TransformPtr = SharedPtr< Transform >;
 using DecoratorsPtr = SharedPtr<const Decorators>;
 using FontEffectsPtr = SharedPtr<const FontEffects>;
 
+// Data binding types
+class DataView;
+using DataViewPtr = UniqueReleaserPtr<DataView>;
+class DataController;
+using DataControllerPtr = UniqueReleaserPtr<DataController>;
+
 }
 }
 

+ 6 - 2
Include/RmlUi/Core/Variant.h

@@ -53,12 +53,14 @@ public:
 	enum Type : size_t
 	{
 		NONE = '-',
+		BOOL = 'B',
 		BYTE = 'b',
 		CHAR = 'c',
 		FLOAT = 'f',
+		DOUBLE = 'd',
 		INT = 'i',
+		INT64 = 'I',
 		STRING = 's',
-		WORD = 'w',
 		VECTOR2 = '2',
 		VECTOR3 = '3',
 		VECTOR4 = '4',
@@ -121,11 +123,13 @@ private:
 	void Set(const Variant& copy);
 	void Set(Variant&& other);
 
+	void Set(const bool value);
 	void Set(const byte value);
 	void Set(const char value);
 	void Set(const float value);
+	void Set(const double value);
 	void Set(const int value);
-	void Set(const Character value);
+	void Set(const int64_t value);
 	void Set(const char* value);
 	void Set(void* value);
 	void Set(const Vector2f& value);

+ 12 - 4
Include/RmlUi/Core/Variant.inl

@@ -54,6 +54,10 @@ bool Variant::GetInto(T& value) const
 {
 	switch (type)
 	{
+	case BOOL:
+		return TypeConverter< bool, T >::Convert(*(bool*)data, value);
+		break;
+
 	case BYTE:
 		return TypeConverter< byte, T >::Convert(*(byte*)data, value);
 		break;
@@ -66,16 +70,20 @@ bool Variant::GetInto(T& value) const
 		return TypeConverter< float, T >::Convert(*(float*)data, value);
 		break;
 
+	case DOUBLE:
+		return TypeConverter< double, T >::Convert(*(double*)data, value);
+		break;
+
 	case INT:
 		return TypeConverter< int, T >::Convert(*(int*)data, value);
 		break;
 
-	case STRING:
-		return TypeConverter< String, T >::Convert(*(String*)data, value);
+	case INT64:
+		return TypeConverter< int64_t, T >::Convert(*(int64_t*)data, value);
 		break;
 
-	case WORD:
-		return TypeConverter< Character, T >::Convert(*(Character*)data, value);
+	case STRING:
+		return TypeConverter< String, T >::Convert(*(String*)data, value);
 		break;
 
 	case VECTOR2:

+ 2 - 1
Include/RmlUi/Core/XMLNodeHandler.h

@@ -38,6 +38,7 @@ namespace Core {
 
 class Element;
 class XMLParser;
+enum class XMLDataType;
 
 /**
 	A handler gets ElementStart, ElementEnd and ElementData called by the XMLParser.
@@ -65,7 +66,7 @@ public:
 	/// Called for element data.
 	/// @param parser The parser executing the parse.
 	/// @param data The element data.
-	virtual bool ElementData(XMLParser* parser, const String& data) = 0;
+	virtual bool ElementData(XMLParser* parser, const String& data, XMLDataType type) = 0;
 };
 
 }

+ 8 - 9
Include/RmlUi/Core/XMLParser.h

@@ -64,9 +64,6 @@ public:
 	/// Returns the XML document's header.
 	/// @return The document header.
 	DocumentHeader* GetDocumentHeader();
-	/// Returns the source URL of this parse.
-	/// @return The URL of the parsing stream.
-	const URL& GetSourceURL() const;
 
 	// The parse stack.
 	struct ParseFrame
@@ -75,13 +72,13 @@ public:
 		String tag;
 
 		// Element representing this frame.
-		Element* element;
+		Element* element = nullptr;
 
 		// Handler used for this frame.
-		XMLNodeHandler* node_handler;
+		XMLNodeHandler* node_handler = nullptr;
 
 		// The default handler used for this frame's children.
-		XMLNodeHandler* child_handler;
+		XMLNodeHandler* child_handler = nullptr;
 	};
 
 	/// Pushes an element handler onto the parse stack for parsing child elements.
@@ -92,20 +89,22 @@ public:
 	void PushDefaultHandler();
 
 	/// Access the current parse frame.
-	/// @return The parser's current parse frame.
 	const ParseFrame* GetParseFrame() const;
 
+	/// Returns the source URL of this parse.
+	const URL& GetSourceURL() const;
+
 protected:
 	/// Called when the parser finds the beginning of an element tag.
 	void HandleElementStart(const String& name, const XMLAttributes& attributes) override;
 	/// Called when the parser finds the end of an element tag.
 	void HandleElementEnd(const String& name) override;
 	/// Called when the parser encounters data.
-	void HandleData(const String& data) override;
+	void HandleData(const String& data, XMLDataType type) override;
 
 private:
 	// The header of the document being parsed.
-	DocumentHeader* header;
+	UniquePtr<DocumentHeader> header;
 
 	// The active node handler.
 	XMLNodeHandler* active_handler;

+ 2 - 1
Samples/basic/bitmapfont/src/FontEngineBitmap.cpp

@@ -310,7 +310,8 @@ void FontParserBitmap::HandleElementEnd(const String& RMLUI_UNUSED_PARAMETER(nam
 }
 
 // Called when the parser encounters data.
-void FontParserBitmap::HandleData(const String& RMLUI_UNUSED_PARAMETER(data))
+void FontParserBitmap::HandleData(const String& RMLUI_UNUSED_PARAMETER(data), Rml::Core::XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {
 	RMLUI_UNUSED(data);
+	RMLUI_UNUSED(type);
 }

+ 1 - 1
Samples/basic/bitmapfont/src/FontEngineBitmap.h

@@ -115,7 +115,7 @@ public:
 	/// Called when the parser finds the end of an element tag.
 	void HandleElementEnd(const String& name) override;
 	/// Called when the parser encounters data.
-	void HandleData(const String& data) override;
+	void HandleData(const String& data, Rml::Core::XMLDataType type) override;
 
 	String family;
 	FontStyle style = FontStyle::Normal;

+ 238 - 0
Samples/basic/databinding/data/databinding.rml

@@ -0,0 +1,238 @@
+<rml>
+<head>
+<link type="text/template" href="../../../assets/window.rml"/>
+<title>Data Binding Sample</title>
+<style>
+body.window
+{
+	width: 1300px;
+	height: 750px;
+	min-width: 1090px;
+	min-height: 300px;
+	max-width: -1px;
+	max-height: -1px;
+}
+div#title_bar div#icon
+{
+	display: none;
+}
+div#content
+{ 
+	position: relative; 
+}
+tabset
+{
+	display: block;
+}
+tabs
+{
+    display: block;
+	position: fixed;
+	clip: none;
+	text-align: right;
+	top: -47px;
+	left: 205px;
+	right: 10px;
+}
+tab
+{
+    width: 100px;
+	padding: 0px 20px;
+	line-height: 40px;
+	
+	font-size: 16px;
+	color: #ddd;
+	text-align: center;
+	
+	decorator: tiled-horizontal( datagridheader-l, datagridheader-c, datagridheader-r );
+	image-color: #cffc;
+}
+tab:hover
+{
+	image-color: #fffe;
+	color: #fff;
+}
+tab:active, tab:selected
+{
+	image-color: #fff;
+	color: #fff;
+}
+panels
+{
+    display: block;
+}
+panel
+{
+    display: block;
+	padding: 30px;
+	margin-left: auto;
+	margin-right: auto;
+	max-width: 500px;
+}
+h1
+{
+	margin: 1.4em 0 0.7em;
+	font-size: 18px;
+}
+p.title
+{
+	font-size: 35px;
+	color: #b33;
+	font-effect: glow(2px #ed5);
+}
+.center {
+	text-align: center;
+}
+.clickable
+{ 
+	cursor: pointer;
+}
+.red {
+	color: #e44;
+}
+.big {
+	font-size: 1.8em;
+}
+.mouse_detector {
+	width: 300px;
+	min-height: 2em;
+	line-height: 30px;
+	background-color: #909090;
+	border: 1px #666;
+	margin: 2em auto;
+	cursor: pointer;
+}
+
+
+/***  Forms  ***/
+
+form
+{
+	display: block;
+	text-align: left;
+}
+form input, form select { margin-left: 0; }
+form h2 
+{
+	display: block;
+	font-size: 16px;
+	font-weight: bold;
+	margin-top: 8px;
+}
+#rating {
+	display: inline-block;
+	width: 40px;
+	padding-left: 1em;
+}
+#rating_emoji { 
+	color: #ffd40f;
+	font-size: 1.7em;
+}
+#controls textarea 
+{
+	font-size: 18px;
+	font-effect: outline(2px #006600);
+	color: #ddd;
+}
+</style>
+</head>
+
+<body template="window">
+<tabset id="menu">
+<tab>Basics</tab>
+<panel data-model="basics">
+	<h1>{{title}}</h1>
+	<p data-if="show_text">The quick brown fox jumps over the lazy {{animal}}.</p>
+	<input type="text" data-value="animal"/>
+</panel>
+<tab>Events</tab>
+<panel id="welcome" data-model="events">
+	<p class="title" style="margin-top: 1.8em;">{{hello_world}}<span style="color: blue;"> Rated: {{rating}}</span></p>
+	<p>Data binding demo. We rate this a good old {{rating}}!</p>
+	<input type="range" name="rating" min="0" max="100" step="1" value="50" data-value="rating"/>
+	<div data-visible="rating > 50">Thanks for the <span data-if="rating < 80">good</span><span data-if="rating >= 80">awesome</span> rating!</div>
+	<div class="mouse_detector" style="height: 70px;"
+		data-event-mousemove="mouse_detector = 'x: ' + ev.mouse_x + '<br/>y: ' + ev.mouse_y"
+		data-event-click="add_mouse_pos(); hello_world = 'Hello click!'"
+		data-rml="mouse_detector">
+	</div>
+	<div class="mouse_detector" data-if="positions.size > 0" data-event-click="clear_positions">
+		Recorded mouse positions.<br/>
+		<span data-for="pos : positions"> x: {{ pos.x }}, y: {{ pos.y }}<br/></span>
+	</div>
+	<img sprite="icon-invader" data-style-image-color="rating < 80 ? 'black' : 'green'"/>
+	<p>
+		For loop with data expressions:<br/>
+		<span style="padding-left: 1em;" data-for="i : list"> {{ i * 2 + (!(i < 10) ? ' wow!' | to_upper : '') }}</span>
+	</p>
+</panel>
+<tab>Invaders</tab>
+<panel id="invaders" data-model="invaders">
+	<p>
+		Incoming invaders:
+		<input type="range" name="rating" min="0" max="20" step="5" data-value="incoming_invaders_rate"/>
+		{{ incoming_invaders_rate }} / min.
+	</p>
+	<button data-event-click="launch_weapons">Launch weapons!</button>
+	<div data-for="invader : invaders">
+		<h1 data-class-red="invader.danger_rating > 70">{{invader.name}}</h1>
+		<p>Invader {{it_index + 1}} of {{ invaders.size }}.</p>
+		<img data-attr-sprite="invader.sprite" data-style-image-color="invader.color"/>
+		<p>
+			Shots fired (damage): <span data-for="invader.damage"> {{it}} </span>
+		</p>
+	</div>
+	<h1 data-if="invaders.size == 0">It's all safe and sound, sir!</h1>
+</panel>
+<tab>Forms</tab>
+<panel id="controls">
+	<h1>Todo</h1>
+	<form onsubmit="submit_form">
+		<h2>Full name</h2>
+		<div>
+			<input type="text" size="20" name="name"/>
+		</div>
+		<h2>Email and password</h2>
+		<div>
+			<input type="text" size="10" name="email"/>
+			<input type="password" size="10" name="password"/>
+		</div>
+		<h2>Favorite animal</h2>
+		<div>
+			<input type="radio" name="animal" value="dog" checked/> Dog
+			<input type="radio" name="animal" value="cat"/> Cat
+			<input type="radio" name="animal" value="narwhal"/> Narwhal
+			<input type="radio" name="animal" value="no"/> I don't like animals
+		</div>
+		<h2>Favorite meals</h2>
+		<div>
+			<input type="checkbox" name="meals" value="pizza" checked/> Pizza
+			<input type="checkbox" name="meals" value="pasta" checked/> Pasta
+			<input type="checkbox" name="meals" value="lasagne" checked/> Lasagne
+		</div>
+		<h2>Rating</h2>
+		<div>
+			<input type="range" name="rating" min="0" max="100" step="1" value="50" onchange="rating"/> <span id="rating"/><span id="rating_emoji">&nbsp;</span>
+		</div>
+		<h2>Subject</h2>
+		<div>
+			<select name="subject">
+				<option value="none" selected>Choose your subject</option>
+				<option value="feature">Feature request</option>
+				<option value="bug">Bug report</option>
+				<option value="praise">Praise</option>
+				<option value="criticism">Criticism</option>
+			</select>
+		</div>
+		<h2>Message</h2>
+		<div>
+			<textarea cols="30" rows="5" wrap="nowrap" name="message">😍 Hello 🌐 World! 😎</textarea>
+		</div>
+		<div style="margin-bottom: 15px;">
+			<input type="submit">Submit</input>
+		</div>
+	</form>
+</panel>
+</tabset>
+</body>
+</rml>

+ 448 - 0
Samples/basic/databinding/src/main.cpp

@@ -0,0 +1,448 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2018 Michael R. P. Ragazzon
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include <RmlUi/Core.h>
+#include <RmlUi/Controls.h>
+#include <RmlUi/Debugger.h>
+#include <Input.h>
+#include <Shell.h>
+#include <ShellRenderInterfaceOpenGL.h>
+#include <numeric>
+
+
+namespace BasicExample {
+
+	Rml::Core::DataModelHandle model_handle;
+
+	struct MyData {
+		Rml::Core::String title = "Simple data binding example";
+		Rml::Core::String animal = "dog";
+		bool show_text = true;
+	} my_data;
+
+	bool Initialize(Rml::Core::Context* context)
+	{
+		Rml::Core::DataModelConstructor constructor = context->CreateDataModel("basics");
+		if (!constructor)
+			return false;
+
+		constructor.Bind("title", &my_data.title);
+		constructor.Bind("animal", &my_data.animal);
+		constructor.Bind("show_text", &my_data.show_text);
+
+		model_handle = constructor.GetModelHandle();
+
+		return true;
+	}
+
+	void Update()
+	{
+		model_handle.Update();
+	}
+}
+
+
+namespace EventsExample {
+
+	Rml::Core::DataModelHandle model_handle;
+
+	struct MyData {
+		Rml::Core::String hello_world = "Hello World!";
+		Rml::Core::String mouse_detector = "Mouse-move <em>Detector</em>.";
+		int rating = 99;
+
+		std::vector<float> list = { 1, 2, 3, 4, 5 };
+
+		std::vector<Rml::Core::Vector2f> positions;
+
+		void AddMousePos(Rml::Core::DataModelHandle model, Rml::Core::Event& ev, const Rml::Core::VariantList& /*arguments*/)
+		{
+			positions.emplace_back(ev.GetParameter("mouse_x", 0.f), ev.GetParameter("mouse_y", 0.f));
+			model.DirtyVariable("positions");
+		}
+
+	} my_data;
+
+
+	void ClearPositions(Rml::Core::DataModelHandle model, Rml::Core::Event& /*ev*/, const Rml::Core::VariantList& /*arguments*/)
+	{
+		my_data.positions.clear();
+		model.DirtyVariable("positions");
+	}
+
+	void HasGoodRating(Rml::Core::Variant& variant)
+	{
+		variant = int(my_data.rating > 50);
+	}
+
+
+	bool Initialize(Rml::Core::Context* context)
+	{
+		using namespace Rml::Core;
+		DataModelConstructor constructor = context->CreateDataModel("events");
+		if (!constructor)
+			return false;
+
+		// Register all the types first
+		constructor.RegisterArray<std::vector<float>>();
+
+		if (auto vec2_handle = constructor.RegisterStruct<Vector2f>())
+		{
+			vec2_handle.RegisterMember("x", &Vector2f::x);
+			vec2_handle.RegisterMember("y", &Vector2f::y);
+		}
+		constructor.RegisterArray<std::vector<Vector2f>>();
+
+		// Bind the variables to the data model
+		constructor.Bind("hello_world", &my_data.hello_world);
+		constructor.Bind("mouse_detector", &my_data.mouse_detector);
+		constructor.Bind("rating", &my_data.rating);
+		constructor.BindFunc("good_rating", &HasGoodRating);
+		constructor.BindFunc("great_rating", [](Variant& variant) {
+			variant = int(my_data.rating > 80);
+		});
+
+		constructor.Bind("list", &my_data.list);
+
+		constructor.Bind("positions", &my_data.positions);
+
+		constructor.BindEventCallback("clear_positions", &ClearPositions);
+		constructor.BindEventCallback("add_mouse_pos", &MyData::AddMousePos, &my_data);
+
+
+		model_handle = constructor.GetModelHandle();
+
+		return true;
+	}
+
+	void Update()
+	{
+		if (model_handle.IsVariableDirty("rating"))
+		{
+			model_handle.DirtyVariable("good_rating");
+			model_handle.DirtyVariable("great_rating");
+
+			size_t new_size = my_data.rating / 10 + 1;
+			if (new_size != my_data.list.size())
+			{
+				my_data.list.resize(new_size);
+				std::iota(my_data.list.begin(), my_data.list.end(), float(new_size));
+				model_handle.DirtyVariable("list");
+			}
+		}
+
+		model_handle.Update();
+	}
+}
+
+
+
+namespace InvadersExample {
+
+	Rml::Core::DataModelHandle model_handle;
+
+	struct Invader {
+		Rml::Core::String name;
+		Rml::Core::String sprite;
+		Rml::Core::Colourb color{ 255, 255, 255 };
+		std::vector<int> damage;
+		float danger_rating = 50;
+
+		void GetColor(Rml::Core::Variant& variant) {
+			variant = "rgba(" + Rml::Core::ToString(color) + ')';
+		}
+		void SetColor(const Rml::Core::Variant& variant) {
+			using namespace Rml::Core;
+			String str = variant.Get<String>();
+			if (str.size() > 6)
+				str = str.substr(5, str.size() - 6);
+			color = Rml::Core::FromString<Colourb>(variant.Get<String>());
+		}
+	};
+
+	struct InvadersData {
+		double time_last_invader_spawn = 0;
+		double time_last_weapons_launched = 0;
+
+		float incoming_invaders_rate = 10; // Per minute
+
+		std::vector<Invader> invaders = {
+			Invader{"Angry invader", "icon-invader", {255, 40, 30}, {3, 6, 7}, 80}
+		};
+
+		void LaunchWeapons(Rml::Core::DataModelHandle model, Rml::Core::Event& /*ev*/, const Rml::Core::VariantList& /*arguments*/)
+		{
+			invaders.clear();
+			model.DirtyVariable("invaders");
+		}
+
+	} invaders_data;
+
+	bool Initialize(Rml::Core::Context* context)
+	{
+		Rml::Core::DataModelConstructor constructor = context->CreateDataModel("invaders");
+		if (!constructor)
+			return false;
+
+		// Since Invader::damage is an array type.
+		constructor.RegisterArray<std::vector<int>>();
+
+		// Structs are registered by adding all its members through the returned handle.
+		if (auto invader_handle = constructor.RegisterStruct<Invader>())
+		{
+			invader_handle.RegisterMember("name", &Invader::name);
+			invader_handle.RegisterMember("sprite", &Invader::sprite);
+			invader_handle.RegisterMember("damage", &Invader::damage);
+			invader_handle.RegisterMember("danger_rating", &Invader::danger_rating);
+
+			// Getter and setter functions can also be used.
+			invader_handle.RegisterMemberFunc("color", &Invader::GetColor);
+		}
+
+		// We can even have an Array of Structs, infinitely nested if we so desire.
+		// Make sure the underlying type (here Invader) is registered before the array.
+		constructor.RegisterArray<std::vector<Invader>>();
+
+		// Now we can bind the variables to the model.
+		constructor.Bind("incoming_invaders_rate", &invaders_data.incoming_invaders_rate);
+		constructor.Bind("invaders", &invaders_data.invaders);
+
+		// This function will be called when the user clicks the 'Launch weapons' button.
+		constructor.BindEventCallback("launch_weapons", &InvadersData::LaunchWeapons, &invaders_data);
+
+		model_handle = constructor.GetModelHandle();
+
+		return true;
+	}
+
+	void Update(const double t)
+	{
+		// Add new invaders at regular time intervals.
+		const double t_next_spawn = invaders_data.time_last_invader_spawn + 60.0 / double(invaders_data.incoming_invaders_rate);
+		if (t >= t_next_spawn)
+		{
+			using namespace Rml::Core;
+			const int num_items = 4;
+			static std::array<String, num_items> names = { "Angry invader", "Harmless invader", "Deceitful invader", "Cute invader" };
+			static std::array<String, num_items> sprites = { "icon-invader", "icon-flag", "icon-game", "icon-waves" };
+			static std::array<Colourb, num_items> colors = { { { 255, 40, 30 }, {20, 40, 255}, {255, 255, 30}, {230, 230, 230} } };
+
+			Invader new_invader;
+			new_invader.name = names[rand() % num_items];
+			new_invader.sprite = sprites[rand() % num_items];
+			new_invader.color = colors[rand() % num_items];
+			new_invader.danger_rating = float((rand() % 100) + 1);
+			invaders_data.invaders.push_back(new_invader);
+
+			model_handle.DirtyVariable("invaders");
+			invaders_data.time_last_invader_spawn = t;
+		}
+
+		// Launch shots from a random invader.
+		if (t >= invaders_data.time_last_weapons_launched + 1.0)
+		{
+			if (!invaders_data.invaders.empty())
+			{
+				const size_t index = size_t(rand() % int(invaders_data.invaders.size()));
+
+				Invader& invader = invaders_data.invaders[index];
+				invader.damage.push_back(rand() % int(invader.danger_rating));
+
+				model_handle.DirtyVariable("invaders");
+			}
+			invaders_data.time_last_weapons_launched = t;
+		}
+
+		model_handle.Update();
+	}
+}
+
+
+
+class DemoWindow : public Rml::Core::EventListener
+{
+public:
+	DemoWindow(const Rml::Core::String &title, const Rml::Core::Vector2f &position, Rml::Core::Context *context)
+	{
+		using namespace Rml::Core;
+		document = context->LoadDocument("basic/databinding/data/databinding.rml");
+		if (document)
+		{
+			document->GetElementById("title")->SetInnerRML(title);
+			document->SetProperty(PropertyId::Left, Property(position.x, Property::PX));
+			document->SetProperty(PropertyId::Top, Property(position.y, Property::PX));
+
+			document->Show();
+		}
+	}
+
+	void Shutdown() 
+	{
+		if (document)
+		{
+			document->Close();
+			document = nullptr;
+		}
+	}
+
+	void ProcessEvent(Rml::Core::Event& event) override
+	{
+		using namespace Rml::Core;
+
+		switch (event.GetId())
+		{
+		case EventId::Keydown:
+		{
+			Rml::Core::Input::KeyIdentifier key_identifier = (Rml::Core::Input::KeyIdentifier) event.GetParameter< int >("key_identifier", 0);
+
+			if (key_identifier == Rml::Core::Input::KI_ESCAPE)
+			{
+				Shell::RequestExit();
+			}
+			else if (key_identifier == Rml::Core::Input::KI_F8)
+			{
+				Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
+			}
+		}
+		break;
+
+		default:
+			break;
+		}
+	}
+
+	Rml::Core::ElementDocument * GetDocument() {
+		return document;
+	}
+
+
+private:
+	Rml::Core::ElementDocument *document = nullptr;
+};
+
+
+
+Rml::Core::Context* context = nullptr;
+ShellRenderInterfaceExtensions *shell_renderer;
+
+void GameLoop()
+{
+	const double t = Rml::Core::GetSystemInterface()->GetElapsedTime();
+	
+	BasicExample::Update();
+	EventsExample::Update();
+	InvadersExample::Update(t);
+
+	context->Update();
+
+	shell_renderer->PrepareRenderBuffer();
+	context->Render();
+	shell_renderer->PresentRenderBuffer();
+}
+
+
+
+#if defined RMLUI_PLATFORM_WIN32
+#include <windows.h>
+int APIENTRY WinMain(HINSTANCE RMLUI_UNUSED_PARAMETER(instance_handle), HINSTANCE RMLUI_UNUSED_PARAMETER(previous_instance_handle), char* RMLUI_UNUSED_PARAMETER(command_line), int RMLUI_UNUSED_PARAMETER(command_show))
+#else
+int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
+#endif
+{
+#ifdef RMLUI_PLATFORM_WIN32
+	RMLUI_UNUSED(instance_handle);
+	RMLUI_UNUSED(previous_instance_handle);
+	RMLUI_UNUSED(command_line);
+	RMLUI_UNUSED(command_show);
+#else
+	RMLUI_UNUSED(argc);
+	RMLUI_UNUSED(argv);
+#endif
+
+	const int width = 1600;
+	const int height = 900;
+
+	ShellRenderInterfaceOpenGL opengl_renderer;
+	shell_renderer = &opengl_renderer;
+
+	// Generic OS initialisation, creates a window and attaches OpenGL.
+	if (!Shell::Initialise() ||
+		!Shell::OpenWindow("Data Binding Sample", shell_renderer, width, height, true))
+	{
+		Shell::Shutdown();
+		return -1;
+	}
+
+	// RmlUi initialisation.
+	Rml::Core::SetRenderInterface(&opengl_renderer);
+	opengl_renderer.SetViewport(width, height);
+
+	ShellSystemInterface system_interface;
+	Rml::Core::SetSystemInterface(&system_interface);
+
+	Rml::Core::Initialise();
+
+	// Create the main RmlUi context and set it on the shell's input layer.
+	context = Rml::Core::CreateContext("main", Rml::Core::Vector2i(width, height));
+
+	if (!context
+		|| !BasicExample::Initialize(context)
+		|| !EventsExample::Initialize(context)
+		|| !InvadersExample::Initialize(context)
+		)
+	{
+		Rml::Core::Shutdown();
+		Shell::Shutdown();
+		return -1;
+	}
+
+	Rml::Controls::Initialise();
+	Rml::Debugger::Initialise(context);
+	Input::SetContext(context);
+	shell_renderer->SetContext(context);
+	
+	Shell::LoadFonts("assets/");
+
+	auto demo_window = std::make_unique<DemoWindow>("Data binding", Rml::Core::Vector2f(150, 50), context);
+	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keydown, demo_window.get());
+	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keyup, demo_window.get());
+
+	Shell::EventLoop(GameLoop);
+
+	demo_window->Shutdown();
+
+	// Shutdown RmlUi.
+	Rml::Core::Shutdown();
+
+	Shell::CloseWindow();
+	Shell::Shutdown();
+
+	demo_window.reset();
+
+	return 0;
+}

+ 7 - 0
Samples/shell/include/x11/InputX11.h

@@ -30,6 +30,13 @@
 #define INPUTX11_H
 
 #include <X11/Xlib.h>
+
+// The None define from X.h conflicts with RmlUi code base,
+// use the constant 0L instead where necessary
+#ifdef None
+  #undef None
+#endif
+
 #include "Input.h"
 
 /**

+ 12 - 6
Source/Controls/WidgetSliderInput.cpp

@@ -46,12 +46,13 @@ WidgetSliderInput::~WidgetSliderInput()
 {
 }
 
-void WidgetSliderInput::SetValue(float value)
+void WidgetSliderInput::SetValue(float target_value)
 {
-	float num_steps = (value - min_value) / step;
+	float num_steps = (target_value - min_value) / step;
 	float new_value = min_value + Rml::Core::Math::RoundFloat(num_steps) * step;
 
-	SetBarPosition(SetValueInternal(new_value));
+	if(new_value != value)
+		SetBarPosition(SetValueInternal(new_value));
 }
 
 float WidgetSliderInput::GetValue() const
@@ -128,9 +129,14 @@ float WidgetSliderInput::SetValueInternal(float new_value)
 		return 0;
 	}
 
-    Rml::Core::Dictionary parameters;
-    parameters["value"] = value;
-    GetParent()->DispatchEvent(Core::EventId::Change, parameters);
+	Rml::Core::Dictionary parameters;
+	parameters["value"] = value;
+	GetParent()->DispatchEvent(Core::EventId::Change, parameters);
+
+
+	// TODO: This might not be the safest approach as this will call SetValue(), 
+	// thus, a slight mismatch will result in infinite recursion.
+	GetParent()->SetAttribute("value", value);
 
 	return (value - min_value) / (max_value - min_value);
 }

+ 2 - 2
Source/Controls/WidgetTextInput.cpp

@@ -155,7 +155,7 @@ void WidgetTextInput::SetMaxLength(int _max_length)
 				num_characters += 1;
 				if (num_characters > max_length)
 				{
-					i_erase = size_t(it.Offset());
+					i_erase = size_t(it.offset());
 					break;
 				}
 			}
@@ -853,7 +853,7 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 	for(auto it = Core::StringIteratorU8(lines[line_index].content, 0, lines[line_index].content_length); it; )
 	{
 		++it;
-		int offset = (int)it.Offset();
+		int offset = (int)it.offset();
 
 		float line_width = (float) Core::ElementUtilities::GetStringWidth(text_element, lines[line_index].content.substr(0, offset));
 		if (line_width > position)

+ 2 - 1
Source/Controls/XMLNodeHandlerDataGrid.cpp

@@ -104,8 +104,9 @@ bool XMLNodeHandlerDataGrid::ElementEnd(Core::XMLParser* RMLUI_UNUSED_PARAMETER(
 	return true;
 }
 
-bool XMLNodeHandlerDataGrid::ElementData(Core::XMLParser* parser, const Rml::Core::String& data)
+bool XMLNodeHandlerDataGrid::ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {
+	RMLUI_UNUSED(type);
 	Core::Element* parent = parser->GetParseFrame()->element;
 
 	// Parse the text into the parent element.

+ 1 - 1
Source/Controls/XMLNodeHandlerDataGrid.h

@@ -52,7 +52,7 @@ public:
 	/// Called when an element is closed.
 	bool ElementEnd(Core::XMLParser* parser, const Rml::Core::String& name) override;
 	/// Called for element data.
-	bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data) override;
+	bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType type) override;
 };
 
 }

+ 5 - 5
Source/Controls/XMLNodeHandlerTabSet.cpp

@@ -113,7 +113,6 @@ Core::Element* XMLNodeHandlerTabSet::ElementStart(Core::XMLParser* parser, const
 
 		Core::Element* parent = parser->GetParseFrame()->element;
 
-		// Attempt to instance the element with the instancer.
 		Core::ElementPtr element = Core::Factory::InstanceElement(parent, name, name, attributes);
 		if (!element)
 		{
@@ -121,9 +120,9 @@ Core::Element* XMLNodeHandlerTabSet::ElementStart(Core::XMLParser* parser, const
 			return nullptr;
 		}
 
-		// Add the element to its parent and remove the initial reference.
-		Core::Element* result = parent->AppendChild(std::move(element));
-		return result;
+		parent->AppendChild(std::move(element));
+
+		return nullptr;
 	}
 
 	return nullptr;
@@ -137,8 +136,9 @@ bool XMLNodeHandlerTabSet::ElementEnd(Core::XMLParser* RMLUI_UNUSED_PARAMETER(pa
 	return true;
 }
 
-bool XMLNodeHandlerTabSet::ElementData(Core::XMLParser* parser, const Rml::Core::String& data)
+bool XMLNodeHandlerTabSet::ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {	
+	RMLUI_UNUSED(type);
 	return Core::Factory::InstanceElementText(parser->GetParseFrame()->element, data);
 }
 

+ 1 - 1
Source/Controls/XMLNodeHandlerTabSet.h

@@ -51,7 +51,7 @@ public:
 	/// Called when an element is closed
 	bool ElementEnd(Core::XMLParser* parser, const Rml::Core::String& name) override;
 	/// Called for element data
-	bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data) override;
+	bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType type) override;
 };
 
 }

+ 3 - 1
Source/Controls/XMLNodeHandlerTextArea.cpp

@@ -69,8 +69,10 @@ bool XMLNodeHandlerTextArea::ElementEnd(Core::XMLParser* RMLUI_UNUSED_PARAMETER(
 	return true;
 }
 
-bool XMLNodeHandlerTextArea::ElementData(Core::XMLParser* parser, const Rml::Core::String& data)
+bool XMLNodeHandlerTextArea::ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {
+	RMLUI_UNUSED(type);
+
 	ElementFormControlTextArea* text_area = rmlui_dynamic_cast< ElementFormControlTextArea* >(parser->GetParseFrame()->element);
 	if (text_area != nullptr)
 	{

+ 1 - 1
Source/Controls/XMLNodeHandlerTextArea.h

@@ -51,7 +51,7 @@ public:
 	/// Called when an element is closed.
 	bool ElementEnd(Core::XMLParser* parser, const Rml::Core::String& name) override;
 	/// Called for element data.
-	bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data) override;
+	bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType type) override;
 };
 
 }

+ 194 - 155
Source/Core/BaseXMLParser.cpp

@@ -29,26 +29,17 @@
 #include "../../Include/RmlUi/Core/BaseXMLParser.h"
 #include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/Stream.h"
+#include "XMLParseTools.h"
 #include <string.h>
 
 namespace Rml {
 namespace Core {
 
-// Most file layers cache 4k.
-const int DEFAULT_BUFFER_SIZE = 4096;
-
 BaseXMLParser::BaseXMLParser()
-{
-	read = nullptr;
-	buffer = nullptr;
-	buffer_used = 0;
-	buffer_size = 0;
-	open_tag_depth = 0;
-}
+{}
 
 BaseXMLParser::~BaseXMLParser()
-{
-}
+{}
 
 // Registers a tag as containing general character data.
 void BaseXMLParser::RegisterCDATATag(const String& tag)
@@ -57,24 +48,41 @@ void BaseXMLParser::RegisterCDATATag(const String& tag)
 		cdata_tags.insert(StringUtilities::ToLower(tag));
 }
 
+void BaseXMLParser::RegisterInnerXMLAttribute(const String& attribute_name)
+{
+	attributes_for_inner_xml_data.insert(attribute_name);
+}
+
 // Parses the given stream as an XML file, and calls the handlers when
 // interesting phenomenon are encountered.
 void BaseXMLParser::Parse(Stream* stream)
 {
-	xml_source = stream;
-	buffer_size = DEFAULT_BUFFER_SIZE;
+	source_url = &stream->GetSourceURL();
+
+	xml_source.clear();
+
+	// We read in the whole XML file here.
+	// TODO: It doesn't look like the Stream interface is used for anything useful. We
+	//   might as well just use a span or StringView, and get completely rid of it.
+	// @performance Otherwise, use the temporary allocator.
+	const size_t source_size = stream->Length();
+	stream->Read(xml_source, source_size);
 
-	buffer = (unsigned char*) malloc(buffer_size);
-	read = buffer;
+	xml_index = 0;
 	line_number = 1;
-	FillBuffer();
+	line_number_open_tag = 1;
+
+	inner_xml_data = false;
+	inner_xml_data_terminate_depth = 0;
+	inner_xml_data_index_begin = 0;
 
 	// Read (er ... skip) the header, if one exists.
 	ReadHeader();
 	// Read the XML body.
 	ReadBody();
 
-	free(buffer);
+	xml_source.clear();
+	source_url = nullptr;
 }
 
 // Get the current file line number
@@ -102,17 +110,56 @@ void BaseXMLParser::HandleElementEnd(const String& RMLUI_UNUSED_PARAMETER(name))
 }
 
 // Called when the parser encounters data.
-void BaseXMLParser::HandleData(const String& RMLUI_UNUSED_PARAMETER(data))
+void BaseXMLParser::HandleData(const String& RMLUI_UNUSED_PARAMETER(data), XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {
 	RMLUI_UNUSED(data);
+	RMLUI_UNUSED(type);
+}
+
+/// Returns the source URL of this parse. Only valid during parsing.
+
+const URL* BaseXMLParser::GetSourceURLPtr() const
+{
+	return source_url;
+}
+
+void BaseXMLParser::Next() {
+	xml_index += 1;
+}
+
+bool BaseXMLParser::AtEnd() const {
+	return xml_index >= xml_source.size();
+}
+
+char BaseXMLParser::Look() const {
+	RMLUI_ASSERT(!AtEnd());
+	return xml_source[xml_index];
+}
+
+void BaseXMLParser::HandleElementStartInternal(const String& name, const XMLAttributes& attributes)
+{
+	if (!inner_xml_data)
+		HandleElementStart(name, attributes);
+}
+
+void BaseXMLParser::HandleElementEndInternal(const String& name)
+{
+	if (!inner_xml_data)
+		HandleElementEnd(name);
+}
+
+void BaseXMLParser::HandleDataInternal(const String& data, XMLDataType type)
+{
+	if (!inner_xml_data)
+		HandleData(data, type);
 }
 
 void BaseXMLParser::ReadHeader()
 {
-	if (PeekString((unsigned char*) "<?"))
+	if (PeekString("<?"))
 	{
 		String temp;
-		FindString((unsigned char*) ">", temp);
+		FindString(">", temp);
 	}
 }
 
@@ -126,35 +173,34 @@ void BaseXMLParser::ReadBody()
 	for(;;)
 	{
 		// Find the next open tag.
-		if (!FindString((unsigned char*) "<", data))
+		if (!FindString("<", data, true))
 			break;
 
+		const size_t xml_index_tag = xml_index - 1;
+
 		// Check what kind of tag this is.
-		if (PeekString((const unsigned char*) "!--"))
+		if (PeekString("!--"))
 		{
 			// Comment.
 			String temp;
-			if (!FindString((const unsigned char*) "-->", temp))
+			if (!FindString("-->", temp))
 				break;
 		}
-		else if (PeekString((const unsigned char*) "![CDATA["))
+		else if (PeekString("![CDATA["))
 		{
 			// CDATA tag; read everything (including markup) until the ending
 			// CDATA tag.
 			if (!ReadCDATA())
 				break;
 		}
-		else if (PeekString((const unsigned char*) "/"))
+		else if (PeekString("/"))
 		{
-			if (!ReadCloseTag())
+			if (!ReadCloseTag(xml_index_tag))
 				break;
 
 			// Bail if we've hit the end of the XML data.
 			if (open_tag_depth == 0)
-			{
-				xml_source->Seek((long)((read - buffer) - buffer_used), SEEK_CUR);
 				break;
-			}
 		}
 		else
 		{
@@ -168,7 +214,7 @@ void BaseXMLParser::ReadBody()
 	// Check for error conditions
 	if (open_tag_depth > 0)
 	{
-		Log::Message(Log::LT_WARNING, "XML parse error on line %d of %s.", GetLineNumber(), xml_source->GetSourceURL().GetURL().c_str());
+		Log::Message(Log::LT_WARNING, "XML parse error on line %d of %s.", GetLineNumber(), source_url->GetURL().c_str());
 	}
 }
 
@@ -180,7 +226,7 @@ bool BaseXMLParser::ReadOpenTag()
 	// Opening tag; send data immediately and open the tag.
 	if (!data.empty())
 	{
-		HandleData(data);
+		HandleDataInternal(data, XMLDataType::Text);
 		data.clear();
 	}
 
@@ -190,18 +236,18 @@ bool BaseXMLParser::ReadOpenTag()
 
 	bool section_opened = false;
 
-	if (PeekString((const unsigned char*) ">"))
+	if (PeekString(">"))
 	{
 		// Simple open tag.
-		HandleElementStart(tag_name, XMLAttributes());
+		HandleElementStartInternal(tag_name, XMLAttributes());
 		section_opened = true;
 	}
-	else if (PeekString((const unsigned char*) "/") &&
-			 PeekString((const unsigned char*) ">"))
+	else if (PeekString("/") &&
+			 PeekString(">"))
 	{
 		// Empty open tag.
-		HandleElementStart(tag_name, XMLAttributes());
-		HandleElementEnd(tag_name);
+		HandleElementStartInternal(tag_name, XMLAttributes());
+		HandleElementEndInternal(tag_name);
 
 		// Tag immediately closed, reduce count
 		open_tag_depth--;
@@ -209,20 +255,21 @@ bool BaseXMLParser::ReadOpenTag()
 	else
 	{
 		// It appears we have some attributes. Let's parse them.
+		bool parse_inner_xml_as_data = false;
 		XMLAttributes attributes;
-		if (!ReadAttributes(attributes))
+		if (!ReadAttributes(attributes, parse_inner_xml_as_data))
 			return false;
 
-		if (PeekString((const unsigned char*) ">"))
+		if (PeekString(">"))
 		{
-			HandleElementStart(tag_name, attributes);
+			HandleElementStartInternal(tag_name, attributes);
 			section_opened = true;
 		}
-		else if (PeekString((const unsigned char*) "/") &&
-				 PeekString((const unsigned char*) ">"))
+		else if (PeekString("/") &&
+				 PeekString(">"))
 		{
-			HandleElementStart(tag_name, attributes);
-			HandleElementEnd(tag_name);
+			HandleElementStartInternal(tag_name, attributes);
+			HandleElementEndInternal(tag_name);
 
 			// Tag immediately closed, reduce count
 			open_tag_depth--;
@@ -231,23 +278,32 @@ bool BaseXMLParser::ReadOpenTag()
 		{
 			return false;
 		}
+
+		if (section_opened && parse_inner_xml_as_data && !inner_xml_data)
+		{
+			inner_xml_data = true;
+			inner_xml_data_terminate_depth = open_tag_depth;
+			inner_xml_data_index_begin = xml_index;
+		}
 	}
 
-	// Check if this tag needs to processed as CDATA.
+	// Check if this tag needs to be processed as CDATA.
 	if (section_opened)
 	{
-		String lcase_tag_name = StringUtilities::ToLower(tag_name);
-		if (cdata_tags.find(lcase_tag_name) != cdata_tags.end())
+		const String lcase_tag_name = StringUtilities::ToLower(tag_name);
+		bool is_cdata_tag = (cdata_tags.find(lcase_tag_name) != cdata_tags.end());
+
+		if (is_cdata_tag)
 		{
 			if (ReadCDATA(lcase_tag_name.c_str()))
 			{
 				open_tag_depth--;
 				if (!data.empty())
 				{
-					HandleData(data);
+					HandleDataInternal(data, XMLDataType::CData);
 					data.clear();
 				}
-				HandleElementEnd(tag_name);
+				HandleElementEndInternal(tag_name);
 
 				return true;
 			}
@@ -259,28 +315,41 @@ bool BaseXMLParser::ReadOpenTag()
 	return true;
 }
 
-bool BaseXMLParser::ReadCloseTag()
+bool BaseXMLParser::ReadCloseTag(const size_t xml_index_tag)
 {
+	if (inner_xml_data && open_tag_depth == inner_xml_data_terminate_depth)
+	{
+		// Closing the tag that initiated the inner xml data parsing. Set all its contents as Data to be
+		// submitted next, and disable the mode to resume normal parsing behavior.
+		RMLUI_ASSERT(inner_xml_data_index_begin <= xml_index_tag);
+		inner_xml_data = false;
+		data = xml_source.substr(inner_xml_data_index_begin, xml_index_tag - inner_xml_data_index_begin);
+		HandleDataInternal(data, XMLDataType::InnerXML);
+		data.clear();
+	}
+
 	// Closing tag; send data immediately and close the tag.
 	if (!data.empty())
 	{
-		HandleData(data);
+		HandleDataInternal(data, XMLDataType::Text);
 		data.clear();
 	}
 
 	String tag_name;
-	if (!FindString((const unsigned char*) ">", tag_name))
+	if (!FindString(">", tag_name))
 		return false;
 
-	HandleElementEnd(StringUtilities::StripWhitespace(tag_name));
+	HandleElementEndInternal(StringUtilities::StripWhitespace(tag_name));
+
 
 	// Tag closed, reduce count
 	open_tag_depth--;
 
+
 	return true;
 }
 
-bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes)
+bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes, bool& parse_raw_xml_content)
 {
 	for (;;)
 	{
@@ -294,16 +363,16 @@ bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes)
 		}
 		
 		// Check if theres an assigned value
-		if (PeekString((const unsigned char*)"="))
+		if (PeekString("="))
 		{
-			if (PeekString((const unsigned char*) "\""))
+			if (PeekString("\""))
 			{
-				if (!FindString((const unsigned char*) "\"", value))
+				if (!FindString("\"", value))
 					return false;
 			}
-			else if (PeekString((const unsigned char*) "'"))
+			else if (PeekString("'"))
 			{
-				if (!FindString((const unsigned char*) "'", value))
+				if (!FindString("'", value))
 					return false;
 			}
 			else if (!FindWord(value, "/>"))
@@ -312,21 +381,23 @@ bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes)
 			}
 		}
 
+		if (attributes_for_inner_xml_data.count(attribute) == 1)
+			parse_raw_xml_content = true;
+
  		attributes[attribute] = value;
 
 		// Check for the end of the tag.
-		if (PeekString((const unsigned char*) "/", false) ||
-			PeekString((const unsigned char*) ">", false))
+		if (PeekString("/", false) || PeekString(">", false))
 			return true;
 	}
 }
 
-bool BaseXMLParser::ReadCDATA(const char* terminator)
+bool BaseXMLParser::ReadCDATA(const char* tag_terminator)
 {
 	String cdata;
-	if (terminator == nullptr)
+	if (tag_terminator == nullptr)
 	{
-		FindString((const unsigned char*) "]]>", cdata);
+		FindString("]]>", cdata);
 		data += cdata;
 		return true;
 	}
@@ -335,26 +406,24 @@ bool BaseXMLParser::ReadCDATA(const char* terminator)
 		for (;;)
 		{
 			// Search for the next tag opening.
-			if (!FindString((const unsigned char*) "<", cdata))
+			if (!FindString("<", cdata))
 				return false;
 
-			if (PeekString((const unsigned char*) "/", false))
+			if (PeekString("/", false))
 			{
 				String tag;
-				if (FindString((const unsigned char*) ">", tag))
+				if (FindString(">", tag))
 				{
 					size_t slash_pos = tag.find('/');
 					String tag_name = StringUtilities::StripWhitespace(slash_pos == String::npos ? tag : tag.substr(slash_pos + 1));
-					if (StringUtilities::ToLower(tag_name) == terminator)
+					if (StringUtilities::ToLower(tag_name) == tag_terminator)
 					{
 						data += cdata;
 						return true;
 					}
 					else
 					{
-						cdata += "<";
-						cdata += tag;
-						cdata += ">";
+						cdata += '<' + tag + '>';
 					}
 				}
 				else
@@ -369,20 +438,16 @@ bool BaseXMLParser::ReadCDATA(const char* terminator)
 // Reads from the stream until a complete word is found.
 bool BaseXMLParser::FindWord(String& word, const char* terminators)
 {
-	for (;;)
+	while (!AtEnd())
 	{
-		if (read >= buffer + buffer_used)
-		{
-			if (!FillBuffer())			
-				return false;
-		}
+		char c = Look();
 
 		// Ignore white space
-		if (StringUtilities::IsWhitespace(*read))
+		if (StringUtilities::IsWhitespace(c))
 		{
 			if (word.empty())
 			{
-				read++;
+				Next();
 				continue;
 			}
 			else
@@ -390,35 +455,49 @@ bool BaseXMLParser::FindWord(String& word, const char* terminators)
 		}
 
 		// Check for termination condition
-		if (terminators && strchr(terminators, *read))
+		if (terminators && strchr(terminators, c))
 		{
 			return !word.empty();
 		}
 
-		word += *read;
-		read++;
+		word += c;
+		Next();
 	}
+
+	return false;
 }
 
 // Reads from the stream until the given character set is found.
-bool BaseXMLParser::FindString(const unsigned char* string, String& data)
+bool BaseXMLParser::FindString(const char* string, String& data, bool escape_brackets)
 {
 	int index = 0;
+	bool in_brackets = false;
+	char previous = 0;
+
 	while (string[index])
 	{
-		if (read >= buffer + buffer_used)
-		{
-			if (!FillBuffer())
-				return false;
-		}
+		if (AtEnd())
+			return false;
+
+		const char c = Look();
 
 		// Count line numbers
-		if (*read == '\n')
+		if (c == '\n')
 		{
 			line_number++;
 		}
 
-		if (*read == string[index])
+		if(escape_brackets)
+		{
+			const char* error_str = XMLParseTools::ParseDataBrackets(in_brackets, c, previous);
+			if (error_str)
+			{
+				Log::Message(Log::LT_WARNING, "XML parse error. %s", error_str);
+				return false;
+			}
+		}
+
+		if (c == string[index] && !in_brackets)
 		{
 			index += 1;
 		}
@@ -426,14 +505,15 @@ bool BaseXMLParser::FindString(const unsigned char* string, String& data)
 		{
 			if (index > 0)
 			{
-				data += String((const char*)string, index);
+				data += String(string, index);
 				index = 0;
 			}
 
-			data += *read;
+			data += c;
 		}
 
-		read++;
+		previous = c;
+		Next();
 	}
 
 	return true;
@@ -441,85 +521,44 @@ bool BaseXMLParser::FindString(const unsigned char* string, String& data)
 
 // Returns true if the next sequence of characters in the stream matches the
 // given string.
-bool BaseXMLParser::PeekString(const unsigned char* string, bool consume)
+bool BaseXMLParser::PeekString(const char* string, bool consume)
 {
-	unsigned char* peek_read = read;
-
+	const size_t start_index = xml_index;
+	bool success = true;
 	int i = 0;
 	while (string[i])
 	{
-		// If we're about to read past the end of the buffer, read into the
-		// overflow buffer.
-		if ((peek_read - buffer) + i >= buffer_used)
+		if (AtEnd())
 		{
-			int peek_offset = (int)(peek_read - read);
-			FillBuffer();
-			peek_read = read + peek_offset;
-
-			if (peek_read - buffer + i >= buffer_used)
-			{
-				// Wierd, seems our buffer is too small, realloc it bigger.
-				buffer_size *= 2;
-				int read_offset = (int)(read - buffer);
-				unsigned char* new_buffer = (unsigned char*) realloc(buffer, buffer_size);
-				RMLUI_ASSERTMSG(new_buffer != nullptr, "Unable to allocate larger buffer for Peek() call");
-				if(new_buffer == nullptr)
-				{
-					return false;
-				}
-				buffer = new_buffer;
-				// Restore the read pointers.
-				read = buffer + read_offset;
-				peek_read = read + peek_offset;
-				
-				// Attempt to fill our new buffer size.
-				if (!FillBuffer())
-					return false;
-			}
+			success = false;
+			break;
 		}
 
+		const char c = Look();
+
 		// Seek past all the whitespace if we haven't hit the initial character yet.
-		if (i == 0 && StringUtilities::IsWhitespace(*peek_read))
+		if (i == 0 && StringUtilities::IsWhitespace(c))
 		{
-			peek_read++;
+			Next();
 		}
 		else
 		{
-			if (*peek_read != string[i])
-				return false;
+			if (c != string[i])
+			{
+				success = false;
+				break;
+			}
 
 			i++;
-			peek_read++;
+			Next();
 		}
 	}
 
-	// Set the read pointer to the end of the peek.
-	if (consume)
-	{		
-		read = peek_read;
-	}
-
-	return true;
-}
-
-// Fill the buffer as much as possible, without removing any content that is still pending
-bool BaseXMLParser::FillBuffer()
-{
-	int bytes_free = buffer_size;
-	int bytes_remaining = Math::Max((int)(buffer_used - (read - buffer)), 0);
-
-	// If theres any data still in the buffer, shift it down, and fill it again
-	if (bytes_remaining > 0)
-	{
-		memmove(buffer, read, bytes_remaining);
-		bytes_free = buffer_size - bytes_remaining;
-	}
-	
-	read = buffer;
-	size_t bytes_read = xml_source->Read(&buffer[bytes_remaining], bytes_free);
-	buffer_used = (int)(bytes_read + bytes_remaining);
+	// Set the index to the start index unless we are consuming.
+	if (!consume || !success)
+		xml_index = start_index;
 
-	return bytes_read > 0;
+	return success;
 }
 
 }

+ 53 - 0
Source/Core/Context.cpp

@@ -36,6 +36,8 @@
 #include "../../Include/RmlUi/Core/RenderInterface.h"
 #include "../../Include/RmlUi/Core/StreamMemory.h"
 #include "../../Include/RmlUi/Core/SystemInterface.h"
+#include "../../Include/RmlUi/Core/DataModel.h"
+#include "../../Include/RmlUi/Core/StreamMemory.h"
 #include "EventDispatcher.h"
 #include "EventIterators.h"
 #include "PluginRegistry.h"
@@ -796,6 +798,49 @@ void Context::SetInstancer(ContextInstancer* _instancer)
 	instancer = _instancer;
 }
 
+DataModelConstructor Context::CreateDataModel(const String& name)
+{
+	if (!data_type_register)
+		data_type_register = std::make_unique<DataTypeRegister>();
+
+	auto result = data_models.emplace(name, std::make_unique<DataModel>(data_type_register->GetTransformFuncRegister()));
+	bool inserted = result.second;
+	if (inserted)
+		return DataModelConstructor(result.first->second.get(), data_type_register.get());
+
+	Log::Message(Log::LT_ERROR, "Data model name '%s' already exists.", name.c_str());
+	return DataModelConstructor();
+}
+
+DataModelConstructor Context::GetDataModel(const String& name)
+{
+	if (data_type_register)
+	{
+		if (DataModel* model = GetDataModelPtr(name))
+			return DataModelConstructor(model, data_type_register.get());
+	}
+
+	Log::Message(Log::LT_ERROR, "Data model name '%s' could not be found.", name.c_str());
+	return DataModelConstructor();
+}
+
+bool Context::RemoveDataModel(const String& name)
+{
+	auto it = data_models.find(name);
+	if (it == data_models.end())
+		return false;
+
+	DataModel* model = it->second.get();
+	ElementList elements = model->GetAttachedModelRootElements();
+
+	for (Element* element : elements)
+		element->SetDataModel(nullptr);
+
+	data_models.erase(it);
+
+	return true;
+}
+
 // Internal callback for when an element is removed from the hierarchy.
 void Context::OnElementDetach(Element* element)
 {
@@ -1147,6 +1192,14 @@ void Context::ReleaseDragClone()
 	}
 }
 
+DataModel* Context::GetDataModelPtr(const String& name) const
+{
+	auto it = data_models.find(name);
+	if (it != data_models.end())
+		return it->second.get();
+	return nullptr;
+}
+
 // Builds the parameters for a generic key event.
 void Context::GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier)
 {

+ 77 - 0
Source/Core/DataController.cpp

@@ -0,0 +1,77 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../../Include/RmlUi/Core/DataController.h"
+#include "../../Include/RmlUi/Core/DataModel.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include "EventSpecification.h"
+
+namespace Rml {
+namespace Core {
+
+
+DataController::DataController(Element* element) : attached_element(element->GetObserverPtr())
+{}
+
+DataController::~DataController()
+{}
+Element* DataController::GetElement() const {
+	return attached_element.get();
+}
+
+bool DataController::IsValid() const {
+	return static_cast<bool>(attached_element);
+}
+
+
+
+DataControllers::DataControllers()
+{}
+
+DataControllers::~DataControllers()
+{}
+
+void DataControllers::Add(DataControllerPtr controller) {
+	RMLUI_ASSERT(controller);
+
+	Element* element = controller->GetElement();
+	RMLUI_ASSERTMSG(element, "Invalid controller, make sure it is valid before adding");
+	if (!element)
+		return;
+
+	controllers.emplace(element, std::move(controller));
+}
+
+void DataControllers::OnElementRemove(Element* element)
+{
+	controllers.erase(element);
+}
+
+
+}
+}

+ 160 - 0
Source/Core/DataControllerDefault.cpp

@@ -0,0 +1,160 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "DataControllerDefault.h"
+#include "../../Include/RmlUi/Core/DataController.h"
+#include "../../Include/RmlUi/Core/DataModel.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include "DataExpression.h"
+#include "EventSpecification.h"
+
+namespace Rml {
+namespace Core {
+
+
+
+DataControllerValue::DataControllerValue(Element* element) : DataController(element)
+{}
+
+DataControllerValue::~DataControllerValue()
+{
+	if (Element* element = GetElement())
+	{
+		element->RemoveEventListener(EventId::Change, this);
+	}
+}
+
+bool DataControllerValue::Initialize(DataModel& model, Element* element, const String& variable_name, const String& /*modifier*/)
+{
+	RMLUI_ASSERT(element);
+
+	DataAddress variable_address = model.ResolveAddress(variable_name, element);
+	if (variable_address.empty())
+		return false;
+
+	if (model.GetVariable(variable_address))
+		address = std::move(variable_address);
+	
+	element->AddEventListener(EventId::Change, this);
+
+	return true;
+}
+
+void DataControllerValue::ProcessEvent(Event& event)
+{
+	if (Element* element = GetElement())
+	{
+		const auto& parameters = event.GetParameters();
+		auto it = parameters.find("value");
+		if (it == parameters.end())
+		{
+			Log::Message(Log::LT_WARNING, "A 'change' event was received, but it did not contain a value. During processing of 'data-value' in %s", element->GetAddress().c_str());
+			return;
+		}
+
+		SetValue(it->second);
+	}
+}
+
+void DataControllerValue::Release()
+{
+	delete this;
+}
+
+void DataControllerValue::SetValue(const Variant& value)
+{
+	Element* element = GetElement();
+	if (!element)
+		return;
+
+	DataModel* model = element->GetDataModel();
+	if (!model)
+		return;
+
+	if (DataVariable variable = model->GetVariable(address))
+	{
+		variable.Set(value);
+		model->DirtyVariable(address.front().name);
+	}
+}
+
+
+DataControllerEvent::DataControllerEvent(Element* element) : DataController(element)
+{}
+
+DataControllerEvent::~DataControllerEvent()
+{
+	if (Element* element = GetElement())
+	{
+		if (id != EventId::Invalid)
+			element->RemoveEventListener(id, this);
+	}
+}
+
+bool DataControllerEvent::Initialize(DataModel& model, Element* element, const String& expression_str, const String& modifier)
+{
+	RMLUI_ASSERT(element);
+
+	expression = std::make_unique<DataExpression>(expression_str);
+	DataExpressionInterface interface(&model, element);
+
+	if (!expression->Parse(interface, true))
+		return false;
+
+	id = EventSpecificationInterface::GetIdOrInsert(modifier);
+	if (id == EventId::Invalid)
+	{
+		Log::Message(Log::LT_WARNING, "Event type '%s' could not be recognized, while adding 'data-event' to %s", modifier.c_str(), element->GetAddress().c_str());
+		return false;
+	}
+
+	element->AddEventListener(id, this);
+
+	return true;
+}
+
+void DataControllerEvent::ProcessEvent(Event& event)
+{
+	if (!expression)
+		return;
+
+	if (Element* element = GetElement())
+	{
+		DataExpressionInterface interface(element->GetDataModel(), element, &event);
+		Variant unused_value_out;
+		expression->Run(interface, unused_value_out);
+	}
+}
+
+void DataControllerEvent::Release()
+{
+	delete this;
+}
+
+}
+}

+ 90 - 0
Source/Core/DataControllerDefault.h

@@ -0,0 +1,90 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATACONTROLLERDEFAULT_H
+#define RMLUICOREDATACONTROLLERDEFAULT_H
+
+#include "../../Include/RmlUi/Core/Header.h"
+#include "../../Include/RmlUi/Core/Types.h"
+#include "../../Include/RmlUi/Core/EventListener.h"
+#include "../../Include/RmlUi/Core/DataVariable.h"
+#include "../../Include/RmlUi/Core/DataController.h"
+
+namespace Rml {
+namespace Core {
+
+class Element;
+class DataModel;
+class DataExpression;
+using DataExpressionPtr = UniquePtr<DataExpression>;
+
+
+class DataControllerValue final : public DataController, private EventListener {
+public:
+    DataControllerValue(Element* element);
+    ~DataControllerValue();
+
+    bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override;
+
+protected:
+    // Responds to 'Change' events.
+    void ProcessEvent(Event& event) override;
+    
+    // Delete this.
+    void Release() override;
+
+private:
+    void SetValue(const Variant& new_value);
+
+    DataAddress address;
+};
+
+
+class DataControllerEvent final : public DataController, private EventListener {
+public:
+    DataControllerEvent(Element* element);
+    ~DataControllerEvent();
+
+    bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override;
+
+protected:
+    // Responds to the event type specified in the attribute modifier.
+    void ProcessEvent(Event& event) override;
+
+    // Delete this.
+    void Release() override;
+
+private:
+    EventId id = EventId::Invalid;
+    DataExpressionPtr expression;
+};
+
+}
+}
+
+#endif

+ 1191 - 0
Source/Core/DataExpression.cpp

@@ -0,0 +1,1191 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "DataExpression.h"
+#include "../../Include/RmlUi/Core/DataModel.h"
+#include "../../Include/RmlUi/Core/Event.h"
+#include "../../Include/RmlUi/Core/Variant.h"
+#include <stack>
+
+#ifdef _MSC_VER
+#pragma warning(default : 4061)
+#pragma warning(default : 4062)
+#endif
+
+namespace Rml {
+namespace Core {
+
+class DataParser;
+
+/*
+	The abstract machine for RmlUi data scripts.
+
+	The machine can execute a program which contains a list of instructions listed below.
+
+	The abstract machine has three registers:
+		R  Typically results and right-hand side arguments.
+		L  Typically left-hand side arguments.
+		C  Typically center arguments (eg. in ternary operator).
+
+	And two stacks:
+		S  The main program stack.
+		A  The arguments stack, only used to pass arguments to an external transform function.
+
+	In addition, each instruction has an optional payload:
+		D  Instruction data (payload).
+
+	Notation used in the instruction list below:
+		S+  Push to stack S.
+		S-  Pop stack S (returns the popped value).
+*/
+enum class Instruction { // Assignment (register/stack) = Read (register R/L/C, instruction data D, or stack)
+	Push         = 'P',     //      S+ = R
+	Pop          = 'o',     // <R/L/C> = S-  (D determines R/L/C)
+	Literal      = 'D',     //       R = D
+	Variable     = 'V',     //       R = DataModel.GetVariable(D)  (D is an index into the variable address list)
+	Add          = '+',     //       R = L + R
+	Subtract     = '-',     //       R = L - R
+	Multiply     = '*',     //       R = L * R
+	Divide       = '/',     //       R = L / R
+	Not          = '!',     //       R = !R
+	And          = '&',     //       R = L && R
+	Or           = '|',     //       R = L || R
+	Less         = '<',     //       R = L < R
+	LessEq       = 'L',     //       R = L <= R
+	Greater      = '>',     //       R = L > R
+	GreaterEq    = 'G',     //       R = L >= R
+	Equal        = '=',     //       R = L == R
+	NotEqual     = 'N',     //       R = L != R
+	Ternary      = '?',     //       R = L ? C : R
+	Arguments    = 'a',     //      A+ = S-  (Repeated D times, where D gives the num. arguments)
+	TransformFnc = 'T',     //       R = DataModel.Execute( D, R, A ); A.Clear();  (D determines function name, R the input value, A the arguments)
+	EventFnc     = 'E',     //       DataModel.EventCallback(D, A); A.Clear();
+	Assign       = 'A',     //       DataModel.SetVariable(D, R)
+};
+enum class Register {
+	R,
+	L,
+	C
+};
+
+struct InstructionData {
+	Instruction instruction;
+	Variant data;
+};
+
+namespace Parse {
+	static void Assignment(DataParser& parser);
+	static void Expression(DataParser& parser);
+}
+
+
+class DataParser {
+public:
+	DataParser(String expression, DataExpressionInterface expression_interface) : expression(std::move(expression)), expression_interface(expression_interface) {}
+
+	char Look() {
+		if (reached_end)
+			return '\0';
+		return expression[index];
+	}
+
+	bool Match(char c, bool skip_whitespace = true) {
+		if (c == Look()) {
+			Next();
+			if (skip_whitespace)
+				SkipWhitespace();
+			return true;
+		}
+		Expected(c);
+		return false;
+	}
+
+	char Next() {
+		++index;
+		if (index >= expression.size())
+			reached_end = true;
+		return Look();
+	}
+
+	void SkipWhitespace() {
+		char c = Look();
+		while (StringUtilities::IsWhitespace(c))
+			c = Next();
+	}
+
+	void Error(const String message)
+	{
+		parse_error = true;
+		Log::Message(Log::LT_WARNING, "Error in data expression at %d. %s", index, message.c_str());
+		Log::Message(Log::LT_WARNING, "  \"%s\"", expression.c_str());
+		
+		const size_t cursor_offset = size_t(index) + 3;
+		const String cursor_string = String(cursor_offset, ' ') + '^';
+		Log::Message(Log::LT_WARNING, cursor_string.c_str());
+	}
+	void Expected(String expected_symbols) {
+		const char c = Look();
+		if (c == '\0')
+			Error(CreateString(expected_symbols.size() + 50, "Expected %s but found end of string.", expected_symbols.c_str()));
+		else
+			Error(CreateString(expected_symbols.size() + 50, "Expected %s but found character '%c'.", expected_symbols.c_str(), c));
+	}
+	void Expected(char expected) {
+		Expected(String(1, '\'') + expected + '\'');
+	}
+
+	bool Parse(bool is_assignment_expression)
+	{
+		program.clear();
+		variable_addresses.clear();
+		index = 0;
+		reached_end = false;
+		parse_error = false;
+		if (expression.empty())
+			reached_end = true;
+
+		SkipWhitespace();
+
+		if (is_assignment_expression)
+			Parse::Assignment(*this);
+		else
+			Parse::Expression(*this);
+		
+		if (!reached_end) {
+			parse_error = true;
+			Error(CreateString(50, "Unexpected character '%c' encountered.", Look()));
+		}
+		if (!parse_error && program_stack_size != 0) {
+			parse_error = true;
+			Error(CreateString(120, "Internal parser error, inconsistent stack operations. Stack size is %d at parse end.", program_stack_size));
+		}
+
+		return !parse_error;
+	}
+
+	Program ReleaseProgram() {
+		RMLUI_ASSERT(!parse_error);
+		return std::move(program);
+	}
+	AddressList ReleaseAddresses() {
+		RMLUI_ASSERT(!parse_error);
+		return std::move(variable_addresses);
+	}
+
+	void Emit(Instruction instruction, Variant data = Variant())
+	{
+		RMLUI_ASSERTMSG(instruction != Instruction::Push && instruction != Instruction::Pop &&
+			instruction != Instruction::Arguments && instruction != Instruction::Variable && instruction != Instruction::Assign,
+			"Use the Push(), Pop(), Arguments(), Variable(), and Assign() procedures for stack manipulation and variable instructions.");
+		program.push_back(InstructionData{ instruction, std::move(data) });
+	}
+	void Push() {
+		program_stack_size += 1;
+		program.push_back(InstructionData{ Instruction::Push, Variant() });
+	}
+	void Pop(Register destination) {
+		if (program_stack_size <= 0) {
+			Error("Internal parser error: Tried to pop an empty stack.");
+			return;
+		}
+		program_stack_size -= 1;
+		program.push_back(InstructionData{ Instruction::Pop, Variant(int(destination)) });
+	}
+	void Arguments(int num_arguments) {
+		if (program_stack_size < num_arguments) {
+			Error(CreateString(128, "Internal parser error: Popping %d arguments, but the stack contains only %d elements.", num_arguments, program_stack_size));
+			return;
+		}
+		program_stack_size -= num_arguments;
+		program.push_back(InstructionData{ Instruction::Arguments, Variant(int(num_arguments)) });
+	}
+	void Variable(const String& name) {
+		VariableGetSet(name, false);
+	}
+	void Assign(const String& name) {
+		VariableGetSet(name, true);
+	}
+
+private:
+	void VariableGetSet(const String& name, bool is_assignment)
+	{
+		DataAddress address = expression_interface.ParseAddress(name);
+		if (address.empty()) {
+			Error(CreateString(name.size() + 50, "Could not find data variable with name '%s'.", name.c_str()));
+			return;
+		}
+		int index = int(variable_addresses.size());
+		variable_addresses.push_back(std::move(address));
+		program.push_back(InstructionData{ is_assignment ? Instruction::Assign : Instruction::Variable, Variant(int(index)) });
+	}
+
+	const String expression;
+	DataExpressionInterface expression_interface;
+
+	size_t index = 0;
+	bool reached_end = false;
+	bool parse_error = true;
+	int program_stack_size = 0;
+
+	Program program;
+	
+	AddressList variable_addresses;
+};
+
+
+namespace Parse {
+
+	// Forward declare all parse functions.
+	static void Assignment(DataParser& parser);
+
+	// The following in order of precedence.
+	static void Expression(DataParser& parser);
+	static void Relational(DataParser& parser);
+	static void Additive(DataParser& parser);
+	static void Term(DataParser& parser);
+	static void Factor(DataParser& parser);
+
+	static void NumberLiteral(DataParser& parser);
+	static void StringLiteral(DataParser& parser);
+	static void Variable(DataParser& parser);
+
+	static void Add(DataParser& parser);
+	static void Subtract(DataParser& parser);
+	static void Multiply(DataParser& parser);
+	static void Divide(DataParser& parser);
+
+	static void Not(DataParser& parser);
+	static void And(DataParser& parser);
+	static void Or(DataParser& parser);
+	static void Less(DataParser& parser);
+	static void Greater(DataParser& parser);
+	static void Equal(DataParser& parser);
+	static void NotEqual(DataParser& parser);
+
+	static void Ternary(DataParser& parser);
+	static void Function(DataParser& parser, Instruction function_type, const String& name);
+
+	// Helper functions
+	static bool IsVariableCharacter(char c, bool is_first_character)
+	{
+		const bool is_alpha = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+
+		if (is_first_character)
+			return is_alpha;
+
+		if (is_alpha || (c >= '0' && c <= '9'))
+			return true;
+
+		for (char valid_char : "_.[] ")
+		{
+			if (c == valid_char && valid_char != '\0')
+				return true;
+		}
+
+		return false;
+	}
+	static String VariableName(DataParser& parser)
+	{
+		String name;
+
+		bool is_first_character = true;
+		char c = parser.Look();
+
+		while (IsVariableCharacter(c, is_first_character))
+		{
+			name += c;
+			c = parser.Next();
+			is_first_character = false;
+		}
+
+		// Right trim spaces in name
+		size_t new_size = String::npos;
+		for (int i = int(name.size()) - 1; i >= 1; i--)
+		{
+			if (name[i] == ' ')
+				new_size = size_t(i);
+			else
+				break;
+		}
+		if (new_size != String::npos)
+			name.resize(new_size);
+
+		return name;
+	}
+
+	// Parser functions
+	static void Assignment(DataParser& parser)
+	{
+		bool looping = true;
+		while (looping)
+		{
+			if (parser.Look() != '\0')
+			{
+				const String variable_name = VariableName(parser);
+				if (variable_name.empty()) {
+					parser.Error("Expected a variable for assignment but got an empty name.");
+					return;
+				}
+
+				const char c = parser.Look();
+				if (c == '=')
+				{
+					parser.Match('=');
+					Expression(parser);
+					parser.Assign(variable_name);
+				}
+				else if (c == '(' || c == ';' || c == '\0')
+				{
+					Function(parser, Instruction::EventFnc, variable_name);
+				}
+				else
+				{
+					parser.Expected("one of  = ; (  or end of string");
+					return;
+				}
+			}
+
+			const char c = parser.Look();
+			if (c == ';')
+				parser.Match(';');
+			else if (c == '\0')
+				looping = false;
+			else
+			{
+				parser.Expected("';' or end of string");
+				looping = false;
+			}
+		}
+	}
+	static void Expression(DataParser& parser)
+	{
+		Relational(parser);
+
+		bool looping = true;
+		while (looping)
+		{
+			switch (parser.Look())
+			{
+			case '&': And(parser); break;
+			case '|':
+			{
+				parser.Match('|', false);
+				if (parser.Look() == '|')
+					Or(parser);
+				else
+				{
+					parser.SkipWhitespace();
+					const String fnc_name = VariableName(parser);
+					if (fnc_name.empty()) {
+						parser.Error("Expected a transform function name but got an empty name.");
+						return;
+					}
+
+					Function(parser, Instruction::TransformFnc, fnc_name);
+				}
+			}
+			break;
+			case '?': Ternary(parser); break;
+			default:
+				looping = false;
+			}
+		}
+	}
+
+	static void Relational(DataParser& parser)
+	{
+		Additive(parser);
+
+		bool looping = true;
+		while (looping)
+		{
+			switch (parser.Look())
+			{
+			case '=': Equal(parser); break;
+			case '!': NotEqual(parser); break;
+			case '<': Less(parser); break;
+			case '>': Greater(parser); break;
+			default:
+				looping = false;
+			}
+		}
+	}
+	
+	static void Additive(DataParser& parser)
+	{
+		Term(parser);
+
+		bool looping = true;
+		while (looping)
+		{
+			switch (parser.Look())
+			{
+			case '+': Add(parser); break;
+			case '-': Subtract(parser); break;
+			default:
+				looping = false;
+			}
+		}
+	}
+
+
+	static void Term(DataParser& parser)
+	{
+		Factor(parser);
+
+		bool looping = true;
+		while (looping)
+		{
+			switch (parser.Look())
+			{
+			case '*': Multiply(parser); break;
+			case '/': Divide(parser); break;
+			default:
+				looping = false;
+			}
+		}
+	}
+	static void Factor(DataParser& parser)
+	{
+		const char c = parser.Look();
+
+		if (c == '(')
+		{
+			parser.Match('(');
+			Expression(parser);
+			parser.Match(')');
+		}
+		else if (c == '\'')
+		{
+			parser.Match('\'', false);
+			StringLiteral(parser);
+			parser.Match('\'');
+		}
+		else if (c == '!')
+		{
+			Not(parser);
+			parser.SkipWhitespace();
+		}
+		else if (c == '-' || (c >= '0' && c <= '9'))
+		{
+			NumberLiteral(parser);
+			parser.SkipWhitespace();
+		}
+		else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
+		{
+			Variable(parser);
+			parser.SkipWhitespace();
+		}
+		else
+			parser.Expected("literal, variable name, parenthesis, or '!'");
+	}
+
+	static void NumberLiteral(DataParser& parser)
+	{
+		String str;
+
+		bool first_match = false;
+		bool has_dot = false;
+		char c = parser.Look();
+		if (c == '-')
+		{
+			str += c;
+			c = parser.Next();
+		}
+
+		while ((c >= '0' && c <= '9') || (c == '.' && !has_dot))
+		{
+			first_match = true;
+			str += c;
+			if (c == '.')
+				has_dot = true;
+			c = parser.Next();
+		}
+
+		if (!first_match)
+		{
+			parser.Error(CreateString(100, "Invalid number literal. Expected '0-9' or '.' but found '%c'.", c));
+			return;
+		}
+
+		const double number = FromString(str, 0.0);
+
+		parser.Emit(Instruction::Literal, Variant(number));
+	}
+	static void StringLiteral(DataParser& parser)
+	{
+		String str;
+
+		char c = parser.Look();
+		char c_prev = '\0';
+
+		while (c != '\0' && (c != '\'' || c_prev == '\\'))
+		{
+			if (c_prev == '\\' && (c == '\\' || c == '\'')) {
+				str.pop_back();
+				c_prev = '\0';
+			}
+			else {
+				c_prev = c;
+			}
+
+			str += c;
+			c = parser.Next();
+		}
+
+		parser.Emit(Instruction::Literal, Variant(str));
+	}
+	static void Variable(DataParser& parser)
+	{
+		String name = VariableName(parser);
+		if (name.empty()) {
+			parser.Error("Expected a variable but got an empty name.");
+			return;
+		}
+
+		// Keywords are parsed like variables, but are really literals.
+		// Check for them here.
+		if (name == "true")
+			parser.Emit(Instruction::Literal, Variant(true));
+		else if (name == "false")
+			parser.Emit(Instruction::Literal, Variant(false));
+		else
+			parser.Variable(name);
+	}
+
+	static void Add(DataParser& parser)
+	{
+		parser.Match('+');
+		parser.Push();
+		Term(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::Add);
+	}
+	static void Subtract(DataParser& parser)
+	{
+		parser.Match('-');
+		parser.Push();
+		Term(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::Subtract);
+	}
+	static void Multiply(DataParser& parser)
+	{
+		parser.Match('*');
+		parser.Push();
+		Factor(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::Multiply);
+	}
+	static void Divide(DataParser& parser)
+	{
+		parser.Match('/');
+		parser.Push();
+		Factor(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::Divide);
+	}
+
+	static void Not(DataParser& parser)
+	{
+		parser.Match('!');
+		Factor(parser);
+		parser.Emit(Instruction::Not);
+	}
+	static void Or(DataParser& parser)
+	{
+		// We already skipped the first '|' during expression
+		parser.Match('|');
+		parser.Push();
+		Relational(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::Or);
+	}
+	static void And(DataParser& parser)
+	{
+		parser.Match('&', false);
+		parser.Match('&');
+		parser.Push();
+		Relational(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::And);
+	}
+	static void Less(DataParser& parser)
+	{
+		Instruction instruction = Instruction::Less;
+		parser.Match('<', false);
+		if (parser.Look() == '=') {
+			parser.Match('=');
+			instruction = Instruction::LessEq;
+		}
+		else {
+			parser.SkipWhitespace();
+		}
+		parser.Push();
+		Additive(parser);
+		parser.Pop(Register::L);
+		parser.Emit(instruction);
+	}
+	static void Greater(DataParser& parser)
+	{
+		Instruction instruction = Instruction::Greater;
+		parser.Match('>', false);
+		if (parser.Look() == '=') {
+			parser.Match('=');
+			instruction = Instruction::GreaterEq;
+		}
+		else {
+			parser.SkipWhitespace();
+		}
+		parser.Push();
+		Additive(parser);
+		parser.Pop(Register::L);
+		parser.Emit(instruction);
+	}
+	static void Equal(DataParser& parser)
+	{
+		parser.Match('=', false);
+		parser.Match('=');
+		parser.Push();
+		Additive(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::Equal);
+	}
+	static void NotEqual(DataParser& parser)
+	{
+		parser.Match('!', false);
+		parser.Match('=');
+		parser.Push();
+		Additive(parser);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::NotEqual);
+	}
+
+	static void Ternary(DataParser& parser)
+	{
+		parser.Match('?');
+		parser.Push();
+		Expression(parser);
+		parser.Push();
+		parser.Match(':');
+		Expression(parser);
+		parser.Pop(Register::C);
+		parser.Pop(Register::L);
+		parser.Emit(Instruction::Ternary);
+	}
+	static void Function(DataParser& parser, Instruction function_type, const String& func_name)
+	{
+		RMLUI_ASSERT(function_type == Instruction::TransformFnc || function_type == Instruction::EventFnc);
+
+		// We already matched the variable name (and '|' for transform functions)
+		if (parser.Look() == '(')
+		{
+			int num_arguments = 0;
+			bool looping = true;
+
+			parser.Match('(');
+			if (parser.Look() == ')') {
+				parser.Match(')');
+				looping = false;
+			}
+			else
+				parser.Push();
+
+			while (looping)
+			{
+				num_arguments += 1;
+				Expression(parser);
+				parser.Push();
+
+				switch (parser.Look()) {
+				case ')': parser.Match(')'); looping = false; break;
+				case ',': parser.Match(','); break;
+				default:
+					parser.Expected("one of ')' or ','");
+					looping = false;
+				}
+			}
+
+			if (num_arguments > 0) {
+				parser.Arguments(num_arguments);
+				parser.Pop(Register::R);
+			}
+		}
+		else {
+			parser.SkipWhitespace();
+		}
+
+		parser.Emit(function_type, Variant(func_name));
+	}
+
+
+} // </namespace Parse>
+
+
+
+class DataInterpreter {
+public:
+	DataInterpreter(const Program& program, const AddressList& addresses, DataExpressionInterface expression_interface)
+		: program(program), addresses(addresses), expression_interface(expression_interface) {}
+
+	bool Error(String message) const
+	{
+		message = "Error during execution. " + message;
+		Log::Message(Log::LT_WARNING, message.c_str());
+		RMLUI_ERROR;
+		return false;
+	}
+
+	bool Run()
+	{
+		bool success = true;
+		for (size_t i = 0; i < program.size(); i++)
+		{
+			if (!Execute(program[i].instruction, program[i].data))
+			{
+				success = false;
+				break;
+			}
+		}
+
+		if(success && !stack.empty())
+			Log::Message(Log::LT_WARNING, "Possible data interpreter stack corruption. Stack size is %d at end of execution (should be zero).", stack.size());
+
+		if(!success)
+		{
+			String program_str = DumpProgram();
+			Log::Message(Log::LT_WARNING, "Failed to execute program with %d instructions:", program.size());
+			Log::Message(Log::LT_WARNING, program_str.c_str());
+		}
+
+		return success;
+	}
+
+	String DumpProgram() const
+	{
+		String str;
+		for (size_t i = 0; i < program.size(); i++)
+		{
+			String instruction_str = program[i].data.Get<String>();
+			str += CreateString(50 + instruction_str.size(), "  %4d  '%c'  %s\n", i, char(program[i].instruction), instruction_str.c_str());
+		}
+		return str;
+	}
+
+	Variant Result() const {
+		return R;
+	}
+
+
+private:
+	Variant R, L, C;
+	std::stack<Variant> stack;
+	std::vector<Variant> arguments;
+
+	const Program& program;
+	const AddressList& addresses;
+	DataExpressionInterface expression_interface;
+
+	bool Execute(const Instruction instruction, const Variant& data)
+	{
+		auto AnyString = [](const Variant& v1, const Variant& v2) {
+			return v1.GetType() == Variant::STRING || v2.GetType() == Variant::STRING;
+		};
+
+		switch (instruction)
+		{
+		case Instruction::Push:
+		{
+			stack.push(std::move(R));
+			R.Clear();
+		}
+		break;
+		case Instruction::Pop:
+		{
+			if (stack.empty())
+				return Error("Cannot pop stack, it is empty.");
+
+			Register reg = Register(data.Get<int>(-1));
+			switch (reg) {
+			case Register::R:  R = stack.top(); stack.pop(); break;
+			case Register::L:  L = stack.top(); stack.pop(); break;
+			case Register::C:  C = stack.top(); stack.pop(); break;
+			default:
+				return Error(CreateString(50, "Invalid register %d.", int(reg)));
+			}
+		}
+		break;
+		case Instruction::Literal:
+		{
+			R = data;
+		}
+		break;
+		case Instruction::Variable:
+		{
+			size_t variable_index = size_t(data.Get<int>(-1));
+			if (variable_index < addresses.size())
+				R = expression_interface.GetValue(addresses[variable_index]);
+			else
+				return Error("Variable address not found.");
+		}
+		break;
+		case Instruction::Add:
+		{
+			if (AnyString(L, R))
+				R = Variant(L.Get<String>() + R.Get<String>());
+			else
+				R = Variant(L.Get<double>() + R.Get<double>());
+		}
+		break;
+		case Instruction::Subtract:  R = Variant(L.Get<double>() - R.Get<double>());  break;
+		case Instruction::Multiply:  R = Variant(L.Get<double>() * R.Get<double>());  break;
+		case Instruction::Divide:    R = Variant(L.Get<double>() / R.Get<double>());  break;
+		case Instruction::Not:       R = Variant(!R.Get<bool>());                     break;
+		case Instruction::And:       R = Variant(L.Get<bool>() && R.Get<bool>());     break;
+		case Instruction::Or:        R = Variant(L.Get<bool>() || R.Get<bool>());     break;
+		case Instruction::Less:      R = Variant(L.Get<double>() < R.Get<double>());  break;
+		case Instruction::LessEq:    R = Variant(L.Get<double>() <= R.Get<double>()); break;
+		case Instruction::Greater:   R = Variant(L.Get<double>() > R.Get<double>());  break;
+		case Instruction::GreaterEq: R = Variant(L.Get<double>() >= R.Get<double>()); break;
+		case Instruction::Equal:
+		{
+			if (AnyString(L, R))
+				R = Variant(L.Get<String>() == R.Get<String>());
+			else
+				R = Variant(L.Get<double>() == R.Get<double>());
+		}
+		break;
+		case Instruction::NotEqual:
+		{
+			if (AnyString(L, R))
+				R = Variant(L.Get<String>() != R.Get<String>());
+			else
+				R = Variant(L.Get<double>() != R.Get<double>());
+		}
+		break;
+		case Instruction::Ternary:
+		{
+			if (L.Get<bool>())
+				R = C;
+		}
+		break;
+		case Instruction::Arguments:
+		{
+			if (!arguments.empty())
+				return Error("Argument stack is not empty.");
+
+			int num_arguments = data.Get<int>(-1);
+			if (num_arguments < 0)
+				return Error("Invalid number of arguments.");
+			if (stack.size() < size_t(num_arguments))
+				return Error(CreateString(100, "Cannot pop %d arguments, stack contains only %d elements.", num_arguments, stack.size()));
+
+			arguments.resize(num_arguments);
+			for (int i = num_arguments - 1; i >= 0; i--)
+			{
+				arguments[i] = std::move(stack.top());
+				stack.pop();
+			}
+		}
+		break;
+		case Instruction::TransformFnc:
+		{
+			const String function_name = data.Get<String>();
+			
+			if (!expression_interface.CallTransform(function_name, R, arguments))
+			{
+				String arguments_str;
+				for (size_t i = 0; i < arguments.size(); i++)
+				{
+					arguments_str += arguments[i].Get<String>();
+					if (i < arguments.size() - 1)
+						arguments_str += ", ";
+				}
+				Error(CreateString(50 + function_name.size() + arguments_str.size(), "Failed to execute data function: %s(%s)", function_name.c_str(), arguments_str.c_str()));
+			}
+
+			arguments.clear();
+		}
+		break;
+		case Instruction::EventFnc:
+		{
+			const String function_name = data.Get<String>();
+
+			if (!expression_interface.EventCallback(function_name, arguments))
+			{
+				String arguments_str;
+				for (size_t i = 0; i < arguments.size(); i++)
+				{
+					arguments_str += arguments[i].Get<String>();
+					if (i < arguments.size() - 1)
+						arguments_str += ", ";
+				}
+				Error(CreateString(50 + function_name.size() + arguments_str.size(), "Failed to execute event callback: %s(%s)", function_name.c_str(), arguments_str.c_str()));
+			}
+
+			arguments.clear();
+		}
+		break;
+		case Instruction::Assign:
+		{
+			size_t variable_index = size_t(data.Get<int>(-1));
+			if (variable_index < addresses.size())
+			{
+				if (!expression_interface.SetValue(addresses[variable_index], R))
+					return Error("Could not assign to variable.");
+			}
+			else
+				return Error("Variable address not found.");
+		}
+		break;
+		default:
+			RMLUI_ERRORMSG("Instruction not implemented."); break;
+		}
+		return true;
+	}
+};
+
+
+
+#ifdef RMLUI_TESTS_ENABLED
+
+struct TestParser {
+	TestParser() : model(type_register.GetTransformFuncRegister())
+	{
+		DataModelConstructor handle(&model, &type_register);
+		handle.Bind("radius", &radius);
+		handle.Bind("color_name", &color_name);
+		handle.BindFunc("color_value", [this](Rml::Core::Variant& variant) {
+			variant = ToString(color_value);
+		});
+
+		String result;
+		result = TestExpression("!!10 - 1 ? 'hello' : 'world' | to_upper",         "WORLD");
+		result = TestExpression("(color_name) + (': rgba(' + color_value + ')')",  "color: rgba(180, 100, 255, 255)");
+		result = TestExpression("'hello world' | to_upper(5 + 12 == 17 ? 'yes' : 'no', 9*2)",  "HELLO WORLD");
+		result = TestExpression("true == false",  "0");
+		result = TestExpression("true != false",  "1");
+		result = TestExpression("true",           "1");
+
+		result = TestExpression("true || false ? true && 3==1+2 ? 'Absolutely!' : 'well..' : 'no'",  "Absolutely!");
+		result = TestExpression(R"('It\'s a fit')",  R"(It's a fit)");
+		result = TestExpression("2 * 2",           "4");
+		result = TestExpression("50000 / 1500",    "33.333");
+		result = TestExpression("5*1+2",           "7");
+		result = TestExpression("5*(1+2)",         "15");
+		result = TestExpression("2*(-2)/4",        "-1");
+		result = TestExpression("5.2 + 19 + 'px'", "24.2px");
+
+		result = TestExpression("(radius | format(2)) + 'm'",    "8.70m");
+		result = TestExpression("radius < 10.5 ? 'smaller' : 'larger'",  "smaller");
+		TestAssignment("radius = 15");
+		result = TestExpression("radius < 10.5 ? 'smaller' : 'larger'",  "larger");
+		TestAssignment("radius = 4; color_name = 'image-color'");
+		result = TestExpression("radius == 4 && color_name == 'image-color'",  "1");
+
+		result = TestExpression("5 == 1 + 2*2 || 8 == 1 + 4  ? 'yes' : 'no'",  "yes");
+		result = TestExpression("!!('fa' + 'lse')", "0");
+		result = TestExpression("!!('tr' + 'ue')", "1");
+		result = TestExpression("'fox' + 'dog' ? 'FoxyDog' : 'hot' + 'dog' | to_upper", "HOTDOG");
+
+		result = TestExpression("3.62345 | round", "4");
+		result = TestExpression("3.62345 | format(0)", "4");
+		result = TestExpression("3.62345 | format(2)", "3.62");
+		result = TestExpression("3.62345 | format(10)", "3.6234500000");
+		result = TestExpression("3.62345 | format(10, true)", "3.62345");
+		result = TestExpression("3.62345 | round | format(2)", "4.00");
+		result = TestExpression("3.0001 | format(2, false)", "3.00");
+		result = TestExpression("3.0001 | format(2, true)", "3");
+
+		result = TestExpression("0.2 + 3.42345 | round", "4");
+		result = TestExpression("(3.42345 | round) + 0.2", "3.2");
+		result = TestExpression("(3.42345 | format(0)) + 0.2", "30.2"); // Here, format(0) returns a string, so the + means string concatenation.
+	}
+
+	String TestExpression(String expression, String expected = String())
+	{
+		String result;
+		DataExpressionInterface interface(&model, nullptr);
+		DataParser parser(expression, interface);
+		if (parser.Parse(false))
+		{
+			Program program = parser.ReleaseProgram();
+			AddressList addresses = parser.ReleaseAddresses();
+
+			DataInterpreter interpreter(program, addresses, interface);
+			if (interpreter.Run())
+				result = interpreter.Result().Get<String>();
+
+			if (!expected.empty() && result != expected)
+			{
+				String program_str = interpreter.DumpProgram();
+				Log::Message(Log::LT_WARNING, "%s", program_str.c_str());
+				RMLUI_ERRORMSG("Got unexpected data parser result.");
+			}
+		}
+		else
+		{
+			RMLUI_ERRORMSG("Could not parse expression.");
+		}
+
+		return result;
+	};
+
+	bool TestAssignment(String expression)
+	{
+		bool result = false;
+		DataExpressionInterface interface(&model, nullptr);
+		DataParser parser(expression, interface);
+		if (parser.Parse(true))
+		{
+			Program program = parser.ReleaseProgram();
+			AddressList addresses = parser.ReleaseAddresses();
+
+			DataInterpreter interpreter(program, addresses, interface);
+			result = interpreter.Run();
+		}
+		RMLUI_ASSERT(result);
+		return result;
+	};
+
+	DataTypeRegister type_register;
+	DataModel model;
+
+	float radius = 8.7f;
+	String color_name = "color";
+	Colourb color_value = Colourb(180, 100, 255);
+};
+
+static TestParser test_parser;
+
+#endif
+
+
+DataExpression::DataExpression(String expression) : expression(expression) {}
+
+DataExpression::~DataExpression()
+{
+}
+
+bool DataExpression::Parse(const DataExpressionInterface& expression_interface, bool is_assignment_expression)
+{
+	DataParser parser(expression, expression_interface);
+	if (!parser.Parse(is_assignment_expression))
+		return false;
+
+	program = parser.ReleaseProgram();
+	addresses = parser.ReleaseAddresses();
+
+	return true;
+}
+
+bool DataExpression::Run(const DataExpressionInterface& expression_interface, Variant& out_value)
+{
+	DataInterpreter interpreter(program, addresses, expression_interface);
+	
+	if (!interpreter.Run())
+		return false;
+
+	out_value = interpreter.Result();
+	return true;
+}
+
+StringList DataExpression::GetVariableNameList() const
+{
+	StringList list;
+	list.reserve(addresses.size());
+	for (const DataAddress& address : addresses)
+	{
+		if (!address.empty())
+			list.push_back(address[0].name);
+	}
+	return list;
+}
+
+DataExpressionInterface::DataExpressionInterface(DataModel* data_model, Element* element, Event* event) : data_model(data_model), element(element), event(event)
+{}
+
+DataAddress DataExpressionInterface::ParseAddress(const String& address_str) const
+{
+	if (address_str.size() >= 4 && address_str[0] == 'e' && address_str[1] == 'v' && address_str[2] == '.')
+		return DataAddress{ DataAddressEntry("ev"), DataAddressEntry(address_str.substr(3)) };
+
+	return data_model ? data_model->ResolveAddress(address_str, element) : DataAddress();
+}
+Variant DataExpressionInterface::GetValue(const DataAddress& address) const
+{
+	Variant result;
+	if(event && address.size() == 2 && address.front().name == "ev")
+	{
+		auto& parameters = event->GetParameters();
+		auto it = parameters.find(address.back().name);
+		if (it != parameters.end())
+			result = it->second;
+	}
+	else if (data_model)
+	{
+		data_model->GetVariableInto(address, result);
+	}
+	return result;
+}
+
+bool DataExpressionInterface::SetValue(const DataAddress& address, const Variant& value) const
+{
+	bool result = false;
+	if (data_model && !address.empty())
+	{
+		if (DataVariable variable = data_model->GetVariable(address))
+			result = variable.Set(value);
+
+		if (result)
+			data_model->DirtyVariable(address.front().name);
+	}
+	return result;
+}
+
+bool DataExpressionInterface::CallTransform(const String& name, Variant& inout_variant, const VariantList& arguments)
+{
+	return data_model ? data_model->CallTransform(name, inout_variant, arguments) : false;
+}
+
+bool DataExpressionInterface::EventCallback(const String& name, const VariantList& arguments)
+{
+	if (!data_model || !event)
+		return false;
+
+	const DataEventFunc* func = data_model->GetEventCallback(name);
+	if (!func || !*func)
+		return false;
+
+	DataModelHandle handle(data_model);
+	func->operator()(handle, *event, arguments);
+	return true;
+}
+
+}
+}

+ 85 - 0
Source/Core/DataExpression.h

@@ -0,0 +1,85 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATAEXPRESSION_H
+#define RMLUICOREDATAEXPRESSION_H
+
+#include "../../Include/RmlUi/Core/Header.h"
+#include "../../Include/RmlUi/Core/Types.h"
+#include "../../Include/RmlUi/Core/DataTypes.h"
+
+namespace Rml {
+namespace Core {
+
+class Element;
+class DataModel;
+struct InstructionData;
+using Program = std::vector<InstructionData>;
+using AddressList = std::vector<DataAddress>;
+
+class DataExpressionInterface {
+public:
+    DataExpressionInterface() = default;
+    DataExpressionInterface(DataModel* data_model, Element* element, Event* event = nullptr);
+
+    DataAddress ParseAddress(const String& address_str) const;
+    Variant GetValue(const DataAddress& address) const;
+    bool SetValue(const DataAddress& address, const Variant& value) const;
+    bool CallTransform(const String& name, Variant& inout_result, const VariantList& arguments);
+    bool EventCallback(const String& name, const VariantList& arguments);
+
+private:
+    DataModel* data_model = nullptr;
+    Element* element = nullptr;
+    Event* event = nullptr;
+};
+
+
+class DataExpression {
+public:
+    DataExpression(String expression);
+    ~DataExpression();
+
+    bool Parse(const DataExpressionInterface& expression_interface, bool is_assignment_expression);
+
+    bool Run(const DataExpressionInterface& expression_interface, Variant& out_value);
+
+    // Available after Parse()
+    StringList GetVariableNameList() const;
+
+private:
+    String expression;
+    
+    Program program;
+    AddressList addresses;
+};
+
+}
+}
+
+#endif

+ 470 - 0
Source/Core/DataModel.cpp

@@ -0,0 +1,470 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../../Include/RmlUi/Core/DataModel.h"
+#include "../../Include/RmlUi/Core/DataController.h"
+#include "../../Include/RmlUi/Core/DataView.h"
+#include "../../Include/RmlUi/Core/Element.h"
+
+namespace Rml {
+namespace Core {
+
+
+static DataAddress ParseAddress(const String& address_str)
+{
+	StringList list;
+	StringUtilities::ExpandString(list, address_str, '.');
+
+	DataAddress address;
+	address.reserve(list.size() * 2);
+
+	for (const auto& item : list)
+	{
+		if (item.empty())
+			return DataAddress();
+
+		size_t i_open = item.find('[', 0);
+		if (i_open == 0)
+			return DataAddress();
+
+		address.emplace_back(item.substr(0, i_open));
+
+		while (i_open != String::npos)
+		{
+			size_t i_close = item.find(']', i_open + 1);
+			if (i_close == String::npos)
+				return DataAddress();
+
+			int index = FromString<int>(item.substr(i_open + 1, i_close - i_open), -1);
+			if (index < 0)
+				return DataAddress();
+
+			address.emplace_back(index);
+
+			i_open = item.find('[', i_close + 1);
+		}
+		// TODO: Abort on invalid characters among [ ] and after the last found bracket?
+	}
+
+	RMLUI_ASSERT(!address.empty() && !address[0].name.empty());
+
+	return address;
+}
+
+// Returns an error string on error, or nullptr on success.
+static const char* LegalVariableName(const String& name)
+{
+	static SmallUnorderedSet<String> reserved_names{ "it", "ev", "true", "false", "size", "literal" };
+	
+	if (name.empty())
+		return "Name cannot be empty.";
+	
+	const String name_lower = StringUtilities::ToLower(name);
+
+	const char first = name_lower.front();
+	if (!(first >= 'a' && first <= 'z'))
+		return "First character must be 'a-z' or 'A-Z'.";
+
+	for (const char c : name_lower)
+	{
+		if (!(c == '_' || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')))
+			return "Name must strictly contain characters a-z, A-Z, 0-9 and under_score.";
+	}
+
+	if (reserved_names.count(name_lower) == 1)
+		return "Name is reserved.";
+
+	return nullptr;
+}
+
+static String DataAddressToString(const DataAddress& address)
+{
+	String result;
+	bool is_first = true;
+	for (auto& entry : address)
+	{
+		if (entry.index >= 0)
+			result += '[' + ToString(entry.index) + ']';
+		else
+		{
+			if (!is_first)
+				result += ".";
+			result += entry.name;
+		}
+		is_first = false;
+	}
+	return result;
+}
+
+DataModel::DataModel(const TransformFuncRegister* transform_register) : transform_register(transform_register)
+{
+	views = std::make_unique<DataViews>();
+	controllers = std::make_unique<DataControllers>();
+}
+
+DataModel::~DataModel()
+{
+	RMLUI_ASSERT(attached_elements.empty());
+}
+
+void DataModel::AddView(DataViewPtr view) {
+	views->Add(std::move(view));
+}
+
+void DataModel::AddController(DataControllerPtr controller) {
+	controllers->Add(std::move(controller));
+}
+
+bool DataModel::BindVariable(const String& name, DataVariable variable)
+{
+	const char* name_error_str = LegalVariableName(name);
+	if (name_error_str)
+	{
+		Log::Message(Log::LT_WARNING, "Could not bind data variable '%s'. %s", name.c_str(), name_error_str);
+		return false;
+	}
+
+	if (!variable)
+	{
+		Log::Message(Log::LT_WARNING, "Could not bind variable '%s' to data model, data type not registered.", name.c_str());
+		return false;
+	}
+
+	bool inserted = variables.emplace(name, variable).second;
+	if (!inserted)
+	{
+		Log::Message(Log::LT_WARNING, "Data model variable with name '%s' already exists.", name.c_str());
+		return false;
+	}
+
+	return true;
+}
+
+bool DataModel::BindFunc(const String& name, DataGetFunc get_func, DataSetFunc set_func)
+{
+	auto result = function_variable_definitions.emplace(name, nullptr);
+	auto& it = result.first;
+	bool inserted = result.second;
+	if (!inserted)
+	{
+		Log::Message(Log::LT_ERROR, "Data get/set function with name %s already exists in model", name.c_str());
+		return false;
+	}
+	auto& func_definition_ptr = it->second;
+	func_definition_ptr = std::make_unique<FuncDefinition>(std::move(get_func), std::move(set_func));
+
+	return BindVariable(name, DataVariable(func_definition_ptr.get(), nullptr));
+}
+
+bool DataModel::BindEventCallback(const String& name, DataEventFunc event_func)
+{
+	const char* name_error_str = LegalVariableName(name);
+	if (name_error_str)
+	{
+		Log::Message(Log::LT_WARNING, "Could not bind data event callback '%s'. %s", name.c_str(), name_error_str);
+		return false;
+	}
+
+	if (!event_func)
+	{
+		Log::Message(Log::LT_WARNING, "Could not bind data event callback '%s' to data model, empty function provided.", name.c_str());
+		return false;
+	}
+
+	bool inserted = event_callbacks.emplace(name, std::move(event_func)).second;
+	if (!inserted)
+	{
+		Log::Message(Log::LT_WARNING, "Data event callback with name '%s' already exists.", name.c_str());
+		return false;
+	}
+
+	return true;
+}
+
+bool DataModel::InsertAlias(Element* element, const String& alias_name, DataAddress replace_with_address)
+{
+	if (replace_with_address.empty() || replace_with_address.front().name.empty())
+	{
+		Log::Message(Log::LT_WARNING, "Could not add alias variable '%s' to data model, replacement address invalid.", alias_name.c_str());
+		return false;
+	}
+
+	if (variables.count(alias_name) == 1)
+		Log::Message(Log::LT_WARNING, "Alias variable '%s' is shadowed by a global variable.", alias_name.c_str());
+
+	auto& map = aliases.emplace(element, SmallUnorderedMap<String, DataAddress>()).first->second;
+	
+	auto it = map.find(alias_name);
+	if (it != map.end())
+		Log::Message(Log::LT_WARNING, "Alias name '%s' in data model already exists, replaced.", alias_name.c_str());
+
+	map[alias_name] = std::move(replace_with_address);
+
+	return true;
+}
+
+bool DataModel::EraseAliases(Element* element)
+{
+	return aliases.erase(element) == 1;
+}
+
+DataAddress DataModel::ResolveAddress(const String& address_str, Element* element) const
+{
+	DataAddress address = ParseAddress(address_str);
+
+	if (address.empty())
+		return address;
+
+	const String& first_name = address.front().name;
+
+	auto it = variables.find(first_name);
+	if (it != variables.end())
+		return address;
+
+	// Look for a variable alias for the first name.
+	
+	Element* ancestor = element;
+	while (ancestor && ancestor->GetDataModel() == this)
+	{
+		auto it_element = aliases.find(ancestor);
+		if (it_element != aliases.end())
+		{
+			const auto& alias_names = it_element->second;
+			auto it_alias_name = alias_names.find(first_name);
+			if (it_alias_name != alias_names.end())
+			{
+				const DataAddress& replace_address = it_alias_name->second;
+				if (replace_address.empty() || replace_address.front().name.empty())
+				{
+					// Variable alias is invalid
+					return DataAddress();
+				}
+
+				// Insert the full alias address, replacing the first element.
+				address[0] = replace_address[0];
+				address.insert(address.begin() + 1, replace_address.begin() + 1, replace_address.end());
+				return address;
+			}
+		}
+
+		ancestor = ancestor->GetParentNode();
+	}
+
+	Log::Message(Log::LT_WARNING, "Could not find variable name '%s' in data model.", address_str.c_str());
+
+	return DataAddress();
+}
+
+DataVariable DataModel::GetVariable(const DataAddress& address) const
+{
+	if (address.empty())
+		return DataVariable();
+
+	auto it = variables.find(address.front().name);
+	if (it != variables.end())
+	{
+		DataVariable variable = it->second;
+
+		for (int i = 1; i < (int)address.size() && variable; i++)
+		{
+			variable = variable.Child(address[i]);
+			if (!variable)
+				return DataVariable();
+		}
+
+		return variable;
+	}
+
+	if (address[0].name == "literal")
+	{
+		if (address.size() > 2 && address[1].name == "int")
+			return MakeLiteralIntVariable(address[2].index);
+	}
+
+	return DataVariable();
+}
+
+const DataEventFunc* DataModel::GetEventCallback(const String& name)
+{
+	auto it = event_callbacks.find(name);
+	if (it == event_callbacks.end())
+	{
+		Log::Message(Log::LT_WARNING, "Could not find data event callback '%s' in data model.", name.c_str());
+		return nullptr;
+	}
+
+	return &it->second;
+}
+
+bool DataModel::GetVariableInto(const DataAddress& address, Variant& out_value) const {
+	DataVariable variable = GetVariable(address);
+	bool result = (variable && variable.Get(out_value));
+	if (!result)
+		Log::Message(Log::LT_WARNING, "Could not get value from data variable '%s'.", DataAddressToString(address).c_str());
+	return result;
+}
+
+void DataModel::DirtyVariable(const String& variable_name)
+{
+	RMLUI_ASSERTMSG(LegalVariableName(variable_name) == nullptr, "Illegal variable name provided. Only top-level variables can be dirtied.");
+	RMLUI_ASSERTMSG(variables.count(variable_name) == 1, "In DirtyVariable: Variable name not found among added variables.");
+	dirty_variables.emplace(variable_name);
+}
+
+bool DataModel::IsVariableDirty(const String& variable_name) const
+{
+	RMLUI_ASSERTMSG(LegalVariableName(variable_name) == nullptr, "Illegal variable name provided. Only top-level variables can be dirtied.");
+	return dirty_variables.count(variable_name) == 1;
+}
+
+bool DataModel::CallTransform(const String& name, Variant& inout_result, const VariantList& arguments) const
+{
+	if (transform_register)
+		return transform_register->Call(name, inout_result, arguments);
+	return false;
+}
+
+void DataModel::AttachModelRootElement(Element* element)
+{
+	attached_elements.insert(element);
+}
+
+ElementList DataModel::GetAttachedModelRootElements() const
+{
+	return ElementList(attached_elements.begin(), attached_elements.end());
+}
+
+void DataModel::OnElementRemove(Element* element)
+{
+	EraseAliases(element);
+	views->OnElementRemove(element);
+	controllers->OnElementRemove(element);
+	attached_elements.erase(element);
+}
+
+bool DataModel::Update() 
+{
+	bool result = views->Update(*this, dirty_variables);
+	dirty_variables.clear();
+	return result;
+}
+
+
+
+#ifdef RMLUI_DEBUG
+
+static struct TestDataVariables {
+	TestDataVariables() 
+	{
+		using IntVector = std::vector<int>;
+
+		struct FunData {
+			int i = 99;
+			String x = "hello";
+			IntVector magic = { 3, 5, 7, 11, 13 };
+		};
+
+		using FunArray = std::array<FunData, 3>;
+
+		struct SmartData {
+			bool valid = true;
+			FunData fun;
+			FunArray more_fun;
+		};
+
+		DataModel model;
+		DataTypeRegister types;
+
+		DataModelConstructor handle(&model, &types);
+
+		{
+			handle.RegisterArray<IntVector>();
+
+			if (auto fun_handle = handle.RegisterStruct<FunData>())
+			{
+				fun_handle.RegisterMember("i", &FunData::i);
+				fun_handle.RegisterMember("x", &FunData::x);
+				fun_handle.RegisterMember("magic", &FunData::magic);
+			}
+
+			handle.RegisterArray<FunArray>();
+
+			if (auto smart_handle = handle.RegisterStruct<SmartData>())
+			{
+				smart_handle.RegisterMember("valid", &SmartData::valid);
+				smart_handle.RegisterMember("fun", &SmartData::fun);
+				smart_handle.RegisterMember("more_fun", &SmartData::more_fun);
+			}
+		}
+
+		SmartData data;
+		data.fun.x = "Hello, we're in SmartData!";
+		
+		handle.Bind("data", &data);
+
+		{
+			std::vector<String> test_addresses = { "data.more_fun[1].magic[3]", "data.more_fun[1].magic.size", "data.fun.x", "data.valid" };
+			std::vector<String> expected_results = { ToString(data.more_fun[1].magic[3]), ToString(int(data.more_fun[1].magic.size())), ToString(data.fun.x), ToString(data.valid) };
+
+			std::vector<String> results;
+
+			for (auto& str_address : test_addresses)
+			{
+				DataAddress address = ParseAddress(str_address);
+
+				Variant result;
+				if(model.GetVariableInto(address, result))
+					results.push_back(result.Get<String>());
+			}
+
+			RMLUI_ASSERT(results == expected_results);
+
+			bool success = true;
+			success &= model.GetVariable(ParseAddress("data.more_fun[1].magic[1]")).Set(Variant(String("199")));
+			RMLUI_ASSERT(success && data.more_fun[1].magic[1] == 199);
+
+			data.fun.magic = { 99, 190, 55, 2000, 50, 60, 70, 80, 90 };
+
+			Variant get_result;
+
+			const int magic_size = int(data.fun.magic.size());
+			success &= model.GetVariable(ParseAddress("data.fun.magic.size")).Get(get_result);
+			RMLUI_ASSERT(success && get_result.Get<String>() == ToString(magic_size));
+			RMLUI_ASSERT(model.GetVariable(ParseAddress("data.fun.magic")).Size() == magic_size);
+
+			success &= model.GetVariable(ParseAddress("data.fun.magic[8]")).Get(get_result);
+			RMLUI_ASSERT(success && get_result.Get<String>() == "90");
+		}
+	}
+} test_data_variables;
+
+
+#endif
+
+}
+}

+ 126 - 0
Source/Core/DataTypeRegister.cpp

@@ -0,0 +1,126 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../../Include/RmlUi/Core/DataTypeRegister.h"
+
+namespace Rml {
+namespace Core {
+
+
+DataTypeRegister::DataTypeRegister()
+{
+    // Add default transform functions.
+
+	transform_register.Register("to_lower", [](Variant& variant, const VariantList& /*arguments*/) -> bool {
+		String value;
+		if (!variant.GetInto(value))
+			return false;
+		variant = StringUtilities::ToLower(value);
+		return true;
+	});
+
+	transform_register.Register("to_upper", [](Variant& variant, const VariantList& /*arguments*/) -> bool {
+		String value;
+		if (!variant.GetInto(value))
+			return false;
+		variant = StringUtilities::ToUpper(value);
+		return true;
+	});
+
+	transform_register.Register("format", [](Variant& variant, const VariantList& arguments) -> bool {
+        // Arguments in:
+        //   0 : int[0,32]  Precision. Number of digits after the decimal point.
+        //  [1]: bool       True to remove trailing zeros (default = false).
+        if (arguments.size() < 1 || arguments.size() > 2) {
+            Log::Message(Log::LT_WARNING, "Transform function 'format' requires at least one argument, at most two arguments.");
+            return false;
+        }
+        int precision = 0;
+        if (!arguments[0].GetInto(precision) || precision < 0 || precision > 32) {
+            Log::Message(Log::LT_WARNING, "Transform function 'format': First argument must be an integer in [0, 32].");
+            return false;
+        }
+        bool remove_trailing_zeros = false;
+        if (arguments.size() >= 2) {
+            if (!arguments[1].GetInto(remove_trailing_zeros))
+                return false;
+        }
+
+		double value = 0;
+		if (!variant.GetInto(value))
+			return false;
+
+        String format_specifier = String(remove_trailing_zeros ? "%#." : "%.") + ToString(precision) + 'f';
+        String result;
+        if (FormatString(result, 64, format_specifier.c_str(), value) == 0)
+            return false;
+
+        if (remove_trailing_zeros)
+            StringUtilities::TrimTrailingDotZeros(result);
+
+        variant = result;
+		return true;
+	});
+
+	transform_register.Register("round", [](Variant& variant, const VariantList& /*arguments*/) -> bool {
+		double value = 0;
+		if (!variant.GetInto(value))
+			return false;
+        variant = Math::RoundFloat(value);
+		return true;
+	});
+}
+
+DataTypeRegister::~DataTypeRegister()
+{}
+
+void TransformFuncRegister::Register(const String& name, DataTransformFunc transform_func)
+{
+    RMLUI_ASSERT(transform_func);
+    bool inserted = transform_functions.emplace(name, std::move(transform_func)).second;
+    if (!inserted)
+    {
+        Log::Message(Log::LT_ERROR, "Transform function '%s' already exists.", name.c_str());
+        RMLUI_ERROR;
+    }
+}
+
+bool TransformFuncRegister::Call(const String& name, Variant& inout_result, const VariantList& arguments) const
+{
+    auto it = transform_functions.find(name);
+    if (it == transform_functions.end())
+        return false;
+
+    const DataTransformFunc& transform_func = it->second;
+    RMLUI_ASSERT(transform_func);
+
+    return transform_func(inout_result, arguments);
+}
+
+}
+}

+ 90 - 0
Source/Core/DataVariable.cpp

@@ -0,0 +1,90 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../../Include/RmlUi/Core/DataVariable.h"
+
+namespace Rml {
+namespace Core {
+
+bool DataVariable::Get(Variant& variant) {
+    return definition->Get(ptr, variant);
+}
+
+bool DataVariable::Set(const Variant& variant) {
+    return definition->Set(ptr, variant);
+}
+
+int DataVariable::Size() {
+    return definition->Size(ptr);
+}
+
+DataVariable DataVariable::Child(const DataAddressEntry& address) {
+    return definition->Child(ptr, address);
+}
+
+DataVariableType DataVariable::Type() {
+    return definition->Type();
+}
+
+
+bool VariableDefinition::Get(void* /*ptr*/, Variant& /*variant*/) {
+    Log::Message(Log::LT_WARNING, "Values can only be retrieved from scalar data types.");
+    return false;
+}
+bool VariableDefinition::Set(void* /*ptr*/, const Variant& /*variant*/) {
+    Log::Message(Log::LT_WARNING, "Values can only be assigned to scalar data types.");
+    return false;
+}
+int VariableDefinition::Size(void* /*ptr*/) {
+    Log::Message(Log::LT_WARNING, "Tried to get the size from a non-array data type.");
+    return 0;
+}
+DataVariable VariableDefinition::Child(void* /*ptr*/, const DataAddressEntry& /*address*/) {
+    Log::Message(Log::LT_WARNING, "Tried to get the child of a scalar type.");
+    return DataVariable();
+}
+
+class LiteralIntDefinition final : public VariableDefinition {
+public:
+    LiteralIntDefinition() : VariableDefinition(DataVariableType::Scalar) {}
+
+    bool Get(void* ptr, Variant& variant) override
+    {
+        variant = static_cast<int>(reinterpret_cast<intptr_t>(ptr));
+        return true;
+    }
+};
+
+DataVariable MakeLiteralIntVariable(int value)
+{
+    static LiteralIntDefinition literal_int_definition;
+    return DataVariable(&literal_int_definition, reinterpret_cast<void*>(static_cast<intptr_t>(value)));
+}
+
+}
+}

+ 162 - 0
Source/Core/DataView.cpp

@@ -0,0 +1,162 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../../Include/RmlUi/Core/DataView.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include <algorithm>
+
+namespace Rml {
+namespace Core {
+
+DataView::~DataView()
+{}
+
+Element* DataView::GetElement() const
+{
+	Element* result = attached_element.get();
+	if (!result)
+		Log::Message(Log::LT_WARNING, "Could not retrieve element in view, was it destroyed?");
+	return result;
+}
+
+int DataView::GetElementDepth() const {
+	return element_depth;
+}
+
+bool DataView::IsValid() const {
+	return static_cast<bool>(attached_element);
+}
+
+DataView::DataView(Element* element) : attached_element(element->GetObserverPtr()), element_depth(0) {
+	if (element)
+	{
+		for (Element* parent = element->GetParentNode(); parent; parent = parent->GetParentNode())
+			element_depth += 1;
+	}
+}
+
+
+DataViews::DataViews()
+{}
+
+DataViews::~DataViews()
+{}
+
+void DataViews::Add(DataViewPtr view) {
+	views_to_add.push_back(std::move(view));
+}
+
+void DataViews::OnElementRemove(Element* element) 
+{
+	for (auto it = views.begin(); it != views.end();)
+	{
+		auto& view = *it;
+		if (view && view->GetElement() == element)
+		{
+			views_to_remove.push_back(std::move(view));
+			it = views.erase(it);
+		}
+		else
+			++it;
+	}
+}
+
+bool DataViews::Update(DataModel& model, const DirtyVariables& dirty_variables)
+{
+	bool result = false;
+
+	// View updates may result in newly added views, thus we do it recursively but with an upper limit.
+	//   Without the loop, newly added views won't be updated until the next Update() call.
+	for(int i = 0; i == 0 || (!views_to_add.empty() && i < 10); i++)
+	{
+		std::vector<DataView*> dirty_views;
+
+		if (!views_to_add.empty())
+		{
+			views.reserve(views.size() + views_to_add.size());
+			for (auto&& view : views_to_add)
+			{
+				dirty_views.push_back(view.get());
+				for (const String& variable_name : view->GetVariableNameList())
+					name_view_map.emplace(variable_name, view.get());
+
+				views.push_back(std::move(view));
+			}
+			views_to_add.clear();
+		}
+
+		for (const String& variable_name : dirty_variables)
+		{
+			auto pair = name_view_map.equal_range(variable_name);
+			for (auto it = pair.first; it != pair.second; ++it)
+				dirty_views.push_back(it->second);
+		}
+
+		// Remove duplicate entries
+		std::sort(dirty_views.begin(), dirty_views.end());
+		auto it_remove = std::unique(dirty_views.begin(), dirty_views.end());
+		dirty_views.erase(it_remove, dirty_views.end());
+
+		// Sort by the element's depth in the document tree so that any structural changes due to a changed variable are reflected in the element's children.
+		// Eg. the 'data-for' view will remove children if any of its data variable array size is reduced.
+		std::sort(dirty_views.begin(), dirty_views.end(), [](auto&& left, auto&& right) { return left->GetElementDepth() < right->GetElementDepth(); });
+
+		for (DataView* view : dirty_views)
+		{
+			RMLUI_ASSERT(view);
+			if (!view)
+				continue;
+
+			if (view->IsValid())
+				result |= view->Update(model);
+		}
+
+		// Destroy views marked for destruction
+		// @performance: Horrible...
+		if (!views_to_remove.empty())
+		{
+			for (const auto& view : views_to_remove)
+			{
+				for (auto it = name_view_map.begin(); it != name_view_map.end(); )
+				{
+					if (it->second == view.get())
+						it = name_view_map.erase(it);
+					else
+						++it;
+				}
+			}
+
+			views_to_remove.clear();
+		}
+	}
+
+	return result;
+}
+
+}
+}

+ 496 - 0
Source/Core/DataViewDefault.cpp

@@ -0,0 +1,496 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "DataViewDefault.h"
+#include "DataExpression.h"
+#include "../../Include/RmlUi/Core/DataModel.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include "../../Include/RmlUi/Core/ElementText.h"
+#include "../../Include/RmlUi/Core/Factory.h"
+#include "../../Include/RmlUi/Core/Variant.h"
+
+namespace Rml {
+namespace Core {
+
+
+DataViewCommon::DataViewCommon(Element* element, String override_modifier) : DataView(element), modifier(override_modifier)
+{}
+
+bool DataViewCommon::Initialize(DataModel& model, Element* element, const String& expression_str, const String& in_modifier)
+{
+	// The modifier can be overriden in the constructor
+	if (modifier.empty())
+		modifier = in_modifier;
+
+	expression = std::make_unique<DataExpression>(expression_str);
+	DataExpressionInterface interface(&model, element);
+
+	bool result = expression->Parse(interface, false);
+	return result;
+}
+
+StringList DataViewCommon::GetVariableNameList() const {
+	RMLUI_ASSERT(expression);
+	return expression->GetVariableNameList();
+}
+
+const String& DataViewCommon::GetModifier() const {
+	return modifier;
+}
+
+DataExpression& DataViewCommon::GetExpression() {
+	RMLUI_ASSERT(expression);
+	return *expression;
+}
+
+void DataViewCommon::Release()
+{
+	delete this;
+}
+
+
+DataViewAttribute::DataViewAttribute(Element* element) : DataViewCommon(element)
+{}
+
+DataViewAttribute::DataViewAttribute(Element * element, String override_attribute) : DataViewCommon(element, std::move(override_attribute))
+{}
+
+bool DataViewAttribute::Update(DataModel& model)
+{
+	const String& attribute_name = GetModifier();
+	bool result = false;
+	Variant variant;
+	Element* element = GetElement();
+	DataExpressionInterface interface(&model, element);
+
+	if (element && GetExpression().Run(interface, variant))
+	{
+		const String value = variant.Get<String>();
+		const Variant* attribute = element->GetAttribute(attribute_name);
+		
+		if (!attribute || (attribute && attribute->Get<String>() != value))
+		{
+			element->SetAttribute(attribute_name, value);
+			result = true;
+		}
+	}
+	return result;
+}
+
+
+DataViewValue::DataViewValue(Element* element) : DataViewAttribute(element, "value")
+{}
+
+
+DataViewStyle::DataViewStyle(Element* element) : DataViewCommon(element)
+{}
+
+bool DataViewStyle::Update(DataModel& model)
+{
+	const String& property_name = GetModifier();
+	bool result = false;
+	Variant variant;
+	Element* element = GetElement();
+	DataExpressionInterface interface(&model, element);
+	
+	if (element && GetExpression().Run(interface, variant))
+	{
+		const String value = variant.Get<String>();
+		const Property* p = element->GetLocalProperty(property_name);
+		if (!p || p->Get<String>() != value)
+		{
+			element->SetProperty(property_name, value);
+			result = true;
+		}
+	}
+	return result;
+}
+
+
+DataViewClass::DataViewClass(Element* element) : DataViewCommon(element)
+{}
+
+bool DataViewClass::Update(DataModel& model)
+{
+	const String& class_name = GetModifier();
+	bool result = false;
+	Variant variant;
+	Element* element = GetElement();
+	DataExpressionInterface interface(&model, element);
+
+	if (element && GetExpression().Run(interface, variant))
+	{
+		const bool activate = variant.Get<bool>();
+		const bool is_set = element->IsClassSet(class_name);
+		if (activate != is_set)
+		{
+			element->SetClass(class_name, activate);
+			result = true;
+		}
+	}
+	return result;
+}
+
+
+DataViewRml::DataViewRml(Element* element) : DataViewCommon(element)
+{}
+
+bool DataViewRml::Update(DataModel & model)
+{
+	bool result = false;
+	Variant variant;
+	Element* element = GetElement();
+	DataExpressionInterface interface(&model, element);
+
+	if (element && GetExpression().Run(interface, variant))
+	{
+		String new_rml = variant.Get<String>();
+		if (new_rml != previous_rml)
+		{
+			element->SetInnerRML(new_rml);
+			previous_rml = std::move(new_rml);
+			result = true;
+		}
+	}
+	return result;
+}
+
+
+DataViewIf::DataViewIf(Element* element) : DataViewCommon(element)
+{}
+
+bool DataViewIf::Update(DataModel& model)
+{
+	bool result = false;
+	Variant variant;
+	Element* element = GetElement();
+	DataExpressionInterface interface(&model, element);
+
+	if (element && GetExpression().Run(interface, variant))
+	{
+		const bool value = variant.Get<bool>();
+		const bool is_visible = (element->GetLocalStyleProperties().count(PropertyId::Display) == 0);
+		if(is_visible != value)
+		{
+			if (value)
+				element->RemoveProperty(PropertyId::Display);
+			else
+				element->SetProperty(PropertyId::Display, Property(Style::Display::None));
+			result = true;
+		}
+	}
+	return result;
+}
+
+
+DataViewVisible::DataViewVisible(Element* element) : DataViewCommon(element)
+{}
+
+bool DataViewVisible::Update(DataModel& model)
+{
+	bool result = false;
+	Variant variant;
+	Element* element = GetElement();
+	DataExpressionInterface interface(&model, element);
+
+	if (element && GetExpression().Run(interface, variant))
+	{
+		const bool value = variant.Get<bool>();
+		const bool is_visible = (element->GetLocalStyleProperties().count(PropertyId::Visibility) == 0);
+		if (is_visible != value)
+		{
+			if (value)
+				element->RemoveProperty(PropertyId::Visibility);
+			else
+				element->SetProperty(PropertyId::Visibility, Property(Style::Visibility::Hidden));
+			result = true;
+		}
+	}
+	return result;
+}
+
+
+DataViewText::DataViewText(Element* element) : DataView(element)
+{}
+
+bool DataViewText::Initialize(DataModel& model, Element* element, const String& RMLUI_UNUSED_PARAMETER(expression), const String& RMLUI_UNUSED_PARAMETER(modifier))
+{
+	RMLUI_UNUSED(expression);
+	RMLUI_UNUSED(modifier);
+
+	ElementText* element_text = rmlui_dynamic_cast<ElementText*>(element);
+	if (!element_text)
+		return false;
+
+	const String& in_text = element_text->GetText();
+	
+	text.reserve(in_text.size());
+
+	DataExpressionInterface expression_interface(&model, element);
+
+	size_t previous_close_brackets = 0;
+	size_t begin_brackets = 0;
+	while ((begin_brackets = in_text.find("{{", begin_brackets)) != String::npos)
+	{
+		text.insert(text.end(), in_text.begin() + previous_close_brackets, in_text.begin() + begin_brackets);
+
+		const size_t begin_name = begin_brackets + 2;
+		const size_t end_name = in_text.find("}}", begin_name);
+
+		if (end_name == String::npos)
+			return false;
+
+		DataEntry entry;
+		entry.index = text.size();
+		entry.data_expression = std::make_unique<DataExpression>(String(in_text.begin() + begin_name, in_text.begin() + end_name));
+
+		if (entry.data_expression->Parse(expression_interface, false))
+			data_entries.push_back(std::move(entry));
+
+		previous_close_brackets = end_name + 2;
+		begin_brackets = previous_close_brackets;
+	}
+
+	if (data_entries.empty())
+		return false;
+
+	if (previous_close_brackets < in_text.size())
+		text.insert(text.end(), in_text.begin() + previous_close_brackets, in_text.end());
+
+	return true;
+}
+
+bool DataViewText::Update(DataModel& model)
+{
+	bool entries_modified = false;
+	{
+		Element* element = GetElement();
+		DataExpressionInterface expression_interface(&model, element);
+
+		for (DataEntry& entry : data_entries)
+		{
+			RMLUI_ASSERT(entry.data_expression);
+			Variant variant;
+			bool result = entry.data_expression->Run(expression_interface, variant);
+			const String value = variant.Get<String>();
+			if (result && entry.value != value)
+			{
+				entry.value = value;
+				entries_modified = true;
+			}
+		}
+	}
+
+	if (entries_modified)
+	{
+		if (Element* element = GetElement())
+		{
+			RMLUI_ASSERTMSG(rmlui_dynamic_cast<ElementText*>(element), "Somehow the element type was changed from ElementText since construction of the view. Should not be possible?");
+
+			if (ElementText* text_element = static_cast<ElementText*>(element))
+			{
+				String new_text = BuildText();
+				text_element->SetText(new_text);
+			}
+		}
+		else
+		{
+			Log::Message(Log::LT_WARNING, "Could not update data view text, element no longer valid. Was it destroyed?");
+		}
+	}
+
+	return entries_modified;
+}
+
+StringList DataViewText::GetVariableNameList() const
+{
+	StringList full_list;
+	full_list.reserve(data_entries.size());
+
+	for (const DataEntry& entry : data_entries)
+	{
+		RMLUI_ASSERT(entry.data_expression);
+
+		StringList entry_list = entry.data_expression->GetVariableNameList();
+		full_list.insert(full_list.end(),
+			std::make_move_iterator(entry_list.begin()),
+			std::make_move_iterator(entry_list.end())
+		);
+	}
+
+	return full_list;
+}
+
+void DataViewText::Release()
+{
+	delete this;
+}
+
+String DataViewText::BuildText() const
+{
+	size_t reserve_size = text.size();
+
+	for (const DataEntry& entry : data_entries)
+		reserve_size += entry.value.size();
+
+	String result;
+	result.reserve(reserve_size);
+
+	size_t previous_index = 0;
+	for (const DataEntry& entry : data_entries)
+	{
+		result += text.substr(previous_index, entry.index - previous_index);
+		result += entry.value;
+		previous_index = entry.index;
+	}
+
+	if (previous_index < text.size())
+		result += text.substr(previous_index);
+
+	return result;
+}
+
+
+
+DataViewFor::DataViewFor(Element* element) : DataView(element)
+{}
+
+bool DataViewFor::Initialize(DataModel& model, Element* element, const String& in_expression, const String& in_rml_content)
+{
+	rml_contents = in_rml_content;
+
+	StringList iterator_container_pair;
+	StringUtilities::ExpandString(iterator_container_pair, in_expression, ':');
+
+	if (iterator_container_pair.empty() || iterator_container_pair.size() > 2 || iterator_container_pair.front().empty() || iterator_container_pair.back().empty())
+	{
+		Log::Message(Log::LT_WARNING, "Invalid syntax in data-for '%s'", in_expression.c_str());
+		return false;
+	}
+
+	if (iterator_container_pair.size() == 2)
+	{
+		StringList iterator_index_pair;
+		StringUtilities::ExpandString(iterator_index_pair, iterator_container_pair.front(), ',');
+
+		if (iterator_index_pair.empty())
+		{
+			Log::Message(Log::LT_WARNING, "Invalid syntax in data-for '%s'", in_expression.c_str());
+			return false;
+		}
+		else if (iterator_index_pair.size() == 1)
+		{
+			iterator_name = iterator_index_pair.front();
+		}
+		else if (iterator_index_pair.size() == 2)
+		{
+			iterator_name = iterator_index_pair.front();
+			iterator_index_name = iterator_index_pair.back();
+		}
+	}
+
+	if (iterator_name.empty())
+		iterator_name = "it";
+
+	if (iterator_index_name.empty())
+		iterator_index_name = "it_index";
+
+	const String& container_name = iterator_container_pair.back();
+
+	container_address = model.ResolveAddress(container_name, element);
+	if (container_address.empty())
+		return false;
+
+	element->SetProperty(PropertyId::Display, Property(Style::Display::None));
+
+	return true;
+}
+
+
+bool DataViewFor::Update(DataModel& model)
+{
+	DataVariable variable = model.GetVariable(container_address);
+	if (!variable)
+		return false;
+
+	bool result = false;
+	const int size = variable.Size();
+	const int num_elements = (int)elements.size();
+	Element* element = GetElement();
+
+	for (int i = 0; i < Math::Max(size, num_elements); i++)
+	{
+		if (i >= num_elements)
+		{
+			ElementPtr new_element_ptr = Factory::InstanceElement(nullptr, element->GetTagName(), element->GetTagName(), attributes);
+
+			DataAddress iterator_address;
+			iterator_address.reserve(container_address.size() + 1);
+			iterator_address = container_address;
+			iterator_address.push_back(DataAddressEntry(i));
+
+			DataAddress iterator_index_address = {
+				{"literal"}, {"int"}, {i}
+			};
+
+			model.InsertAlias(new_element_ptr.get(), iterator_name, std::move(iterator_address));
+			model.InsertAlias(new_element_ptr.get(), iterator_index_name, std::move(iterator_index_address));
+
+			Element* new_element = element->GetParentNode()->InsertBefore(std::move(new_element_ptr), element);
+			elements.push_back(new_element);
+
+			elements[i]->SetInnerRML(rml_contents);
+
+			RMLUI_ASSERT(i < (int)elements.size());
+		}
+		if (i >= size)
+		{
+			model.EraseAliases(elements[i]);
+			elements[i]->GetParentNode()->RemoveChild(elements[i]).reset();
+			elements[i] = nullptr;
+		}
+	}
+
+	if (num_elements > size)
+		elements.resize(size);
+
+	return result;
+}
+
+StringList DataViewFor::GetVariableNameList() const {
+	RMLUI_ASSERT(!container_address.empty());
+	return StringList{ container_address.front().name };
+}
+
+void DataViewFor::Release()
+{
+	delete this;
+}
+
+}
+}

+ 176 - 0
Source/Core/DataViewDefault.h

@@ -0,0 +1,176 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICOREDATAVIEWDEFAULT_H
+#define RMLUICOREDATAVIEWDEFAULT_H
+
+#include "../../Include/RmlUi/Core/Header.h"
+#include "../../Include/RmlUi/Core/Types.h"
+#include "../../Include/RmlUi/Core/DataView.h"
+#include "../../Include/RmlUi/Core/Variant.h"
+
+namespace Rml {
+namespace Core {
+
+class Element;
+class DataExpression;
+using DataExpressionPtr = UniquePtr<DataExpression>;
+
+
+class DataViewCommon : public DataView {
+public:
+	DataViewCommon(Element* element, String override_modifier = String());
+
+	bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override;
+
+	StringList GetVariableNameList() const override;
+
+protected:
+	const String& GetModifier() const;
+	DataExpression& GetExpression();
+
+	// Delete this
+	void Release() override;
+
+private:
+	String modifier;
+	DataExpressionPtr expression;
+};
+
+
+class DataViewAttribute : public DataViewCommon {
+public:
+	DataViewAttribute(Element* element);
+	DataViewAttribute(Element* element, String override_attribute);
+
+	bool Update(DataModel& model) override;
+};
+
+
+class DataViewValue final : public DataViewAttribute {
+public:
+	DataViewValue(Element* element);
+};
+
+
+class DataViewStyle final : public DataViewCommon {
+public:
+	DataViewStyle(Element* element);
+
+	bool Update(DataModel& model) override;
+};
+
+
+class DataViewClass final : public DataViewCommon {
+public:
+	DataViewClass(Element* element);
+
+	bool Update(DataModel& model) override;
+};
+
+
+class DataViewRml final : public DataViewCommon {
+public:
+	DataViewRml(Element* element);
+
+	bool Update(DataModel& model) override;
+
+private:
+	String previous_rml;
+};
+
+
+class DataViewIf final : public DataViewCommon {
+public:
+	DataViewIf(Element* element);
+
+	bool Update(DataModel& model) override;
+};
+
+
+class DataViewVisible final : public DataViewCommon {
+public:
+	DataViewVisible(Element* element);
+
+	bool Update(DataModel& model) override;
+};
+
+
+class DataViewText final : public DataView {
+public:
+	DataViewText(Element* in_element);
+
+	bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override;
+
+	bool Update(DataModel& model) override;
+	StringList GetVariableNameList() const override;
+
+protected:
+	void Release() override;
+
+private:
+	String BuildText() const;
+
+	struct DataEntry {
+		size_t index = 0; // Index into 'text'
+		DataExpressionPtr data_expression;
+		String value;
+	};
+
+	String text;
+	std::vector<DataEntry> data_entries;
+};
+
+
+class DataViewFor final : public DataView {
+public:
+	DataViewFor(Element* element);
+
+	bool Initialize(DataModel& model, Element* element, const String& expression, const String& inner_rml) override;
+
+	bool Update(DataModel& model) override;
+
+	StringList GetVariableNameList() const override;
+
+protected:
+	void Release() override;
+
+private:
+	DataAddress container_address;
+	String iterator_name;
+	String iterator_index_name;
+	String rml_contents;
+	ElementAttributes attributes;
+
+	ElementList elements;
+};
+
+}
+}
+
+#endif

+ 60 - 4
Source/Core/Element.cpp

@@ -30,6 +30,7 @@
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/Context.h"
 #include "../../Include/RmlUi/Core/Core.h"
+#include "../../Include/RmlUi/Core/DataModel.h"
 #include "../../Include/RmlUi/Core/ElementDocument.h"
 #include "../../Include/RmlUi/Core/ElementInstancer.h"
 #include "../../Include/RmlUi/Core/ElementScroll.h"
@@ -147,6 +148,7 @@ transform_state(), dirty_transform(false), dirty_perspective(false), dirty_anima
 	clipping_state_dirty = true;
 
 	meta = element_meta_chunk_pool.AllocateAndConstruct(this);
+	data_model = nullptr;
 }
 
 Element::~Element()
@@ -1282,7 +1284,6 @@ Element* Element::AppendChild(ElementPtr child, bool dom_element)
 {
 	RMLUI_ASSERT(child);
 	Element* child_ptr = child.get();
-	child_ptr->SetParent(this);
 	if (dom_element)
 		children.insert(children.end() - num_non_dom_children, std::move(child));
 	else
@@ -1290,6 +1291,8 @@ Element* Element::AppendChild(ElementPtr child, bool dom_element)
 		children.push_back(std::move(child));
 		num_non_dom_children++;
 	}
+	// Set parent just after inserting into children. This allows us to eg. get our previous sibling in SetParent.
+	child_ptr->SetParent(this);
 
 	Element* ancestor = child_ptr;
 	for (int i = 0; i <= ChildNotifyLevels && ancestor; i++, ancestor = ancestor->GetParentNode())
@@ -1331,7 +1334,6 @@ Element* Element::InsertBefore(ElementPtr child, Element* adjacent_element)
 	if (found_child)
 	{
 		child_ptr = child.get();
-		child_ptr->SetParent(this);
 
 		if ((int) child_index >= GetNumChildren())
 			num_non_dom_children++;
@@ -1339,6 +1341,7 @@ Element* Element::InsertBefore(ElementPtr child, Element* adjacent_element)
 			DirtyLayout();
 
 		children.insert(children.begin() + child_index, std::move(child));
+		child_ptr->SetParent(this);
 
 		Element* ancestor = child_ptr;
 		for (int i = 0; i <= ChildNotifyLevels && ancestor; i++, ancestor = ancestor->GetParentNode())
@@ -1373,9 +1376,9 @@ ElementPtr Element::ReplaceChild(ElementPtr inserted_element, Element* replaced_
 		return nullptr;
 	}
 
+	children.insert(insertion_point, std::move(inserted_element));
 	inserted_element_ptr->SetParent(this);
 
-	children.insert(insertion_point, std::move(inserted_element));
 	ElementPtr result = RemoveChild(replaced_element);
 
 	Element* ancestor = inserted_element_ptr;
@@ -1513,6 +1516,11 @@ ElementScroll* Element::GetElementScroll() const
 {
 	return &meta->scroll;
 }
+
+DataModel* Element::GetDataModel() const
+{
+	return data_model;
+}
 	
 int Element::GetClippingIgnoreDepth()
 {
@@ -1606,7 +1614,6 @@ void Element::OnAttributeChange(const ElementAttributes& changed_attributes)
 		meta->style.SetClassNames(it->second.Get<String>());
 	}
 
-	// Add any inline style declarations.
 	it = changed_attributes.find("style");
 	if (it != changed_attributes.end())
 	{
@@ -1933,6 +1940,25 @@ void Element::SetOwnerDocument(ElementDocument* document)
 	}
 }
 
+void Element::SetDataModel(DataModel* new_data_model) 
+{
+	RMLUI_ASSERTMSG(!data_model || !new_data_model, "We must either attach a new data model, or detach the old one.");
+
+	if (data_model == new_data_model)
+		return;
+
+	if (data_model)
+		data_model->OnElementRemove(this);
+
+	data_model = new_data_model;
+
+	if (data_model)
+		ElementUtilities::ApplyDataViewsControllers(this);
+
+	for (ElementPtr& child : children)
+		child->SetDataModel(new_data_model);
+}
+
 void Element::Release()
 {
 	if (instancer)
@@ -1960,6 +1986,36 @@ void Element::SetParent(Element* _parent)
 		DirtyTransformState(true, true);
 
 	SetOwnerDocument(parent ? parent->GetOwnerDocument() : nullptr);
+
+	if (!parent)
+	{
+		if (data_model)
+			SetDataModel(nullptr);
+	}
+	else 
+	{
+		auto it = attributes.find("data-model");
+		if (it == attributes.end())
+		{
+			SetDataModel(parent->data_model);
+		}
+		else if (parent->data_model)
+		{
+			String name = it->second.Get<String>();
+			Log::Message(Log::LT_ERROR, "Nested data models are not allowed. Data model '%s' given in element %s.", name.c_str(), GetAddress().c_str());
+		}
+		else if (Context* context = GetContext())
+		{
+			String name = it->second.Get<String>();
+			if (DataModel* model = context->GetDataModelPtr(name))
+			{
+				model->AttachModelRootElement(this);
+				SetDataModel(model);
+			}
+			else
+				Log::Message(Log::LT_ERROR, "Could not locate data model '%s' in element %s.", name.c_str(), GetAddress().c_str());
+		}
+	}
 }
 
 void Element::DirtyOffset()

+ 125 - 6
Source/Core/ElementUtilities.cpp

@@ -27,18 +27,21 @@
  */
 
 #include "../../Include/RmlUi/Core/ElementUtilities.h"
-#include "../../Include/RmlUi/Core/TransformState.h"
+#include "../../Include/RmlUi/Core/Context.h"
+#include "../../Include/RmlUi/Core/Core.h"
+#include "../../Include/RmlUi/Core/DataController.h"
+#include "../../Include/RmlUi/Core/DataModel.h"
+#include "../../Include/RmlUi/Core/DataView.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/ElementScroll.h"
-#include "../../Include/RmlUi/Core/Context.h"
+#include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/FontEngineInterface.h"
 #include "../../Include/RmlUi/Core/RenderInterface.h"
-#include "../../Include/RmlUi/Core/Core.h"
-#include "../../Include/RmlUi/Core/Factory.h"
+#include "../../Include/RmlUi/Core/TransformState.h"
+#include "ElementStyle.h"
+#include "LayoutEngine.h"
 #include <queue>
 #include <limits>
-#include "LayoutEngine.h"
-#include "ElementStyle.h"
 
 namespace Rml {
 namespace Core {
@@ -381,5 +384,121 @@ bool ElementUtilities::ApplyTransform(Element &element)
 	return true;
 }
 
+
+static bool ApplyDataViewsControllersInternal(Element* element, const bool construct_structural_view, const String& structural_view_inner_rml)
+{
+	RMLUI_ASSERT(element);
+	bool result = false;
+
+	// If we have an active data model, check the attributes for any data bindings
+	if (DataModel* data_model = element->GetDataModel())
+	{
+		struct ViewControllerInitializer {
+			String type;
+			String modifier_or_inner_rml;
+			String expression;
+			DataViewPtr view;
+			DataControllerPtr controller;
+			explicit operator bool() const { return view || controller; }
+		};
+
+		// Since data views and controllers may modify the element's attributes during initialization, we 
+		// need to iterate over all the attributes _before_ initializing any views or controllers. We store
+		// the information needed to initialize them in the following container.
+		std::vector<ViewControllerInitializer> initializer_list;
+
+		for (auto& attribute : element->GetAttributes())
+		{
+			// Data views and controllers are declared by the following element attribute:
+			//     data-[type]-[modifier]="[expression]"
+
+			constexpr size_t data_str_length = sizeof("data-") - 1;
+
+			const String& name = attribute.first;
+
+			if (name.size() > data_str_length && name[0] == 'd' && name[1] == 'a' && name[2] == 't' && name[3] == 'a' && name[4] == '-')
+			{
+				const size_t type_end = name.find('-', data_str_length);
+				const size_t type_size = (type_end == String::npos ? String::npos : type_end - data_str_length);
+				String type_name = name.substr(data_str_length, type_size);
+
+				ViewControllerInitializer initializer;
+
+				// Structural data views are applied in a separate step from the normal views and controllers.
+				if (construct_structural_view)
+				{
+					if (DataViewPtr view = Factory::InstanceDataView(type_name, element, true))
+					{
+						initializer.modifier_or_inner_rml = structural_view_inner_rml;
+						initializer.view = std::move(view);
+					}
+				}
+				else
+				{
+					const size_t modifier_offset = data_str_length + type_name.size() + 1;
+					if (modifier_offset < name.size())
+						initializer.modifier_or_inner_rml = name.substr(modifier_offset);
+
+					if (DataViewPtr view = Factory::InstanceDataView(type_name, element, false))
+						initializer.view = std::move(view);
+
+					if (DataControllerPtr controller = Factory::InstanceDataController(type_name, element))
+						initializer.controller = std::move(controller);
+				}
+
+				if (initializer)
+				{
+					initializer.type = std::move(type_name);
+					initializer.expression = attribute.second.Get<String>();
+
+					initializer_list.push_back(std::move(initializer));
+				}
+			}
+		}
+
+		// Now, we can safely initialize the data views and controllers, even modifying the element's attributes when desired.
+		for (ViewControllerInitializer& initializer : initializer_list)
+		{
+			DataViewPtr& view = initializer.view;
+			DataControllerPtr& controller = initializer.controller;
+
+			if (view)
+			{
+				if (view->Initialize(*data_model, element, initializer.expression, initializer.modifier_or_inner_rml))
+				{
+					data_model->AddView(std::move(view));
+					result = true;
+				}
+				else
+					Log::Message(Log::LT_WARNING, "Could not add data-%s view to element: %s", initializer.type.c_str(), element->GetAddress().c_str());
+			}
+
+			if (controller)
+			{
+				if (controller->Initialize(*data_model, element, initializer.expression, initializer.modifier_or_inner_rml))
+				{
+					data_model->AddController(std::move(controller));
+					result = true;
+				}
+				else
+					Log::Message(Log::LT_WARNING, "Could not add data-%s controller to element: %s", initializer.type.c_str(), element->GetAddress().c_str());
+			}
+		}
+	}
+
+	return result;
+}
+
+
+bool ElementUtilities::ApplyDataViewsControllers(Element* element)
+{
+	return ApplyDataViewsControllersInternal(element, false, String());
+}
+
+bool ElementUtilities::ApplyStructuralDataViews(Element* element, const String& inner_rml)
+{
+	return ApplyDataViewsControllersInternal(element, true, inner_rml);
+}
+
 }
 }

+ 160 - 31
Source/Core/Factory.cpp

@@ -39,6 +39,8 @@
 #include "../../Include/RmlUi/Core/SystemInterface.h"
 
 #include "ContextInstancerDefault.h"
+#include "DataControllerDefault.h"
+#include "DataViewDefault.h"
 #include "DecoratorTiledBoxInstancer.h"
 #include "DecoratorTiledHorizontalInstancer.h"
 #include "DecoratorTiledImageInstancer.h"
@@ -63,22 +65,38 @@
 #include "XMLNodeHandlerHead.h"
 #include "XMLNodeHandlerTemplate.h"
 #include "XMLParseTools.h"
+#include <algorithm>
 
 namespace Rml {
 namespace Core {
 
 // Element instancers.
-typedef UnorderedMap< String, ElementInstancer* > ElementInstancerMap;
+using ElementInstancerMap = UnorderedMap< String, ElementInstancer* >;
 static ElementInstancerMap element_instancers;
 
 // Decorator instancers.
-typedef UnorderedMap< String, DecoratorInstancer* > DecoratorInstancerMap;
+using DecoratorInstancerMap = UnorderedMap< String, DecoratorInstancer* >;
 static DecoratorInstancerMap decorator_instancers;
 
 // Font effect instancers.
-typedef UnorderedMap< String, FontEffectInstancer* > FontEffectInstancerMap;
+using FontEffectInstancerMap = UnorderedMap< String, FontEffectInstancer* >;
 static FontEffectInstancerMap font_effect_instancers;
 
+// Data view instancers.
+using DataViewInstancerMap = UnorderedMap< String, DataViewInstancer* >;
+static DataViewInstancerMap data_view_instancers;
+
+// Data controller instancers.
+using DataControllerInstancerMap = UnorderedMap< String, DataControllerInstancer* >;
+static DataControllerInstancerMap data_controller_instancers;
+
+// Structural data view instancers.
+using StructuralDataViewInstancerMap = SmallUnorderedMap< String, DataViewInstancer* >;
+static StructuralDataViewInstancerMap structural_data_view_instancers;
+
+// Structural data view names.
+static StringList structural_data_view_attribute_names;
+
 // The context instancer.
 static ContextInstancer* context_instancer = nullptr;
 
@@ -113,6 +131,21 @@ struct DefaultInstancers {
 	Ptr<FontEffectInstancer> font_effect_glow = std::make_unique<FontEffectGlowInstancer>();
 	Ptr<FontEffectInstancer> font_effect_outline = std::make_unique<FontEffectOutlineInstancer>();
 	Ptr<FontEffectInstancer> font_effect_shadow = std::make_unique<FontEffectShadowInstancer>();
+
+	Ptr<DataViewInstancer> data_view_attribute = std::make_unique<DataViewInstancerDefault< DataViewAttribute >>();
+	Ptr<DataViewInstancer> data_view_class     = std::make_unique<DataViewInstancerDefault< DataViewClass >>();
+	Ptr<DataViewInstancer> data_view_if        = std::make_unique<DataViewInstancerDefault< DataViewIf >>();
+	Ptr<DataViewInstancer> data_view_visible   = std::make_unique<DataViewInstancerDefault< DataViewVisible >>();
+	Ptr<DataViewInstancer> data_view_rml       = std::make_unique<DataViewInstancerDefault< DataViewRml >>();
+	Ptr<DataViewInstancer> data_view_style     = std::make_unique<DataViewInstancerDefault< DataViewStyle >>();
+	Ptr<DataViewInstancer> data_view_text      = std::make_unique<DataViewInstancerDefault< DataViewText >>();
+	Ptr<DataViewInstancer> data_view_value     = std::make_unique<DataViewInstancerDefault< DataViewValue >>();
+
+	Ptr<DataViewInstancer> structural_data_view_for = std::make_unique<DataViewInstancerDefault< DataViewFor >>();
+
+	Ptr<DataControllerInstancer> data_controller_value = std::make_unique<DataControllerInstancerDefault< DataControllerValue >>();
+	Ptr<DataControllerInstancer> data_controller_event = std::make_unique<DataControllerInstancerDefault< DataControllerEvent >>();
+
 };
 
 static UniquePtr<DefaultInstancers> default_instancers;
@@ -175,6 +208,21 @@ bool Factory::Initialise()
 	XMLParser::RegisterNodeHandler("head", std::make_shared<XMLNodeHandlerHead>());
 	XMLParser::RegisterNodeHandler("template", std::make_shared<XMLNodeHandlerTemplate>());
 
+	// Register the default data views
+	RegisterDataViewInstancer(default_instancers->data_view_attribute.get(), "attr",     false);
+	RegisterDataViewInstancer(default_instancers->data_view_class.get(),     "class",    false);
+	RegisterDataViewInstancer(default_instancers->data_view_if.get(),        "if",       false);
+	RegisterDataViewInstancer(default_instancers->data_view_visible.get(),   "visible",  false);
+	RegisterDataViewInstancer(default_instancers->data_view_rml.get(),       "rml",      false);
+	RegisterDataViewInstancer(default_instancers->data_view_style.get(),     "style",    false);
+	RegisterDataViewInstancer(default_instancers->data_view_text.get(),      "text",     false);
+	RegisterDataViewInstancer(default_instancers->data_view_value.get(),     "value",    false);
+	RegisterDataViewInstancer(default_instancers->structural_data_view_for.get(), "for", true );
+
+	RegisterDataControllerInstancer(default_instancers->data_controller_value.get(), "value");
+	RegisterDataControllerInstancer(default_instancers->data_controller_event.get(), "event");
+
+
 	return true;
 }
 
@@ -186,6 +234,10 @@ void Factory::Shutdown()
 
 	font_effect_instancers.clear();
 
+	data_view_instancers.clear();
+	structural_data_view_instancers.clear();
+	structural_data_view_attribute_names.clear();
+
 	context_instancer = nullptr;
 
 	event_listener_instancer = nullptr;
@@ -257,21 +309,49 @@ ElementPtr Factory::InstanceElement(Element* parent, const String& instancer_nam
 }
 
 // Instances a single text element containing a string.
-bool Factory::InstanceElementText(Element* parent, const String& text)
+bool Factory::InstanceElementText(Element* parent, const String& in_text)
 {
-	SystemInterface* system_interface = GetSystemInterface();
-
-	// Do any necessary translation. If any substitutions were made then new XML may have been introduced, so we'll
-	// have to run the data through the XML parser again.
-	String translated_data;
-	if (system_interface != nullptr &&
-		(system_interface->TranslateString(translated_data, text) > 0 ||
-		 translated_data.find("<") != String::npos))
+	RMLUI_ASSERT(parent);
+
+	String text;
+	if (SystemInterface* system_interface = GetSystemInterface())
+		system_interface->TranslateString(text, in_text);
+
+	// If this text node only contains white-space we don't want to construct it.
+	const bool only_white_space = std::all_of(text.begin(), text.end(), &StringUtilities::IsWhitespace);
+	if (only_white_space)
+		return true;
+
+	// See if we need to parse it as RML, and whether the text contains data expressions (curly brackets).
+	bool parse_as_rml = false;
+	bool has_data_expression = false;
+
+	bool inside_brackets = false;
+	char previous = 0;
+	for (const char c : text)
+	{
+		const char* error_str = XMLParseTools::ParseDataBrackets(inside_brackets, c, previous);
+		if (error_str)
+		{
+			Log::Message(Log::LT_WARNING, "Failed to instance text element '%s'. %s", text.c_str(), error_str);
+			return false;
+		}
+
+		if (inside_brackets)
+			has_data_expression = true;
+		else if (c == '<')
+			parse_as_rml = true;
+
+		previous = c;
+	}
+
+	// If the text contains RML elements then run it through the XML parser again.
+	if (parse_as_rml)
 	{
 		RMLUI_ZoneScopedNC("InstanceStream", 0xDC143C);
-		auto stream = std::make_unique<StreamMemory>(translated_data.size() + 32);
+		auto stream = std::make_unique<StreamMemory>(text.size() + 32);
 		stream->Write("<body>", 6);
-		stream->Write(translated_data);
+		stream->Write(text);
 		stream->Write("</body>", 7);
 		stream->Seek(0, SEEK_SET);
 
@@ -280,26 +360,18 @@ bool Factory::InstanceElementText(Element* parent, const String& text)
 	else
 	{
 		RMLUI_ZoneScopedNC("InstanceText", 0x8FBC8F);
-		// Check if this text node contains only white-space; if so, we don't want to construct it.
-		bool only_white_space = true;
-		for (size_t i = 0; i < translated_data.size(); ++i)
-		{
-			if (!StringUtilities::IsWhitespace(translated_data[i]))
-			{
-				only_white_space = false;
-				break;
-			}
-		}
-
-		if (only_white_space)
-			return true;
-
+		
 		// Attempt to instance the element.
 		XMLAttributes attributes;
+
+		// If we have curly brackets in the text, we tag the element so that the appropriate data view (DataViewText) is constructed.
+		if (has_data_expression)
+			attributes.emplace("data-text", Variant());
+
 		ElementPtr element = Factory::InstanceElement(parent, "#text", "#text", attributes);
 		if (!element)
 		{
-			Log::Message(Log::LT_ERROR, "Failed to instance text element '%s', instancer returned nullptr.", translated_data.c_str());
+			Log::Message(Log::LT_ERROR, "Failed to instance text element '%s', instancer returned nullptr.", text.c_str());
 			return false;
 		}
 
@@ -307,11 +379,11 @@ bool Factory::InstanceElementText(Element* parent, const String& text)
 		ElementText* text_element = rmlui_dynamic_cast< ElementText* >(element.get());
 		if (!text_element)
 		{
-			Log::Message(Log::LT_ERROR, "Failed to instance text element '%s'. Found type '%s', was expecting a derivative of ElementText.", translated_data.c_str(), rmlui_type_name(*element));
+			Log::Message(Log::LT_ERROR, "Failed to instance text element '%s'. Found type '%s', was expecting a derivative of ElementText.", text.c_str(), rmlui_type_name(*element));
 			return false;
 		}
 
-		text_element->SetText(translated_data);
+		text_element->SetText(text);
 
 		// Add to active node.
 		parent->AppendChild(std::move(element));
@@ -459,5 +531,62 @@ EventListener* Factory::InstanceEventListener(const String& value, Element* elem
 	return nullptr;
 }
 
+void Factory::RegisterDataViewInstancer(DataViewInstancer* instancer, const String& name, bool is_structural_view)
+{
+	bool inserted = false;
+	if (is_structural_view)
+	{
+		inserted = structural_data_view_instancers.emplace(name, instancer).second;
+		if (inserted)
+			structural_data_view_attribute_names.push_back(String("data-") + name);
+	}
+	else
+	{
+		inserted = data_view_instancers.emplace(name, instancer).second;
+	}
+	
+	if (!inserted)
+		Log::Message(Log::LT_WARNING, "Could not register data view instancer '%s'. The given name is already registered.", name.c_str());
+}
+
+void Factory::RegisterDataControllerInstancer(DataControllerInstancer* instancer, const String& name)
+{
+	bool inserted = data_controller_instancers.emplace(name, instancer).second;
+	if (!inserted)
+		Log::Message(Log::LT_WARNING, "Could not register data controller instancer '%s'. The given name is already registered.", name.c_str());
+}
+
+DataViewPtr Factory::InstanceDataView(const String& type_name, Element* element, bool is_structural_view)
+{
+	RMLUI_ASSERT(element);
+
+	if (is_structural_view)
+	{
+		auto it = structural_data_view_instancers.find(type_name);
+		if (it != structural_data_view_instancers.end())
+			return it->second->InstanceView(element);
+	}
+	else
+	{
+		auto it = data_view_instancers.find(type_name);
+		if (it != data_view_instancers.end())
+			return it->second->InstanceView(element);
+	}
+	return nullptr;
+}
+
+DataControllerPtr Factory::InstanceDataController(const String& type_name, Element* element)
+{
+	auto it = data_controller_instancers.find(type_name);
+	if (it != data_controller_instancers.end())
+		return it->second->InstanceController(element);
+	return DataControllerPtr();
+}
+
+const StringList& Factory::GetStructuralDataViewAttributeNames()
+{
+	return structural_data_view_attribute_names;
+}
+
 }
 }

+ 6 - 0
Source/Core/Math.cpp

@@ -130,6 +130,12 @@ RMLUICORE_API float RoundFloat(float value)
 	return roundf(value);
 }
 
+// Rounds a floating-point value to the nearest integer.
+RMLUICORE_API double RoundFloat(double value)
+{
+	return round(value);
+}
+
 // Rounds a floating-point value to the nearest integer.
 RMLUICORE_API int RoundToInteger(float value)
 {

+ 1 - 1
Source/Core/Property.cpp

@@ -40,7 +40,7 @@ Property::Property() : unit(UNKNOWN), specificity(-1)
 
 String Property::ToString() const
 {
-	if (definition == nullptr)
+	if (!definition)
 		return value.Get< String >();
 
 	String string;

+ 70 - 3
Source/Core/StringUtilities.cpp

@@ -94,6 +94,18 @@ String StringUtilities::ToLower(const String& string) {
 	return str_lower;
 }
 
+String StringUtilities::ToUpper(const String& string)
+{
+	String str_upper = string;
+	std::transform(str_upper.begin(), str_upper.end(), str_upper.begin(), [](char c) {
+		if (c >= 'a' && c <= 'z')
+			c -= char('a' - 'A');
+		return c;
+		}
+	);
+	return str_upper;
+}
+
 RMLUICORE_API String StringUtilities::EncodeRml(const String& string)
 {
 	String result;
@@ -270,6 +282,61 @@ RMLUICORE_API String StringUtilities::StripWhitespace(StringView string)
 	return String();
 }
 
+void StringUtilities::TrimTrailingDotZeros(String& string)
+{
+	RMLUI_ASSERTMSG(string.find('.') != String::npos, "This function probably does not do what you want if the string is not a number with a decimal point.")
+
+	size_t new_size = string.size();
+	for (size_t i = string.size() - 1; i < string.size(); i--)
+	{
+		if (string[i] == '.')
+		{
+			new_size = i;
+			break;
+		}
+		else if (string[i] == '0')
+			new_size = i;
+		else
+			break;
+	}
+
+	if (new_size < string.size())
+		string.resize(new_size);
+}
+
+#ifdef RMLUI_DEBUG
+static struct TestTrimTrailingDotZeros {
+	TestTrimTrailingDotZeros() {
+		auto test = [](const String test_string, const String expected) {
+			String result = test_string;
+			StringUtilities::TrimTrailingDotZeros(result);
+			RMLUI_ASSERT(result == expected);
+		};
+		
+		test("0.1", "0.1");
+		test("0.10", "0.1");
+		test("0.1000", "0.1");
+		test("0.01", "0.01");
+		test("0.", "0");
+		test("5.", "5");
+		test("5.5", "5.5");
+		test("5.50", "5.5");
+		test("5.501", "5.501");
+		test("10.0", "10");
+		test("11.0", "11");
+
+		// Some test cases for behavior that are probably not what you want.
+		//test("test0", "test");
+		//test("1000", "1");
+		//test(".", "");
+		//test("0", "");
+		//test(".0", "");
+		//test(" 11 2121 3.00", " 11 2121 3");
+		//test("11", "11");
+	}
+} test_trim_trailing_dot_zeros;
+#endif
+
 bool StringUtilities::StringCompareCaseInsensitive(const StringView lhs, const StringView rhs)
 {
 	if (lhs.size() != rhs.size())
@@ -520,19 +587,19 @@ StringIteratorU8::StringIteratorU8(const String& string, size_t offset) : view(s
 StringIteratorU8::StringIteratorU8(const String& string, size_t offset, size_t count) : view(string, 0, offset + count), p(string.data() + offset)
 {}
 StringIteratorU8& StringIteratorU8::operator++() {
-	RMLUI_ASSERT(p != view.end());
+	RMLUI_ASSERT(p < view.end());
 	++p;
 	SeekForward();
 	return *this;
 }
 StringIteratorU8& StringIteratorU8::operator--() {
-	RMLUI_ASSERT(p - 1 != view.begin());
+	RMLUI_ASSERT(p >= view.begin());
 	--p;
 	SeekBack();
 	return *this;
 }
 inline void StringIteratorU8::SeekBack() {
-	p = StringUtilities::SeekBackwardUTF8(p, view.end());
+	p = StringUtilities::SeekBackwardUTF8(p, view.begin());
 }
 
 inline void StringIteratorU8::SeekForward() {

+ 22 - 5
Source/Core/Variant.cpp

@@ -192,6 +192,12 @@ void Variant::Set(Variant&& other)
 	RMLUI_ASSERT(type == other.type);
 }
 
+void Variant::Set(const bool value)
+{
+	type = BOOL;
+	SET_VARIANT(bool);
+}
+
 void Variant::Set(const byte value)
 {
 	type = BYTE;
@@ -210,15 +216,22 @@ void Variant::Set(const float value)
 	SET_VARIANT(float);
 }
 
+void Variant::Set(const double value)
+{
+	type = DOUBLE;
+	SET_VARIANT(double);
+}
+
 void Variant::Set(const int value)
 {
 	type = INT;
 	SET_VARIANT(int);
 }
-void Variant::Set(const Character value)
+
+void Variant::Set(const int64_t value)
 {
-	type = WORD;
-	SET_VARIANT(Character);
+	type = INT64;
+	SET_VARIANT(int64_t);
 }
 
 void Variant::Set(const char* value) 
@@ -444,18 +457,22 @@ bool Variant::operator==(const Variant & other) const
 
 	switch (type)
 	{
+	case BOOL:
+		return DEFAULT_VARIANT_COMPARE(bool);
 	case BYTE:
 		return DEFAULT_VARIANT_COMPARE(byte);
 	case CHAR:
 		return DEFAULT_VARIANT_COMPARE(char);
 	case FLOAT:
 		return DEFAULT_VARIANT_COMPARE(float);
+	case DOUBLE:
+		return DEFAULT_VARIANT_COMPARE(double);
 	case INT:
 		return DEFAULT_VARIANT_COMPARE(int);
+	case INT64:
+		return DEFAULT_VARIANT_COMPARE(int64_t);
 	case STRING:
 		return DEFAULT_VARIANT_COMPARE(String);
-	case WORD:
-		return DEFAULT_VARIANT_COMPARE(Character);
 	case VECTOR2:
 		return DEFAULT_VARIANT_COMPARE(Vector2f);
 	case VECTOR3:

+ 2 - 1
Source/Core/XMLNodeHandlerBody.cpp

@@ -76,8 +76,9 @@ bool XMLNodeHandlerBody::ElementEnd(XMLParser* RMLUI_UNUSED_PARAMETER(parser), c
 	return true;
 }
 
-bool XMLNodeHandlerBody::ElementData(XMLParser* parser, const String& data)
+bool XMLNodeHandlerBody::ElementData(XMLParser* parser, const String& data, XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {
+	RMLUI_UNUSED(type);
 	return Factory::InstanceElementText(parser->GetParseFrame()->element, data);
 }
 

+ 1 - 1
Source/Core/XMLNodeHandlerBody.h

@@ -51,7 +51,7 @@ public:
 	/// Called when an element is closed
 	bool ElementEnd(XMLParser* parser, const String& name) override;
 	/// Called for element data
-	bool ElementData(XMLParser* parser, const String& data) override;
+	bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override;
 };
 
 }

+ 11 - 2
Source/Core/XMLNodeHandlerDefault.cpp

@@ -33,6 +33,7 @@
 #include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/XMLParser.h"
+#include "../../Include/RmlUi/Core/ElementUtilities.h"
 
 
 namespace Rml {
@@ -61,7 +62,7 @@ Element* XMLNodeHandlerDefault::ElementStart(XMLParser* parser, const String& na
 		return nullptr;
 	}
 
-	// Add the element to its parent and remove the reference
+	// Move and append the element to the parent
 	Element* result = parent->AppendChild(std::move(element));
 
 	return result;
@@ -75,12 +76,20 @@ bool XMLNodeHandlerDefault::ElementEnd(XMLParser* RMLUI_UNUSED_PARAMETER(parser)
 	return true;
 }
 
-bool XMLNodeHandlerDefault::ElementData(XMLParser* parser, const String& data)
+bool XMLNodeHandlerDefault::ElementData(XMLParser* parser, const String& data, XMLDataType type)
 {
 	RMLUI_ZoneScopedC(0x006400);
 
 	// Determine the parent
 	Element* parent = parser->GetParseFrame()->element;
+	RMLUI_ASSERT(parent);
+
+	if (type == XMLDataType::InnerXML)
+	{
+		// Structural data views use the raw inner xml contents of the node, submit them now.
+		if (ElementUtilities::ApplyStructuralDataViews(parent, data))
+			return true;
+	}
 
 	// Parse the text into the element
 	return Factory::InstanceElementText(parent, data);

+ 1 - 1
Source/Core/XMLNodeHandlerDefault.h

@@ -52,7 +52,7 @@ public:
 	/// Called when an element is closed
 	bool ElementEnd(XMLParser* parser, const String& name) override;	
 	/// Called for element data
-	bool ElementData(XMLParser* parser, const String& data) override;
+	bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override;
 };
 
 }

+ 2 - 1
Source/Core/XMLNodeHandlerHead.cpp

@@ -120,8 +120,9 @@ bool XMLNodeHandlerHead::ElementEnd(XMLParser* parser, const String& name)
 	return true;
 }
 
-bool XMLNodeHandlerHead::ElementData(XMLParser* parser, const String& data)
+bool XMLNodeHandlerHead::ElementData(XMLParser* parser, const String& data, XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {
+	RMLUI_UNUSED(type);
 	const String& tag = parser->GetParseFrame()->tag;
 
 	// Store the title

+ 1 - 1
Source/Core/XMLNodeHandlerHead.h

@@ -51,7 +51,7 @@ public:
 	/// Called when an element is closed
 	bool ElementEnd(XMLParser* parser, const String& name) override;
 	/// Called for element data
-	bool ElementData(XMLParser* parser, const String& data) override;
+	bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override;
 };
 
 }

+ 2 - 1
Source/Core/XMLNodeHandlerTemplate.cpp

@@ -66,8 +66,9 @@ bool XMLNodeHandlerTemplate::ElementEnd(XMLParser* RMLUI_UNUSED_PARAMETER(parser
 	return true;
 }
 
-bool XMLNodeHandlerTemplate::ElementData(XMLParser* parser, const String& data)
+bool XMLNodeHandlerTemplate::ElementData(XMLParser* parser, const String& data, XMLDataType RMLUI_UNUSED_PARAMETER(type))
 {	
+	RMLUI_UNUSED(type);
 	return Factory::InstanceElementText(parser->GetParseFrame()->element, data);
 }
 

+ 1 - 1
Source/Core/XMLNodeHandlerTemplate.h

@@ -51,7 +51,7 @@ public:
 	/// Called when an element is closed
 	bool ElementEnd(XMLParser* parser, const String& name) override;
 	/// Called for element data
-	bool ElementData(XMLParser* parser, const String& data) override;
+	bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override;
 };
 
 }

+ 34 - 0
Source/Core/XMLParseTools.cpp

@@ -154,5 +154,39 @@ Element* XMLParseTools::ParseTemplate(Element* element, const String& template_n
 	return parse_template->ParseTemplate(element);
 }
 
+const char* XMLParseTools::ParseDataBrackets(bool& inside_brackets, char c, char previous)
+{
+	if (inside_brackets)
+	{
+		if (c == '}' && previous == '}')
+			inside_brackets = false;
+
+		else if (c == '{' && previous == '{')
+			return "Nested double curly brackets are illegal.";
+
+		else if (previous == '}' && c != '}')
+			return "Single closing curly bracket encountered, use double curly brackets to close an expression.";
+
+		else if (previous == '/' && c == '>')
+			return "Closing double curly brackets not found, XML end node encountered first.";
+
+		else if (previous == '<' && c == '/')
+			return "Closing double curly brackets not found, XML end node encountered first.";
+	}
+	else
+	{
+		if (c == '{' && previous == '{')
+		{
+			inside_brackets = true;
+		}
+		else if (c == '}' && previous == '}')
+		{
+			return "Closing double curly brackets encountered outside an expression.";
+		}
+	}
+
+	return nullptr;
+}
+
 }
 }

+ 6 - 0
Source/Core/XMLParseTools.h

@@ -61,6 +61,12 @@ public:
 	/// @param template_name Name of the template to apply, in TEMPLATE:ELEMENT_ID form
 	/// @returns Element to continue the parse from
 	static Element* ParseTemplate(Element* element, const String& template_name);
+
+    /// Determine the presence of data expression brackets inside XML data.
+    /// Call this for each iteration through the data string.
+    /// 'inside_brackets' should be initialized to false.
+    /// Returns nullptr on success, or an error string on failure.
+    static const char* ParseDataBrackets(bool& inside_brackets, char c, char previous);
 };
 
 }

+ 16 - 16
Source/Core/XMLParser.cpp

@@ -34,11 +34,12 @@
 #include "../../Include/RmlUi/Core/XMLNodeHandler.h"
 #include "../../Include/RmlUi/Core/URL.h"
 #include "../../Include/RmlUi/Core/XMLParser.h"
+#include "../../Include/RmlUi/Core/Factory.h"
 
 namespace Rml {
 namespace Core {
 
-using NodeHandlers = UnorderedMap< String, SharedPtr<XMLNodeHandler> > ;
+using NodeHandlers = UnorderedMap< String, SharedPtr<XMLNodeHandler> >;
 static NodeHandlers node_handlers;
 static SharedPtr<XMLNodeHandler> default_node_handler;
 
@@ -46,23 +47,21 @@ XMLParser::XMLParser(Element* root)
 {
 	RegisterCDATATag("script");
 
+	for (const String& name : Factory::GetStructuralDataViewAttributeNames())
+		RegisterInnerXMLAttribute(name);
+
 	// Add the first frame.
 	ParseFrame frame;
-	frame.node_handler = nullptr;
-	frame.child_handler = nullptr;
 	frame.element = root;
-	frame.tag = "";
 	stack.push(frame);
 
 	active_handler = nullptr;
 
-	header = new DocumentHeader();
+	header = std::make_unique<DocumentHeader>();
 }
 
 XMLParser::~XMLParser()
-{
-	delete header;
-}
+{}
 
 // Registers a custom node handler to be used to a given tag.
 XMLNodeHandler* XMLParser::RegisterNodeHandler(const String& _tag, SharedPtr<XMLNodeHandler> handler)
@@ -90,12 +89,7 @@ void XMLParser::ReleaseHandlers()
 
 DocumentHeader* XMLParser::GetDocumentHeader()
 {
-	return header;
-}
-
-const URL& XMLParser::GetSourceURL() const
-{
-	return xml_source->GetSourceURL();
+	return header.get();
 }
 
 // Pushes the default element handler onto the parse stack.
@@ -120,6 +114,12 @@ const XMLParser::ParseFrame* XMLParser::GetParseFrame() const
 	return &stack.top();
 }
 
+const URL& XMLParser::GetSourceURL() const
+{
+	RMLUI_ASSERT(GetSourceURLPtr());
+	return *GetSourceURLPtr();
+}
+
 /// Called when the parser finds the beginning of an element tag.
 void XMLParser::HandleElementStart(const String& _name, const XMLAttributes& attributes)
 {
@@ -178,11 +178,11 @@ void XMLParser::HandleElementEnd(const String& _name)
 }
 
 /// Called when the parser encounters data.
-void XMLParser::HandleData(const String& data)
+void XMLParser::HandleData(const String& data, XMLDataType type)
 {
 	RMLUI_ZoneScoped;
 	if (stack.top().node_handler)
-		stack.top().node_handler->ElementData(this, data);
+		stack.top().node_handler->ElementData(this, data, type);
 }
 
 }

+ 18 - 95
Source/Debugger/ElementInfo.cpp

@@ -29,6 +29,7 @@
 #include "ElementInfo.h"
 #include "../../Include/RmlUi/Core/Core.h"
 #include "../../Include/RmlUi/Core/ElementUtilities.h"
+#include "../../Include/RmlUi/Core/ElementText.h"
 #include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/Property.h"
 #include "../../Include/RmlUi/Core/PropertiesIteratorView.h"
@@ -43,70 +44,6 @@
 namespace Rml {
 namespace Debugger {
 
-static Core::String PrettyFormatNumbers(const Core::String& in_string)
-{
-	// Removes trailing zeros and truncates decimal digits to the specified number of significant digits.
-	constexpr int num_significant_digits = 4;
-
-	Core::String string = in_string;
-
-	if (string.empty())
-		return string;
-
-	// First, check for a decimal point. No point, no chance of trailing zeroes!
-	size_t decimal_point_position = 0;
-
-	while ((decimal_point_position = string.find('.', decimal_point_position + 1)) != Core::String::npos)
-	{
-		// Find the left-most digit.
-		int pos_left = (int)decimal_point_position - 1; // non-inclusive
-		while (pos_left >= 0 && string[pos_left] >= '0' && string[pos_left] <= '9')
-			pos_left--;
-
-		// Significant digits left of the decimal point. We also consider all zero digits significant on the left side.
-		const int significant_left = (int)decimal_point_position - (pos_left + 1);
-
-		// Let's not touch numbers that don't start with a digit before the decimal.
-		if (significant_left == 0)
-			continue;
-
-		const int max_significant_right = std::max(num_significant_digits - significant_left, 0);
-
-		// Find the right-most digit and number of non-zero digits less than our maximum.
-		int pos_right = (int)decimal_point_position + 1; // non-inclusive
-		int significant_right = 0;
-		while (pos_right < (int)string.size() && string[pos_right] >= '0' && string[pos_right] <= '9')
-		{
-			const int current_digit_right = pos_right - (int)decimal_point_position;
-			if (string[pos_right] != '0' && current_digit_right <= max_significant_right)
-				significant_right = current_digit_right;
-			pos_right++;
-		}
-
-		size_t pos_cut_start = decimal_point_position + (size_t)(significant_right + 1);
-		size_t pos_cut_end = (size_t)pos_right;
-
-		// Remove the decimal point if we don't have any right digits.
-		if (pos_cut_start == decimal_point_position + 1)
-			pos_cut_start = decimal_point_position;
-
-		string.erase(string.begin() + pos_cut_start, string.begin() + pos_cut_end);
-	}
-
-	return string;
-}
-
-#ifdef RMLUI_DEBUG
-static bool TestPrettyFormat(Core::String original, Core::String should_be)
-{
-	Core::String formatted = PrettyFormatNumbers(original);
-	bool result = (formatted == should_be);
-	if (!result)
-		Core::Log::Message(Core::Log::LT_ERROR, "Remove trailing string failed. PrettyFormatNumbers('%s') == '%s' != '%s'", original.c_str(), formatted.c_str(), should_be.c_str());
-	return result;
-}
-#endif
-
 ElementInfo::ElementInfo(const Core::String& tag) : Core::ElementDocument(tag)
 {
 	hover_element = nullptr;
@@ -117,25 +54,6 @@ ElementInfo::ElementInfo(const Core::String& tag) : Core::ElementDocument(tag)
 	force_update_once = false;
 	title_dirty = true;
 	previous_update_time = 0.0;
-
-	RMLUI_ASSERT(TestPrettyFormat("0.15", "0.15"));
-	RMLUI_ASSERT(TestPrettyFormat("0.150", "0.15"));
-	RMLUI_ASSERT(TestPrettyFormat("1.15", "1.15"));
-	RMLUI_ASSERT(TestPrettyFormat("1.150", "1.15"));
-	RMLUI_ASSERT(TestPrettyFormat("123.15", "123.1"));
-	RMLUI_ASSERT(TestPrettyFormat("1234.5", "1234"));
-	RMLUI_ASSERT(TestPrettyFormat("12.15", "12.15"));
-	RMLUI_ASSERT(TestPrettyFormat("12.154", "12.15"));
-	RMLUI_ASSERT(TestPrettyFormat("12.154666", "12.15"));
-	RMLUI_ASSERT(TestPrettyFormat("15889", "15889"));
-	RMLUI_ASSERT(TestPrettyFormat("15889.1", "15889"));
-	RMLUI_ASSERT(TestPrettyFormat("0.00660", "0.006"));
-	RMLUI_ASSERT(TestPrettyFormat("0.000001", "0"));
-	RMLUI_ASSERT(TestPrettyFormat("0.00000100", "0"));
-	RMLUI_ASSERT(TestPrettyFormat("a .", "a ."));
-	RMLUI_ASSERT(TestPrettyFormat("a .0", "a .0"));
-	RMLUI_ASSERT(TestPrettyFormat("a 0.0", "a 0"));
-	RMLUI_ASSERT(TestPrettyFormat("hello.world: 14.5600 1.1 0.55623 more.values: 0.1544 0.", "hello.world: 14.56 1.1 0.556 more.values: 0.154 0"));
 }
 
 ElementInfo::~ElementInfo()
@@ -502,6 +420,13 @@ void ElementInfo::UpdateSourceElement()
 				if(name != "class" && name != "style" && name != "id") 
 					attributes += Core::CreateString(name.size() + value.size() + 32, "%s: <em>%s</em><br />", name.c_str(), value.c_str());
 			}
+
+			// Text is not an attribute but useful nonetheless
+			if (auto text_element = rmlui_dynamic_cast<Core::ElementText*>(source_element))
+			{
+				const Core::String& text_content = text_element->GetText();
+				attributes += Core::CreateString(text_content.size() + 32, "Text: <em>%s</em><br />", text_content.c_str());
+			}
 		}
 
 		if (attributes.empty())
@@ -566,18 +491,16 @@ void ElementInfo::UpdateSourceElement()
 		// left, top, width, height.
 		if (source_element != nullptr)
 		{
-			Core::Vector2f element_offset = source_element->GetRelativeOffset(Core::Box::BORDER);
-			Core::Vector2f element_size = source_element->GetBox().GetSize(Core::Box::BORDER);
-
-			Core::String positions = Core::CreateString(400, R"(
-				<span class='name'>left: </span><em>%fpx</em><br/>
-				<span class='name'>top: </span><em>%fpx</em><br/>
-				<span class='name'>width: </span><em>%fpx</em><br/>
-				<span class='name'>height: </span><em>%fpx</em><br/>)",
-				element_offset.x, element_offset.y, element_size.x, element_size.y
-			);
+			const Core::Vector2f element_offset = source_element->GetRelativeOffset(Core::Box::BORDER);
+			const Core::Vector2f element_size = source_element->GetBox().GetSize(Core::Box::BORDER);
+
+			const Core::String positions = 
+				"<span class='name'>left: </span><em>"   + Core::ToString(element_offset.x) + "px</em><br/>" +
+				"<span class='name'>top: </span><em>"    + Core::ToString(element_offset.y) + "px</em><br/>" +
+				"<span class='name'>width: </span><em>"  + Core::ToString(element_size.x)   + "px</em><br/>" +
+				"<span class='name'>height: </span><em>" + Core::ToString(element_size.y)   + "px</em><br/>";
 
-			position_content->SetInnerRML( PrettyFormatNumbers(positions) );
+			position_content->SetInnerRML( positions );
 		}
 		else
 		{
@@ -727,7 +650,7 @@ void ElementInfo::BuildElementPropertiesRML(Core::String& property_rml, Core::El
 
 void ElementInfo::BuildPropertyRML(Core::String& property_rml, const Core::String& name, const Core::Property* property)
 {
-	Core::String property_value = PrettyFormatNumbers(property->ToString());
+	const Core::String property_value = property->ToString();
 
 	property_rml += "<span class='name'>" + name + "</span>: " + property_value + "<br/>";
 }

+ 10 - 15
Source/Debugger/ElementLog.cpp

@@ -131,12 +131,10 @@ bool ElementLog::Initialise()
 // Adds a log message to the debug log.
 void ElementLog::AddLogMessage(Core::Log::Type type, const Core::String& message)
 {
-	using Core::StringUtilities::Replace;
-
 	// Add the message to the list of messages for the specified log type.
 	LogMessage log_message;
 	log_message.index = current_index++;
-	log_message.message = Replace(Replace(Core::String(message), "<", "&lt;"), ">", "&gt;");
+	log_message.message = Core::StringUtilities::EncodeRml(message);
 	log_types[type].log_messages.push_back(log_message);
 	if (log_types[type].log_messages.size() >= MAX_LOG_MESSAGES)
 	{
@@ -159,21 +157,18 @@ void ElementLog::AddLogMessage(Core::Log::Type type, const Core::String& message
 	// Trigger the beacon if we're hidden. Override any lower-level log type if it is already visible.
 	else
 	{
-		if (!IsVisible())
+		if (beacon != nullptr)
 		{
-			if (beacon != nullptr)
+			if (type < current_beacon_level)
 			{
-				if (type < current_beacon_level)
-				{
-					beacon->SetProperty(Core::PropertyId::Visibility, Core::Property(Core::Style::Visibility::Visible));
+				beacon->SetProperty(Core::PropertyId::Visibility, Core::Property(Core::Style::Visibility::Visible));
 
-					current_beacon_level = type;
-					Rml::Core::Element* beacon_button = beacon->GetFirstChild();
-					if (beacon_button)
-					{
-						beacon_button->SetClassNames(log_types[type].class_name);
-						beacon_button->SetInnerRML(log_types[type].alert_contents);
-					}
+				current_beacon_level = type;
+				Rml::Core::Element* beacon_button = beacon->GetFirstChild();
+				if (beacon_button)
+				{
+					beacon_button->SetClassNames(log_types[type].class_name);
+					beacon_button->SetInnerRML(log_types[type].alert_contents);
 				}
 			}
 		}

+ 1 - 0
Source/Debugger/LogSource.h

@@ -85,6 +85,7 @@ div.button.last
 div.log-entry p.message
 {
 	display: block;
+	white-space: pre-wrap;
 	margin-left: 20dp;
 }
 )RCSS";

+ 13 - 0
changelog.md

@@ -5,6 +5,19 @@
 * [RmlUi 2.0](#rmlui-20)
 
 
+## RmlUi 4.0 (WIP)
+
+### Model-view-controller (MVC) implementation
+
+RmlUi now supports a model-view-controller (MVC) approach through data bindings. This is a powerful approach for making documents respond to data changes, or in reverse, updating data based on user actions.
+
+For now, this is considered an experimental feature.
+
+- See the work-in-progress [documentation for this feature](https://gist.github.com/mikke89/030cca078e36749580c975692d03cbee).
+- Have a look at the 'databinding' sample for usage examples.
+- See discussion in [#83](https://github.com/mikke89/RmlUi/pull/83) and [#25](https://github.com/mikke89/RmlUi/issues/25).
+
+
 ## RmlUi 3.3
 
 ###  Rml `select` element improvements