Browse Source

- Add a visual test suite for navigating and comparing formatting of documents for correctness, in particular for testing the layout engine. Includes some examples from the CSS specifications.
- A python script for a "best-effort" conversion of the full CSS 2.1 test suite into RML, which you can load into the visual tests suite (see CMake option VISUAL_TESTS_DIRECTORIES).
- Split the tests into separate executables: UnitTests, VisualTests, and Benchmarks.

Michael Ragazzon 5 years ago
parent
commit
61a14fd25a
43 changed files with 2438 additions and 266 deletions
  1. 4 1
      CMakeLists.txt
  2. 4 0
      Include/RmlUi/Core/Element.h
  3. 4 0
      Include/RmlUi/Core/XMLParser.h
  4. 19 56
      Samples/basic/treeview/src/FileSystem.cpp
  5. 2 4
      Samples/basic/treeview/src/main.cpp
  6. 5 0
      Samples/shell/include/Shell.h
  7. 88 0
      Samples/shell/src/Shell.cpp
  8. 9 0
      Source/Core/XMLParser.cpp
  9. 69 15
      Tests/CMakeLists.txt
  10. 27 0
      Tests/Data/VisualTests/LICENSE.txt
  11. 7 16
      Tests/Data/VisualTests/acid1.rml
  12. 50 0
      Tests/Data/VisualTests/css1_clear.rml
  13. 28 0
      Tests/Data/VisualTests/position_01_normal_flow.rml
  14. 28 0
      Tests/Data/VisualTests/position_02_relative_positioning.rml
  15. 28 0
      Tests/Data/VisualTests/position_03_floating_a_box.rml
  16. 29 0
      Tests/Data/VisualTests/position_04_floating_a_box_sibling.rml
  17. 29 0
      Tests/Data/VisualTests/position_05_floating_a_box_clear.rml
  18. 33 0
      Tests/Data/VisualTests/position_06_absolute_positioning.rml
  19. 36 0
      Tests/Data/VisualTests/position_07_absolute_positioning_relative.rml
  20. 33 0
      Tests/Data/VisualTests/position_08_absolute_positioning_no_relative.rml
  21. 26 0
      Tests/Data/VisualTests/position_09_absolute_positioning_change_bars.rml
  22. 55 0
      Tests/Data/description.rml
  23. 95 0
      Tests/Data/style.rcss
  24. 31 0
      Tests/Data/view_source.rml
  25. 129 0
      Tests/Source/Benchmarks/DataExpression.cpp
  26. 293 0
      Tests/Source/Benchmarks/Element.cpp
  27. 55 0
      Tests/Source/Benchmarks/main.cpp
  28. 7 0
      Tests/Source/Common/TestsInterface.cpp
  29. 22 0
      Tests/Source/Common/TestsInterface.h
  30. 144 0
      Tests/Source/Common/TestsShell.cpp
  31. 57 0
      Tests/Source/Common/TestsShell.h
  32. 0 136
      Tests/Source/Element.cpp
  33. 9 34
      Tests/Source/UnitTests/DataExpression.cpp
  34. 0 0
      Tests/Source/UnitTests/DataModel.cpp
  35. 0 0
      Tests/Source/UnitTests/GeometryDatabase.cpp
  36. 1 1
      Tests/Source/UnitTests/Selectors.cpp
  37. 2 3
      Tests/Source/UnitTests/main.cpp
  38. 353 0
      Tests/Source/VisualTests/Window.cpp
  39. 88 0
      Tests/Source/VisualTests/Window.h
  40. 148 0
      Tests/Source/VisualTests/XmlNodeHandlers.cpp
  41. 51 0
      Tests/Source/VisualTests/XmlNodeHandlers.h
  42. 115 0
      Tests/Source/VisualTests/main.cpp
  43. 225 0
      Tests/Tools/convert_css_test_suite_to_rml.py

+ 4 - 1
CMakeLists.txt

@@ -75,7 +75,10 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
 	
 	if(BUILD_TESTING)
 		set(RMLUI_TESTS_ENABLED ON)
-		option(ENABLE_BENCHMARKS "Will enable benchmarks while running Tests." ON)
+		option(BUILD_UNIT_TESTS "Enable to build UnitTests." ON)
+		option(BUILD_VISUAL_TESTS "Enable to build VisualTests." ON)
+		set(VISUAL_TESTS_DIRECTORIES "" CACHE STRING "Specify additional directories containing *.rml documents for the visual tests. Backslashes must be escaped. Separate multiple directories by semicolon.")
+		option(BUILD_BENCHMARKS "Enable to build Benchmarks." ON)
 	endif()
 endif()
 

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

@@ -466,8 +466,12 @@ public:
 	/// @param[in] event Event to attach to.
 	/// @param[in] listener The listener object to be attached.
 	/// @param[in] in_capture_phase True to attach in the capture phase, false in bubble phase.
+	/// @lifetime The added listener must stay alive until after the dispatched call from EventListener::OnDetach(). This occurs
+	///     eg. when the element is destroyed or when RemoveEventListener() is called with the same parameters passed here.
 	void AddEventListener(const String& event, EventListener* listener, bool in_capture_phase = false);
 	/// Adds an event listener to this element by id.
+	/// @lifetime The added listener must stay alive until after the dispatched call from EventListener::OnDetach(). This occurs
+	///     eg. when the element is destroyed or when RemoveEventListener() is called with the same parameters passed here.
 	void AddEventListener(EventId id, EventListener* listener, bool in_capture_phase = false);
 	/// Removes an event listener from this element.
 	/// @param[in] event Event to detach from.

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

@@ -57,6 +57,10 @@ public:
 	/// @param[in] handler The custom handler.
 	/// @return The registered XML node handler.
 	static XMLNodeHandler* RegisterNodeHandler(const String& tag, SharedPtr<XMLNodeHandler> handler);
+	/// Retrieve a registered node handler.
+	/// @param[in] tag The tag the custom parser handles.
+	/// @return The registered XML node handler or nullptr if it does not exist for the given tag.
+	static XMLNodeHandler* GetNodeHandler(const String& tag);
 	/// Releases all registered node handlers. This is called internally.
 	static void ReleaseHandlers();
 

+ 19 - 56
Samples/basic/treeview/src/FileSystem.cpp

@@ -31,19 +31,14 @@
 #include <cstdlib>
 #include <cstdio>
 #include <string.h>
-
-#ifdef RMLUI_PLATFORM_WIN32
-#include <io.h>
-#else
-#include <dirent.h>
-#endif
+#include <Shell.h>
 
 struct FileSystemNode;
 
-typedef Rml::UnorderedMap< Rml::String, FileSystemNode* > NodeMap;
+using NodeMap = Rml::UnorderedMap< Rml::String, FileSystemNode* >;
 
