浏览代码

Basic implementation of data binding (WIP)

Michael Ragazzon 6 年之前
父节点
当前提交
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/ContextInstancer.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ConvolutionFilter.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ConvolutionFilter.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Core.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/Debug.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Decorator.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Decorator.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DecoratorInstancer.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/ContextInstancerDefault.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/ConvolutionFilter.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/ConvolutionFilter.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Core.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/Decorator.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorGradient.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorGradient.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorInstancer.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorInstancer.cpp

+ 1 - 0
Include/RmlUi/Core.h

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

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

@@ -34,6 +34,7 @@
 #include "Traits.h"
 #include "Traits.h"
 #include "Input.h"
 #include "Input.h"
 #include "ScriptInterface.h"
 #include "ScriptInterface.h"
+#include "DataBinding.h"
 
 
 namespace Rml {
 namespace Rml {
 namespace Core {
 namespace Core {
@@ -218,6 +219,24 @@ public:
 	/// @param[in] instancer The context's instancer.
 	/// @param[in] instancer The context's instancer.
 	void SetInstancer(ContextInstancer* 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:
 protected:
 	void Release() override;
 	void Release() override;
 
 
@@ -286,6 +305,9 @@ private:
 	Vector2i clip_origin;
 	Vector2i clip_origin;
 	Vector2i clip_dimensions;
 	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.
 	// Internal callback for when an element is detached or removed from the hierarchy.
 	void OnElementDetach(Element* element);
 	void OnElementDetach(Element* element);
 	// Internal callback for when a new element gains focus.
 	// 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 Element;
 class XMLNodeHandler;
 class XMLNodeHandler;
 class URL;
 class URL;
+class DataModel;
 
 
 /**
 /**
 	RmlUi's XML parsing engine. The factory creates an instance of this class for each RML parse.
 	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.
 		// The default handler used for this frame's children.
 		XMLNodeHandler* child_handler;
 		XMLNodeHandler* child_handler;
+
+		DataModel* data_model;
 	};
 	};
 
 
 	/// Pushes an element handler onto the parse stack for parsing child elements.
 	/// Pushes an element handler onto the parse stack for parsing child elements.
@@ -95,6 +98,9 @@ public:
 	/// @return The parser's current parse frame.
 	/// @return The parser's current parse frame.
 	const ParseFrame* GetParseFrame() const;
 	const ParseFrame* GetParseFrame() const;
 
 
+	/// Get the data model name for the current frame.
+	DataModel* GetDataModel() const;
+
 protected:
 protected:
 	/// Called when the parser finds the beginning of an element tag.
 	/// Called when the parser finds the beginning of an element tag.
 	void HandleElementStart(const String& name, const XMLAttributes& attributes) override;
 	void HandleElementStart(const String& name, const XMLAttributes& attributes) override;
@@ -110,6 +116,8 @@ private:
 	// The active node handler.
 	// The active node handler.
 	XMLNodeHandler* active_handler;
 	XMLNodeHandler* active_handler;
 
 
+	DataModel* active_data_model;
+
 	// The parser stack.
 	// The parser stack.
 	using ParserStack = std::stack< ParseFrame >;
 	using ParserStack = std::stack< ParseFrame >;
 	ParserStack stack;
 	ParserStack stack;

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

@@ -162,9 +162,9 @@ form h2
 <body template="window">
 <body template="window">
 <tabset id="menu">
 <tabset id="menu">
 <tab>Welcome</tab>
 <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 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>
 </panel>
 <tab>Decorators</tab>
 <tab>Decorators</tab>
 <panel id="decorators">
 <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;
 Rml::Core::Context* context = nullptr;
 ShellRenderInterfaceExtensions *shell_renderer;
 ShellRenderInterfaceExtensions *shell_renderer;
 std::unique_ptr<DemoWindow> demo_window;
 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.
 	// Create the main RmlUi context and set it on the shell's input layer.
 	context = Rml::Core::CreateContext("main", Rml::Core::Vector2i(width, height));
 	context = Rml::Core::CreateContext("main", Rml::Core::Vector2i(width, height));
-	if (context == nullptr)
+
+	if (!context || !SetupDataBinding(context))
 	{
 	{
 		Rml::Core::Shutdown();
 		Rml::Core::Shutdown();
 		Shell::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::Keydown, demo_window.get());
 	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keyup, demo_window.get());
 	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keyup, demo_window.get());
 
 
+	my_model.UpdateViews();
+
 	Shell::EventLoop(GameLoop);
 	Shell::EventLoop(GameLoop);
 
 
 	demo_window->Shutdown();
 	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;
 		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));
 	Element* result = parent->AppendChild(std::move(element));
 
 
 	return result;
 	return result;
@@ -82,6 +82,24 @@ bool XMLNodeHandlerDefault::ElementData(XMLParser* parser, const String& data)
 	// Determine the parent
 	// Determine the parent
 	Element* parent = parser->GetParseFrame()->element;
 	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
 	// Parse the text into the element
 	return Factory::InstanceElementText(parent, data);
 	return Factory::InstanceElementText(parent, data);
 }
 }

+ 23 - 0
Source/Core/XMLParser.cpp

@@ -55,6 +55,7 @@ XMLParser::XMLParser(Element* root)
 	stack.push(frame);
 	stack.push(frame);
 
 
 	active_handler = nullptr;
 	active_handler = nullptr;
+	active_data_model = nullptr;
 
 
 	header = new DocumentHeader();
 	header = new DocumentHeader();
 }
 }
@@ -120,6 +121,11 @@ const XMLParser::ParseFrame* XMLParser::GetParseFrame() const
 	return &stack.top();
 	return &stack.top();
 }
 }
 
 
+DataModel* XMLParser::GetDataModel() const
+{
+	return active_data_model;
+}
+
 /// Called when the parser finds the beginning of an element tag.
 /// Called when the parser finds the beginning of an element tag.
 void XMLParser::HandleElementStart(const String& _name, const XMLAttributes& attributes)
 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);
 		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
 	// Push onto the stack
 	ParseFrame frame;
 	ParseFrame frame;
 	frame.node_handler = node_handler;
 	frame.node_handler = node_handler;
 	frame.child_handler = active_handler;
 	frame.child_handler = active_handler;
 	frame.element = (element ? element : stack.top().element);
 	frame.element = (element ? element : stack.top().element);
 	frame.tag = name;
 	frame.tag = name;
+	frame.data_model = active_data_model;
 	stack.push(frame);
 	stack.push(frame);
 }
 }
 
 
@@ -163,6 +184,8 @@ void XMLParser::HandleElementEnd(const String& _name)
 	stack.pop();
 	stack.pop();
 	// Restore active handler to the previous frame's child handler
 	// Restore active handler to the previous frame's child handler
 	active_handler = stack.top().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
 	// Check frame names
 	if (name != frame.tag)
 	if (name != frame.tag)