Browse Source

Basic implementation of data binding (WIP)

Michael Ragazzon 6 years ago
parent
commit
1c607796a8

+ 2 - 0
CMake/FileList.cmake

@@ -108,6 +108,7 @@ set(Core_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ContextInstancer.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ConvolutionFilter.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Core.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataBinding.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Debug.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Decorator.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DecoratorInstancer.h
@@ -192,6 +193,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/ContextInstancerDefault.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/ConvolutionFilter.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Core.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/DataBinding.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Decorator.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorGradient.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorInstancer.cpp

+ 1 - 0
Include/RmlUi/Core.h

@@ -39,6 +39,7 @@
 #include "Core/ComputedValues.h"
 #include "Core/Context.h"
 #include "Core/ContextInstancer.h"
+#include "Core/DataBinding.h"
 #include "Core/Decorator.h"
 #include "Core/DecoratorInstancer.h"
 #include "Core/Element.h"

+ 22 - 0
Include/RmlUi/Core/Context.h

@@ -34,6 +34,7 @@
 #include "Traits.h"
 #include "Input.h"
 #include "ScriptInterface.h"
+#include "DataBinding.h"
 
 namespace Rml {
 namespace Core {
@@ -218,6 +219,24 @@ public:
 	/// @param[in] instancer The context's instancer.
 	void SetInstancer(ContextInstancer* instancer);
 
+
+	DataModelHandle CreateDataModel(String name) 
+	{
+		auto result = data_models.emplace(name, std::make_unique<DataModel>());
+		if (result.second)
+			return DataModelHandle(result.first->second.get());
+
+		return DataModelHandle(nullptr);
+	}
+
+	DataModel* GetDataModel(const String& name)
+	{
+		auto it = data_models.find(name);
+		if (it != data_models.end())
+			return it->second.get();
+		return nullptr;
+	}
+
 protected:
 	void Release() override;
 
@@ -286,6 +305,9 @@ private:
 	Vector2i clip_origin;
 	Vector2i clip_dimensions;
 
+	using DataModels = UnorderedMap<String, UniquePtr<DataModel>>;
+	DataModels data_models;
+
 	// Internal callback for when an element is detached or removed from the hierarchy.
 	void OnElementDetach(Element* element);
 	// Internal callback for when a new element gains focus.

+ 142 - 0
Include/RmlUi/Core/DataBinding.h

@@ -0,0 +1,142 @@
+/*
+ * 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 RMLUICOREDATABINDING_H
+#define RMLUICOREDATABINDING_H
+
+#include "Header.h"
+#include "Types.h"
+#include "Variant.h"
+#include "StringUtilities.h"
+
+namespace Rml {
+namespace Core {
+
+class Element;
+class DataModel;
+
+
+class DataViewText {
+public:
+	DataViewText(Element* in_parent_element, const String& in_text, size_t index_begin_search = 0);
+
+	inline operator bool() const {
+		return !data_entries.empty() && parent_element;
+	}
+
+	inline bool IsDirty() const {
+		return is_dirty;
+	}
+
+	bool Update(const DataModel& model);
+
+private:
+	String CreateText() const;
+
+	struct DataEntry {
+		size_t index = 0; // Index into 'text'
+		String name;
+		String value;
+	};
+
+	ObserverPtr<Element> parent_element;
+	String text;
+	std::vector<DataEntry> data_entries;
+	bool is_dirty = false;
+};
+
+
+class DataViews {
+public:
+
+	void AddTextView(DataViewText&& text_view) {
+		text_views.push_back(std::move(text_view));
+	}
+
+	bool Update(const DataModel& model)
+	{
+		bool result = false;
+		for (auto& view : text_views)
+			result |= view.Update(model);
+		return result;
+	}
+
+private:
+	std::vector<DataViewText> text_views;
+};
+
+
+class DataModel {
+public:
+	using Type = Variant::Type;
+
+	struct Binding {
+		Type type = Type::NONE;
+		const void* ptr = nullptr;
+	};
+
+	bool GetValue(const String& name, String& out_value) const;
+
+	using Bindings = Rml::Core::UnorderedMap<Rml::Core::String, Binding>;
+
+	Bindings bindings;
+
+	DataViews views;
+};
+
+
+class DataModelHandle {
+public:
+	using Type = Variant::Type;
+
+	DataModelHandle() : model(nullptr) {}
+	DataModelHandle(DataModel* model) : model(model) {}
+
+	DataModelHandle& BindData(String name, Type type, const void* ptr)
+	{
+		RMLUI_ASSERT(model);
+		model->bindings.emplace(name, DataModel::Binding{ type, ptr });
+		return *this;
+	}
+
+	void UpdateViews() {
+		RMLUI_ASSERT(model);
+		model->views.Update(*model);
+	}
+
+	operator bool() { return model != nullptr; }
+
+private:
+	DataModel* model;
+};
+
+
+}
+}
+
+#endif

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

@@ -40,6 +40,7 @@ class DocumentHeader;
 class Element;
 class XMLNodeHandler;
 class URL;
+class DataModel;
 
 /**
 	RmlUi's XML parsing engine. The factory creates an instance of this class for each RML parse.
@@ -82,6 +83,8 @@ public:
 
 		// The default handler used for this frame's children.
 		XMLNodeHandler* child_handler;
+
+		DataModel* data_model;
 	};
 
 	/// Pushes an element handler onto the parse stack for parsing child elements.
@@ -95,6 +98,9 @@ public:
 	/// @return The parser's current parse frame.
 	const ParseFrame* GetParseFrame() const;
 
+	/// Get the data model name for the current frame.
+	DataModel* GetDataModel() const;
+
 protected:
 	/// Called when the parser finds the beginning of an element tag.
 	void HandleElementStart(const String& name, const XMLAttributes& attributes) override;
@@ -110,6 +116,8 @@ private:
 	// The active node handler.
 	XMLNodeHandler* active_handler;
 
+	DataModel* active_data_model;
+
 	// The parser stack.
 	using ParserStack = std::stack< ParseFrame >;
 	ParserStack stack;

+ 2 - 2
Samples/basic/databinding/data/databinding.rml

@@ -162,9 +162,9 @@ form h2
 <body template="window">
 <tabset id="menu">
 <tab>Welcome</tab>
-<panel id="welcome">
+<panel id="welcome" data-model="my_model">
 	<p class="title" style="margin-top: 1.8em;">{{hello_world}}</p>
-	<p>Data binding demo.</p>
+	<p>Data binding demo. We rate this a good old {{rating}}!</p>
 </panel>
 <tab>Decorators</tab>
 <panel id="decorators">

+ 28 - 1
Samples/basic/databinding/src/main.cpp

@@ -102,6 +102,30 @@ private:
 };
 
 
+
+struct MyData {
+	Rml::Core::String hello_world = "Hello World!";
+	int rating = 99;
+} my_data;
+
+Rml::Core::DataModelHandle my_model;
+
+bool SetupDataBinding(Rml::Core::Context* context)
+{
+	using Type = Rml::Core::Variant::Type;
+
+	my_model = context->CreateDataModel("my_model");
+	if (!my_model)
+		return false;
+
+	my_model.BindData("hello_world", Type::STRING, &my_data.hello_world);
+	my_model.BindData("rating", Type::INT, &my_data.rating);
+
+	return true;
+}
+
+
+
 Rml::Core::Context* context = nullptr;
 ShellRenderInterfaceExtensions *shell_renderer;
 std::unique_ptr<DemoWindow> demo_window;
@@ -195,7 +219,8 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 
 	// Create the main RmlUi context and set it on the shell's input layer.
 	context = Rml::Core::CreateContext("main", Rml::Core::Vector2i(width, height));
-	if (context == nullptr)
+
+	if (!context || !SetupDataBinding(context))
 	{
 		Rml::Core::Shutdown();
 		Shell::Shutdown();
@@ -216,6 +241,8 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keydown, demo_window.get());
 	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keyup, demo_window.get());
 
+	my_model.UpdateViews();
+
 	Shell::EventLoop(GameLoop);
 
 	demo_window->Shutdown();

+ 175 - 0
Source/Core/DataBinding.cpp

@@ -0,0 +1,175 @@
+/*
+ * 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 "precompiled.h"
+#include "../../Include/RmlUi/Core/DataBinding.h"
+#include "../../Include/RmlUi/Core/Element.h"
+
+namespace Rml {
+namespace Core {
+
+
+DataViewText::DataViewText(Element* in_parent_element, const String& in_text, const size_t index_begin_search) : parent_element(in_parent_element->GetObserverPtr())
+{
+	text.reserve(in_text.size());
+
+	bool success = true;
+
+	size_t previous_close_brackets = 0;
+	size_t begin_brackets = index_begin_search;
+	while ((begin_brackets = in_text.find("{{", begin_brackets)) != String::npos)
+	{
+		text.insert(text.end(), in_text.begin() + previous_close_brackets, in_text.begin() + begin_brackets);
+
+		const size_t begin_name = begin_brackets + 2;
+		const size_t end_name = in_text.find("}}", begin_name);
+
+		if (end_name == String::npos)
+		{
+			success = false;
+			break;
+		}
+
+		DataEntry entry;
+		entry.index = text.size();
+		entry.name = (String)StringUtilities::StripWhitespace(StringView(in_text.data() + begin_name, in_text.data() + end_name));
+		data_entries.push_back(std::move(entry));
+
+		previous_close_brackets = end_name + 2;
+		begin_brackets = previous_close_brackets;
+	}
+
+	if (data_entries.empty())
+		success = false;
+
+	if (success && previous_close_brackets < in_text.size())
+		text.insert(text.end(), in_text.begin() + previous_close_brackets, in_text.end());
+
+	if (success)
+	{
+		is_dirty = true;
+	}
+	else
+	{
+		text.clear();
+		data_entries.clear();
+	}
+
+
+}
+
+bool DataViewText::Update(const DataModel& model)
+{
+	bool entries_modified = is_dirty;
+
+	for (DataEntry& entry : data_entries)
+	{
+		String value;
+		bool result = model.GetValue(entry.name, value);
+
+		if (result && entry.value != value)
+		{
+			entry.value = value;
+			entries_modified = true;
+		}
+	}
+
+	if (entries_modified)
+	{
+		if (parent_element)
+		{
+			String rml = CreateText();
+			parent_element->SetInnerRML(rml);
+		}
+		else
+		{
+			Log::Message(Log::LT_WARNING, "Could not update data view text, parent element no longer valid. Was it destroyed?");
+		}
+	}
+
+	is_dirty = false;
+
+	return entries_modified;
+}
+
+String DataViewText::CreateText() const
+{
+	size_t reserve_size = text.size();
+
+	for (const DataEntry& entry : data_entries)
+		reserve_size += entry.value.size();
+
+	String result;
+	result.reserve(reserve_size);
+
+	size_t previous_index = 0;
+	for (const DataEntry& entry : data_entries)
+	{
+		result += text.substr(previous_index, entry.index - previous_index);
+		result += entry.value;
+		previous_index = entry.index;
+	}
+
+	if (previous_index < text.size())
+		result += text.substr(previous_index);
+
+	return result;
+}
+
+
+bool DataModel::GetValue(const String& name, String& out_value) const
+{
+	auto it = bindings.find(name);
+	if (it != bindings.end())
+	{
+		const Binding& binding = it->second;
+
+		bool result = true;
+
+		if (binding.type == Type::STRING)
+			out_value = *static_cast<const String*>(binding.ptr);
+		else if (binding.type == Type::INT)
+			TypeConverter<int, String>::Convert(*static_cast<const int*>(binding.ptr), out_value);
+		else
+		{
+			RMLUI_ERRORMSG("TODO: Implementation for the provided binding type has not been made yet.");
+			result = false;
+		}
+
+		return result;
+	}
+	else
+	{
+		Log::Message(Log::LT_WARNING, "Could not find value named '%s' in data model.", name.c_str());
+	}
+
+	return false;
+}
+
+}
+}

+ 19 - 1
Source/Core/XMLNodeHandlerDefault.cpp

@@ -61,7 +61,7 @@ Element* XMLNodeHandlerDefault::ElementStart(XMLParser* parser, const String& na
 		return nullptr;
 	}
 
-	// Add the element to its parent and remove the reference
+	// Move and append the element to the parent
 	Element* result = parent->AppendChild(std::move(element));
 
 	return result;
@@ -82,6 +82,24 @@ bool XMLNodeHandlerDefault::ElementData(XMLParser* parser, const String& data)
 	// Determine the parent
 	Element* parent = parser->GetParseFrame()->element;
 
+	if (DataModel* data_model = parser->GetDataModel())
+	{
+		size_t i_open = data.find("{{", 0);
+
+		if (parent && i_open != String::npos)
+		{
+			DataViewText data_view(parent, data, i_open);
+			if (data_view)
+			{
+				data_model->views.AddTextView(std::move(data_view));
+				return true;
+			}
+
+			Log::Message(Log::LT_WARNING, "Could not add data binding view to element '%s'.", parent->GetAddress().c_str());
+		}
+	}
+
+
 	// Parse the text into the element
 	return Factory::InstanceElementText(parent, data);
 }

+ 23 - 0
Source/Core/XMLParser.cpp

@@ -55,6 +55,7 @@ XMLParser::XMLParser(Element* root)
 	stack.push(frame);
 
 	active_handler = nullptr;
+	active_data_model = nullptr;
 
 	header = new DocumentHeader();
 }
@@ -120,6 +121,11 @@ const XMLParser::ParseFrame* XMLParser::GetParseFrame() const
 	return &stack.top();
 }
 
+DataModel* XMLParser::GetDataModel() const
+{
+	return active_data_model;
+}
+
 /// Called when the parser finds the beginning of an element tag.
 void XMLParser::HandleElementStart(const String& _name, const XMLAttributes& attributes)
 {
@@ -142,12 +148,27 @@ void XMLParser::HandleElementStart(const String& _name, const XMLAttributes& att
 		element = node_handler->ElementStart(this, name, attributes);
 	}
 
+	static const String data_model = "data-model";
+	auto it = attributes.find(data_model);
+	if (element && it != attributes.end())
+	{
+		String data_model = it->second.Get<String>();
+
+		active_data_model = nullptr;
+		if (auto context = element->GetContext())
+			active_data_model = context->GetDataModel( data_model );
+
+		if(!active_data_model)
+			Log::Message(Log::LT_WARNING, "Could not locate data model '%s'.", data_model.c_str());
+	}
+
 	// Push onto the stack
 	ParseFrame frame;
 	frame.node_handler = node_handler;
 	frame.child_handler = active_handler;
 	frame.element = (element ? element : stack.top().element);
 	frame.tag = name;
+	frame.data_model = active_data_model;
 	stack.push(frame);
 }
 
@@ -163,6 +184,8 @@ void XMLParser::HandleElementEnd(const String& _name)
 	stack.pop();
 	// Restore active handler to the previous frame's child handler
 	active_handler = stack.top().child_handler;	
+	// Restore the active data model to the current frame's model
+	active_data_model = stack.top().data_model;
 
 	// Check frame names
 	if (name != frame.tag)