-FileSystemNode* file_system_root = nullptr;
-NodeMap node_map;
+static Rml::UniquePtr<FileSystemNode> file_system_root;
+static NodeMap node_map;
 
 
 /**
@@ -64,60 +59,29 @@ struct FileSystemNode
 	}
 
 	~FileSystemNode()
-	{
-		for (size_t i = 0; i < child_nodes.size(); ++i)
-			delete child_nodes[i];
-	}
+	{}
 
 	// Build the list of files and directories within this directory.
 	void BuildTree(const Rml::String& root = "")
 	{
-#ifdef RMLUI_PLATFORM_WIN32
-		_finddata_t find_data;
-		intptr_t find_handle = _findfirst((root + name + "/*.*").c_str(), &find_data);
-		if (find_handle != -1)
-		{
-			do
-			{
-				if (strcmp(find_data.name, ".") == 0 ||
-					strcmp(find_data.name, "..") == 0)
-					continue;
+		const Rml::String current_directory = root + name + '/';
 
-				child_nodes.push_back(new FileSystemNode(find_data.name, (find_data.attrib & _A_SUBDIR) == _A_SUBDIR, depth + 1));
+		const Rml::StringList directories = Shell::ListDirectories(current_directory);
 
-			} while (_findnext(find_handle, &find_data) == 0);
-
-			_findclose(find_handle);
+		for (const Rml::String& directory : directories)
+		{
+			child_nodes.push_back(Rml::MakeUnique<FileSystemNode>(directory, true, depth + 1));
+			child_nodes.back()->BuildTree(current_directory);
 		}
-#else
-			struct dirent** file_list = nullptr;
-			int file_count = -1;
-			file_count = scandir((root + name).c_str(), &file_list, 0, alphasort);
-			if (file_count == -1)
-				return;
-
-			while (file_count--)
-			{
-				if (strcmp(file_list[file_count]->d_name, ".") == 0 ||
-					strcmp(file_list[file_count]->d_name, "..") == 0)
-					continue;
-
-				child_nodes.push_back(new FileSystemNode(file_list[file_count]->d_name, (file_list[file_count]->d_type & DT_DIR) == DT_DIR, depth + 1));
-
-				free(file_list[file_count]);
-			}
-			free(file_list);
-#endif
-
-		// Generate the trees of all of our subdirectories.
-		for (size_t i = 0; i < child_nodes.size(); ++i)
+
+		const Rml::StringList files = Shell::ListFiles(current_directory);
+		for (const Rml::String& file : files)
 		{
-			if (child_nodes[i]->directory)
-				child_nodes[i]->BuildTree(root + name + "/");
+			child_nodes.push_back(Rml::MakeUnique<FileSystemNode>(file, false, depth + 1));
 		}
 	}
 
-	typedef Rml::Vector< FileSystemNode* > NodeList;
+	using NodeList = Rml::Vector< Rml::UniquePtr<FileSystemNode> >;
 
 	Rml::String id;
 	Rml::String name;
@@ -131,14 +95,13 @@ struct FileSystemNode
 FileSystem::FileSystem(const Rml::String& root) : Rml::DataSource("file")
 {
 	// Generate the file system nodes starting at the RmlUi's root directory.
-	file_system_root = new FileSystemNode(".", true);
+	file_system_root = Rml::MakeUnique<FileSystemNode>(".", true);
 	file_system_root->BuildTree(root);
 }
 
 FileSystem::~FileSystem()
 {
-	delete file_system_root;
-	file_system_root = nullptr;
+	file_system_root.reset();
 }
 
 void FileSystem::GetRow(Rml::StringList& row, const Rml::String& table, int row_index, const Rml::StringList& columns)
@@ -183,7 +146,7 @@ FileSystemNode* FileSystem::GetNode(const Rml::String& table)
 {
 	// Determine which node the row is being requested from.
 	if (table == "root")
-		return file_system_root;
+		return file_system_root.get();
 	else
 	{
 		NodeMap::iterator i = node_map.find(table);

+ 2 - 4
Samples/basic/treeview/src/main.cpp

@@ -106,9 +106,8 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 
 	Shell::LoadFonts("assets/");
 
-	// Create the file data source and formatter.
-	Rml::String root = Shell::FindSamplesRoot();
-	FileSystem file_system(root + "basic/");
+	// Create the file data source and formatter. The samples directory '/' acts as our root.
+	FileSystem file_system("/");
 	FileFormatter file_formatter;
 
 	// Load and show the demo document.
@@ -121,7 +120,6 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 
 	Shell::EventLoop(GameLoop);
 
-	// Shutdown RmlUi.
 	Rml::Shutdown();
 
 	Shell::CloseWindow();

+ 5 - 0
Samples/shell/include/Shell.h

@@ -53,6 +53,11 @@ public:
 	/// Loads the default fonts from the given path.
 	static void LoadFonts(const char* directory);
 
+	/// List files in the given directory. An initial forward slash '/' makes it relative to the samples root.
+	static Rml::StringList ListFiles(const Rml::String& in_directory, const Rml::String& extension = Rml::String());
+	/// List subdirectories in the given directory. An initial forward slash '/' makes it relative to the samples root.
+	static Rml::StringList ListDirectories(const Rml::String& in_directory);
+
 	/// Open a platform specific window, optionally initialising an OpenGL context on it.
 	/// @param[in] title Title of the window.
 	/// @param[in] srie Provides the interface for attaching a renderer to the window and performing related bits of interface.

+ 88 - 0
Samples/shell/src/Shell.cpp

@@ -29,6 +29,12 @@
 #include "Shell.h"
 #include <RmlUi/Core/Core.h>
 
+#ifdef RMLUI_PLATFORM_WIN32
+#include <io.h>
+#else
+#include <dirent.h>
+#endif
+
 /// Loads the default fonts from the given path.
 void Shell::LoadFonts(const char* directory)
 {
@@ -47,3 +53,85 @@ void Shell::LoadFonts(const char* directory)
 	}
 }
 
+
+enum class ListType { Files, Directories };
+
+static Rml::StringList ListFilesOrDirectories(ListType type, const Rml::String& in_directory, const Rml::String& extension)
+{
+	if (in_directory.empty())
+		return Rml::StringList();
+
+	Rml::String directory;
+
+	if (in_directory[0] == '/')
+		directory = Shell::FindSamplesRoot() + in_directory.substr(1);
+	else
+		directory = in_directory;
+
+	const Rml::String find_path = directory + "/*." + (extension.empty() ? Rml::String("*") : extension);
+
+	Rml::StringList result;
+
+#ifdef RMLUI_PLATFORM_WIN32
+	_finddata_t find_data;
+	intptr_t find_handle = _findfirst(find_path.c_str(), &find_data);
+	if (find_handle != -1)
+	{
+		do
+		{
+			if (strcmp(find_data.name, ".") == 0 ||
+				strcmp(find_data.name, "..") == 0)
+				continue;
+
+			bool is_directory = ((find_data.attrib & _A_SUBDIR) == _A_SUBDIR);
+			bool is_file = (!is_directory && ((find_data.attrib & _A_NORMAL) == _A_NORMAL));
+
+			if (((type == ListType::Files) && is_file) ||
+				((type == ListType::Directories) && is_directory))
+			{
+				result.push_back(find_data.name);
+			}
+
+		} while (_findnext(find_handle, &find_data) == 0);
+
+		_findclose(find_handle);
+	}
+#else
+	struct dirent** file_list = nullptr;
+	int file_count = -1;
+	file_count = scandir(find_path.c_str(), &file_list, 0, alphasort);
+	if (file_count == -1)
+		return;
+
+	// TODO: Untested
+	while (file_count--)
+	{
+		if (strcmp(file_list[file_count]->d_name, ".") == 0 ||
+			strcmp(file_list[file_count]->d_name, "..") == 0)
+			continue;
+
+		bool is_directory = ((file_list[file_count]->d_type & DT_DIR) == DT_DIR);
+		bool is_file = ((file_list[file_count]->d_type & DT_REG) == DT_REG);
+
+		if ((type == ListType::Files && is_file) ||
+			(type == ListType::Directories && is_directory))
+		{
+			result.push_back(file_list[file_count]->d_name);
+		}
+	}
+	free(file_list);
+#endif
+
+	return result;
+}
+
+Rml::StringList Shell::ListDirectories(const Rml::String& in_directory)
+{
+	return ListFilesOrDirectories(ListType::Directories, in_directory, Rml::String());
+}
+
+Rml::StringList Shell::ListFiles(const Rml::String& in_directory, const Rml::String& extension)
+{
+	return ListFilesOrDirectories(ListType::Files, in_directory, extension);
+}
+

+ 9 - 0
Source/Core/XMLParser.cpp

@@ -79,6 +79,15 @@ XMLNodeHandler* XMLParser::RegisterNodeHandler(const String& _tag, SharedPtr<XML
 	return result;
 }
 
+XMLNodeHandler* XMLParser::GetNodeHandler(const String& tag)
+{
+	auto it = node_handlers.find(tag);
+	if (it != node_handlers.end())
+		return it->second.get();
+	
+	return nullptr;
+}
+
 // Releases all registered node handlers. This is called internally.
 void XMLParser::ReleaseHandlers()
 {

+ 69 - 15
Tests/CMakeLists.txt

@@ -1,3 +1,9 @@
+#===================================
+# RmlUi tests definitions ==========
+#===================================
+target_compile_definitions(RmlCore PUBLIC RMLUI_TESTS_ENABLED)
+
+
 #===================================
 # Include dependencies =============
 #===================================
@@ -6,33 +12,81 @@ set(DOCTEST_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Dependencies/doctest)
 add_library(doctest::doctest IMPORTED INTERFACE)
 set_property(TARGET doctest::doctest PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${DOCTEST_INCLUDE_DIR}")
 
+# Include doctest's discovery module
+include(${DOCTEST_INCLUDE_DIR}/cmake/doctest.cmake)
+
 set(NANOBENCH_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Dependencies/nanobench)
 
 add_library(nanobench::nanobench IMPORTED INTERFACE)
 set_property(TARGET nanobench::nanobench PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${NANOBENCH_INCLUDE_DIR}")
 
 #===================================
-# Create binary for tests ==========
+# Common source files ==============
 #===================================
-file(GLOB RmlTest_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/*.cpp)
-add_executable(Tests ${RmlTest_SRC_FILES})
-target_link_libraries(Tests RmlCore RmlDebugger doctest::doctest nanobench::nanobench ${sample_LIBRARIES})
 
-set_target_properties(Tests PROPERTIES CXX_STANDARD 14)
+file(GLOB TestsCommon_HDR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/Common/*.h )
+file(GLOB TestsCommon_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/Common/*.cpp )
 
-# Enable compiler warnings
-add_common_target_options(Tests)
+#===================================
+# Unit Tests =======================
+#===================================
+if(BUILD_UNIT_TESTS)
+	file(GLOB UnitTests_HDR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/UnitTests/*.h )
+	file(GLOB UnitTests_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/UnitTests/*.cpp )
 
-target_compile_definitions(RmlCore PUBLIC RMLUI_TESTS_ENABLED)
+	add_executable(UnitTests ${UnitTests_HDR_FILES} ${UnitTests_SRC_FILES})
+	target_link_libraries(UnitTests RmlCore doctest::doctest)
+	set_target_properties(UnitTests PROPERTIES CXX_STANDARD 14)
 
-if(TESTING_BENCHMARKS)
-	target_compile_definitions(Tests PRIVATE RMLUI_ENABLE_BENCHMARKS)
+	add_common_target_options(UnitTests)
+
+	if(MSVC)
+		target_compile_definitions(UnitTests PUBLIC DOCTEST_CONFIG_USE_STD_HEADERS)
+	endif()
+
+	doctest_discover_tests(UnitTests)
 endif()
 
-if(MSVC)
-	target_compile_definitions(Tests PUBLIC DOCTEST_CONFIG_USE_STD_HEADERS)
+
+#===================================
+# Visual Tests =====================
+#===================================
+if(BUILD_VISUAL_TESTS)
+	file(GLOB VisualTests_HDR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/VisualTests/*.h )
+	file(GLOB VisualTests_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/VisualTests/*.cpp )
+
+	add_executable(VisualTests ${VisualTests_HDR_FILES} ${VisualTests_SRC_FILES} ${TestsCommon_HDR_FILES} ${TestsCommon_SRC_FILES})
+	target_link_libraries(VisualTests RmlCore RmlDebugger doctest::doctest ${sample_LIBRARIES})
+	set_target_properties(VisualTests PROPERTIES CXX_STANDARD 14)
+
+	# Enable compiler warnings
+	add_common_target_options(VisualTests)
+	
+	if(VISUAL_TESTS_DIRECTORIES)
+		target_compile_definitions(VisualTests PRIVATE RMLUI_VISUAL_TESTS_DIRECTORIES="${VISUAL_TESTS_DIRECTORIES}")
+	endif()
+
+	if(MSVC)
+		target_compile_definitions(VisualTests PUBLIC DOCTEST_CONFIG_USE_STD_HEADERS)
+	endif()
 endif()
 
-# Add tests using doctest's discovery module
-include(${DOCTEST_INCLUDE_DIR}/cmake/doctest.cmake)
-doctest_discover_tests(Tests)
+
+#===================================
+# Benchmarks =======================
+#===================================
+if(BUILD_BENCHMARKS)
+	file(GLOB Benchmarks_HDR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/Benchmarks/*.h )
+	file(GLOB Benchmarks_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Source/Benchmarks/*.cpp )
+
+	add_executable(Benchmarks ${Benchmarks_HDR_FILES} ${Benchmarks_SRC_FILES} ${TestsCommon_HDR_FILES} ${TestsCommon_SRC_FILES})
+	target_link_libraries(Benchmarks RmlCore RmlDebugger doctest::doctest nanobench::nanobench ${sample_LIBRARIES})
+	set_target_properties(Benchmarks PROPERTIES CXX_STANDARD 14)
+
+	# Enable compiler warnings
+	add_common_target_options(Benchmarks)
+
+	if(MSVC)
+		target_compile_definitions(Benchmarks PUBLIC DOCTEST_CONFIG_USE_STD_HEADERS)
+	endif()
+endif()

+ 27 - 0
Tests/Data/VisualTests/LICENSE.txt

@@ -0,0 +1,27 @@
+This software or document includes material copied and modified from the CSS specifications [1], in particular from examples there-in. In addition, parts of the CSS test suites [2] have been modified and included with this software. The license [3] of this material is restated below. Copyright © 2020 W3C® (MIT, ERCIM, Keio, Beihang).
+[1] https://drafts.csswg.org/
+[2] https://www.w3.org/Style/CSS/Test/
+[3] https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
+
+--------------------------------------------
+
+W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE
+Status: This license takes effect 13 May, 2015.
+
+This work is being provided by the copyright holders under the following license.
+
+License
+By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions.
+
+Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the work or portions thereof, including modifications:
+
+- The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
+- Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included.
+- Notice of any changes or modifications, through a copyright statement on the new code or document such as "This software or document includes material copied from or derived from [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)."
+
+Disclaimers
+THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
+
+COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT.
+
+The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders.

+ 7 - 16
Tests/Data/acid1.rml → Tests/Data/VisualTests/acid1.rml

@@ -1,31 +1,22 @@
 <rml>
 <head>
 <title>display/box/float/clear test</title>
+<link type="text/rcss" href="../style.rcss"/>
+<link rel="source" href="https://www.w3.org/Style/CSS/Test/CSS1/current/test5526c.htm" />
+<link rel="help" href="http://www.w3.org/TR/REC-CSS1#clear" />
+<link rel="reference" href="https://www.w3.org/Style/CSS/Test/CSS1/current/sec5526c.gif" />
+<meta name="description" content="This is the ACID1 test. Some minor modifications have been applied such as matching fonts and borders to the RmlUi syntax." />
 <style type="text/css">
-/* Add default values for elements (RmlUi does not provide a default style sheet) */
-dl,dt,dd,ul,li,blockquote,address,h1,p {
-display: block;
-}
-input.radio {
-width: 10px;
-height: 10px;
-border: 1px #666;
-background: #fff;
-}
-
 /* RmlUi does not accept styling of the html/rml element, they have been moved into body. */
 body {
-font-family: rmlui-debugger-font;
-font-weight: normal;
-font-style: normal;
 font-size: 10px;
-background-color: blue;
 color: white;
 margin: 1.5em;
 border: .5em black;
 padding: 0;
 width: 48em;
 background-color: white;
+min-height: -1px;
 }
 
 dl {
@@ -177,7 +168,7 @@ font-size: 1em;
 	<p style="color: black; font-size: 1em; line-height: 1.3em; clear: both">
 	 This is a nonsensical document, but syntactically valid HTML 4.0. All 100%-conformant CSS1 agents should be able to render the document elements above this paragraph indistinguishably (to the pixel) from this 
 		<a href="sec5526c.gif">reference rendering,</a>
-	 (except font rasterization and form widgets). All discrepancies should be traceable to CSS1 implementation shortcomings. Once you have finished evaluating this test, you can return to the <A HREF="sec5526c.htm">parent page</A>. 
+	 (except font rasterization and form widgets). All discrepancies should be traceable to CSS1 implementation shortcomings. Once you have finished evaluating this test, you can return to the <a href="sec5526c.htm">parent page</a>. 
 	</p>
 </body>
 </rml>

+ 50 - 0
Tests/Data/VisualTests/css1_clear.rml

@@ -0,0 +1,50 @@
+<rml>
+<head>
+<title>CSS1 Test Suite: 5.5.26 clear</title>
+<link type="text/rcss" href="../style.rcss"/>
+<link rel="source" href="https://www.w3.org/Style/CSS/Test/CSS1/current/sec5526.htm"/>
+<link rel="help" href="http://www.w3.org/TR/REC-CSS1#clear"/>
+<meta name="description" content="This is the ACID1 test. Some minor modifications have been applied such as matching fonts and borders to the RmlUi syntax." />
+<style type="text/css">
+@spritesheet theme 
+{
+	src: /assets/invader.tga;
+	vblank.gif: 68px 158px  9px 30px;
+}
+
+.one {clear: left;}
+.two {clear: right;}
+.three {clear: both;}
+.four {clear: none;}
+</style>
+</head>
+
+<body>
+<img sprite="vblank.gif" height="50" style="float: left" alt="[Image]"/>
+<p>
+This text should be flowing past a tall orange rectangle on the left side of the browser window.
+</p>
+<hr/>
+<img sprite="vblank.gif" height="50" style="float: left" alt="[Image]"/>
+<p class="one">
+This paragraph should appear below the tall orange rectangle above and to the left, and not flow past it. 
+</p>
+<hr/>
+<img sprite="vblank.gif" height="50" style="float: right" alt="[Image]"/>
+<p class="two">
+This paragraph should appear below the tall orange rectangle above and to the right, and not flow past it. 
+</p>
+<hr/>
+<img sprite="vblank.gif" height="50" style="float: left" alt="[Image]"/>
+<img sprite="vblank.gif" height="50" style="float: right" alt="[Image]"/>
+<p class="three">
+This paragraph should appear below the two tall orange rectangles, and not flow between them. 
+</p>
+<img sprite="vblank.gif" height="50" style="float: left" alt="[Image]"/>
+<img sprite="vblank.gif" height="50" style="float: right" alt="[Image]"/>
+<p class="four">
+This paragraph should be between both tall orange rectangles.
+</p>
+<hr/>
+</body>
+</rml>

+ 28 - 0
Tests/Data/VisualTests/position_01_normal_flow.rml

@@ -0,0 +1,28 @@
+<rml>
+<head>
+	<title>CSS Position: Normal flow</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content="Position demo" />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#outer { color: red }
+		#inner { color: blue }
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 28 - 0
Tests/Data/VisualTests/position_02_relative_positioning.rml

@@ -0,0 +1,28 @@
+<rml>
+<head>
+	<title>CSS Position: Relative positioning</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='The result is identical to normal flow, except that the "outer" text is shifted 12px upward, without affecting the flow of the "body" or "inner" text.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#outer { position: relative; top: -12px; color: red }
+		#inner { position: relative; top: 12px; color: blue }
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 28 - 0
Tests/Data/VisualTests/position_03_floating_a_box.rml

@@ -0,0 +1,28 @@
+<rml>
+<head>
+	<title>CSS Position: Floating a box</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='The "inner" text lays out in an independent box on the right, causing the remaining "body" and "outer" text to flow around it.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#outer { color: red }
+		#inner { float: right; width: 130px; color: blue }
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 29 - 0
Tests/Data/VisualTests/position_04_floating_a_box_sibling.rml

@@ -0,0 +1,29 @@
+<rml>
+<head>
+	<title>CSS Position: Floating a box - Sibling</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='Identical to the previous example, save that there is now "sibling" text flowing with the "body" and "outer" text.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#inner { float: right; width: 130px; color: blue }
+		#sibling { color: red }
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		<span id="sibling"> Sibling contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 29 - 0
Tests/Data/VisualTests/position_05_floating_a_box_clear.rml

@@ -0,0 +1,29 @@
+<rml>
+<head>
+	<title>CSS Position: Floating a box - Clear</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='Now the "sibling" text moves down to below the "inner" text’s box, leaving blank space behind. The text following the "sibling" text flows after it as normal.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#inner { float: right; width: 130px; color: blue }
+		#sibling { clear: right; color: red }
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		<span id="sibling"> Sibling contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 33 - 0
Tests/Data/VisualTests/position_06_absolute_positioning.rml

@@ -0,0 +1,33 @@
+<rml>
+<head>
+	<title>CSS Position: Absolute positioning</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='All of the text within #outer (the "outer" and "inner" text) moves down to an independent box in the lower right corner. The two halves of "body" text flow together.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#outer {
+			position: absolute;
+			top: 200px; left: 200px;
+			width: 200px;
+			color: red;
+		}
+		#inner { color: blue }
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 36 - 0
Tests/Data/VisualTests/position_07_absolute_positioning_relative.rml

@@ -0,0 +1,36 @@
+<rml>
+<head>
+	<title>CSS Position: Absolute positioning - Relative</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='The "inner" text is positioned in an independent box, relative to the top-left corner of the start of the "outer" text.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#outer {
+			position: relative;
+			color: red
+		}
+		#inner {
+			position: absolute;
+			top: 200px; left: -100px;
+			height: 130px; width: 130px;
+			color: blue;
+		}
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 33 - 0
Tests/Data/VisualTests/position_08_absolute_positioning_no_relative.rml

@@ -0,0 +1,33 @@
+<rml>
+<head>
+	<title>CSS Position: Absolute positioning - No relative</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='Same as before, except now the "inner text" is positioned relative to the top-left corner of the page itself.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		#outer { color: red }
+		#inner {
+			position: absolute;
+			top: 200px; left: -100px;
+			height: 130px; width: 130px;
+			color: blue;
+		}
+	</style>
+</head>
+<body>
+	<p>
+		Beginning of p contents.
+		<span id="outer"> Start of outer contents.
+		<span id="inner"> Inner contents.</span>
+		End of outer contents.</span>
+		End of p contents.
+	</p>
+</body>
+</rml>

+ 26 - 0
Tests/Data/VisualTests/position_09_absolute_positioning_change_bars.rml

@@ -0,0 +1,26 @@
+<rml>
+<head>
+	<title>CSS Position: Absolute positioning - Change bars</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-position-3/#comparison" />
+	<meta name="description" content='The two red hyphens, indicating a change, sit in the left margin of the page on the line containing the word "THIS", regardless of what line that ends up being.' />
+	<style>
+		body {
+			font-size: 20px;
+			display: block;
+			background: #ddd;
+			color: #444;
+			line-height: 200%;
+		}
+		body { padding: 30px; }
+	</style>
+</head>
+<body>
+	<p style="position: relative; margin-right: 10px; left: 10px;">
+	  I used two red hyphens to serve as a change bar. They
+	  will "float" to the left of the line containing THIS
+	  <span style="position: absolute; top: auto; left: -1em; color: red;">--</span>
+	  word.
+	</p>
+</body>
+</rml>

+ 55 - 0
Tests/Data/description.rml

@@ -0,0 +1,55 @@
+<rml>
+<head>
+<title>RCSS Test description</title>
+<link type="text/rcss" href="style.rcss"/>
+<style>
+	body {
+		font-family: Delicious;
+		font-weight: normal;
+		font-style: normal;
+		font-size: 17px;
+		color: #444;
+		position: absolute;
+		top: 0; bottom: 0;
+		right: 0;
+		width: 400px;
+		background: #333;
+		color: #ccc;
+		padding: 20px 20px;
+		z-index: 100;
+	}
+	#content, #content > * { padding-top: 0.8em; }
+	code {
+		display: block;
+		white-space: pre-wrap;
+		font-size: 0.9em;
+		color: #aaa;
+	}
+	h1   { color: white; font-size: 1.3em; }
+	h3   { color: white; font-size: 1.15em; }
+	
+	p.links a { margin: 0 0.7em; }
+	#test_suite {
+		position: absolute;
+		text-align: center;
+		top: 15px;
+		left: 20px;
+		right: 20px;
+		color: #ddb;
+	}
+	#goto {
+		position: absolute;
+		left: 20px;
+		bottom: 20px;
+		width: 200px;
+		color: #ddb;
+	}
+	
+</style>
+</head>
+<body>
+<div id="test_suite"/>
+<div id="content"/>
+<div id="goto"/>
+</body>
+</rml>

+ 95 - 0
Tests/Data/style.rcss

@@ -0,0 +1,95 @@
+dl,dt,dd,ul,li,blockquote,address,h1,h2,h3,h4,h5,h6,p,pre,div {
+	display: block;
+}
+
+p, pre {
+	padding: 0.5em 0.5em;
+}
+pre { white-space: pre; }
+hr {
+	display: block;
+	clear: both;
+	padding: 1px;
+	background-color: #999;
+	margin: 5px 0;
+}
+
+input.radio {
+	width: 10px;
+	height: 10px;
+	border: 1px #666;
+	background: #fff;
+}
+
+body {
+	font-family: Delicious;
+	font-weight: normal;
+	font-style: normal;
+	font-size: 14px;
+	width: 500px;
+	min-height: 500px;
+	color: black;
+	background: #ccc;
+	overflow: auto;
+}
+strong {
+	font-weight: bold;
+}
+em {
+	font-style: italic;
+}
+* {
+	border-color: black;
+}
+a {
+	color: #9ab7ef;
+}
+a:hover {
+	color: #5285e6;
+}
+scrollbarvertical
+{
+	width: 16dp;
+	scrollbar-margin: 16px;
+}
+scrollbarhorizontal
+{
+	height: 16dp;
+	scrollbar-margin: 16px;
+}
+scrollbarvertical slidertrack,
+scrollbarhorizontal slidertrack
+{
+	background: #aaa;
+	border-color: #888;
+}
+scrollbarvertical slidertrack
+{
+	border-left-width: 1px;
+}
+scrollbarhorizontal slidertrack
+{
+	height: 15dp;
+	border-top-width: 1px;
+}
+scrollbarvertical sliderbar,
+scrollbarhorizontal sliderbar
+{
+	background: #ddd;
+	border-color: #888;
+}
+scrollbarvertical sliderbar
+{
+	border-width: 1px 0px;
+	margin-left: 1dp;
+}
+scrollbarhorizontal sliderbar
+{
+	height: 15dp;
+	border-width: 0px 1px;
+	margin-top: 1dp;
+}
+scrollbarcorner
+{
+	background: #888;
+}

+ 31 - 0
Tests/Data/view_source.rml

@@ -0,0 +1,31 @@
+<rml>
+<head>
+<title>RCSS Test description</title>
+<link type="text/rcss" href="style.rcss"/>
+<style>
+	body {
+		font-family: rmlui-debugger-font;
+		font-weight: normal;
+		font-style: normal;
+		font-size: 16px;
+		width: auto;
+		color: #444;
+		position: absolute;
+		top: 0px; right: 0px;
+		bottom: 0px; left: 0px;
+		background-color: #222222f9;
+		color: #ddd;
+		z-index: 200;
+	}
+	#code {
+		display: block;
+		white-space: pre-wrap;
+		font-size: 0.9em;
+		padding: 20px 30px;
+	}
+</style>
+</head>
+<body>
+<div id="code"/>
+</body>
+</rml>

+ 129 - 0
Tests/Source/Benchmarks/DataExpression.cpp

@@ -0,0 +1,129 @@
+/*
+ * 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 "../../../Source/Core/DataExpression.cpp"
+
+#include <RmlUi/Core/DataModel.h>
+#include <doctest.h>
+#include <nanobench.h>
+
+using namespace Rml;
+using namespace ankerl;
+
+static DataTypeRegister type_register;
+static DataModel model(type_register.GetTransformFuncRegister());
+static DataExpressionInterface interface(&model, nullptr);
+
+
+TEST_CASE("Data expressions")
+{
+	float radius = 6.0f;
+	String color_name = "color";
+	Colourb color_value = Colourb(180, 100, 255);
+
+
+	DataModelConstructor constructor(&model, &type_register);
+	constructor.Bind("radius", &radius);
+	constructor.Bind("color_name", &color_name);
+	constructor.BindFunc("color_value", [&](Variant& variant) {
+		variant = ToString(color_value);
+	});
+
+
+	nanobench::Bench bench;
+	bench.title("Data expression");
+	bench.relative(true);
+
+	auto bench_expression = [&](const String& expression, const char* parse_name, const char* execute_name) {
+		DataParser parser(expression, interface);
+
+		bool result = true;
+		bench.run(parse_name, [&] {
+			result &= parser.Parse(false);
+			});
+
+		REQUIRE(result);
+
+		Program program = parser.ReleaseProgram();
+		AddressList addresses = parser.ReleaseAddresses();
+		DataInterpreter interpreter(program, addresses, interface);
+
+		bench.run(execute_name, [&] {
+			result &= interpreter.Run();
+		});
+
+		REQUIRE(result);
+	};
+
+
+	bench_expression(
+		"2 * 2",
+		"Simple (parse)",
+		"Simple (execute)"
+	);
+
+	bench_expression(
+		"true || false ? true && radius==1+2 ? 'Absolutely!' : color_value : 'no'",
+		"Complex (parse)",
+		"Complex (execute)"
+	);
+
+	auto bench_assignment = [&](const String& expression, const char* parse_name, const char* execute_name) {
+		DataParser parser(expression, interface); 
+		
+		bool result = true;
+		bench.run(parse_name, [&] {
+			result &= parser.Parse(true);
+			});
+
+		REQUIRE(result);
+
+		Program program = parser.ReleaseProgram();
+		AddressList addresses = parser.ReleaseAddresses();
+		DataInterpreter interpreter(program, addresses, interface);
+
+		bench.run(execute_name, [&] {
+			result &= interpreter.Run();
+		});
+
+		REQUIRE(result);
+	};
+
+	bench_assignment(
+		"radius = 15",
+		"Simple assign (parse)",
+		"Simple assign (execute)"
+	);
+
+	bench_assignment(
+		"radius = radius*radius*3.14; color_name = 'image-color'",
+		"Complex assign (parse)",
+		"Complex assign (execute)"
+	);
+}

+ 293 - 0
Tests/Source/Benchmarks/Element.cpp

@@ -0,0 +1,293 @@
+/*
+ * 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 "../Common/TestsShell.h"
+#include "../Common/TestsInterface.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Element.h>
+#include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/Types.h>
+
+#include <doctest.h>
+#include <nanobench.h>
+
+using namespace ankerl;
+using namespace Rml;
+
+static String GenerateRml(const int num_rows)
+{
+	static nanobench::Rng rng;
+
+	Rml::String rml;
+	rml.reserve(1000 * num_rows);
+
+	for (int i = 0; i < num_rows; i++)
+	{
+		int index = rng() % 1000;
+		int route = rng() % 50;
+		int max = (rng() % 40) + 10;
+		int value = rng() % max;
+		Rml::String rml_row = Rml::CreateString(1000, R"(
+			<div class="row">
+				<div class="col col1"><button class="expand" index="%d">+</button>&nbsp;<a>Route %d</a></div>
+				<div class="col col23"><input type="range" class="assign_range" min="0" max="%d" value="%d"/></div>
+				<div class="col col4">Assigned</div>
+				<select>
+					<option>Red</option><option>Blue</option><option selected>Green</option><option style="background-color: yellow;">Yellow</option>
+				</select>
+				<div class="inrow unmark_collapse">
+					<div class="col col123 assign_text">Assign to route</div>
+					<div class="col col4">
+						<input type="submit" class="vehicle_depot_assign_confirm" quantity="0">Confirm</input>
+					</div>
+				</div>
+			</div>)",
+			index,
+			route,
+			max,
+			value
+		);
+		rml += rml_row;
+	}
+
+	return rml;
+}
+
+
+TEST_CASE("Elements (shell)")
+{
+	Context* context = TestsShell::GetMainContext();
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocument("basic/benchmark/data/benchmark.rml");
+	REQUIRE(document);
+	document->Show();
+
+	Element* el = document->GetElementById("performance");
+	REQUIRE(el);
+
+	nanobench::Bench bench;
+	bench.title("Elements (shell)");
+	bench.relative(true);
+
+	constexpr int num_rows = 50;
+	const String rml = GenerateRml(num_rows);
+
+	el->SetInnerRML(rml);
+	context->Update();
+	context->Render();
+
+	bench.run("Update (unmodified)", [&] {
+		context->Update();
+	});
+
+	bench.run("Render", [&] {
+		TestsShell::PrepareRenderBuffer();
+		context->Render();
+		TestsShell::PresentRenderBuffer();
+	});
+
+	bench.run("SetInnerRML", [&] {
+		el->SetInnerRML(rml);
+	});
+
+	bench.run("SetInnerRML + Update", [&] {
+		el->SetInnerRML(rml);
+		context->Update();
+	});
+
+	bench.run("SetInnerRML + Update + Render", [&] {
+		el->SetInnerRML(rml);
+		context->Update();
+		TestsShell::PrepareRenderBuffer();
+		context->Render();
+		TestsShell::PresentRenderBuffer();
+	});
+
+	document->Close();
+}
+
+
+TEST_CASE("Elements (dummy interface)")
+{
+	TestsRenderInterface render_interface;
+	Context* context = TestsShell::CreateContext("element_dummy", &render_interface);
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocument("basic/benchmark/data/benchmark.rml");
+	REQUIRE(document);
+	document->Show();
+
+	Element* el = document->GetElementById("performance");
+	REQUIRE(el);
+
+	nanobench::Bench bench;
+	bench.title("Elements (dummy interface)");
+	bench.relative(true);
+
+	constexpr int num_rows = 50;
+	const String rml = GenerateRml(num_rows);
+
+	el->SetInnerRML(rml);
+	context->Update();
+	context->Render();
+
+	bench.run("Update (unmodified)", [&] {
+		context->Update();
+	});
+
+	bench.run("Render", [&] {
+		context->Render();
+	});
+
+	bench.run("SetInnerRML", [&] {
+		el->SetInnerRML(rml);
+	});
+
+	bench.run("SetInnerRML + Update", [&] {
+		el->SetInnerRML(rml);
+		context->Update();
+	});
+
+	bench.run("SetInnerRML + Update + Render", [&] {
+		el->SetInnerRML(rml);
+		context->Update();
+		context->Render();
+	});
+
+	render_interface.ResetCounters();
+	context->Render();
+	auto& counters = render_interface.GetCounters();
+
+	const String msg = CreateString(256,
+		"Stats for single Context::Render() with n=%d rows: \n"
+		"Render calls: %d\n"
+		"Scissor enable: %d\n"
+		"Scissor set: %d\n"
+		"Texture load: %d\n"
+		"Texture generate: %d\n"
+		"Texture release: %d\n"
+		"Transform set: %d\n",
+		num_rows,
+		counters.render_calls,
+		counters.enable_scissor,
+		counters.set_scissor,
+		counters.load_texture,
+		counters.generate_texture,
+		counters.release_texture,
+		counters.set_transform
+	);
+	MESSAGE(msg);
+
+	document->Close();
+	TestsShell::RemoveContext(context);
+}
+
+
+TEST_CASE("Elements asymptotic complexity (dummy interface)")
+{
+	TestsRenderInterface render_interface;
+	Context* context = TestsShell::CreateContext("element_complexity", &render_interface);
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocument("basic/benchmark/data/benchmark.rml");
+	REQUIRE(document);
+	document->Show();
+
+	Element* el = document->GetElementById("performance");
+	REQUIRE(el);
+
+
+	struct BenchDef {
+		const char* title;
+		Function<void(const String& rml)> run;
+	};
+
+	Vector<BenchDef> bench_list = {
+		{
+			"SetInnerRML",
+			[&](const String& rml) {
+				el->SetInnerRML(rml);
+			}
+		},
+		{
+			"Update (unmodified)",
+			[&](const String& /*rml*/) {
+				context->Update();
+			}
+		},
+		{
+			"Render",
+			[&](const String& /*rml*/) {
+				context->Render();
+			}
+		},
+		{
+			"SetInnerRML + Update",
+			[&](const String& rml) {
+				el->SetInnerRML(rml);
+				context->Update();
+			}
+		},
+		{
+			"SetInnerRML + Update + Render",
+			[&](const String& rml) {
+				el->SetInnerRML(rml);
+				context->Update();
+				context->Render();
+			}
+		},
+	};
+
+	for (auto& bench_def : bench_list)
+	{
+		nanobench::Bench bench;
+		bench.title(bench_def.title);
+		bench.relative(true);
+
+		// Running the benchmark multiple times, with different number of rows.
+		for (const int num_rows : { 1, 2, 5, 10, 20, 50, 100, 200, 500 })
+		{
+			const String rml = GenerateRml(num_rows);
+
+			el->SetInnerRML(rml);
+			context->Update();
+			context->Render();
+
+			bench.complexityN(num_rows).run(bench_def.title, [&]() {
+				bench_def.run(rml);
+			});
+		}
+
+#ifdef RMLUI_BENCHMARKS_SHOW_COMPLEXITY
+		MESSAGE(bench.complexityBigO());
+#endif
+	}
+
+	TestsShell::RemoveContext(context);
+}

+ 55 - 0
Tests/Source/Benchmarks/main.cpp

@@ -0,0 +1,55 @@
+/*
+ * 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 "../Common/TestsShell.h"
+
+#define ANKERL_NANOBENCH_IMPLEMENT
+#include <nanobench.h>
+
+#define DOCTEST_CONFIG_IMPLEMENT
+#include <doctest.h>
+
+
+int main(int argc, char** argv) {
+
+    // Initialize and run doctest
+    doctest::Context doctest_context;
+
+    doctest_context.applyCommandLine(argc, argv);
+
+    int doctest_result = doctest_context.run();
+
+    if (doctest_context.shouldExit())
+        return doctest_result;
+
+    // RmlUi is initialized during doctest run above as necessary.
+    // Clean everything up here.
+    TestsShell::ShutdownShell();
+
+    return doctest_result;
+}

+ 7 - 0
Tests/Source/TestsInterface.cpp → Tests/Source/Common/TestsInterface.cpp

@@ -50,18 +50,22 @@ bool TestsSystemInterface::LogMessage(Rml::Log::Type type, const Rml::String& me
 
 void TestsRenderInterface::RenderGeometry(Rml::Vertex* /*vertices*/, int /*num_vertices*/, int* /*indices*/, int /*num_indices*/, const Rml::TextureHandle /*texture*/, const Rml::Vector2f& /*translation*/)
 {
+	counters.render_calls += 1;
 }
 
 void TestsRenderInterface::EnableScissorRegion(bool /*enable*/)
 {
+	counters.enable_scissor += 1;
 }
 
 void TestsRenderInterface::SetScissorRegion(int /*x*/, int /*y*/, int /*width*/, int /*height*/)
 {
+	counters.set_scissor += 1;
 }
 
 bool TestsRenderInterface::LoadTexture(Rml::TextureHandle& texture_handle, Rml::Vector2i& texture_dimensions, const Rml::String& /*source*/)
 {
+	counters.load_texture += 1;
 	texture_handle = 1;
 	texture_dimensions.x = 512;
 	texture_dimensions.y = 256;
@@ -70,14 +74,17 @@ bool TestsRenderInterface::LoadTexture(Rml::TextureHandle& texture_handle, Rml::
 
 bool TestsRenderInterface::GenerateTexture(Rml::TextureHandle& texture_handle, const Rml::byte* /*source*/, const Rml::Vector2i& /*source_dimensions*/)
 {
+	counters.generate_texture += 1;
 	texture_handle = 1;
 	return true;
 }
 
 void TestsRenderInterface::ReleaseTexture(Rml::TextureHandle /*texture_handle*/)
 {
+	counters.release_texture += 1;
 }
 
 void TestsRenderInterface::SetTransform(const Rml::Matrix4f* /*transform*/)
 {
+	counters.set_transform += 1;
 }

+ 22 - 0
Tests/Source/TestsInterface.h → Tests/Source/Common/TestsInterface.h

@@ -40,9 +40,20 @@ public:
 };
 
 
+
 class TestsRenderInterface : public Rml::RenderInterface
 {
 public:
+	struct Counters {
+		size_t render_calls;
+		size_t enable_scissor;
+		size_t set_scissor;
+		size_t load_texture;
+		size_t generate_texture;
+		size_t release_texture;
+		size_t set_transform;
+	};
+
 	void RenderGeometry(Rml::Vertex* vertices, int num_vertices, int* indices, int num_indices, Rml::TextureHandle texture, const Rml::Vector2f& translation) override;
 
 	void EnableScissorRegion(bool enable) override;
@@ -53,5 +64,16 @@ public:
 	void ReleaseTexture(Rml::TextureHandle texture_handle) override;
 
 	void SetTransform(const Rml::Matrix4f* transform) override;
+
+	const Counters& GetCounters() const {
+		return counters;
+	}
+
+	void ResetCounters() {
+		counters = {};
+	}
+
+private:
+	Counters counters = {};
 };
 #endif

+ 144 - 0
Tests/Source/Common/TestsShell.cpp

@@ -0,0 +1,144 @@
+/*
+ * 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 "TestsShell.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Core.h>
+#include <RmlUi/Debugger.h>
+#include <Shell.h>
+#include <Input.h>
+#include <ShellRenderInterfaceOpenGL.h>
+
+#include <doctest.h>
+
+namespace {
+	bool shell_initialized = false;
+
+	ShellRenderInterfaceOpenGL shell_render_interface;
+	ShellSystemInterface shell_system_interface;
+
+	Rml::Context* shell_context = nullptr;
+
+	const Rml::Vector2i window_size(1500, 800);
+}
+
+static void InitializeShell()
+{
+	if (!shell_initialized)
+	{
+		Rml::SetSystemInterface(&shell_system_interface);
+		Rml::SetRenderInterface(&shell_render_interface);
+
+		shell_initialized = true;
+
+		// Generic OS initialisation, creates a window and attaches OpenGL.
+		REQUIRE(Shell::Initialise());
+		REQUIRE(Shell::OpenWindow("Element benchmark", &shell_render_interface, window_size.x, window_size.y, true));
+
+		// RmlUi initialisation.
+		shell_render_interface.SetViewport(window_size.x, window_size.y);
+
+		REQUIRE(Rml::Initialise());
+
+		REQUIRE(!shell_context);
+		shell_context = Rml::CreateContext("main", window_size);
+		REQUIRE(shell_context);
+
+		REQUIRE(Rml::Debugger::Initialise(shell_context));
+		::Input::SetContext(shell_context);
+		shell_render_interface.SetContext(shell_context);
+
+		Shell::LoadFonts("assets/");
+	}
+}
+
+
+Rml::Context* TestsShell::GetMainContext()
+{
+	InitializeShell();
+	return shell_context;
+}
+
+
+Rml::Context* TestsShell::CreateContext(const Rml::String& name, Rml::RenderInterface* render_interface)
+{
+	InitializeShell();
+
+	if (!render_interface)
+		render_interface = &shell_render_interface;
+
+	Rml::Context* context = Rml::CreateContext(name, window_size, render_interface);
+	REQUIRE(context);
+
+	return context;
+}
+
+
+void TestsShell::RemoveContext(Rml::Context* context)
+{
+	REQUIRE(context);
+	REQUIRE(context != shell_context);
+
+	const Rml::String& name = context->GetName();
+	REQUIRE(name != "main");
+
+	REQUIRE(Rml::RemoveContext(name));
+}
+
+void TestsShell::EventLoop(ShellIdleFunction idle_func)
+{
+	Shell::EventLoop(idle_func);
+}
+
+void TestsShell::PrepareRenderBuffer()
+{
+	shell_render_interface.PrepareRenderBuffer();
+}
+
+void TestsShell::PresentRenderBuffer()
+{
+	shell_render_interface.PresentRenderBuffer();
+}
+
+void TestsShell::RequestExit()
+{
+	Shell::RequestExit();
+}
+
+void TestsShell::ShutdownShell()
+{
+	if (shell_initialized)
+	{
+		Rml::Shutdown();
+		Shell::CloseWindow();
+		Shell::Shutdown();
+
+		shell_context = nullptr;
+		shell_initialized = false;
+	}
+}

+ 57 - 0
Tests/Source/Common/TestsShell.h

@@ -0,0 +1,57 @@
+/*
+ * 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 RMLUI_TESTS_COMMON_TESTSSHELL_H
+#define RMLUI_TESTS_COMMON_TESTSSHELL_H
+
+#include <RmlUi/Core/Types.h>
+namespace Rml { class RenderInterface; }
+
+namespace TestsShell {
+
+// Will initialize the shell when necessary.
+// No need to call RemoveContext with this.
+Rml::Context* GetMainContext();
+
+// If no interface is passed, it will use the shell renderer's interface. Will initialize the shell when necessary.
+// Call RemoveContext() when you are done with the test.
+Rml::Context* CreateContext(const Rml::String& name, Rml::RenderInterface* render_interface = nullptr);
+void RemoveContext(Rml::Context* context);
+
+using ShellIdleFunction = void(*)();
+void EventLoop(ShellIdleFunction idle_func);
+void PrepareRenderBuffer();
+void PresentRenderBuffer();
+void RequestExit();
+
+void ShutdownShell();
+
+
+}
+
+#endif

+ 0 - 136
Tests/Source/Element.cpp

@@ -1,136 +0,0 @@
-/*
- * 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.
- *
- */
-
-#ifdef RMLUI_ENABLE_BENCHMARKS
-
-#include "TestsInterface.h"
-#include <RmlUi/Core/Context.h>
-#include <RmlUi/Core/Core.h>
-#include <RmlUi/Core/Element.h>
-#include <RmlUi/Core/ElementDocument.h>
-#include <RmlUi/Core/Types.h>
-#include <RmlUi/Debugger.h>
-
-#include <Shell.h>
-#include <Input.h>
-#include <ShellRenderInterfaceOpenGL.h>
-
-#include <doctest.h>
-#include <nanobench.h>
-
-using namespace ankerl;
-using namespace Rml;
-
-static Rml::Context* context = nullptr;
-static ShellRenderInterfaceExtensions* shell_renderer;
-
-
-TEST_CASE("Element benchmark")
-{
-	const Vector2i window_size(1024, 768);
-
-	ShellRenderInterfaceOpenGL opengl_renderer;
-	shell_renderer = &opengl_renderer;
-
-	// Generic OS initialisation, creates a window and attaches OpenGL.
-	REQUIRE(Shell::Initialise());
-	REQUIRE(Shell::OpenWindow("Element benchmark", shell_renderer, window_size.x, window_size.y, true));
-
-	// RmlUi initialisation.
-	Rml::SetRenderInterface(&opengl_renderer);
-	shell_renderer->SetViewport(window_size.x, window_size.y);
-
-	ShellSystemInterface system_interface;
-	Rml::SetSystemInterface(&system_interface);
-
-	//TestsSystemInterface system_interface;
-	//TestsRenderInterface render_interface;
-
-	//SetRenderInterface(&render_interface);
-	//SetSystemInterface(&system_interface);
-
-	REQUIRE(Initialise());
-
-	context = Rml::CreateContext("main", window_size);
-	REQUIRE(context);
-
-	Rml::Debugger::Initialise(context);
-	::Input::SetContext(context);
-	shell_renderer->SetContext(context);
-
-	Shell::LoadFonts("assets/");
-
-	ElementDocument* document = context->LoadDocument("basic/benchmark/data/benchmark.rml");
-	REQUIRE(document);
-	document->Show();
-
-	Rml::String rml;
-	Element* el = document->GetElementById("performance");
-	REQUIRE(el);
-
-	nanobench::Bench bench;
-	bench.title("Elements")
-		.run("SetInnerRML", [&] {
-		for (int i = 0; i < 50; i++)
-		{
-			int index = rand() % 1000;
-			int route = rand() % 50;
-			int max = (rand() % 40) + 10;
-			int value = rand() % max;
-			Rml::String rml_row = Rml::CreateString(1000, R"(
-				<div class="row">
-					<div class="col col1"><button class="expand" index="%d">+</button>&nbsp;<a>Route %d</a></div>
-					<div class="col col23"><input type="range" class="assign_range" min="0" max="%d" value="%d"/></div>
-					<div class="col col4">Assigned</div>
-					<select>
-						<option>Red</option><option>Blue</option><option selected>Green</option><option style="background-color: yellow;">Yellow</option>
-					</select>
-					<div class="inrow unmark_collapse">
-						<div class="col col123 assign_text">Assign to route</div>
-						<div class="col col4">
-							<input type="submit" class="vehicle_depot_assign_confirm" quantity="0">Confirm</input>
-						</div>
-					</div>
-				</div>)",
-				index,
-				route,
-				max,
-				value
-			);
-			rml += rml_row;
-		}
-
-		el->SetInnerRML(rml);
-	});
-
-	Rml::Shutdown();
-
-	Shell::CloseWindow();
-}
-
-#endif

+ 9 - 34
Tests/Source/DataExpression.cpp → Tests/Source/UnitTests/DataExpression.cpp

@@ -26,32 +26,27 @@
  *
  */
 
+
 #include "../../../Source/Core/DataExpression.cpp"
+
+#include <RmlUi/Core/DataModel.h>
+#include <RmlUi/Core/Types.h>
+
 #include <doctest.h>
-#include <nanobench.h>
 
-using namespace ankerl;
 using namespace Rml;
 
 static DataTypeRegister type_register;
 static DataModel model(type_register.GetTransformFuncRegister());
 static DataExpressionInterface interface(&model, nullptr);
 
-String TestExpression(const String& expression, const char* benchmark_name = nullptr)
+
+String TestExpression(const String& expression)
 {
 	String result;
 
 	DataParser parser(expression, interface);
 
-	nanobench::Bench bench;
-	if (benchmark_name)
-	{
-		bench.title(benchmark_name);
-		bench.run("Parse", [&] {
-			parser.Parse(false);
-			});
-	}
-
 	if (parser.Parse(false))
 	{
 		Program program = parser.ReleaseProgram();
@@ -60,20 +55,9 @@ String TestExpression(const String& expression, const char* benchmark_name = nul
 		DataInterpreter interpreter(program, addresses, interface);
 
 		if (interpreter.Run())
-		{
 			result = interpreter.Result().Get<String>();
-
-			if (benchmark_name)
-			{
-				bench.run("Execute", [&] {
-					interpreter.Run();
-					});
-			}
-		}
 		else
-		{
 			FAIL_CHECK("Could not execute expression: " << expression << "\n\n  Parsed program: \n" << interpreter.DumpProgram());
-		}
 	}
 	else
 	{
@@ -94,13 +78,9 @@ bool TestAssignment(const String& expression)
 
 		DataInterpreter interpreter(program, addresses, interface);
 		if (interpreter.Run())
-		{
 			result = true;
-		}
 		else
-		{
 			FAIL_CHECK("Could not execute assignment expression: " << expression << "\n\n  Parsed program: \n" << interpreter.DumpProgram());
-		}
 	}
 	else
 	{
@@ -110,6 +90,7 @@ bool TestAssignment(const String& expression)
 };
 
 
+
 TEST_CASE("Data expressions")
 {
 	float radius = 8.7f;
@@ -121,7 +102,7 @@ TEST_CASE("Data expressions")
 	handle.Bind("color_name", &color_name);
 	handle.BindFunc("color_value", [&](Variant& variant) {
 		variant = ToString(color_value);
-		});
+	});
 
 	CHECK(TestExpression("!!10 - 1 ? 'hello' : 'world' | to_upper") == "WORLD");
 	CHECK(TestExpression("(color_name) + (': rgba(' + color_value + ')')") == "color: rgba(180, 100, 255, 255)");
@@ -166,12 +147,6 @@ TEST_CASE("Data expressions")
 	CHECK(TestExpression("0.2 + 3.42345 | round") == "4");
 	CHECK(TestExpression("(3.42345 | round) + 0.2") == "3.2");
 	CHECK(TestExpression("(3.42345 | format(0)) + 0.2") == "30.2"); // Here, format(0) returns a string, so the + means string concatenation.
-
-	// Benchmark
-#ifdef RMLUI_ENABLE_BENCHMARKS
-	TestExpression("2 * 2", "Data expression simple");
-	TestExpression("true || false ? true && 3==1+2 ? 'Absolutely!' : 'well..' : 'no'", "Data expression complex");
-#endif
 }
 
 

+ 0 - 0
Tests/Source/DataModel.cpp → Tests/Source/UnitTests/DataModel.cpp


+ 0 - 0
Tests/Source/GeometryDatabase.cpp → Tests/Source/UnitTests/GeometryDatabase.cpp


+ 1 - 1
Tests/Source/Selectors.cpp → Tests/Source/UnitTests/Selectors.cpp

@@ -26,7 +26,7 @@
  *
  */
 
-#include "TestsInterface.h"
+#include "../Common/TestsInterface.h"
 #include <RmlUi/Core/Context.h>
 #include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/Element.h>

+ 2 - 3
Tests/Source/main.cpp → Tests/Source/UnitTests/main.cpp

@@ -26,8 +26,7 @@
  *
  */
 
-#define ANKERL_NANOBENCH_IMPLEMENT
-#include <nanobench.h>
-
 #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
 #include <doctest.h>
+
+#include "../Common/TestsInterface.cpp"

+ 353 - 0
Tests/Source/VisualTests/Window.cpp

@@ -0,0 +1,353 @@
+/*
+ * 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 "Window.h"
+#include "../Common/TestsShell.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Core.h>
+#include <RmlUi/Core/Element.h>
+#include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/FileInterface.h>
+#include <RmlUi/Core/Types.h>
+#include <Shell.h>
+
+#include <doctest.h>
+
+using namespace Rml;
+
+
+Window::Window(Rml::Context* context, TestSuiteList test_suites) : context(context), test_suites(std::move(test_suites))
+{
+	InitializeXmlNodeHandlers(&meta_list, &link_list);
+
+	const String local_data_path_prefix = "/../Tests/Data/";
+
+	document_description = context->LoadDocument(local_data_path_prefix + "description.rml");
+	REQUIRE(document_description);
+	document_description->Show();
+
+	document_source = context->LoadDocument(local_data_path_prefix + "view_source.rml");
+	REQUIRE(document_source);
+
+	ReloadDocument();
+
+	context->GetRootElement()->AddEventListener(Rml::EventId::Keydown, this);
+	context->GetRootElement()->AddEventListener(Rml::EventId::Textinput, this);
+}
+
+Window::~Window()
+{
+	context->GetRootElement()->RemoveEventListener(Rml::EventId::Keydown, this);
+	context->GetRootElement()->RemoveEventListener(Rml::EventId::Textinput, this);
+
+	for (ElementDocument* doc : { document, document_description, document_source, document_match })
+	{
+		if (doc)
+			doc->Close();
+	}
+}
+
+const TestSuite& Window::GetCurrentTestSuite() const {
+	REQUIRE(current_test_suite >= 0);
+	REQUIRE(current_test_suite < (int)test_suites.size());
+	return test_suites[current_test_suite];
+}
+
+String Window::GetCurrentPath() const {
+	const TestSuite& test_suite = GetCurrentTestSuite();
+	REQUIRE(current_id >= 0);
+	REQUIRE(current_id < (int)test_suite.files.size());
+
+	return test_suite.directory + '\\' + test_suite.files[current_id];
+}
+
+String Window::GetReferencePath() const {
+	if (reference_file.empty())
+		return String();
+	const TestSuite& test_suite = GetCurrentTestSuite();
+	return test_suite.directory + '\\' + reference_file;
+}
+
+void Window::OpenSource(const String& file_path)
+{
+	FileInterface* file = Rml::GetFileInterface();
+	FileHandle handle = file->Open(file_path);
+	if (!handle)
+		return;
+
+	const size_t length = file->Length(handle);
+	UniquePtr<char[]> buf = UniquePtr<char[]>(new char[length + 1]);
+
+	const size_t read_length = file->Read(buf.get(), length, handle);
+	file->Close(handle);
+	REQUIRE(read_length > 0);
+	REQUIRE(read_length <= length);
+	buf[read_length] = '\0';
+	const String rml_source = StringUtilities::EncodeRml(String(buf.get()));
+
+	Element* element = document_source->GetElementById("code");
+	REQUIRE(element);
+	element->SetInnerRML(rml_source);
+
+	document_source->Show();
+}
+
+void Window::OpenSource()
+{
+	const String file_path = (viewing_reference_source ? GetReferencePath() : GetCurrentPath());
+	OpenSource(file_path);
+}
+
+void Window::SwitchSource()
+{
+	if (document_source->IsVisible())
+	{
+		viewing_reference_source = !viewing_reference_source;
+		OpenSource();
+	}
+}
+
+void Window::CloseSource()
+{
+	document_source->Hide();
+	viewing_reference_source = false;
+}
+
+void Window::ReloadDocument()
+{
+	const String current_path = GetCurrentPath();
+	const TestSuite& test_suite = GetCurrentTestSuite();
+	const String& current_filename = test_suite.files[current_id];
+
+	if (document)
+	{
+		document->Close();
+		document = nullptr;
+	}
+	if (document_match)
+	{
+		document_match->Close();
+		document_match = nullptr;
+	}
+
+	meta_list.clear();
+	link_list.clear();
+
+	document = context->LoadDocument(current_path);
+	REQUIRE(document);
+	document->Show();
+
+	reference_file.clear();
+	for (const LinkItem& item : link_list)
+	{
+		if (item.rel == "match")
+		{
+			reference_file = item.href;
+			break;
+		}
+	}
+
+	if (!reference_file.empty())
+	{
+		const String reference_path = GetReferencePath();
+
+		// See if we can open the file first to avoid logging warnings.
+		FileInterface* file = Rml::GetFileInterface();
+		FileHandle handle = file->Open(reference_path);
+		
+		if (handle)
+		{
+			file->Close(handle);
+
+			document_match = context->LoadDocument(reference_path);
+			if (document_match)
+			{
+				document_match->SetProperty(PropertyId::Left, Property(510.f, Property::PX));
+				document_match->Show();
+			}
+		}
+	}
+
+	String rml_description = Rml::CreateString(512, "<h1>%s</h1><p>Test %d of %d.<br/>%s", document->GetTitle().c_str(), current_id + 1, (int)test_suite.files.size(), current_filename.c_str());
+	if (!reference_file.empty())
+	{
+		if (document_match)
+			rml_description += "<br/>" + reference_file;
+		else
+			rml_description += "<br/>(X " + reference_file + ")";
+	}
+	rml_description += "</p>";
+
+	if(!link_list.empty())
+	{
+		rml_description += "<p class=\"links\">";
+		for (const LinkItem& item : link_list)
+		{
+			if (item.rel == "match")
+				continue;
+
+			rml_description += "<a href=\"" + item.href + "\">" + item.rel + "</a> ";
+		}
+		rml_description += "</p>";
+	}
+
+	for (const MetaItem& item : meta_list)
+	{
+		rml_description += "<h3>" + item.name + "</h3>";
+		rml_description += "<p style=\"min-height: 120px;\">" + item.content + "</p>";
+	}
+
+	Element* description_content = document_description->GetElementById("content");
+	REQUIRE(description_content);
+	description_content->SetInnerRML(rml_description);
+
+	Element* description_test_suite = document_description->GetElementById("test_suite");
+	REQUIRE(description_test_suite);
+	description_test_suite->SetInnerRML(CreateString(64, "Test suite %d of %d", current_test_suite + 1, (int)test_suites.size()));
+
+	Element* description_goto = document_description->GetElementById("goto");
+	REQUIRE(description_goto);
+	description_goto->SetInnerRML("");
+	goto_id = 0;
+
+	CloseSource();
+}
+
+void Window::ProcessEvent(Rml::Event& event)
+{
+	if (event == EventId::Keydown)
+	{
+		auto key_identifier = (Rml::Input::KeyIdentifier)event.GetParameter< int >("key_identifier", 0);
+		bool key_ctrl = event.GetParameter< bool >("ctrl_key", false);
+		bool key_shift = event.GetParameter< bool >("shift_key", false);
+
+		if (key_identifier == Rml::Input::KI_LEFT)
+		{
+			current_id = std::max(0, current_id - 1);
+			ReloadDocument();
+		}
+		else if (key_identifier == Rml::Input::KI_RIGHT)
+		{
+			current_id = std::min((int)GetCurrentTestSuite().files.size() - 1, current_id + 1);
+			ReloadDocument();
+		}
+		else if (key_identifier == Rml::Input::KI_UP)
+		{
+			current_test_suite = std::max(0, current_test_suite - 1);
+			current_id = 0;
+			ReloadDocument();
+		}
+		else if (key_identifier == Rml::Input::KI_DOWN)
+		{
+			current_test_suite = std::min((int)test_suites.size() - 1, current_test_suite + 1);
+			current_id = 0;
+			ReloadDocument();
+		}
+		else if (key_identifier == Rml::Input::KI_S)
+		{
+			if (document_source->IsVisible())
+			{
+				if (key_shift)
+					SwitchSource();
+				else
+					CloseSource();
+			}
+			else
+			{
+				viewing_reference_source = key_shift;
+				OpenSource();
+			}
+		}
+		else if (key_identifier == Rml::Input::KI_ESCAPE)
+		{
+			if (document_source->IsVisible())
+				CloseSource();
+			else
+				TestsShell::RequestExit();
+		}
+		else if (key_identifier == Rml::Input::KI_C && key_ctrl)
+		{
+			if (key_shift)
+				Shell::SetClipboardText(GetCurrentTestSuite().directory + '\\' + reference_file);
+			else
+				Shell::SetClipboardText(GetCurrentPath());
+		}
+		else if (key_identifier == Rml::Input::KI_HOME)
+		{
+			current_id = 0;
+			ReloadDocument();
+		}
+		else if (key_identifier == Rml::Input::KI_END)
+		{
+			current_id = (int)GetCurrentTestSuite().files.size() - 1;
+			ReloadDocument();
+		}
+		else if (goto_id >= 0 && key_identifier == Rml::Input::KI_BACK)
+		{
+			if (goto_id <= 0)
+			{
+				goto_id = -1;
+				document_description->GetElementById("goto")->SetInnerRML("");
+			}
+			else
+			{
+				goto_id = goto_id / 10;
+				document_description->GetElementById("goto")->SetInnerRML(CreateString(64, "Go To: %d", goto_id));
+			}
+		}
+	}
+
+	if (event == EventId::Textinput)
+	{
+		const String text = event.GetParameter< String >("text", "");
+		for (const char c : text)
+		{
+			if (c >= '0' && c <= '9')
+			{
+				if (goto_id < 0)
+					goto_id = 0;
+
+				goto_id = goto_id * 10 + int(c - '0');
+				document_description->GetElementById("goto")->SetInnerRML(CreateString(64, "Go To: %d", goto_id));
+			}
+			else if (goto_id >= 0 && c == '\n')
+			{
+				if (goto_id > 0 && goto_id <= (int)GetCurrentTestSuite().files.size())
+				{
+					current_id = goto_id - 1;
+					ReloadDocument();
+				}
+				else
+				{
+					document_description->GetElementById("goto")->SetInnerRML("Error: Go To out of bounds.");
+				}
+				goto_id = -1;
+			}
+		}
+	}
+}

+ 88 - 0
Tests/Source/VisualTests/Window.h

@@ -0,0 +1,88 @@
+/*
+ * 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 RMLUI_TESTS_VISUALTESTS_WINDOW_H
+#define RMLUI_TESTS_VISUALTESTS_WINDOW_H
+
+#include <RmlUi/Core/EventListener.h>
+#include <RmlUi/Core/Types.h>
+#include "XmlNodeHandlers.h"
+
+namespace Rml { class Context; class ElementDocument; }
+
+struct TestSuite {
+	Rml::String directory;
+	Rml::StringList files;
+};
+using TestSuiteList = Rml::Vector<TestSuite>;
+
+
+class Window : public Rml::EventListener, Rml::NonCopyMoveable
+{
+public:
+	Window(Rml::Context* context, TestSuiteList test_suites);
+	~Window();
+	
+private:
+	const TestSuite& GetCurrentTestSuite() const;
+	Rml::String GetCurrentPath() const;
+	Rml::String GetReferencePath() const;
+
+	void OpenSource(const Rml::String& file_path);
+
+	void OpenSource();
+
+	void SwitchSource();
+
+	void CloseSource();
+
+	void ReloadDocument();
+
+	void ProcessEvent(Rml::Event& event) override;
+
+	Rml::Context* context;
+
+	int goto_id = -1;
+
+	int current_id = 0;
+	Rml::ElementDocument* document = nullptr;
+	Rml::ElementDocument* document_description = nullptr;
+	Rml::ElementDocument* document_source = nullptr;
+	Rml::ElementDocument* document_match = nullptr;
+	Rml::String reference_file;
+	bool viewing_reference_source = false;
+
+	const TestSuiteList test_suites;
+	int current_test_suite = 0;
+
+	MetaList meta_list;
+	LinkList link_list;
+};
+
+
+#endif

+ 148 - 0
Tests/Source/VisualTests/XmlNodeHandlers.cpp

@@ -0,0 +1,148 @@
+/*
+ * 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 "XmlNodeHandlers.h"
+#include <RmlUi/Core/Types.h>
+#include <RmlUi/Core/XMLNodeHandler.h>
+#include <RmlUi/Core/XMLParser.h>
+#include <doctest.h>
+
+using namespace Rml;
+
+
+
+class XMLNodeHandlerMeta : public Rml::XMLNodeHandler
+{
+public:
+	XMLNodeHandlerMeta(MetaList* meta_list) : meta_list(meta_list)
+	{}
+	~XMLNodeHandlerMeta()
+	{}
+
+	/// Called when a new element start is opened
+	Element* ElementStart(XMLParser* /*parser*/, const String& /*name*/, const XMLAttributes& attributes) override
+	{
+		MetaItem item;
+
+		auto it_name = attributes.find("name");
+		if (it_name != attributes.end())
+			item.name = it_name->second.Get<String>();
+		
+		auto it_content = attributes.find("content");
+		if (it_content != attributes.end())
+			item.content = it_content->second.Get<String>();
+
+		if (!item.name.empty() && !item.content.empty())
+			meta_list->push_back(std::move(item));
+
+		return nullptr;
+	}
+
+	/// Called when an element is closed
+	bool ElementEnd(XMLParser* /*parser*/, const String& /*name*/) override
+	{
+		return true;
+	}
+	/// Called for element data
+	bool ElementData(XMLParser* /*parser*/, const String& /*data*/, XMLDataType /*type*/) override
+	{
+		return true;
+	}
+
+private:
+	MetaList* meta_list;
+};
+
+
+class XMLNodeHandlerLink : public Rml::XMLNodeHandler
+{
+public:
+	XMLNodeHandlerLink(LinkList* link_list) : link_list(link_list)
+	{
+		node_handler_head = XMLParser::GetNodeHandler("head");
+		RMLUI_ASSERT(node_handler_head);
+	}
+	~XMLNodeHandlerLink() {}
+
+	Element* ElementStart(XMLParser* parser, const String& name, const XMLAttributes& attributes) override
+	{
+		RMLUI_ASSERT(name == "link");
+
+		const String rel = StringUtilities::ToLower(Get<String>(attributes, "rel", ""));
+		const String type = StringUtilities::ToLower(Get<String>(attributes, "type", ""));
+		const String href = Get<String>(attributes, "href", "");
+
+		if (!type.empty() && !href.empty())
+		{
+			// Pass it on to the head handler if it's a type it handles.
+			if (type == "text/rcss" || type == "text/css" || type == "text/template")
+			{
+				return node_handler_head->ElementStart(parser, name, attributes);
+			}
+		}
+
+		LinkItem item{ rel, href };
+
+		if (rel == "match")
+			item.href = StringUtilities::Replace(href, '/', '\\');
+		else
+			item.href = href;
+
+		link_list->push_back(std::move(item));
+
+		return nullptr;
+	}
+
+	bool ElementEnd(XMLParser* parser, const String& name) override
+	{
+		return node_handler_head->ElementEnd(parser, name);
+	}
+	bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override
+	{
+		return node_handler_head->ElementData(parser, data, type);
+	}
+
+private:
+	LinkList* link_list;
+	Rml::XMLNodeHandler* node_handler_head;
+};
+
+
+static SharedPtr<XMLNodeHandlerMeta> meta_handler;
+static SharedPtr<XMLNodeHandlerLink> link_handler;
+
+void InitializeXmlNodeHandlers(MetaList* meta_list, LinkList* link_list)
+{
+	meta_handler = MakeShared<XMLNodeHandlerMeta>(meta_list);
+	REQUIRE(meta_handler);
+	Rml::XMLParser::RegisterNodeHandler("meta", meta_handler);
+
+	link_handler = MakeShared<XMLNodeHandlerLink>(link_list);
+	REQUIRE(link_handler);
+	Rml::XMLParser::RegisterNodeHandler("link", link_handler);
+}

+ 51 - 0
Tests/Source/VisualTests/XmlNodeHandlers.h

@@ -0,0 +1,51 @@
+/*
+ * 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 RMLUI_TESTS_VISUALTESTS_XMLNODEHANDLERS_H
+#define RMLUI_TESTS_VISUALTESTS_XMLNODEHANDLERS_H
+
+#include <RmlUi/Core/EventListener.h>
+#include <RmlUi/Core/Types.h>
+
+struct MetaItem {
+	Rml::String name;
+	Rml::String content;
+};
+using MetaList = Rml::Vector<MetaItem>;
+
+
+struct LinkItem {
+	Rml::String rel;
+	Rml::String href;
+};
+using LinkList = Rml::Vector<LinkItem>;
+
+void InitializeXmlNodeHandlers(MetaList* meta_list, LinkList* link_list);
+
+
+#endif

+ 115 - 0
Tests/Source/VisualTests/main.cpp

@@ -0,0 +1,115 @@
+/*
+ * 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 "Window.h"
+#include "../Common/TestsShell.h"
+#include <RmlUi/Core/StringUtilities.h>
+
+#define DOCTEST_CONFIG_IMPLEMENT
+#include <doctest.h>
+
+#include <Shell.h>
+
+
+using namespace Rml;
+
+Rml::Context* context = nullptr;
+
+bool run_loop = true;
+bool single_loop = true;
+
+void GameLoop()
+{
+	if (run_loop || single_loop)
+	{
+		single_loop = false;
+
+		context->Update();
+
+		TestsShell::PrepareRenderBuffer();
+		context->Render();
+		TestsShell::PresentRenderBuffer();
+	}
+}
+
+
+TEST_CASE("Run VisualTests")
+{
+	// Start the visual test suite
+	context = TestsShell::GetMainContext();
+	REQUIRE(context);
+
+	StringList directories = { "/../Tests/Data/VisualTests" };
+
+#ifdef RMLUI_VISUAL_TESTS_DIRECTORIES
+	StringUtilities::ExpandString(directories, RMLUI_VISUAL_TESTS_DIRECTORIES, ';');
+#endif
+
+	TestSuiteList test_suites;
+
+	for (const String& directory : directories)
+	{
+		const StringList files = Shell::ListFiles(directory, "rml");
+
+		if (files.empty())
+		{
+			MESSAGE("Could not find any *.rml* files in directory '" << directory << "'. Ignoring.'");
+		}
+
+		test_suites.push_back(
+			TestSuite{ directory, std::move(files) }
+		);
+	}
+
+	REQUIRE_MESSAGE(!test_suites.empty(), "RML test files directory not found or empty.");
+
+	Window window(context, std::move(test_suites));
+
+	TestsShell::EventLoop(GameLoop);
+}
+
+
+
+int main(int argc, char** argv) {
+
+    // Initialize and run doctest
+    doctest::Context doctest_context;
+
+    doctest_context.applyCommandLine(argc, argv);
+
+    int doctest_result = doctest_context.run();
+
+    if (doctest_context.shouldExit())
+        return doctest_result;
+	
+    // RmlUi is initialized during doctest run above as necessary.
+    // Clean everything up here.
+    TestsShell::ShutdownShell();
+
+    return doctest_result;
+}

+ 225 - 0
Tests/Tools/convert_css_test_suite_to_rml.py

@@ -0,0 +1,225 @@
+import os
+import re
+import sys
+import argparse
+
+parser = argparse.ArgumentParser(description=\
+'''Convert the W3C CSS 2.1 test suite to RML documents for testing in RmlUi.
+
+Fetch the CSS tests archive from here: https://www.w3.org/Style/CSS/Test/CSS2.1/
+Extract the 'xhtml1' folder and point the 'in_dir' argument to this directory.''')
+
+parser.add_argument('in_dir',
+                    help="Input directory which contains the 'xhtml1' (.xht) files to be converted.")
+parser.add_argument('out_dir',
+                    help="Output directory for the converted RML files.")
+parser.add_argument('--clean', action='store_true',
+                    help='Will *delete* all existing *.rml files in the output directory.')
+parser.add_argument('--match',
+                    help="Only process file names containing the given string.")
+
+args = parser.parse_args()
+
+in_dir = args.in_dir
+out_dir = args.out_dir
+out_ref_dir = os.path.join(out_dir, r'reference')
+match_files = args.match
+
+if not os.path.isdir(in_dir):
+	print("Error: Specified input directory '{}' does not exist.".format(out_dir))
+	exit()
+
+if not os.path.exists(out_dir):
+	try: 
+		os.mkdir(out_dir)
+	except Exception as e:
+		print('Error: Failed to create output directory {}'.format(out_dir))
+
+if not os.path.exists(out_ref_dir):
+	try: 
+		os.mkdir(out_ref_dir)
+	except Exception as e:
+		print('Error: Failed to create reference output directory {}'.format(out_ref_dir))
+
+if not os.path.isdir(out_dir) or not os.path.isdir(out_ref_dir):
+	print("Error: Specified output directory '{}' or reference '{}' are not directories.".format(out_dir, out_ref_dir))
+	exit()
+
+if args.clean:
+	print("Deleting all *.rml files in output directory '{}' and reference directory '{}'".format(out_dir, out_ref_dir))
+
+	for del_dir in [out_dir, out_ref_dir]:
+		for file in os.listdir(del_dir):
+			path = os.path.join(del_dir, file)
+			try:
+				if os.path.isfile(path) and file.endswith('.rml'):
+					os.unlink(path)
+			except Exception as e:
+				print('Failed to delete {}. Reason: {}'.format(path, e))
+
+reference_links = []
+
+def process_file(in_file):
+	
+	in_path = os.path.join(in_dir, in_file)
+	out_file = os.path.splitext(in_file)[0] + '.rml'
+	out_path = os.path.join(out_dir, out_file)
+	
+	f = open(in_path, 'r', encoding="utf8")
+	lines = f.readlines()
+	f.close()
+
+	data = ''
+	reference_link = ''
+	in_style = False
+
+	for line in lines:
+		if re.search(r'<style', line, flags = re.IGNORECASE):
+			in_style = True
+		if re.search(r'</style', line, flags = re.IGNORECASE):
+			in_style = False
+		
+		if in_style:
+			line = re.sub(r'(^|[^<])html', r'\1body', line, flags = re.IGNORECASE)
+
+		reference_link_search_candidates = [
+			r'(<link href="(reference/[^"]+))\.xht(" rel="match" ?/>)',
+			r'(<link rel="match" href="(reference/[^"]+))\.xht(" ?/>)',
+			]
+
+		for reference_link_search in reference_link_search_candidates:
+			reference_link_match = re.search(reference_link_search, line, flags = re.IGNORECASE)
+			if reference_link_match:
+				reference_link = reference_link_match[2] + '.xht'
+				line = re.sub(reference_link_search, r'\1.rml\3', line, flags = re.IGNORECASE)
+				break
+
+		line = re.sub(r'<!DOCTYPE[^>]*>\s*', '', line, flags = re.IGNORECASE)
+		line = re.sub(r' xmlns="[^"]+"', '', line, flags = re.IGNORECASE)
+		line = re.sub(r'<(/?)html[^>]*>', r'<\1rml>', line, flags = re.IGNORECASE)
+		line = re.sub(r'^(\s*)(.*<head[^>]*>)', r'\1\2\n\1\1<link type="text/rcss" href="/../Tests/Data/style.rcss" />', line, flags = re.IGNORECASE)
+		line = re.sub(r'direction:\s*ltr\s*;?', r'', line, flags = re.IGNORECASE)
+		line = re.sub(r'list-style(-type)?:\s*none\s*;?', r'', line, flags = re.IGNORECASE)
+		line = re.sub(r'max-height:\s*none;', r'max-height: -1px;', line, flags = re.IGNORECASE)
+		line = re.sub(r'max-width:\s*none;', r'max-width: -1px;', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*xxx-large', r'\1 2.0em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*xx-large', r'\1 1.7em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*x-large', r'\1 1.3em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*large', r'\1 1.15em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*medium', r'\1 1.0em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*small', r'\1 0.9em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*x-small', r'\1 0.7em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(font-size:)\s*xx-small', r'\1 0.5em', line, flags = re.IGNORECASE)
+		line = re.sub(r'(line-height:)\s*normal', r'\1 1.2em', line, flags = re.IGNORECASE)
+		line = re.sub(r'cyan', r'aqua', line, flags = re.IGNORECASE)
+
+		if re.search(r'background:[^;}\"]*fixed', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses unsupported background.".format(in_file))
+			return False
+		line = re.sub(r'background:(\s*([a-z]+|#[0-9a-f]+)\s*[;}\"])', r'background-color:\1', line, flags = re.IGNORECASE)
+
+		# Try to fix up borders to match the RmlUi syntax. This conversion might ruin some tests.
+		line = re.sub(r'(border(-(top|right|bottom|left))?)-style:\s*solid(\s*[;}"])', r'\1-width: 3px\4', line, flags = re.IGNORECASE)
+
+		line = re.sub(r'(border(-(top|right|bottom|left))?):\s*none(\s*[;}"])', r'\1-width: 0px\4', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border[^:]*:[^;]*)thin', r'\1 1px', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border[^:]*:[^;]*)medium', r'\1 3px', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border[^:]*:[^;]*)thick', r'\1 5px', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border[^:]*:[^;]*)none', r'\1 0px', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border[^:]*:\s*[0-9][^\s;}]*)\s+soli?d', r'\1 ', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border[^:]*:\s*[^0-9;}]*)soli?d', r'\1 3px', line, flags = re.IGNORECASE)
+
+		if re.search(r'border[^;]*(hidden|dotted|dashed|double|groove|ridge|inset|outset)', line, flags = re.IGNORECASE) \
+			or re.search(r'border[^:]*-style:', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses unsupported border styles.".format(in_file))
+			return False
+
+		line = re.sub(r'(border(-(top|right|bottom|left))?:\s*)[0-9][^\s;}]*(\s+[0-9][^\s;}]*[;}])', r'\1 \4', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border(-(top|right|bottom|left))?:\s*[0-9\.]+[a-z]+\s+)[0-9\.]+[a-z]+([^;}]*[;}])', r'\1 \4', line, flags = re.IGNORECASE)
+		line = re.sub(r'(border(-(top|right|bottom|left))?:\s*[0-9\.]+[a-z]+\s+)[0-9\.]+[a-z]+([^;}]*[;}])', r'\1 \4', line, flags = re.IGNORECASE)
+
+		line = re.sub(r'(border:)[^;]*none([^;]*;)', r'\1 0px \2', line, flags = re.IGNORECASE)
+		if in_style and not '<' in line:
+			line = line.replace('&gt;', '>')
+		flags_match = re.search(r'<meta.*name="flags" content="([^"]*)" ?/>', line, flags = re.IGNORECASE)
+		if flags_match and flags_match[1] != '' and flags_match[1] != 'interactive':
+			print("File '{}' skipped due to flags '{}'".format(in_file, flags_match[1]))
+			return False
+		if re.search(r'(display:[^;]*(table|run-in|list-item))|(<table)', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses tables.".format(in_file))
+			return False
+		if re.search(r'visibility:[^;]*collapse|z-index:\s*[0-9\.]+%', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses unsupported visibility.".format(in_file))
+			return False
+		if re.search(r'data:|support/|<img|<iframe', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses data or images.".format(in_file))
+			return False
+		if re.search(r'<script>', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses scripts.".format(in_file))
+			return False
+		if in_style and re.search(r':before|:after|@media|\s\+\s', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses unsupported CSS selectors.".format(in_file))
+			return False
+		if re.search(r'(: ?inherit ?;)|(!\s*important)|[0-9\.]+(ch|ex)[\s;}]', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses unsupported CSS values.".format(in_file))
+			return False
+		if re.search(r'font(-family)?:', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it modifies fonts.".format(in_file))
+			return False
+		if re.search(r'(direction:[^;]*[;"])|(content:[^;]*[;"])|(outline:[^;]*[;"])|(quote:[^;]*[;"])|(border-spacing:[^;]*[;"])|(border-collapse:[^;]*[;"])|(background:[^;]*[;"])|(box-sizing:[^;]*[;"])', line, flags = re.IGNORECASE)\
+			or re.search(r'(font-variant:[^;]*[;"])|(font-kerning:[^;]*[;"])|(font-feature-settings:[^;]*[;"])|(background-image:[^;]*[;"])|(caption-side:[^;]*[;"])|(clip:[^;]*[;"])|(page-break-inside:[^;]*[;"])|(word-spacing:[^;]*[;"])', line, flags = re.IGNORECASE)\
+			or re.search(r'(writing-mode:[^;]*[;"])|(text-orientation:[^;]*[;"])|(text-indent:[^;]*[;"])|(page-break-after:[^;]*[;"])|(column[^:]*:[^;]*[;"])|(empty-cells:[^;]*[;"])', line, flags = re.IGNORECASE):
+			print("File '{}' skipped since it uses unsupported CSS properties.".format(in_file))
+			return False
+		data += line
+
+	f = open(out_path, 'w', encoding="utf8")
+	f.write(data)
+	f.close()
+
+	if reference_link:
+		reference_links.append(reference_link)
+	
+	print("File '{}' processed successfully!".format(in_file))
+
+	return True
+
+
+file_block_filters = ['charset','font','list','text-decoration','text-indent','text-transform','bidi','cursor',
+					'uri','stylesheet','word-spacing','table','outline','at-rule','at-import','attribute',
+					'style','quote','rtl','ltr','first-line','first-letter','first-page','import','border',
+					'chapter','character-encoding','escape','media','contain-','grid','case-insensitive',
+					'containing-block-initial','multicol','system-colors']
+
+def should_block(name):
+
+	for file_block_filter in file_block_filters:
+		if file_block_filter in name:
+			print("File '{}' skipped due to unsupported feature '{}'".format(name, file_block_filter))
+			return True
+	return False
+
+in_dir_list = os.listdir(in_dir)
+if match_files:
+	in_dir_list = [ name for name in in_dir_list if match_files in name ]
+total_files = len(in_dir_list)
+
+in_dir_list = [ name for name in in_dir_list if name.endswith(".xht") and not should_block(name) ]
+
+processed_files = 0
+processed_reference_files = 0
+
+for in_file in in_dir_list:
+	if process_file(in_file):
+		processed_files += 1
+
+final_reference_links = reference_links[:]
+total_reference_files = len(final_reference_links)
+reference_links.clear()
+
+for in_ref_file in final_reference_links:
+	if process_file(in_ref_file):
+		processed_reference_files += 1
+
+print('\nDone!\n\nTotal test files: {}\nSkipped test files: {}\nParsed test files: {}\n\nTotal reference files: {}\nSkipped reference files: {}\nIgnored alternate references: {}\nParsed reference files: {}'\
+	.format(total_files, total_files - processed_files, processed_files, total_reference_files, total_reference_files - processed_reference_files, len(reference_links), processed_reference_files ))