diff --git a/CMake/FileList.cmake b/CMake/FileList.cmake index e06a197ff..916e1543d 100644 --- a/CMake/FileList.cmake +++ b/CMake/FileList.cmake @@ -4,6 +4,9 @@ set(Core_HDR_FILES ${PROJECT_SOURCE_DIR}/Source/Core/Clock.h ${PROJECT_SOURCE_DIR}/Source/Core/ComputeProperty.h ${PROJECT_SOURCE_DIR}/Source/Core/ContextInstancerDefault.h + ${PROJECT_SOURCE_DIR}/Source/Core/DataControllerDefault.h + ${PROJECT_SOURCE_DIR}/Source/Core/DataExpression.h + ${PROJECT_SOURCE_DIR}/Source/Core/DataViewDefault.h ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorGradient.h ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorNinePatch.h ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorTiled.h @@ -107,6 +110,12 @@ set(Core_PUB_HDR_FILES ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ContextInstancer.h ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ConvolutionFilter.h ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Core.h + ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataController.h + ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataModel.h + ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataTypeRegister.h + ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataTypes.h + ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataVariable.h + ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DataView.h ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Debug.h ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Decorator.h ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/DecoratorInstancer.h @@ -191,6 +200,14 @@ set(Core_SRC_FILES ${PROJECT_SOURCE_DIR}/Source/Core/ContextInstancerDefault.cpp ${PROJECT_SOURCE_DIR}/Source/Core/ConvolutionFilter.cpp ${PROJECT_SOURCE_DIR}/Source/Core/Core.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataController.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataControllerDefault.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataExpression.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataModel.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataTypeRegister.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataVariable.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataView.cpp + ${PROJECT_SOURCE_DIR}/Source/Core/DataViewDefault.cpp ${PROJECT_SOURCE_DIR}/Source/Core/Decorator.cpp ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorGradient.cpp ${PROJECT_SOURCE_DIR}/Source/Core/DecoratorInstancer.cpp diff --git a/CMake/SampleFileList.cmake b/CMake/SampleFileList.cmake index cf79d4211..3c532627f 100644 --- a/CMake/SampleFileList.cmake +++ b/CMake/SampleFileList.cmake @@ -53,6 +53,13 @@ set(customlog_SRC_FILES ${PROJECT_SOURCE_DIR}/Samples/basic/customlog/src/SystemInterface.cpp ) +set(databinding_HDR_FILES +) + +set(databinding_SRC_FILES + ${PROJECT_SOURCE_DIR}/Samples/basic/databinding/src/main.cpp +) + set(demo_HDR_FILES ) diff --git a/CMake/gen_samplelists.sh b/CMake/gen_samplelists.sh index c30e5834b..51c56fe73 100755 --- a/CMake/gen_samplelists.sh +++ b/CMake/gen_samplelists.sh @@ -7,7 +7,7 @@ hdr='set(sample_HDR_FILES' srcdir='${PROJECT_SOURCE_DIR}' srcpath=Samples samples=( 'shell' - 'basic/animation' 'basic/benchmark' 'basic/bitmapfont' 'basic/customlog' 'basic/demo' 'basic/drag' 'basic/loaddocument' 'basic/treeview' 'basic/transform' + 'basic/animation' 'basic/benchmark' 'basic/bitmapfont' 'basic/customlog' 'basic/databinding' 'basic/demo' 'basic/drag' 'basic/loaddocument' 'basic/treeview' 'basic/transform' 'basic/sdl2' 'basic/sfml2' 'tutorial/template' 'tutorial/datagrid' 'tutorial/datagrid_tree' 'tutorial/drag' 'invaders' 'luainvaders' diff --git a/CMakeLists.txt b/CMakeLists.txt index b0dcc11e9..491cb73a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -468,7 +468,7 @@ endmacro() if(BUILD_SAMPLES) include(SampleFileList) - set(samples treeview customlog drag loaddocument transform bitmapfont animation benchmark demo) + set(samples treeview customlog drag loaddocument transform bitmapfont animation benchmark demo databinding) set(tutorials template datagrid datagrid_tree drag) if(NOT BUILD_FRAMEWORK) @@ -668,6 +668,9 @@ if(BUILD_SAMPLES) install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/bitmapfont/data DESTINATION ${SAMPLES_DIR}/basic/bitmapfont ) + install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/databinding/data + DESTINATION ${SAMPLES_DIR}/basic/databinding + ) install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/demo/data DESTINATION ${SAMPLES_DIR}/basic/demo ) diff --git a/Include/RmlUi/Core.h b/Include/RmlUi/Core.h index 3381ed420..f80ac87b9 100644 --- a/Include/RmlUi/Core.h +++ b/Include/RmlUi/Core.h @@ -39,6 +39,12 @@ #include "Core/ComputedValues.h" #include "Core/Context.h" #include "Core/ContextInstancer.h" +#include "Core/DataController.h" +#include "Core/DataModel.h" +#include "Core/DataTypeRegister.h" +#include "Core/DataTypes.h" +#include "Core/DataVariable.h" +#include "Core/DataView.h" #include "Core/Decorator.h" #include "Core/DecoratorInstancer.h" #include "Core/Element.h" diff --git a/Include/RmlUi/Core/BaseXMLParser.h b/Include/RmlUi/Core/BaseXMLParser.h index b47682de1..e3b5ae3e7 100644 --- a/Include/RmlUi/Core/BaseXMLParser.h +++ b/Include/RmlUi/Core/BaseXMLParser.h @@ -37,6 +37,10 @@ namespace Rml { namespace Core { class Stream; +class URL; +using XMLAttributes = Dictionary; + +enum class XMLDataType { Text, CData, InnerXML }; /** @author Peter Curry @@ -53,6 +57,14 @@ class RMLUICORE_API BaseXMLParser /// @param[in] tag The tag to register as containing generic character data. void RegisterCDATATag(const String& tag); + /// When an XML attribute with the given name is encountered during parsing, then all content below the current + /// node is treated as data. + /// @note While children nodes are treated as data (text), it is assumed that the content represents valid XML. + /// The parsing proceeds as normal except that the Handle...() functions are not called until the + /// starting node is closed. Then, all its contents are submitted as Data (raw text string). + /// @note In particular, this behavior is useful for some data-binding views. + void RegisterInnerXMLAttribute(const String& attribute_name); + /// Parses the given stream as an XML file, and calls the handlers when /// interesting phenomena are encountered. void Parse(Stream* stream); @@ -68,20 +80,31 @@ class RMLUICORE_API BaseXMLParser /// Called when the parser finds the end of an element tag. virtual void HandleElementEnd(const String& name); /// Called when the parser encounters data. - virtual void HandleData(const String& data); + virtual void HandleData(const String& data, XMLDataType type); protected: - // The stream we're reading the XML from. - Stream* xml_source; + const URL* GetSourceURLPtr() const; private: + const URL* source_url = nullptr; + String xml_source; + size_t xml_index = 0; + + void Next(); + bool AtEnd() const; + char Look() const; + + void HandleElementStartInternal(const String& name, const XMLAttributes& attributes); + void HandleElementEndInternal(const String& name); + void HandleDataInternal(const String& data, XMLDataType type); + void ReadHeader(); void ReadBody(); - bool ReadOpenTag(); - bool ReadCloseTag(); - bool ReadAttributes(XMLAttributes& attributes); - bool ReadCDATA(const char* terminator = nullptr); + + bool ReadCloseTag(size_t xml_index_tag); + bool ReadAttributes(XMLAttributes& attributes, bool& parse_raw_xml_content); + bool ReadCDATA(const char* tag_terminator = nullptr); // Reads from the stream until a complete word is found. // @param[out] word Word thats been found @@ -89,22 +112,20 @@ class RMLUICORE_API BaseXMLParser bool FindWord(String& word, const char* terminators = nullptr); // Reads from the stream until the given character set is found. All // intervening characters will be returned in data. - bool FindString(const unsigned char* string, String& data); + bool FindString(const char* string, String& data, bool escape_brackets = false); // Returns true if the next sequence of characters in the stream // matches the given string. If consume is set and this returns true, // the characters will be consumed. - bool PeekString(const unsigned char* string, bool consume = true); + bool PeekString(const char* string, bool consume = true); - // Fill the buffer as much as possible, without removing any content that is still pending - bool FillBuffer(); + int line_number = 0; + int line_number_open_tag = 0; + int open_tag_depth = 0; - unsigned char* read; - unsigned char* buffer; - int buffer_size; - int buffer_used; - int line_number; - int line_number_open_tag; - int open_tag_depth; + // Enabled when an attribute for inner xml data is encountered (see description in Register...() above). + bool inner_xml_data = false; + int inner_xml_data_terminate_depth = 0; + size_t inner_xml_data_index_begin = 0; // The element attributes being read. XMLAttributes attributes; @@ -112,6 +133,7 @@ class RMLUICORE_API BaseXMLParser String data; SmallUnorderedSet< String > cdata_tags; + SmallUnorderedSet< String > attributes_for_inner_xml_data; }; } diff --git a/Include/RmlUi/Core/Containers/chobo/flat_map.hpp b/Include/RmlUi/Core/Containers/chobo/flat_map.hpp index 85fdc6d08..c53f326ce 100644 --- a/Include/RmlUi/Core/Containers/chobo/flat_map.hpp +++ b/Include/RmlUi/Core/Containers/chobo/flat_map.hpp @@ -254,7 +254,7 @@ class flat_map template std::pair emplace(Args&&... args) { - value_type val(args...); + value_type val(std::forward(args)...); return insert(std::move(val)); } diff --git a/Include/RmlUi/Core/Containers/chobo/flat_set.hpp b/Include/RmlUi/Core/Containers/chobo/flat_set.hpp index a94776833..28676e92c 100644 --- a/Include/RmlUi/Core/Containers/chobo/flat_set.hpp +++ b/Include/RmlUi/Core/Containers/chobo/flat_set.hpp @@ -238,7 +238,7 @@ class flat_set template std::pair emplace(Args&&... args) { - value_type val(args...); + value_type val(std::forward(args)...); return insert(std::move(val)); } diff --git a/Include/RmlUi/Core/Context.h b/Include/RmlUi/Core/Context.h index e98baa8f9..42a563a4b 100644 --- a/Include/RmlUi/Core/Context.h +++ b/Include/RmlUi/Core/Context.h @@ -43,6 +43,9 @@ class ContextInstancer; class ElementDocument; class EventListener; class RenderInterface; +class DataModel; +class DataModelConstructor; +class DataTypeRegister; enum class EventId : uint16_t; /** @@ -218,6 +221,25 @@ class RMLUICORE_API Context : public ScriptInterface /// @param[in] instancer The context's instancer. void SetInstancer(ContextInstancer* instancer); + /// Creates a data model. + /// The returned constructor can be used to bind data variables. Elements can bind to the model using the attribute 'data-model="name"'. + /// @param[in] name The name of the data model. + /// @return A constructor for the data model, or empty if it could not be created. + DataModelConstructor CreateDataModel(const String& name); + + /// Retrieves the constructor for an existing data model. + /// The returned constructor can be used to add additional bindings to an existing model. + /// @param[in] name The name of the data model. + /// @return A constructor for the data model, or empty if it could not be found. + DataModelConstructor GetDataModel(const String& name); + + /// Removes the given data model. + /// This also removes all data views, controllers and bindings contained by the data model. + /// @warning Invalidates all handles and constructors pointing to the data model. + /// @param[in] name The name of the data model. + /// @return True if succesfully removed, false if no data model was found. + bool RemoveDataModel(const String& name); + protected: void Release() override; @@ -286,6 +308,11 @@ class RMLUICORE_API Context : public ScriptInterface Vector2i clip_origin; Vector2i clip_dimensions; + using DataModels = UnorderedMap>; + DataModels data_models; + + UniquePtr data_type_register; + // Internal callback for when an element is detached or removed from the hierarchy. void OnElementDetach(Element* element); // Internal callback for when a new element gains focus. @@ -297,13 +324,14 @@ class RMLUICORE_API Context : public ScriptInterface // Updates the current hover elements, sending required events. void UpdateHoverChain(const Dictionary& parameters, const Dictionary& drag_parameters, const Vector2i& old_mouse_position); - // Creates the drag clone from the given element. The old drag clone will be released if - // necessary. - // @param[in] element The element to clone. + // Creates the drag clone from the given element. The old drag clone will be released if necessary. void CreateDragClone(Element* element); // Releases the drag clone, if one exists. void ReleaseDragClone(); + // Returns the data model with the provided name, or nullptr if it does not exist. + DataModel* GetDataModelPtr(const String& name) const; + // Builds the parameters for a generic key event. void GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier); // Builds the parameters for a generic mouse event. diff --git a/Include/RmlUi/Core/DataController.h b/Include/RmlUi/Core/DataController.h new file mode 100644 index 000000000..3fc5fe81b --- /dev/null +++ b/Include/RmlUi/Core/DataController.h @@ -0,0 +1,123 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATACONTROLLER_H +#define RMLUICOREDATACONTROLLER_H + +#include "Header.h" +#include "Types.h" +#include "Traits.h" +#include + +namespace Rml { +namespace Core { + +class Element; +class DataModel; + + +class RMLUICORE_API DataControllerInstancer : public NonCopyMoveable { +public: + DataControllerInstancer() {} + virtual ~DataControllerInstancer() {} + virtual DataControllerPtr InstanceController(Element* element) = 0; +}; + +template +class DataControllerInstancerDefault final : public DataControllerInstancer { +public: + DataControllerPtr InstanceController(Element* element) override { + return DataControllerPtr(new T(element)); + } +}; + + +/** + Data controller. + + Data controllers are used to respond to some change in the document, + usually by setting data variables. Such document changes are usually + a result of user input. + A data controller is declared in the document by the element attribute: + + data-[type]-[modifier]="[assignment_expression]" + + This is similar to declaration of data views, except that controllers + instead take an assignment expression to set a variable. Note that, as + opposed to views, controllers only respond to certain changes in the + document, not to changed data variables. + + The modifier may or may not be required depending on the data controller. + + */ + +class RMLUICORE_API DataController : public Releasable { +public: + virtual ~DataController(); + + // Initialize the data controller. + // @param[in] model The data model the controller will be attached to. + // @param[in] element The element which spawned the controller. + // @param[in] expression The value of the element's 'data-' attribute which spawned the controller (see above). + // @param[in] modifier The modifier for the given controller type (see above). + // @return True on success. + virtual bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) = 0; + + // Returns the attached element if it still exists. + Element* GetElement() const; + + // Returns true if the element still exists. + bool IsValid() const; + +protected: + DataController(Element* element); + +private: + ObserverPtr attached_element; +}; + + +class RMLUICORE_API DataControllers : NonCopyMoveable { +public: + DataControllers(); + ~DataControllers(); + + void Add(DataControllerPtr controller); + + void OnElementRemove(Element* element); + +private: + using ElementControllersMap = std::unordered_multimap; + ElementControllersMap controllers; +}; + + +} +} + +#endif diff --git a/Include/RmlUi/Core/DataModel.h b/Include/RmlUi/Core/DataModel.h new file mode 100644 index 000000000..f7b16e942 --- /dev/null +++ b/Include/RmlUi/Core/DataModel.h @@ -0,0 +1,195 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATAMODEL_H +#define RMLUICOREDATAMODEL_H + +#include "Header.h" +#include "Types.h" +#include "Traits.h" +#include "DataTypes.h" +#include "DataTypeRegister.h" + +namespace Rml { +namespace Core { + +class DataViews; +class DataControllers; +class Element; + + +class RMLUICORE_API DataModel : NonCopyMoveable { +public: + DataModel(const TransformFuncRegister* transform_register = nullptr); + ~DataModel(); + + void AddView(DataViewPtr view); + void AddController(DataControllerPtr controller); + + bool BindVariable(const String& name, DataVariable variable); + bool BindFunc(const String& name, DataGetFunc get_func, DataSetFunc set_func); + + bool BindEventCallback(const String& name, DataEventFunc event_func); + + bool InsertAlias(Element* element, const String& alias_name, DataAddress replace_with_address); + bool EraseAliases(Element* element); + + DataAddress ResolveAddress(const String& address_str, Element* element) const; + const DataEventFunc* GetEventCallback(const String& name); + + DataVariable GetVariable(const DataAddress& address) const; + bool GetVariableInto(const DataAddress& address, Variant& out_value) const; + + void DirtyVariable(const String& variable_name); + bool IsVariableDirty(const String& variable_name) const; + + bool CallTransform(const String& name, Variant& inout_result, const VariantList& arguments) const; + + // Elements declaring 'data-model' need to be attached. + void AttachModelRootElement(Element* element); + ElementList GetAttachedModelRootElements() const; + + void OnElementRemove(Element* element); + + bool Update(); + +private: + UniquePtr views; + UniquePtr controllers; + + UnorderedMap variables; + DirtyVariables dirty_variables; + + UnorderedMap> function_variable_definitions; + UnorderedMap event_callbacks; + + using ScopedAliases = UnorderedMap>; + ScopedAliases aliases; + + const TransformFuncRegister* transform_register; + + SmallUnorderedSet attached_elements; +}; + + + +class RMLUICORE_API DataModelHandle { +public: + DataModelHandle(DataModel* model = nullptr) : model(model) + {} + + void Update() { + model->Update(); + } + + bool IsVariableDirty(const String& variable_name) { + return model->IsVariableDirty(variable_name); + } + void DirtyVariable(const String& variable_name) { + model->DirtyVariable(variable_name); + } + + explicit operator bool() { return model; } + +private: + DataModel* model; +}; + + +class RMLUICORE_API DataModelConstructor { +public: + template + using DataEventMemberFunc = void(T::*)(DataModelHandle, Event&, const VariantList&); + + DataModelConstructor() : model(nullptr), type_register(nullptr) {} + DataModelConstructor(DataModel* model, DataTypeRegister* type_register) : model(model), type_register(type_register) { + RMLUI_ASSERT(model && type_register); + } + + // Return a handle to the data model being constructed, which can later be used to synchronize variables and update the model. + DataModelHandle GetModelHandle() const { + return DataModelHandle(model); + } + + // Bind a data variable. + // @note For non-scalar types make sure they first have been registered with the appropriate 'Register...()' functions. + template bool Bind(const String& name, T* ptr) { + RMLUI_ASSERTMSG(ptr, "Invalid pointer to data variable"); + return model->BindVariable(name, DataVariable(type_register->GetOrAddScalar(), ptr)); + } + + // Bind a get/set function pair. + bool BindFunc(const String& name, DataGetFunc get_func, DataSetFunc set_func = {}) { + return model->BindFunc(name, std::move(get_func), std::move(set_func)); + } + + // Bind an event callback. + bool BindEventCallback(const String& name, DataEventFunc event_func) { + return model->BindEventCallback(name, std::move(event_func)); + } + // Convenience wrapper around BindEventCallback for member functions. + template + bool BindEventCallback(const String& name, DataEventMemberFunc member_func, T* object_pointer) { + return BindEventCallback(name, std::bind(member_func, object_pointer, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + } + + // Register a struct type. + // @note The type applies to every data model associated with the current Context. + // @return A handle which can be used to register struct members. + template + StructHandle RegisterStruct() { + return type_register->RegisterStruct(); + } + + // Register an array type. + // @note The type applies to every data model associated with the current Context. + // @note If 'Container::value_type' represents a non-scalar type, that type must already have been registered with the appropriate 'Register...()' functions. + // @note Container requires the following functions to be implemented: size() and begin(). This is satisfied by several containers such as std::vector and std::array. + template + bool RegisterArray() { + return type_register->RegisterArray(); + } + + // Register a transform function. + // A transform function modifies a variant with optional arguments. It can be called in data expressions using the pipe '|' operator. + // @note The transform function applies to every data model associated with the current Context. + void RegisterTransformFunc(const String& name, DataTransformFunc transform_func) { + type_register->GetTransformFuncRegister()->Register(name, std::move(transform_func)); + } + + explicit operator bool() { return model && type_register; } + +private: + DataModel* model; + DataTypeRegister* type_register; +}; + +} +} + +#endif diff --git a/Include/RmlUi/Core/DataTypeRegister.h b/Include/RmlUi/Core/DataTypeRegister.h new file mode 100644 index 000000000..a4f68abaa --- /dev/null +++ b/Include/RmlUi/Core/DataTypeRegister.h @@ -0,0 +1,208 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATATYPEREGISTER_H +#define RMLUICOREDATATYPEREGISTER_H + +#include "Header.h" +#include "Types.h" +#include "Traits.h" +#include "Variant.h" +#include "DataTypes.h" +#include "DataVariable.h" + + +namespace Rml { +namespace Core { + + +template +struct is_valid_data_scalar { + static constexpr bool value = std::is_arithmetic::value + || std::is_same::type, String>::value; +}; + + +template +class StructHandle { +public: + StructHandle(DataTypeRegister* type_register, StructDefinition* struct_definition) : type_register(type_register), struct_definition(struct_definition) {} + + template + StructHandle& RegisterMember(const String& name, MemberType Object::* member_ptr); + + StructHandle& RegisterMemberFunc(const String& name, MemberGetFunc get_func, MemberSetFunc set_func = nullptr); + + explicit operator bool() const { + return type_register && struct_definition; + } + +private: + DataTypeRegister* type_register; + StructDefinition* struct_definition; +}; + + +class RMLUICORE_API TransformFuncRegister { +public: + void Register(const String& name, DataTransformFunc transform_func); + + bool Call(const String& name, Variant& inout_result, const VariantList& arguments) const; + +private: + UnorderedMap transform_functions; +}; + + + +class RMLUICORE_API DataTypeRegister : NonCopyMoveable { +public: + DataTypeRegister(); + ~DataTypeRegister(); + + template + StructHandle RegisterStruct() + { + static_assert(std::is_class::value, "Type must be a struct or class type."); + FamilyId id = Family::Id(); + + auto struct_variable = std::make_unique(); + StructDefinition* struct_variable_raw = struct_variable.get(); + + bool inserted = type_register.emplace(id, std::move(struct_variable)).second; + if (!inserted) + { + RMLUI_ERRORMSG("Type already declared"); + return StructHandle(nullptr, nullptr); + } + + return StructHandle(this, struct_variable_raw); + } + + template + bool RegisterArray() + { + using value_type = typename Container::value_type; + VariableDefinition* value_variable = GetOrAddScalar(); + RMLUI_ASSERTMSG(value_variable, "Underlying value type of array has not been registered."); + if (!value_variable) + return false; + + FamilyId container_id = Family::Id(); + + auto array_variable = std::make_unique>(value_variable); + + bool inserted = type_register.emplace(container_id, std::move(array_variable)).second; + if (!inserted) + { + RMLUI_ERRORMSG("Array type already declared."); + return false; + } + + return true; + } + + template + VariableDefinition* RegisterMemberFunc(MemberGetFunc get_func, MemberSetFunc set_func) + { + FamilyId id = Family>::Id(); + + auto result = type_register.emplace(id, nullptr); + auto& it = result.first; + bool inserted = result.second; + + if (inserted) + it->second = std::make_unique>(get_func, set_func); + + return it->second.get(); + } + + template::value, int>::type = 0> + VariableDefinition* GetOrAddScalar() + { + FamilyId id = Family::Id(); + + auto result = type_register.emplace(id, nullptr); + bool inserted = result.second; + UniquePtr& definition = result.first->second; + + if (inserted) + definition = std::make_unique>(); + + return definition.get(); + } + + template::value, int>::type = 0> + VariableDefinition* GetOrAddScalar() + { + return Get(); + } + + template + VariableDefinition* Get() + { + FamilyId id = Family::Id(); + auto it = type_register.find(id); + if (it == type_register.end()) + { + RMLUI_ERRORMSG("Desired data type T not registered with the type register, please use the 'Register...()' functions before binding values, adding members, or registering arrays of non-scalar types.") + return nullptr; + } + + return it->second.get(); + } + + TransformFuncRegister* GetTransformFuncRegister() { + return &transform_register; + } + +private: + UnorderedMap> type_register; + + TransformFuncRegister transform_register; + +}; + +template +template +inline StructHandle& StructHandle::RegisterMember(const String& name, MemberType Object::* member_ptr) { + VariableDefinition* member_type = type_register->GetOrAddScalar(); + struct_definition->AddMember(name, std::make_unique>(member_type, member_ptr)); + return *this; +} +template +inline StructHandle& StructHandle::RegisterMemberFunc(const String& name, MemberGetFunc get_func, MemberSetFunc set_func) { + VariableDefinition* definition = type_register->RegisterMemberFunc(get_func, set_func); + struct_definition->AddMember(name, std::make_unique(definition)); + return *this; +} + +} +} + +#endif diff --git a/Include/RmlUi/Core/DataTypes.h b/Include/RmlUi/Core/DataTypes.h new file mode 100644 index 000000000..f17b20f65 --- /dev/null +++ b/Include/RmlUi/Core/DataTypes.h @@ -0,0 +1,66 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATADEFINITIONS_H +#define RMLUICOREDATADEFINITIONS_H + +#include "Header.h" +#include "Types.h" +#include + +namespace Rml { +namespace Core { + +class VariableDefinition; +class DataTypeRegister; +class TransformFuncRegister; +class DataModelHandle; +class DataVariable; + +using DataGetFunc = std::function; +using DataSetFunc = std::function; +using DataTransformFunc = std::function; +using DataEventFunc = std::function; + +template using MemberGetFunc = void(T::*)(Variant&); +template using MemberSetFunc = void(T::*)(const Variant&); + +using DirtyVariables = SmallUnorderedSet; + +struct DataAddressEntry { + DataAddressEntry(String name) : name(name), index(-1) { } + DataAddressEntry(int index) : index(index) { } + String name; + int index; +}; +using DataAddress = std::vector; + +} +} + +#endif diff --git a/Include/RmlUi/Core/DataVariable.h b/Include/RmlUi/Core/DataVariable.h new file mode 100644 index 000000000..a4efd1eb8 --- /dev/null +++ b/Include/RmlUi/Core/DataVariable.h @@ -0,0 +1,269 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATAVARIABLE_H +#define RMLUICOREDATAVARIABLE_H + +#include "Header.h" +#include "Types.h" +#include "Traits.h" +#include "Variant.h" +#include "DataTypes.h" +#include + +namespace Rml { +namespace Core { + + +enum class DataVariableType { Scalar, Array, Struct, Function, MemberFunction }; + + +class RMLUICORE_API DataVariable { +public: + DataVariable() {} + DataVariable(VariableDefinition* definition, void* ptr) : definition(definition), ptr(ptr) {} + + explicit operator bool() const { return definition; } + + bool Get(Variant& variant); + bool Set(const Variant& variant); + int Size(); + DataVariable Child(const DataAddressEntry& address); + DataVariableType Type(); + +private: + VariableDefinition* definition = nullptr; + void* ptr = nullptr; +}; + + + +class RMLUICORE_API VariableDefinition { +public: + virtual ~VariableDefinition() = default; + DataVariableType Type() const { return type; } + + virtual bool Get(void* ptr, Variant& variant); + virtual bool Set(void* ptr, const Variant& variant); + + virtual int Size(void* ptr); + virtual DataVariable Child(void* ptr, const DataAddressEntry& address); + +protected: + VariableDefinition(DataVariableType type) : type(type) {} + +private: + DataVariableType type; +}; + + +RMLUICORE_API DataVariable MakeLiteralIntVariable(int value); + + +template +class ScalarDefinition final : public VariableDefinition { +public: + ScalarDefinition() : VariableDefinition(DataVariableType::Scalar) {} + + bool Get(void* ptr, Variant& variant) override + { + variant = *static_cast(ptr); + return true; + } + bool Set(void* ptr, const Variant& variant) override + { + return variant.GetInto(*static_cast(ptr)); + } +}; + + +class FuncDefinition final : public VariableDefinition { +public: + + FuncDefinition(DataGetFunc get, DataSetFunc set) : VariableDefinition(DataVariableType::Function), get(std::move(get)), set(std::move(set)) {} + + bool Get(void* /*ptr*/, Variant& variant) override + { + if (!get) + return false; + get(variant); + return true; + } + bool Set(void* /*ptr*/, const Variant& variant) override + { + if (!set) + return false; + set(variant); + return true; + } +private: + DataGetFunc get; + DataSetFunc set; +}; + + +template +class ArrayDefinition final : public VariableDefinition { +public: + ArrayDefinition(VariableDefinition* underlying_definition) : VariableDefinition(DataVariableType::Array), underlying_definition(underlying_definition) {} + + int Size(void* ptr) override { + return int(static_cast(ptr)->size()); + } + +protected: + DataVariable Child(void* void_ptr, const DataAddressEntry& address) override + { + Container* ptr = static_cast(void_ptr); + const int index = address.index; + + const int container_size = int(ptr->size()); + if (index < 0 || index >= container_size) + { + if (address.name == "size") + return MakeLiteralIntVariable(container_size); + + Log::Message(Log::LT_WARNING, "Data array index out of bounds."); + return DataVariable(); + } + + auto it = ptr->begin(); + std::advance(it, index); + + void* next_ptr = &(*it); + return DataVariable(underlying_definition, next_ptr); + } + +private: + VariableDefinition* underlying_definition; +}; + + +class StructMember { +public: + StructMember(VariableDefinition* definition) : definition(definition) {} + virtual ~StructMember() = default; + + VariableDefinition* GetDefinition() const { return definition; } + + virtual void* GetPointer(void* base_ptr) = 0; + +private: + VariableDefinition* definition; +}; + +template +class StructMemberObject final : public StructMember { +public: + StructMemberObject(VariableDefinition* definition, MemberType Object::* member_ptr) : StructMember(definition), member_ptr(member_ptr) {} + + void* GetPointer(void* base_ptr) override { + return &(static_cast(base_ptr)->*member_ptr); + } + +private: + MemberType Object::* member_ptr; +}; + +class StructMemberFunc final : public StructMember { +public: + StructMemberFunc(VariableDefinition* definition) : StructMember(definition) {} + void* GetPointer(void* base_ptr) override { + return base_ptr; + } +}; + + +class StructDefinition final : public VariableDefinition { +public: + StructDefinition() : VariableDefinition(DataVariableType::Struct) + {} + + DataVariable Child(void* ptr, const DataAddressEntry& address) override + { + const String& name = address.name; + if (name.empty()) + { + Log::Message(Log::LT_WARNING, "Expected a struct member name but none given."); + return DataVariable(); + } + + auto it = members.find(name); + if (it == members.end()) + { + Log::Message(Log::LT_WARNING, "Member %s not found in data struct.", name.c_str()); + return DataVariable(); + } + + void* next_ptr = it->second->GetPointer(ptr); + VariableDefinition* next_definition = it->second->GetDefinition(); + + return DataVariable(next_definition, next_ptr); + } + + void AddMember(const String& name, UniquePtr member) + { + RMLUI_ASSERT(member); + bool inserted = members.emplace(name, std::move(member)).second; + RMLUI_ASSERTMSG(inserted, "Member name already exists."); + (void)inserted; + } + +private: + SmallUnorderedMap> members; +}; + + +template +class MemberFuncDefinition final : public VariableDefinition { +public: + MemberFuncDefinition(MemberGetFunc get, MemberSetFunc set) : VariableDefinition(DataVariableType::MemberFunction), get(get), set(set) {} + + bool Get(void* ptr, Variant& variant) override + { + if (!get) + return false; + (static_cast(ptr)->*get)(variant); + return true; + } + bool Set(void* ptr, const Variant& variant) override + { + if (!set) + return false; + (static_cast(ptr)->*set)(variant); + return true; + } +private: + MemberGetFunc get; + MemberSetFunc set; +}; + +} +} + +#endif diff --git a/Include/RmlUi/Core/DataView.h b/Include/RmlUi/Core/DataView.h new file mode 100644 index 000000000..a175a924b --- /dev/null +++ b/Include/RmlUi/Core/DataView.h @@ -0,0 +1,135 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATAVIEW_H +#define RMLUICOREDATAVIEW_H + +#include "Header.h" +#include "Types.h" +#include "Traits.h" +#include "DataTypes.h" +#include + +namespace Rml { +namespace Core { + +class Element; +class DataModel; + + +class RMLUICORE_API DataViewInstancer : public NonCopyMoveable { +public: + DataViewInstancer() {} + virtual ~DataViewInstancer() {} + virtual DataViewPtr InstanceView(Element* element) = 0; +}; + +template +class DataViewInstancerDefault final : public DataViewInstancer { +public: + DataViewPtr InstanceView(Element* element) override { + return DataViewPtr(new T(element)); + } +}; + +/** + Data view. + + Data views are used to present a data variable in the document by different means. + A data view is declared in the document by the element attribute: + + data-[type]-[modifier]="[expression]" + + The modifier may or may not be required depending on the data view. + */ + +class RMLUICORE_API DataView : public Releasable { +public: + virtual ~DataView(); + + // Initialize the data view. + // @param[in] model The data model the view will be attached to. + // @param[in] element The element which spawned the view. + // @param[in] expression The value of the element's 'data-' attribute which spawned the view (see above). + // @param[in] modifier_or_inner_rml The modifier for the given view type (see above), or the inner rml contents for structural data views. + // @return True on success. + virtual bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier_or_inner_rml) = 0; + + // Update the data view. + // Returns true if the update resulted in a document change. + virtual bool Update(DataModel& model) = 0; + + // Returns the list of data variable name(s) which can modify this view. + virtual StringList GetVariableNameList() const = 0; + + // Returns the attached element if it still exists. + Element* GetElement() const; + + // Returns the depth of the attached element in the document tree. + int GetElementDepth() const; + + // Returns true if the element still exists. + bool IsValid() const; + +protected: + DataView(Element* element); + +private: + ObserverPtr attached_element; + int element_depth; +}; + + + +class RMLUICORE_API DataViews : NonCopyMoveable { +public: + DataViews(); + ~DataViews(); + + void Add(DataViewPtr view); + + void OnElementRemove(Element* element); + + bool Update(DataModel& model, const DirtyVariables& dirty_variables); + +private: + using DataViewList = std::vector; + + DataViewList views; + + DataViewList views_to_add; + DataViewList views_to_remove; + + using NameViewMap = std::unordered_multimap; + NameViewMap name_view_map; +}; + +} +} + +#endif diff --git a/Include/RmlUi/Core/Element.h b/Include/RmlUi/Core/Element.h index 94e3c23e8..34a8a036f 100644 --- a/Include/RmlUi/Core/Element.h +++ b/Include/RmlUi/Core/Element.h @@ -44,6 +44,7 @@ namespace Rml { namespace Core { class Context; +class DataModel; class Decorator; class ElementInstancer; class EventDispatcher; @@ -533,23 +534,19 @@ class RMLUICORE_API Element : public ScriptInterface, public EnableObserverPtr + static FamilyId GetId() { + static int id = GetNewId(); + return static_cast(id); + } +}; + +template +class Family : FamilyBase { +public: + // Get a unique ID for a given type. + // Note: IDs will be unique across DLL-boundaries even for the same type. + static FamilyId Id() { + return GetId< typename std::remove_cv< typename std::remove_reference< T >::type >::type >(); + } +}; + } } diff --git a/Include/RmlUi/Core/TypeConverter.h b/Include/RmlUi/Core/TypeConverter.h index 4be530563..3d5dd32d1 100644 --- a/Include/RmlUi/Core/TypeConverter.h +++ b/Include/RmlUi/Core/TypeConverter.h @@ -35,6 +35,7 @@ #include "StringUtilities.h" #include #include +#include namespace Rml { namespace Core { @@ -55,6 +56,20 @@ class TypeConverter static bool Convert(const SourceType& src, DestType& dest); }; +template +inline String ToString(const T& value, String default_value = String()) { + String result = default_value; + TypeConverter::Convert(value, result); + return result; +} + +template +inline T FromString(const String& string, T default_value = T()) { + T result = default_value; + TypeConverter::Convert(string, result); + return result; +} + // Some more complex types are defined in cpp-file diff --git a/Include/RmlUi/Core/TypeConverter.inl b/Include/RmlUi/Core/TypeConverter.inl index 103d3fd10..c804e29f1 100644 --- a/Include/RmlUi/Core/TypeConverter.inl +++ b/Include/RmlUi/Core/TypeConverter.inl @@ -72,7 +72,9 @@ public: \ ///////////////////////////////////////////////// PASS_THROUGH(int); PASS_THROUGH(unsigned int); +PASS_THROUGH(int64_t); PASS_THROUGH(float); +PASS_THROUGH(double); PASS_THROUGH(bool); PASS_THROUGH(char); PASS_THROUGH(Character); @@ -98,16 +100,34 @@ PASS_THROUGH(voidPtr); ///////////////////////////////////////////////// BASIC_CONVERTER(bool, int); BASIC_CONVERTER(bool, unsigned int); +BASIC_CONVERTER(bool, int64_t); BASIC_CONVERTER(bool, float); +BASIC_CONVERTER(bool, double); -BASIC_CONVERTER(int, unsigned int); BASIC_CONVERTER_BOOL(int, bool); +BASIC_CONVERTER(int, unsigned int); +BASIC_CONVERTER(int, int64_t); BASIC_CONVERTER(int, float); +BASIC_CONVERTER(int, double); + +BASIC_CONVERTER_BOOL(int64_t, bool); +BASIC_CONVERTER(int64_t, int); +BASIC_CONVERTER(int64_t, float); +BASIC_CONVERTER(int64_t, double); +BASIC_CONVERTER(int64_t, unsigned int); BASIC_CONVERTER_BOOL(float, bool); BASIC_CONVERTER(float, int); +BASIC_CONVERTER(float, int64_t); +BASIC_CONVERTER(float, double); BASIC_CONVERTER(float, unsigned int); +BASIC_CONVERTER_BOOL(double, bool); +BASIC_CONVERTER(double, int); +BASIC_CONVERTER(double, int64_t); +BASIC_CONVERTER(double, float); +BASIC_CONVERTER(double, unsigned int); + BASIC_CONVERTER(char, Character); ///////////////////////////////////////////////// @@ -148,16 +168,23 @@ public: } }; +template<> +class TypeConverter< String, int64_t > +{ +public: + static bool Convert(const String& src, int64_t& dest) + { + return sscanf(src.c_str(), "%" SCNd64, &dest) == 1; + } +}; + template<> class TypeConverter< String, byte > { public: static bool Convert(const String& src, byte& dest) { - int value; - bool ret = sscanf(src.c_str(), "%d", &value) == 1; - dest = (byte) value; - return ret && (value <= 255); + return sscanf(src.c_str(), "%hhu", &dest) == 1; } }; @@ -232,7 +259,10 @@ class TypeConverter< type, String > \ public: \ static bool Convert(const type& src, String& dest) \ { \ - return FormatString(dest, 32, "%.4f", src) > 0; \ + if(FormatString(dest, 32, "%.3f", src) == 0) \ + return false; \ + StringUtilities::TrimTrailingDotZeros(dest); \ + return true; \ } \ } FLOAT_STRING_CONVERTER(float); @@ -258,13 +288,23 @@ public: } }; +template<> +class TypeConverter< int64_t, String > +{ +public: + static bool Convert(const int64_t& src, String& dest) + { + return FormatString(dest, 32, "%" PRId64, src) > 0; + } +}; + template<> class TypeConverter< byte, String > { public: static bool Convert(const byte& src, String& dest) { - return FormatString(dest, 32, "%u", src) > 0; + return FormatString(dest, 32, "%hhu", src) > 0; } }; diff --git a/Include/RmlUi/Core/Types.h b/Include/RmlUi/Core/Types.h index 4fcd853a3..120b4c424 100644 --- a/Include/RmlUi/Core/Types.h +++ b/Include/RmlUi/Core/Types.h @@ -161,6 +161,7 @@ using SmallOrderedSet = chobo::flat_set< T >; // Container types for common classes using ElementList = std::vector< Element* >; using OwnedElementList = std::vector< ElementPtr >; +using VariantList = std::vector< Variant >; using ElementAnimationList = std::vector< ElementAnimation >; using PseudoClassList = SmallUnorderedSet< String >; @@ -189,6 +190,12 @@ using TransformPtr = SharedPtr< Transform >; using DecoratorsPtr = SharedPtr; using FontEffectsPtr = SharedPtr; +// Data binding types +class DataView; +using DataViewPtr = UniqueReleaserPtr; +class DataController; +using DataControllerPtr = UniqueReleaserPtr; + } } diff --git a/Include/RmlUi/Core/Variant.h b/Include/RmlUi/Core/Variant.h index de2096077..d968273de 100644 --- a/Include/RmlUi/Core/Variant.h +++ b/Include/RmlUi/Core/Variant.h @@ -53,12 +53,14 @@ class RMLUICORE_API Variant enum Type : size_t { NONE = '-', + BOOL = 'B', BYTE = 'b', CHAR = 'c', FLOAT = 'f', + DOUBLE = 'd', INT = 'i', + INT64 = 'I', STRING = 's', - WORD = 'w', VECTOR2 = '2', VECTOR3 = '3', VECTOR4 = '4', @@ -121,11 +123,13 @@ class RMLUICORE_API Variant void Set(const Variant& copy); void Set(Variant&& other); + void Set(const bool value); void Set(const byte value); void Set(const char value); void Set(const float value); + void Set(const double value); void Set(const int value); - void Set(const Character value); + void Set(const int64_t value); void Set(const char* value); void Set(void* value); void Set(const Vector2f& value); diff --git a/Include/RmlUi/Core/Variant.inl b/Include/RmlUi/Core/Variant.inl index d14cd7631..739283117 100644 --- a/Include/RmlUi/Core/Variant.inl +++ b/Include/RmlUi/Core/Variant.inl @@ -54,6 +54,10 @@ bool Variant::GetInto(T& value) const { switch (type) { + case BOOL: + return TypeConverter< bool, T >::Convert(*(bool*)data, value); + break; + case BYTE: return TypeConverter< byte, T >::Convert(*(byte*)data, value); break; @@ -66,16 +70,20 @@ bool Variant::GetInto(T& value) const return TypeConverter< float, T >::Convert(*(float*)data, value); break; + case DOUBLE: + return TypeConverter< double, T >::Convert(*(double*)data, value); + break; + case INT: return TypeConverter< int, T >::Convert(*(int*)data, value); break; - case STRING: - return TypeConverter< String, T >::Convert(*(String*)data, value); + case INT64: + return TypeConverter< int64_t, T >::Convert(*(int64_t*)data, value); break; - case WORD: - return TypeConverter< Character, T >::Convert(*(Character*)data, value); + case STRING: + return TypeConverter< String, T >::Convert(*(String*)data, value); break; case VECTOR2: diff --git a/Include/RmlUi/Core/XMLNodeHandler.h b/Include/RmlUi/Core/XMLNodeHandler.h index efe175104..90b45a0b9 100644 --- a/Include/RmlUi/Core/XMLNodeHandler.h +++ b/Include/RmlUi/Core/XMLNodeHandler.h @@ -38,6 +38,7 @@ namespace Core { class Element; class XMLParser; +enum class XMLDataType; /** A handler gets ElementStart, ElementEnd and ElementData called by the XMLParser. @@ -65,7 +66,7 @@ class RMLUICORE_API XMLNodeHandler : public NonCopyMoveable /// Called for element data. /// @param parser The parser executing the parse. /// @param data The element data. - virtual bool ElementData(XMLParser* parser, const String& data) = 0; + virtual bool ElementData(XMLParser* parser, const String& data, XMLDataType type) = 0; }; } diff --git a/Include/RmlUi/Core/XMLParser.h b/Include/RmlUi/Core/XMLParser.h index 9608b905b..4c53b3d4b 100644 --- a/Include/RmlUi/Core/XMLParser.h +++ b/Include/RmlUi/Core/XMLParser.h @@ -64,9 +64,6 @@ class RMLUICORE_API XMLParser : public BaseXMLParser /// Returns the XML document's header. /// @return The document header. DocumentHeader* GetDocumentHeader(); - /// Returns the source URL of this parse. - /// @return The URL of the parsing stream. - const URL& GetSourceURL() const; // The parse stack. struct ParseFrame @@ -75,13 +72,13 @@ class RMLUICORE_API XMLParser : public BaseXMLParser String tag; // Element representing this frame. - Element* element; + Element* element = nullptr; // Handler used for this frame. - XMLNodeHandler* node_handler; + XMLNodeHandler* node_handler = nullptr; // The default handler used for this frame's children. - XMLNodeHandler* child_handler; + XMLNodeHandler* child_handler = nullptr; }; /// Pushes an element handler onto the parse stack for parsing child elements. @@ -92,20 +89,22 @@ class RMLUICORE_API XMLParser : public BaseXMLParser void PushDefaultHandler(); /// Access the current parse frame. - /// @return The parser's current parse frame. const ParseFrame* GetParseFrame() const; + /// Returns the source URL of this parse. + const URL& GetSourceURL() const; + protected: /// Called when the parser finds the beginning of an element tag. void HandleElementStart(const String& name, const XMLAttributes& attributes) override; /// Called when the parser finds the end of an element tag. void HandleElementEnd(const String& name) override; /// Called when the parser encounters data. - void HandleData(const String& data) override; + void HandleData(const String& data, XMLDataType type) override; private: // The header of the document being parsed. - DocumentHeader* header; + UniquePtr header; // The active node handler. XMLNodeHandler* active_handler; diff --git a/Samples/basic/bitmapfont/src/FontEngineBitmap.cpp b/Samples/basic/bitmapfont/src/FontEngineBitmap.cpp index 7835bd4cf..c034c48f2 100644 --- a/Samples/basic/bitmapfont/src/FontEngineBitmap.cpp +++ b/Samples/basic/bitmapfont/src/FontEngineBitmap.cpp @@ -310,7 +310,8 @@ void FontParserBitmap::HandleElementEnd(const String& RMLUI_UNUSED_PARAMETER(nam } // Called when the parser encounters data. -void FontParserBitmap::HandleData(const String& RMLUI_UNUSED_PARAMETER(data)) +void FontParserBitmap::HandleData(const String& RMLUI_UNUSED_PARAMETER(data), Rml::Core::XMLDataType RMLUI_UNUSED_PARAMETER(type)) { RMLUI_UNUSED(data); + RMLUI_UNUSED(type); } diff --git a/Samples/basic/bitmapfont/src/FontEngineBitmap.h b/Samples/basic/bitmapfont/src/FontEngineBitmap.h index 1667eb675..7de26231d 100644 --- a/Samples/basic/bitmapfont/src/FontEngineBitmap.h +++ b/Samples/basic/bitmapfont/src/FontEngineBitmap.h @@ -115,7 +115,7 @@ class FontParserBitmap : public Rml::Core::BaseXMLParser /// Called when the parser finds the end of an element tag. void HandleElementEnd(const String& name) override; /// Called when the parser encounters data. - void HandleData(const String& data) override; + void HandleData(const String& data, Rml::Core::XMLDataType type) override; String family; FontStyle style = FontStyle::Normal; diff --git a/Samples/basic/databinding/data/databinding.rml b/Samples/basic/databinding/data/databinding.rml new file mode 100644 index 000000000..9cc65857b --- /dev/null +++ b/Samples/basic/databinding/data/databinding.rml @@ -0,0 +1,238 @@ + + + +Data Binding Sample + + + + + +Basics + +

{{title}}

+

The quick brown fox jumps over the lazy {{animal}}.

+ +
+Events + +

{{hello_world}} Rated: {{rating}}

+

Data binding demo. We rate this a good old {{rating}}!

+ +
Thanks for the goodawesome rating!
+
+
+
+ Recorded mouse positions.
+ x: {{ pos.x }}, y: {{ pos.y }}
+
+ +

+ For loop with data expressions:
+ {{ i * 2 + (!(i < 10) ? ' wow!' | to_upper : '') }} +

+
+Invaders + +

+ Incoming invaders: + + {{ incoming_invaders_rate }} / min. +

+ +
+

{{invader.name}}

+

Invader {{it_index + 1}} of {{ invaders.size }}.

+ +

+ Shots fired (damage): {{it}} +

+
+

It's all safe and sound, sir!

+
+Forms + +

Todo

+
+

Full name

+
+ +
+

Email and password

+
+ + +
+

Favorite animal

+
+ Dog + Cat + Narwhal + I don't like animals +
+

Favorite meals

+
+ Pizza + Pasta + Lasagne +
+

Rating

+
+   +
+

Subject

+
+ +
+

Message

+
+ +
+
+ Submit +
+
+
+
+ +
diff --git a/Samples/basic/databinding/src/main.cpp b/Samples/basic/databinding/src/main.cpp new file mode 100644 index 000000000..9e1a9bd4c --- /dev/null +++ b/Samples/basic/databinding/src/main.cpp @@ -0,0 +1,448 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2018 Michael R. P. Ragazzon + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include +#include +#include +#include +#include +#include +#include + + +namespace BasicExample { + + Rml::Core::DataModelHandle model_handle; + + struct MyData { + Rml::Core::String title = "Simple data binding example"; + Rml::Core::String animal = "dog"; + bool show_text = true; + } my_data; + + bool Initialize(Rml::Core::Context* context) + { + Rml::Core::DataModelConstructor constructor = context->CreateDataModel("basics"); + if (!constructor) + return false; + + constructor.Bind("title", &my_data.title); + constructor.Bind("animal", &my_data.animal); + constructor.Bind("show_text", &my_data.show_text); + + model_handle = constructor.GetModelHandle(); + + return true; + } + + void Update() + { + model_handle.Update(); + } +} + + +namespace EventsExample { + + Rml::Core::DataModelHandle model_handle; + + struct MyData { + Rml::Core::String hello_world = "Hello World!"; + Rml::Core::String mouse_detector = "Mouse-move Detector."; + int rating = 99; + + std::vector list = { 1, 2, 3, 4, 5 }; + + std::vector positions; + + void AddMousePos(Rml::Core::DataModelHandle model, Rml::Core::Event& ev, const Rml::Core::VariantList& /*arguments*/) + { + positions.emplace_back(ev.GetParameter("mouse_x", 0.f), ev.GetParameter("mouse_y", 0.f)); + model.DirtyVariable("positions"); + } + + } my_data; + + + void ClearPositions(Rml::Core::DataModelHandle model, Rml::Core::Event& /*ev*/, const Rml::Core::VariantList& /*arguments*/) + { + my_data.positions.clear(); + model.DirtyVariable("positions"); + } + + void HasGoodRating(Rml::Core::Variant& variant) + { + variant = int(my_data.rating > 50); + } + + + bool Initialize(Rml::Core::Context* context) + { + using namespace Rml::Core; + DataModelConstructor constructor = context->CreateDataModel("events"); + if (!constructor) + return false; + + // Register all the types first + constructor.RegisterArray>(); + + if (auto vec2_handle = constructor.RegisterStruct()) + { + vec2_handle.RegisterMember("x", &Vector2f::x); + vec2_handle.RegisterMember("y", &Vector2f::y); + } + constructor.RegisterArray>(); + + // Bind the variables to the data model + constructor.Bind("hello_world", &my_data.hello_world); + constructor.Bind("mouse_detector", &my_data.mouse_detector); + constructor.Bind("rating", &my_data.rating); + constructor.BindFunc("good_rating", &HasGoodRating); + constructor.BindFunc("great_rating", [](Variant& variant) { + variant = int(my_data.rating > 80); + }); + + constructor.Bind("list", &my_data.list); + + constructor.Bind("positions", &my_data.positions); + + constructor.BindEventCallback("clear_positions", &ClearPositions); + constructor.BindEventCallback("add_mouse_pos", &MyData::AddMousePos, &my_data); + + + model_handle = constructor.GetModelHandle(); + + return true; + } + + void Update() + { + if (model_handle.IsVariableDirty("rating")) + { + model_handle.DirtyVariable("good_rating"); + model_handle.DirtyVariable("great_rating"); + + size_t new_size = my_data.rating / 10 + 1; + if (new_size != my_data.list.size()) + { + my_data.list.resize(new_size); + std::iota(my_data.list.begin(), my_data.list.end(), float(new_size)); + model_handle.DirtyVariable("list"); + } + } + + model_handle.Update(); + } +} + + + +namespace InvadersExample { + + Rml::Core::DataModelHandle model_handle; + + struct Invader { + Rml::Core::String name; + Rml::Core::String sprite; + Rml::Core::Colourb color{ 255, 255, 255 }; + std::vector damage; + float danger_rating = 50; + + void GetColor(Rml::Core::Variant& variant) { + variant = "rgba(" + Rml::Core::ToString(color) + ')'; + } + void SetColor(const Rml::Core::Variant& variant) { + using namespace Rml::Core; + String str = variant.Get(); + if (str.size() > 6) + str = str.substr(5, str.size() - 6); + color = Rml::Core::FromString(variant.Get()); + } + }; + + struct InvadersData { + double time_last_invader_spawn = 0; + double time_last_weapons_launched = 0; + + float incoming_invaders_rate = 10; // Per minute + + std::vector invaders = { + Invader{"Angry invader", "icon-invader", {255, 40, 30}, {3, 6, 7}, 80} + }; + + void LaunchWeapons(Rml::Core::DataModelHandle model, Rml::Core::Event& /*ev*/, const Rml::Core::VariantList& /*arguments*/) + { + invaders.clear(); + model.DirtyVariable("invaders"); + } + + } invaders_data; + + bool Initialize(Rml::Core::Context* context) + { + Rml::Core::DataModelConstructor constructor = context->CreateDataModel("invaders"); + if (!constructor) + return false; + + // Since Invader::damage is an array type. + constructor.RegisterArray>(); + + // Structs are registered by adding all its members through the returned handle. + if (auto invader_handle = constructor.RegisterStruct()) + { + invader_handle.RegisterMember("name", &Invader::name); + invader_handle.RegisterMember("sprite", &Invader::sprite); + invader_handle.RegisterMember("damage", &Invader::damage); + invader_handle.RegisterMember("danger_rating", &Invader::danger_rating); + + // Getter and setter functions can also be used. + invader_handle.RegisterMemberFunc("color", &Invader::GetColor); + } + + // We can even have an Array of Structs, infinitely nested if we so desire. + // Make sure the underlying type (here Invader) is registered before the array. + constructor.RegisterArray>(); + + // Now we can bind the variables to the model. + constructor.Bind("incoming_invaders_rate", &invaders_data.incoming_invaders_rate); + constructor.Bind("invaders", &invaders_data.invaders); + + // This function will be called when the user clicks the 'Launch weapons' button. + constructor.BindEventCallback("launch_weapons", &InvadersData::LaunchWeapons, &invaders_data); + + model_handle = constructor.GetModelHandle(); + + return true; + } + + void Update(const double t) + { + // Add new invaders at regular time intervals. + const double t_next_spawn = invaders_data.time_last_invader_spawn + 60.0 / double(invaders_data.incoming_invaders_rate); + if (t >= t_next_spawn) + { + using namespace Rml::Core; + const int num_items = 4; + static std::array names = { "Angry invader", "Harmless invader", "Deceitful invader", "Cute invader" }; + static std::array sprites = { "icon-invader", "icon-flag", "icon-game", "icon-waves" }; + static std::array colors = { { { 255, 40, 30 }, {20, 40, 255}, {255, 255, 30}, {230, 230, 230} } }; + + Invader new_invader; + new_invader.name = names[rand() % num_items]; + new_invader.sprite = sprites[rand() % num_items]; + new_invader.color = colors[rand() % num_items]; + new_invader.danger_rating = float((rand() % 100) + 1); + invaders_data.invaders.push_back(new_invader); + + model_handle.DirtyVariable("invaders"); + invaders_data.time_last_invader_spawn = t; + } + + // Launch shots from a random invader. + if (t >= invaders_data.time_last_weapons_launched + 1.0) + { + if (!invaders_data.invaders.empty()) + { + const size_t index = size_t(rand() % int(invaders_data.invaders.size())); + + Invader& invader = invaders_data.invaders[index]; + invader.damage.push_back(rand() % int(invader.danger_rating)); + + model_handle.DirtyVariable("invaders"); + } + invaders_data.time_last_weapons_launched = t; + } + + model_handle.Update(); + } +} + + + +class DemoWindow : public Rml::Core::EventListener +{ +public: + DemoWindow(const Rml::Core::String &title, const Rml::Core::Vector2f &position, Rml::Core::Context *context) + { + using namespace Rml::Core; + document = context->LoadDocument("basic/databinding/data/databinding.rml"); + if (document) + { + document->GetElementById("title")->SetInnerRML(title); + document->SetProperty(PropertyId::Left, Property(position.x, Property::PX)); + document->SetProperty(PropertyId::Top, Property(position.y, Property::PX)); + + document->Show(); + } + } + + void Shutdown() + { + if (document) + { + document->Close(); + document = nullptr; + } + } + + void ProcessEvent(Rml::Core::Event& event) override + { + using namespace Rml::Core; + + switch (event.GetId()) + { + case EventId::Keydown: + { + Rml::Core::Input::KeyIdentifier key_identifier = (Rml::Core::Input::KeyIdentifier) event.GetParameter< int >("key_identifier", 0); + + if (key_identifier == Rml::Core::Input::KI_ESCAPE) + { + Shell::RequestExit(); + } + else if (key_identifier == Rml::Core::Input::KI_F8) + { + Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible()); + } + } + break; + + default: + break; + } + } + + Rml::Core::ElementDocument * GetDocument() { + return document; + } + + +private: + Rml::Core::ElementDocument *document = nullptr; +}; + + + +Rml::Core::Context* context = nullptr; +ShellRenderInterfaceExtensions *shell_renderer; + +void GameLoop() +{ + const double t = Rml::Core::GetSystemInterface()->GetElapsedTime(); + + BasicExample::Update(); + EventsExample::Update(); + InvadersExample::Update(t); + + context->Update(); + + shell_renderer->PrepareRenderBuffer(); + context->Render(); + shell_renderer->PresentRenderBuffer(); +} + + + +#if defined RMLUI_PLATFORM_WIN32 +#include +int APIENTRY WinMain(HINSTANCE RMLUI_UNUSED_PARAMETER(instance_handle), HINSTANCE RMLUI_UNUSED_PARAMETER(previous_instance_handle), char* RMLUI_UNUSED_PARAMETER(command_line), int RMLUI_UNUSED_PARAMETER(command_show)) +#else +int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv)) +#endif +{ +#ifdef RMLUI_PLATFORM_WIN32 + RMLUI_UNUSED(instance_handle); + RMLUI_UNUSED(previous_instance_handle); + RMLUI_UNUSED(command_line); + RMLUI_UNUSED(command_show); +#else + RMLUI_UNUSED(argc); + RMLUI_UNUSED(argv); +#endif + + const int width = 1600; + const int height = 900; + + ShellRenderInterfaceOpenGL opengl_renderer; + shell_renderer = &opengl_renderer; + + // Generic OS initialisation, creates a window and attaches OpenGL. + if (!Shell::Initialise() || + !Shell::OpenWindow("Data Binding Sample", shell_renderer, width, height, true)) + { + Shell::Shutdown(); + return -1; + } + + // RmlUi initialisation. + Rml::Core::SetRenderInterface(&opengl_renderer); + opengl_renderer.SetViewport(width, height); + + ShellSystemInterface system_interface; + Rml::Core::SetSystemInterface(&system_interface); + + Rml::Core::Initialise(); + + // Create the main RmlUi context and set it on the shell's input layer. + context = Rml::Core::CreateContext("main", Rml::Core::Vector2i(width, height)); + + if (!context + || !BasicExample::Initialize(context) + || !EventsExample::Initialize(context) + || !InvadersExample::Initialize(context) + ) + { + Rml::Core::Shutdown(); + Shell::Shutdown(); + return -1; + } + + Rml::Controls::Initialise(); + Rml::Debugger::Initialise(context); + Input::SetContext(context); + shell_renderer->SetContext(context); + + Shell::LoadFonts("assets/"); + + auto demo_window = std::make_unique("Data binding", Rml::Core::Vector2f(150, 50), context); + demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keydown, demo_window.get()); + demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keyup, demo_window.get()); + + Shell::EventLoop(GameLoop); + + demo_window->Shutdown(); + + // Shutdown RmlUi. + Rml::Core::Shutdown(); + + Shell::CloseWindow(); + Shell::Shutdown(); + + demo_window.reset(); + + return 0; +} diff --git a/Samples/shell/include/x11/InputX11.h b/Samples/shell/include/x11/InputX11.h index f6b7f48d8..62d650e82 100644 --- a/Samples/shell/include/x11/InputX11.h +++ b/Samples/shell/include/x11/InputX11.h @@ -30,6 +30,13 @@ #define INPUTX11_H #include + +// The None define from X.h conflicts with RmlUi code base, +// use the constant 0L instead where necessary +#ifdef None + #undef None +#endif + #include "Input.h" /** diff --git a/Source/Controls/WidgetSliderInput.cpp b/Source/Controls/WidgetSliderInput.cpp index 11d73e95e..6b05ed7c9 100644 --- a/Source/Controls/WidgetSliderInput.cpp +++ b/Source/Controls/WidgetSliderInput.cpp @@ -46,12 +46,13 @@ WidgetSliderInput::~WidgetSliderInput() { } -void WidgetSliderInput::SetValue(float value) +void WidgetSliderInput::SetValue(float target_value) { - float num_steps = (value - min_value) / step; + float num_steps = (target_value - min_value) / step; float new_value = min_value + Rml::Core::Math::RoundFloat(num_steps) * step; - SetBarPosition(SetValueInternal(new_value)); + if(new_value != value) + SetBarPosition(SetValueInternal(new_value)); } float WidgetSliderInput::GetValue() const @@ -128,9 +129,14 @@ float WidgetSliderInput::SetValueInternal(float new_value) return 0; } - Rml::Core::Dictionary parameters; - parameters["value"] = value; - GetParent()->DispatchEvent(Core::EventId::Change, parameters); + Rml::Core::Dictionary parameters; + parameters["value"] = value; + GetParent()->DispatchEvent(Core::EventId::Change, parameters); + + + // TODO: This might not be the safest approach as this will call SetValue(), + // thus, a slight mismatch will result in infinite recursion. + GetParent()->SetAttribute("value", value); return (value - min_value) / (max_value - min_value); } diff --git a/Source/Controls/WidgetTextInput.cpp b/Source/Controls/WidgetTextInput.cpp index c9d9a497a..64da16319 100644 --- a/Source/Controls/WidgetTextInput.cpp +++ b/Source/Controls/WidgetTextInput.cpp @@ -155,7 +155,7 @@ void WidgetTextInput::SetMaxLength(int _max_length) num_characters += 1; if (num_characters > max_length) { - i_erase = size_t(it.Offset()); + i_erase = size_t(it.offset()); break; } } @@ -853,7 +853,7 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position) for(auto it = Core::StringIteratorU8(lines[line_index].content, 0, lines[line_index].content_length); it; ) { ++it; - int offset = (int)it.Offset(); + int offset = (int)it.offset(); float line_width = (float) Core::ElementUtilities::GetStringWidth(text_element, lines[line_index].content.substr(0, offset)); if (line_width > position) diff --git a/Source/Controls/XMLNodeHandlerDataGrid.cpp b/Source/Controls/XMLNodeHandlerDataGrid.cpp index 3e7e53462..52bdc579a 100644 --- a/Source/Controls/XMLNodeHandlerDataGrid.cpp +++ b/Source/Controls/XMLNodeHandlerDataGrid.cpp @@ -104,8 +104,9 @@ bool XMLNodeHandlerDataGrid::ElementEnd(Core::XMLParser* RMLUI_UNUSED_PARAMETER( return true; } -bool XMLNodeHandlerDataGrid::ElementData(Core::XMLParser* parser, const Rml::Core::String& data) +bool XMLNodeHandlerDataGrid::ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType RMLUI_UNUSED_PARAMETER(type)) { + RMLUI_UNUSED(type); Core::Element* parent = parser->GetParseFrame()->element; // Parse the text into the parent element. diff --git a/Source/Controls/XMLNodeHandlerDataGrid.h b/Source/Controls/XMLNodeHandlerDataGrid.h index e57a0d9a8..e1ca5c22d 100644 --- a/Source/Controls/XMLNodeHandlerDataGrid.h +++ b/Source/Controls/XMLNodeHandlerDataGrid.h @@ -52,7 +52,7 @@ class XMLNodeHandlerDataGrid : public Core::XMLNodeHandler /// Called when an element is closed. bool ElementEnd(Core::XMLParser* parser, const Rml::Core::String& name) override; /// Called for element data. - bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data) override; + bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType type) override; }; } diff --git a/Source/Controls/XMLNodeHandlerTabSet.cpp b/Source/Controls/XMLNodeHandlerTabSet.cpp index da8a5667c..103f83b9f 100644 --- a/Source/Controls/XMLNodeHandlerTabSet.cpp +++ b/Source/Controls/XMLNodeHandlerTabSet.cpp @@ -113,7 +113,6 @@ Core::Element* XMLNodeHandlerTabSet::ElementStart(Core::XMLParser* parser, const Core::Element* parent = parser->GetParseFrame()->element; - // Attempt to instance the element with the instancer. Core::ElementPtr element = Core::Factory::InstanceElement(parent, name, name, attributes); if (!element) { @@ -121,9 +120,9 @@ Core::Element* XMLNodeHandlerTabSet::ElementStart(Core::XMLParser* parser, const return nullptr; } - // Add the element to its parent and remove the initial reference. - Core::Element* result = parent->AppendChild(std::move(element)); - return result; + parent->AppendChild(std::move(element)); + + return nullptr; } return nullptr; @@ -137,8 +136,9 @@ bool XMLNodeHandlerTabSet::ElementEnd(Core::XMLParser* RMLUI_UNUSED_PARAMETER(pa return true; } -bool XMLNodeHandlerTabSet::ElementData(Core::XMLParser* parser, const Rml::Core::String& data) +bool XMLNodeHandlerTabSet::ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType RMLUI_UNUSED_PARAMETER(type)) { + RMLUI_UNUSED(type); return Core::Factory::InstanceElementText(parser->GetParseFrame()->element, data); } diff --git a/Source/Controls/XMLNodeHandlerTabSet.h b/Source/Controls/XMLNodeHandlerTabSet.h index 54ff3f35b..c5c5d4a86 100644 --- a/Source/Controls/XMLNodeHandlerTabSet.h +++ b/Source/Controls/XMLNodeHandlerTabSet.h @@ -51,7 +51,7 @@ class XMLNodeHandlerTabSet : public Core::XMLNodeHandler /// Called when an element is closed bool ElementEnd(Core::XMLParser* parser, const Rml::Core::String& name) override; /// Called for element data - bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data) override; + bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType type) override; }; } diff --git a/Source/Controls/XMLNodeHandlerTextArea.cpp b/Source/Controls/XMLNodeHandlerTextArea.cpp index 1d8433b93..331a92ea0 100644 --- a/Source/Controls/XMLNodeHandlerTextArea.cpp +++ b/Source/Controls/XMLNodeHandlerTextArea.cpp @@ -69,8 +69,10 @@ bool XMLNodeHandlerTextArea::ElementEnd(Core::XMLParser* RMLUI_UNUSED_PARAMETER( return true; } -bool XMLNodeHandlerTextArea::ElementData(Core::XMLParser* parser, const Rml::Core::String& data) +bool XMLNodeHandlerTextArea::ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType RMLUI_UNUSED_PARAMETER(type)) { + RMLUI_UNUSED(type); + ElementFormControlTextArea* text_area = rmlui_dynamic_cast< ElementFormControlTextArea* >(parser->GetParseFrame()->element); if (text_area != nullptr) { diff --git a/Source/Controls/XMLNodeHandlerTextArea.h b/Source/Controls/XMLNodeHandlerTextArea.h index 3a4082b6a..88ab08f99 100644 --- a/Source/Controls/XMLNodeHandlerTextArea.h +++ b/Source/Controls/XMLNodeHandlerTextArea.h @@ -51,7 +51,7 @@ class XMLNodeHandlerTextArea : public Core::XMLNodeHandler /// Called when an element is closed. bool ElementEnd(Core::XMLParser* parser, const Rml::Core::String& name) override; /// Called for element data. - bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data) override; + bool ElementData(Core::XMLParser* parser, const Rml::Core::String& data, Core::XMLDataType type) override; }; } diff --git a/Source/Core/BaseXMLParser.cpp b/Source/Core/BaseXMLParser.cpp index 95e44e548..8cb30a79a 100644 --- a/Source/Core/BaseXMLParser.cpp +++ b/Source/Core/BaseXMLParser.cpp @@ -29,26 +29,17 @@ #include "../../Include/RmlUi/Core/BaseXMLParser.h" #include "../../Include/RmlUi/Core/Profiling.h" #include "../../Include/RmlUi/Core/Stream.h" +#include "XMLParseTools.h" #include namespace Rml { namespace Core { -// Most file layers cache 4k. -const int DEFAULT_BUFFER_SIZE = 4096; - BaseXMLParser::BaseXMLParser() -{ - read = nullptr; - buffer = nullptr; - buffer_used = 0; - buffer_size = 0; - open_tag_depth = 0; -} +{} BaseXMLParser::~BaseXMLParser() -{ -} +{} // Registers a tag as containing general character data. void BaseXMLParser::RegisterCDATATag(const String& tag) @@ -57,24 +48,41 @@ void BaseXMLParser::RegisterCDATATag(const String& tag) cdata_tags.insert(StringUtilities::ToLower(tag)); } +void BaseXMLParser::RegisterInnerXMLAttribute(const String& attribute_name) +{ + attributes_for_inner_xml_data.insert(attribute_name); +} + // Parses the given stream as an XML file, and calls the handlers when // interesting phenomenon are encountered. void BaseXMLParser::Parse(Stream* stream) { - xml_source = stream; - buffer_size = DEFAULT_BUFFER_SIZE; + source_url = &stream->GetSourceURL(); + + xml_source.clear(); + + // We read in the whole XML file here. + // TODO: It doesn't look like the Stream interface is used for anything useful. We + // might as well just use a span or StringView, and get completely rid of it. + // @performance Otherwise, use the temporary allocator. + const size_t source_size = stream->Length(); + stream->Read(xml_source, source_size); - buffer = (unsigned char*) malloc(buffer_size); - read = buffer; + xml_index = 0; line_number = 1; - FillBuffer(); + line_number_open_tag = 1; + + inner_xml_data = false; + inner_xml_data_terminate_depth = 0; + inner_xml_data_index_begin = 0; // Read (er ... skip) the header, if one exists. ReadHeader(); // Read the XML body. ReadBody(); - free(buffer); + xml_source.clear(); + source_url = nullptr; } // Get the current file line number @@ -102,17 +110,56 @@ void BaseXMLParser::HandleElementEnd(const String& RMLUI_UNUSED_PARAMETER(name)) } // Called when the parser encounters data. -void BaseXMLParser::HandleData(const String& RMLUI_UNUSED_PARAMETER(data)) +void BaseXMLParser::HandleData(const String& RMLUI_UNUSED_PARAMETER(data), XMLDataType RMLUI_UNUSED_PARAMETER(type)) { RMLUI_UNUSED(data); + RMLUI_UNUSED(type); +} + +/// Returns the source URL of this parse. Only valid during parsing. + +const URL* BaseXMLParser::GetSourceURLPtr() const +{ + return source_url; +} + +void BaseXMLParser::Next() { + xml_index += 1; +} + +bool BaseXMLParser::AtEnd() const { + return xml_index >= xml_source.size(); +} + +char BaseXMLParser::Look() const { + RMLUI_ASSERT(!AtEnd()); + return xml_source[xml_index]; +} + +void BaseXMLParser::HandleElementStartInternal(const String& name, const XMLAttributes& attributes) +{ + if (!inner_xml_data) + HandleElementStart(name, attributes); +} + +void BaseXMLParser::HandleElementEndInternal(const String& name) +{ + if (!inner_xml_data) + HandleElementEnd(name); +} + +void BaseXMLParser::HandleDataInternal(const String& data, XMLDataType type) +{ + if (!inner_xml_data) + HandleData(data, type); } void BaseXMLParser::ReadHeader() { - if (PeekString((unsigned char*) "", temp); + FindString(">", temp); } } @@ -126,35 +173,34 @@ void BaseXMLParser::ReadBody() for(;;) { // Find the next open tag. - if (!FindString((unsigned char*) "<", data)) + if (!FindString("<", data, true)) break; + const size_t xml_index_tag = xml_index - 1; + // Check what kind of tag this is. - if (PeekString((const unsigned char*) "!--")) + if (PeekString("!--")) { // Comment. String temp; - if (!FindString((const unsigned char*) "-->", temp)) + if (!FindString("-->", temp)) break; } - else if (PeekString((const unsigned char*) "![CDATA[")) + else if (PeekString("![CDATA[")) { // CDATA tag; read everything (including markup) until the ending // CDATA tag. if (!ReadCDATA()) break; } - else if (PeekString((const unsigned char*) "/")) + else if (PeekString("/")) { - if (!ReadCloseTag()) + if (!ReadCloseTag(xml_index_tag)) break; // Bail if we've hit the end of the XML data. if (open_tag_depth == 0) - { - xml_source->Seek((long)((read - buffer) - buffer_used), SEEK_CUR); break; - } } else { @@ -168,7 +214,7 @@ void BaseXMLParser::ReadBody() // Check for error conditions if (open_tag_depth > 0) { - Log::Message(Log::LT_WARNING, "XML parse error on line %d of %s.", GetLineNumber(), xml_source->GetSourceURL().GetURL().c_str()); + Log::Message(Log::LT_WARNING, "XML parse error on line %d of %s.", GetLineNumber(), source_url->GetURL().c_str()); } } @@ -180,7 +226,7 @@ bool BaseXMLParser::ReadOpenTag() // Opening tag; send data immediately and open the tag. if (!data.empty()) { - HandleData(data); + HandleDataInternal(data, XMLDataType::Text); data.clear(); } @@ -190,18 +236,18 @@ bool BaseXMLParser::ReadOpenTag() bool section_opened = false; - if (PeekString((const unsigned char*) ">")) + if (PeekString(">")) { // Simple open tag. - HandleElementStart(tag_name, XMLAttributes()); + HandleElementStartInternal(tag_name, XMLAttributes()); section_opened = true; } - else if (PeekString((const unsigned char*) "/") && - PeekString((const unsigned char*) ">")) + else if (PeekString("/") && + PeekString(">")) { // Empty open tag. - HandleElementStart(tag_name, XMLAttributes()); - HandleElementEnd(tag_name); + HandleElementStartInternal(tag_name, XMLAttributes()); + HandleElementEndInternal(tag_name); // Tag immediately closed, reduce count open_tag_depth--; @@ -209,20 +255,21 @@ bool BaseXMLParser::ReadOpenTag() else { // It appears we have some attributes. Let's parse them. + bool parse_inner_xml_as_data = false; XMLAttributes attributes; - if (!ReadAttributes(attributes)) + if (!ReadAttributes(attributes, parse_inner_xml_as_data)) return false; - if (PeekString((const unsigned char*) ">")) + if (PeekString(">")) { - HandleElementStart(tag_name, attributes); + HandleElementStartInternal(tag_name, attributes); section_opened = true; } - else if (PeekString((const unsigned char*) "/") && - PeekString((const unsigned char*) ">")) + else if (PeekString("/") && + PeekString(">")) { - HandleElementStart(tag_name, attributes); - HandleElementEnd(tag_name); + HandleElementStartInternal(tag_name, attributes); + HandleElementEndInternal(tag_name); // Tag immediately closed, reduce count open_tag_depth--; @@ -231,23 +278,32 @@ bool BaseXMLParser::ReadOpenTag() { return false; } + + if (section_opened && parse_inner_xml_as_data && !inner_xml_data) + { + inner_xml_data = true; + inner_xml_data_terminate_depth = open_tag_depth; + inner_xml_data_index_begin = xml_index; + } } - // Check if this tag needs to processed as CDATA. + // Check if this tag needs to be processed as CDATA. if (section_opened) { - String lcase_tag_name = StringUtilities::ToLower(tag_name); - if (cdata_tags.find(lcase_tag_name) != cdata_tags.end()) + const String lcase_tag_name = StringUtilities::ToLower(tag_name); + bool is_cdata_tag = (cdata_tags.find(lcase_tag_name) != cdata_tags.end()); + + if (is_cdata_tag) { if (ReadCDATA(lcase_tag_name.c_str())) { open_tag_depth--; if (!data.empty()) { - HandleData(data); + HandleDataInternal(data, XMLDataType::CData); data.clear(); } - HandleElementEnd(tag_name); + HandleElementEndInternal(tag_name); return true; } @@ -259,28 +315,41 @@ bool BaseXMLParser::ReadOpenTag() return true; } -bool BaseXMLParser::ReadCloseTag() +bool BaseXMLParser::ReadCloseTag(const size_t xml_index_tag) { + if (inner_xml_data && open_tag_depth == inner_xml_data_terminate_depth) + { + // Closing the tag that initiated the inner xml data parsing. Set all its contents as Data to be + // submitted next, and disable the mode to resume normal parsing behavior. + RMLUI_ASSERT(inner_xml_data_index_begin <= xml_index_tag); + inner_xml_data = false; + data = xml_source.substr(inner_xml_data_index_begin, xml_index_tag - inner_xml_data_index_begin); + HandleDataInternal(data, XMLDataType::InnerXML); + data.clear(); + } + // Closing tag; send data immediately and close the tag. if (!data.empty()) { - HandleData(data); + HandleDataInternal(data, XMLDataType::Text); data.clear(); } String tag_name; - if (!FindString((const unsigned char*) ">", tag_name)) + if (!FindString(">", tag_name)) return false; - HandleElementEnd(StringUtilities::StripWhitespace(tag_name)); + HandleElementEndInternal(StringUtilities::StripWhitespace(tag_name)); + // Tag closed, reduce count open_tag_depth--; + return true; } -bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes) +bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes, bool& parse_raw_xml_content) { for (;;) { @@ -294,16 +363,16 @@ bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes) } // Check if theres an assigned value - if (PeekString((const unsigned char*)"=")) + if (PeekString("=")) { - if (PeekString((const unsigned char*) "\"")) + if (PeekString("\"")) { - if (!FindString((const unsigned char*) "\"", value)) + if (!FindString("\"", value)) return false; } - else if (PeekString((const unsigned char*) "'")) + else if (PeekString("'")) { - if (!FindString((const unsigned char*) "'", value)) + if (!FindString("'", value)) return false; } else if (!FindWord(value, "/>")) @@ -312,21 +381,23 @@ bool BaseXMLParser::ReadAttributes(XMLAttributes& attributes) } } + if (attributes_for_inner_xml_data.count(attribute) == 1) + parse_raw_xml_content = true; + attributes[attribute] = value; // Check for the end of the tag. - if (PeekString((const unsigned char*) "/", false) || - PeekString((const unsigned char*) ">", false)) + if (PeekString("/", false) || PeekString(">", false)) return true; } } -bool BaseXMLParser::ReadCDATA(const char* terminator) +bool BaseXMLParser::ReadCDATA(const char* tag_terminator) { String cdata; - if (terminator == nullptr) + if (tag_terminator == nullptr) { - FindString((const unsigned char*) "]]>", cdata); + FindString("]]>", cdata); data += cdata; return true; } @@ -335,26 +406,24 @@ bool BaseXMLParser::ReadCDATA(const char* terminator) for (;;) { // Search for the next tag opening. - if (!FindString((const unsigned char*) "<", cdata)) + if (!FindString("<", cdata)) return false; - if (PeekString((const unsigned char*) "/", false)) + if (PeekString("/", false)) { String tag; - if (FindString((const unsigned char*) ">", tag)) + if (FindString(">", tag)) { size_t slash_pos = tag.find('/'); String tag_name = StringUtilities::StripWhitespace(slash_pos == String::npos ? tag : tag.substr(slash_pos + 1)); - if (StringUtilities::ToLower(tag_name) == terminator) + if (StringUtilities::ToLower(tag_name) == tag_terminator) { data += cdata; return true; } else { - cdata += "<"; - cdata += tag; - cdata += ">"; + cdata += '<' + tag + '>'; } } else @@ -369,20 +438,16 @@ bool BaseXMLParser::ReadCDATA(const char* terminator) // Reads from the stream until a complete word is found. bool BaseXMLParser::FindWord(String& word, const char* terminators) { - for (;;) + while (!AtEnd()) { - if (read >= buffer + buffer_used) - { - if (!FillBuffer()) - return false; - } + char c = Look(); // Ignore white space - if (StringUtilities::IsWhitespace(*read)) + if (StringUtilities::IsWhitespace(c)) { if (word.empty()) { - read++; + Next(); continue; } else @@ -390,35 +455,49 @@ bool BaseXMLParser::FindWord(String& word, const char* terminators) } // Check for termination condition - if (terminators && strchr(terminators, *read)) + if (terminators && strchr(terminators, c)) { return !word.empty(); } - word += *read; - read++; + word += c; + Next(); } + + return false; } // Reads from the stream until the given character set is found. -bool BaseXMLParser::FindString(const unsigned char* string, String& data) +bool BaseXMLParser::FindString(const char* string, String& data, bool escape_brackets) { int index = 0; + bool in_brackets = false; + char previous = 0; + while (string[index]) { - if (read >= buffer + buffer_used) - { - if (!FillBuffer()) - return false; - } + if (AtEnd()) + return false; + + const char c = Look(); // Count line numbers - if (*read == '\n') + if (c == '\n') { line_number++; } - if (*read == string[index]) + if(escape_brackets) + { + const char* error_str = XMLParseTools::ParseDataBrackets(in_brackets, c, previous); + if (error_str) + { + Log::Message(Log::LT_WARNING, "XML parse error. %s", error_str); + return false; + } + } + + if (c == string[index] && !in_brackets) { index += 1; } @@ -426,14 +505,15 @@ bool BaseXMLParser::FindString(const unsigned char* string, String& data) { if (index > 0) { - data += String((const char*)string, index); + data += String(string, index); index = 0; } - data += *read; + data += c; } - read++; + previous = c; + Next(); } return true; @@ -441,85 +521,44 @@ bool BaseXMLParser::FindString(const unsigned char* string, String& data) // Returns true if the next sequence of characters in the stream matches the // given string. -bool BaseXMLParser::PeekString(const unsigned char* string, bool consume) +bool BaseXMLParser::PeekString(const char* string, bool consume) { - unsigned char* peek_read = read; - + const size_t start_index = xml_index; + bool success = true; int i = 0; while (string[i]) { - // If we're about to read past the end of the buffer, read into the - // overflow buffer. - if ((peek_read - buffer) + i >= buffer_used) + if (AtEnd()) { - int peek_offset = (int)(peek_read - read); - FillBuffer(); - peek_read = read + peek_offset; - - if (peek_read - buffer + i >= buffer_used) - { - // Wierd, seems our buffer is too small, realloc it bigger. - buffer_size *= 2; - int read_offset = (int)(read - buffer); - unsigned char* new_buffer = (unsigned char*) realloc(buffer, buffer_size); - RMLUI_ASSERTMSG(new_buffer != nullptr, "Unable to allocate larger buffer for Peek() call"); - if(new_buffer == nullptr) - { - return false; - } - buffer = new_buffer; - // Restore the read pointers. - read = buffer + read_offset; - peek_read = read + peek_offset; - - // Attempt to fill our new buffer size. - if (!FillBuffer()) - return false; - } + success = false; + break; } + const char c = Look(); + // Seek past all the whitespace if we haven't hit the initial character yet. - if (i == 0 && StringUtilities::IsWhitespace(*peek_read)) + if (i == 0 && StringUtilities::IsWhitespace(c)) { - peek_read++; + Next(); } else { - if (*peek_read != string[i]) - return false; + if (c != string[i]) + { + success = false; + break; + } i++; - peek_read++; + Next(); } } - // Set the read pointer to the end of the peek. - if (consume) - { - read = peek_read; - } - - return true; -} - -// Fill the buffer as much as possible, without removing any content that is still pending -bool BaseXMLParser::FillBuffer() -{ - int bytes_free = buffer_size; - int bytes_remaining = Math::Max((int)(buffer_used - (read - buffer)), 0); - - // If theres any data still in the buffer, shift it down, and fill it again - if (bytes_remaining > 0) - { - memmove(buffer, read, bytes_remaining); - bytes_free = buffer_size - bytes_remaining; - } - - read = buffer; - size_t bytes_read = xml_source->Read(&buffer[bytes_remaining], bytes_free); - buffer_used = (int)(bytes_read + bytes_remaining); + // Set the index to the start index unless we are consuming. + if (!consume || !success) + xml_index = start_index; - return bytes_read > 0; + return success; } } diff --git a/Source/Core/Context.cpp b/Source/Core/Context.cpp index b87ba6936..26fd39953 100644 --- a/Source/Core/Context.cpp +++ b/Source/Core/Context.cpp @@ -36,6 +36,8 @@ #include "../../Include/RmlUi/Core/RenderInterface.h" #include "../../Include/RmlUi/Core/StreamMemory.h" #include "../../Include/RmlUi/Core/SystemInterface.h" +#include "../../Include/RmlUi/Core/DataModel.h" +#include "../../Include/RmlUi/Core/StreamMemory.h" #include "EventDispatcher.h" #include "EventIterators.h" #include "PluginRegistry.h" @@ -796,6 +798,49 @@ void Context::SetInstancer(ContextInstancer* _instancer) instancer = _instancer; } +DataModelConstructor Context::CreateDataModel(const String& name) +{ + if (!data_type_register) + data_type_register = std::make_unique(); + + auto result = data_models.emplace(name, std::make_unique(data_type_register->GetTransformFuncRegister())); + bool inserted = result.second; + if (inserted) + return DataModelConstructor(result.first->second.get(), data_type_register.get()); + + Log::Message(Log::LT_ERROR, "Data model name '%s' already exists.", name.c_str()); + return DataModelConstructor(); +} + +DataModelConstructor Context::GetDataModel(const String& name) +{ + if (data_type_register) + { + if (DataModel* model = GetDataModelPtr(name)) + return DataModelConstructor(model, data_type_register.get()); + } + + Log::Message(Log::LT_ERROR, "Data model name '%s' could not be found.", name.c_str()); + return DataModelConstructor(); +} + +bool Context::RemoveDataModel(const String& name) +{ + auto it = data_models.find(name); + if (it == data_models.end()) + return false; + + DataModel* model = it->second.get(); + ElementList elements = model->GetAttachedModelRootElements(); + + for (Element* element : elements) + element->SetDataModel(nullptr); + + data_models.erase(it); + + return true; +} + // Internal callback for when an element is removed from the hierarchy. void Context::OnElementDetach(Element* element) { @@ -1147,6 +1192,14 @@ void Context::ReleaseDragClone() } } +DataModel* Context::GetDataModelPtr(const String& name) const +{ + auto it = data_models.find(name); + if (it != data_models.end()) + return it->second.get(); + return nullptr; +} + // Builds the parameters for a generic key event. void Context::GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier) { diff --git a/Source/Core/DataController.cpp b/Source/Core/DataController.cpp new file mode 100644 index 000000000..09b2564f3 --- /dev/null +++ b/Source/Core/DataController.cpp @@ -0,0 +1,77 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "../../Include/RmlUi/Core/DataController.h" +#include "../../Include/RmlUi/Core/DataModel.h" +#include "../../Include/RmlUi/Core/Element.h" +#include "EventSpecification.h" + +namespace Rml { +namespace Core { + + +DataController::DataController(Element* element) : attached_element(element->GetObserverPtr()) +{} + +DataController::~DataController() +{} +Element* DataController::GetElement() const { + return attached_element.get(); +} + +bool DataController::IsValid() const { + return static_cast(attached_element); +} + + + +DataControllers::DataControllers() +{} + +DataControllers::~DataControllers() +{} + +void DataControllers::Add(DataControllerPtr controller) { + RMLUI_ASSERT(controller); + + Element* element = controller->GetElement(); + RMLUI_ASSERTMSG(element, "Invalid controller, make sure it is valid before adding"); + if (!element) + return; + + controllers.emplace(element, std::move(controller)); +} + +void DataControllers::OnElementRemove(Element* element) +{ + controllers.erase(element); +} + + +} +} diff --git a/Source/Core/DataControllerDefault.cpp b/Source/Core/DataControllerDefault.cpp new file mode 100644 index 000000000..21e90a0b3 --- /dev/null +++ b/Source/Core/DataControllerDefault.cpp @@ -0,0 +1,160 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "DataControllerDefault.h" +#include "../../Include/RmlUi/Core/DataController.h" +#include "../../Include/RmlUi/Core/DataModel.h" +#include "../../Include/RmlUi/Core/Element.h" +#include "DataExpression.h" +#include "EventSpecification.h" + +namespace Rml { +namespace Core { + + + +DataControllerValue::DataControllerValue(Element* element) : DataController(element) +{} + +DataControllerValue::~DataControllerValue() +{ + if (Element* element = GetElement()) + { + element->RemoveEventListener(EventId::Change, this); + } +} + +bool DataControllerValue::Initialize(DataModel& model, Element* element, const String& variable_name, const String& /*modifier*/) +{ + RMLUI_ASSERT(element); + + DataAddress variable_address = model.ResolveAddress(variable_name, element); + if (variable_address.empty()) + return false; + + if (model.GetVariable(variable_address)) + address = std::move(variable_address); + + element->AddEventListener(EventId::Change, this); + + return true; +} + +void DataControllerValue::ProcessEvent(Event& event) +{ + if (Element* element = GetElement()) + { + const auto& parameters = event.GetParameters(); + auto it = parameters.find("value"); + if (it == parameters.end()) + { + Log::Message(Log::LT_WARNING, "A 'change' event was received, but it did not contain a value. During processing of 'data-value' in %s", element->GetAddress().c_str()); + return; + } + + SetValue(it->second); + } +} + +void DataControllerValue::Release() +{ + delete this; +} + +void DataControllerValue::SetValue(const Variant& value) +{ + Element* element = GetElement(); + if (!element) + return; + + DataModel* model = element->GetDataModel(); + if (!model) + return; + + if (DataVariable variable = model->GetVariable(address)) + { + variable.Set(value); + model->DirtyVariable(address.front().name); + } +} + + +DataControllerEvent::DataControllerEvent(Element* element) : DataController(element) +{} + +DataControllerEvent::~DataControllerEvent() +{ + if (Element* element = GetElement()) + { + if (id != EventId::Invalid) + element->RemoveEventListener(id, this); + } +} + +bool DataControllerEvent::Initialize(DataModel& model, Element* element, const String& expression_str, const String& modifier) +{ + RMLUI_ASSERT(element); + + expression = std::make_unique(expression_str); + DataExpressionInterface interface(&model, element); + + if (!expression->Parse(interface, true)) + return false; + + id = EventSpecificationInterface::GetIdOrInsert(modifier); + if (id == EventId::Invalid) + { + Log::Message(Log::LT_WARNING, "Event type '%s' could not be recognized, while adding 'data-event' to %s", modifier.c_str(), element->GetAddress().c_str()); + return false; + } + + element->AddEventListener(id, this); + + return true; +} + +void DataControllerEvent::ProcessEvent(Event& event) +{ + if (!expression) + return; + + if (Element* element = GetElement()) + { + DataExpressionInterface interface(element->GetDataModel(), element, &event); + Variant unused_value_out; + expression->Run(interface, unused_value_out); + } +} + +void DataControllerEvent::Release() +{ + delete this; +} + +} +} diff --git a/Source/Core/DataControllerDefault.h b/Source/Core/DataControllerDefault.h new file mode 100644 index 000000000..0280f0241 --- /dev/null +++ b/Source/Core/DataControllerDefault.h @@ -0,0 +1,90 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATACONTROLLERDEFAULT_H +#define RMLUICOREDATACONTROLLERDEFAULT_H + +#include "../../Include/RmlUi/Core/Header.h" +#include "../../Include/RmlUi/Core/Types.h" +#include "../../Include/RmlUi/Core/EventListener.h" +#include "../../Include/RmlUi/Core/DataVariable.h" +#include "../../Include/RmlUi/Core/DataController.h" + +namespace Rml { +namespace Core { + +class Element; +class DataModel; +class DataExpression; +using DataExpressionPtr = UniquePtr; + + +class DataControllerValue final : public DataController, private EventListener { +public: + DataControllerValue(Element* element); + ~DataControllerValue(); + + bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override; + +protected: + // Responds to 'Change' events. + void ProcessEvent(Event& event) override; + + // Delete this. + void Release() override; + +private: + void SetValue(const Variant& new_value); + + DataAddress address; +}; + + +class DataControllerEvent final : public DataController, private EventListener { +public: + DataControllerEvent(Element* element); + ~DataControllerEvent(); + + bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override; + +protected: + // Responds to the event type specified in the attribute modifier. + void ProcessEvent(Event& event) override; + + // Delete this. + void Release() override; + +private: + EventId id = EventId::Invalid; + DataExpressionPtr expression; +}; + +} +} + +#endif diff --git a/Source/Core/DataExpression.cpp b/Source/Core/DataExpression.cpp new file mode 100644 index 000000000..6217d7fcd --- /dev/null +++ b/Source/Core/DataExpression.cpp @@ -0,0 +1,1191 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "DataExpression.h" +#include "../../Include/RmlUi/Core/DataModel.h" +#include "../../Include/RmlUi/Core/Event.h" +#include "../../Include/RmlUi/Core/Variant.h" +#include + +#ifdef _MSC_VER +#pragma warning(default : 4061) +#pragma warning(default : 4062) +#endif + +namespace Rml { +namespace Core { + +class DataParser; + +/* + The abstract machine for RmlUi data scripts. + + The machine can execute a program which contains a list of instructions listed below. + + The abstract machine has three registers: + R Typically results and right-hand side arguments. + L Typically left-hand side arguments. + C Typically center arguments (eg. in ternary operator). + + And two stacks: + S The main program stack. + A The arguments stack, only used to pass arguments to an external transform function. + + In addition, each instruction has an optional payload: + D Instruction data (payload). + + Notation used in the instruction list below: + S+ Push to stack S. + S- Pop stack S (returns the popped value). +*/ +enum class Instruction { // Assignment (register/stack) = Read (register R/L/C, instruction data D, or stack) + Push = 'P', // S+ = R + Pop = 'o', // = S- (D determines R/L/C) + Literal = 'D', // R = D + Variable = 'V', // R = DataModel.GetVariable(D) (D is an index into the variable address list) + Add = '+', // R = L + R + Subtract = '-', // R = L - R + Multiply = '*', // R = L * R + Divide = '/', // R = L / R + Not = '!', // R = !R + And = '&', // R = L && R + Or = '|', // R = L || R + Less = '<', // R = L < R + LessEq = 'L', // R = L <= R + Greater = '>', // R = L > R + GreaterEq = 'G', // R = L >= R + Equal = '=', // R = L == R + NotEqual = 'N', // R = L != R + Ternary = '?', // R = L ? C : R + Arguments = 'a', // A+ = S- (Repeated D times, where D gives the num. arguments) + TransformFnc = 'T', // R = DataModel.Execute( D, R, A ); A.Clear(); (D determines function name, R the input value, A the arguments) + EventFnc = 'E', // DataModel.EventCallback(D, A); A.Clear(); + Assign = 'A', // DataModel.SetVariable(D, R) +}; +enum class Register { + R, + L, + C +}; + +struct InstructionData { + Instruction instruction; + Variant data; +}; + +namespace Parse { + static void Assignment(DataParser& parser); + static void Expression(DataParser& parser); +} + + +class DataParser { +public: + DataParser(String expression, DataExpressionInterface expression_interface) : expression(std::move(expression)), expression_interface(expression_interface) {} + + char Look() { + if (reached_end) + return '\0'; + return expression[index]; + } + + bool Match(char c, bool skip_whitespace = true) { + if (c == Look()) { + Next(); + if (skip_whitespace) + SkipWhitespace(); + return true; + } + Expected(c); + return false; + } + + char Next() { + ++index; + if (index >= expression.size()) + reached_end = true; + return Look(); + } + + void SkipWhitespace() { + char c = Look(); + while (StringUtilities::IsWhitespace(c)) + c = Next(); + } + + void Error(const String message) + { + parse_error = true; + Log::Message(Log::LT_WARNING, "Error in data expression at %d. %s", index, message.c_str()); + Log::Message(Log::LT_WARNING, " \"%s\"", expression.c_str()); + + const size_t cursor_offset = size_t(index) + 3; + const String cursor_string = String(cursor_offset, ' ') + '^'; + Log::Message(Log::LT_WARNING, cursor_string.c_str()); + } + void Expected(String expected_symbols) { + const char c = Look(); + if (c == '\0') + Error(CreateString(expected_symbols.size() + 50, "Expected %s but found end of string.", expected_symbols.c_str())); + else + Error(CreateString(expected_symbols.size() + 50, "Expected %s but found character '%c'.", expected_symbols.c_str(), c)); + } + void Expected(char expected) { + Expected(String(1, '\'') + expected + '\''); + } + + bool Parse(bool is_assignment_expression) + { + program.clear(); + variable_addresses.clear(); + index = 0; + reached_end = false; + parse_error = false; + if (expression.empty()) + reached_end = true; + + SkipWhitespace(); + + if (is_assignment_expression) + Parse::Assignment(*this); + else + Parse::Expression(*this); + + if (!reached_end) { + parse_error = true; + Error(CreateString(50, "Unexpected character '%c' encountered.", Look())); + } + if (!parse_error && program_stack_size != 0) { + parse_error = true; + Error(CreateString(120, "Internal parser error, inconsistent stack operations. Stack size is %d at parse end.", program_stack_size)); + } + + return !parse_error; + } + + Program ReleaseProgram() { + RMLUI_ASSERT(!parse_error); + return std::move(program); + } + AddressList ReleaseAddresses() { + RMLUI_ASSERT(!parse_error); + return std::move(variable_addresses); + } + + void Emit(Instruction instruction, Variant data = Variant()) + { + RMLUI_ASSERTMSG(instruction != Instruction::Push && instruction != Instruction::Pop && + instruction != Instruction::Arguments && instruction != Instruction::Variable && instruction != Instruction::Assign, + "Use the Push(), Pop(), Arguments(), Variable(), and Assign() procedures for stack manipulation and variable instructions."); + program.push_back(InstructionData{ instruction, std::move(data) }); + } + void Push() { + program_stack_size += 1; + program.push_back(InstructionData{ Instruction::Push, Variant() }); + } + void Pop(Register destination) { + if (program_stack_size <= 0) { + Error("Internal parser error: Tried to pop an empty stack."); + return; + } + program_stack_size -= 1; + program.push_back(InstructionData{ Instruction::Pop, Variant(int(destination)) }); + } + void Arguments(int num_arguments) { + if (program_stack_size < num_arguments) { + Error(CreateString(128, "Internal parser error: Popping %d arguments, but the stack contains only %d elements.", num_arguments, program_stack_size)); + return; + } + program_stack_size -= num_arguments; + program.push_back(InstructionData{ Instruction::Arguments, Variant(int(num_arguments)) }); + } + void Variable(const String& name) { + VariableGetSet(name, false); + } + void Assign(const String& name) { + VariableGetSet(name, true); + } + +private: + void VariableGetSet(const String& name, bool is_assignment) + { + DataAddress address = expression_interface.ParseAddress(name); + if (address.empty()) { + Error(CreateString(name.size() + 50, "Could not find data variable with name '%s'.", name.c_str())); + return; + } + int index = int(variable_addresses.size()); + variable_addresses.push_back(std::move(address)); + program.push_back(InstructionData{ is_assignment ? Instruction::Assign : Instruction::Variable, Variant(int(index)) }); + } + + const String expression; + DataExpressionInterface expression_interface; + + size_t index = 0; + bool reached_end = false; + bool parse_error = true; + int program_stack_size = 0; + + Program program; + + AddressList variable_addresses; +}; + + +namespace Parse { + + // Forward declare all parse functions. + static void Assignment(DataParser& parser); + + // The following in order of precedence. + static void Expression(DataParser& parser); + static void Relational(DataParser& parser); + static void Additive(DataParser& parser); + static void Term(DataParser& parser); + static void Factor(DataParser& parser); + + static void NumberLiteral(DataParser& parser); + static void StringLiteral(DataParser& parser); + static void Variable(DataParser& parser); + + static void Add(DataParser& parser); + static void Subtract(DataParser& parser); + static void Multiply(DataParser& parser); + static void Divide(DataParser& parser); + + static void Not(DataParser& parser); + static void And(DataParser& parser); + static void Or(DataParser& parser); + static void Less(DataParser& parser); + static void Greater(DataParser& parser); + static void Equal(DataParser& parser); + static void NotEqual(DataParser& parser); + + static void Ternary(DataParser& parser); + static void Function(DataParser& parser, Instruction function_type, const String& name); + + // Helper functions + static bool IsVariableCharacter(char c, bool is_first_character) + { + const bool is_alpha = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + + if (is_first_character) + return is_alpha; + + if (is_alpha || (c >= '0' && c <= '9')) + return true; + + for (char valid_char : "_.[] ") + { + if (c == valid_char && valid_char != '\0') + return true; + } + + return false; + } + static String VariableName(DataParser& parser) + { + String name; + + bool is_first_character = true; + char c = parser.Look(); + + while (IsVariableCharacter(c, is_first_character)) + { + name += c; + c = parser.Next(); + is_first_character = false; + } + + // Right trim spaces in name + size_t new_size = String::npos; + for (int i = int(name.size()) - 1; i >= 1; i--) + { + if (name[i] == ' ') + new_size = size_t(i); + else + break; + } + if (new_size != String::npos) + name.resize(new_size); + + return name; + } + + // Parser functions + static void Assignment(DataParser& parser) + { + bool looping = true; + while (looping) + { + if (parser.Look() != '\0') + { + const String variable_name = VariableName(parser); + if (variable_name.empty()) { + parser.Error("Expected a variable for assignment but got an empty name."); + return; + } + + const char c = parser.Look(); + if (c == '=') + { + parser.Match('='); + Expression(parser); + parser.Assign(variable_name); + } + else if (c == '(' || c == ';' || c == '\0') + { + Function(parser, Instruction::EventFnc, variable_name); + } + else + { + parser.Expected("one of = ; ( or end of string"); + return; + } + } + + const char c = parser.Look(); + if (c == ';') + parser.Match(';'); + else if (c == '\0') + looping = false; + else + { + parser.Expected("';' or end of string"); + looping = false; + } + } + } + static void Expression(DataParser& parser) + { + Relational(parser); + + bool looping = true; + while (looping) + { + switch (parser.Look()) + { + case '&': And(parser); break; + case '|': + { + parser.Match('|', false); + if (parser.Look() == '|') + Or(parser); + else + { + parser.SkipWhitespace(); + const String fnc_name = VariableName(parser); + if (fnc_name.empty()) { + parser.Error("Expected a transform function name but got an empty name."); + return; + } + + Function(parser, Instruction::TransformFnc, fnc_name); + } + } + break; + case '?': Ternary(parser); break; + default: + looping = false; + } + } + } + + static void Relational(DataParser& parser) + { + Additive(parser); + + bool looping = true; + while (looping) + { + switch (parser.Look()) + { + case '=': Equal(parser); break; + case '!': NotEqual(parser); break; + case '<': Less(parser); break; + case '>': Greater(parser); break; + default: + looping = false; + } + } + } + + static void Additive(DataParser& parser) + { + Term(parser); + + bool looping = true; + while (looping) + { + switch (parser.Look()) + { + case '+': Add(parser); break; + case '-': Subtract(parser); break; + default: + looping = false; + } + } + } + + + static void Term(DataParser& parser) + { + Factor(parser); + + bool looping = true; + while (looping) + { + switch (parser.Look()) + { + case '*': Multiply(parser); break; + case '/': Divide(parser); break; + default: + looping = false; + } + } + } + static void Factor(DataParser& parser) + { + const char c = parser.Look(); + + if (c == '(') + { + parser.Match('('); + Expression(parser); + parser.Match(')'); + } + else if (c == '\'') + { + parser.Match('\'', false); + StringLiteral(parser); + parser.Match('\''); + } + else if (c == '!') + { + Not(parser); + parser.SkipWhitespace(); + } + else if (c == '-' || (c >= '0' && c <= '9')) + { + NumberLiteral(parser); + parser.SkipWhitespace(); + } + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) + { + Variable(parser); + parser.SkipWhitespace(); + } + else + parser.Expected("literal, variable name, parenthesis, or '!'"); + } + + static void NumberLiteral(DataParser& parser) + { + String str; + + bool first_match = false; + bool has_dot = false; + char c = parser.Look(); + if (c == '-') + { + str += c; + c = parser.Next(); + } + + while ((c >= '0' && c <= '9') || (c == '.' && !has_dot)) + { + first_match = true; + str += c; + if (c == '.') + has_dot = true; + c = parser.Next(); + } + + if (!first_match) + { + parser.Error(CreateString(100, "Invalid number literal. Expected '0-9' or '.' but found '%c'.", c)); + return; + } + + const double number = FromString(str, 0.0); + + parser.Emit(Instruction::Literal, Variant(number)); + } + static void StringLiteral(DataParser& parser) + { + String str; + + char c = parser.Look(); + char c_prev = '\0'; + + while (c != '\0' && (c != '\'' || c_prev == '\\')) + { + if (c_prev == '\\' && (c == '\\' || c == '\'')) { + str.pop_back(); + c_prev = '\0'; + } + else { + c_prev = c; + } + + str += c; + c = parser.Next(); + } + + parser.Emit(Instruction::Literal, Variant(str)); + } + static void Variable(DataParser& parser) + { + String name = VariableName(parser); + if (name.empty()) { + parser.Error("Expected a variable but got an empty name."); + return; + } + + // Keywords are parsed like variables, but are really literals. + // Check for them here. + if (name == "true") + parser.Emit(Instruction::Literal, Variant(true)); + else if (name == "false") + parser.Emit(Instruction::Literal, Variant(false)); + else + parser.Variable(name); + } + + static void Add(DataParser& parser) + { + parser.Match('+'); + parser.Push(); + Term(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::Add); + } + static void Subtract(DataParser& parser) + { + parser.Match('-'); + parser.Push(); + Term(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::Subtract); + } + static void Multiply(DataParser& parser) + { + parser.Match('*'); + parser.Push(); + Factor(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::Multiply); + } + static void Divide(DataParser& parser) + { + parser.Match('/'); + parser.Push(); + Factor(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::Divide); + } + + static void Not(DataParser& parser) + { + parser.Match('!'); + Factor(parser); + parser.Emit(Instruction::Not); + } + static void Or(DataParser& parser) + { + // We already skipped the first '|' during expression + parser.Match('|'); + parser.Push(); + Relational(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::Or); + } + static void And(DataParser& parser) + { + parser.Match('&', false); + parser.Match('&'); + parser.Push(); + Relational(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::And); + } + static void Less(DataParser& parser) + { + Instruction instruction = Instruction::Less; + parser.Match('<', false); + if (parser.Look() == '=') { + parser.Match('='); + instruction = Instruction::LessEq; + } + else { + parser.SkipWhitespace(); + } + parser.Push(); + Additive(parser); + parser.Pop(Register::L); + parser.Emit(instruction); + } + static void Greater(DataParser& parser) + { + Instruction instruction = Instruction::Greater; + parser.Match('>', false); + if (parser.Look() == '=') { + parser.Match('='); + instruction = Instruction::GreaterEq; + } + else { + parser.SkipWhitespace(); + } + parser.Push(); + Additive(parser); + parser.Pop(Register::L); + parser.Emit(instruction); + } + static void Equal(DataParser& parser) + { + parser.Match('=', false); + parser.Match('='); + parser.Push(); + Additive(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::Equal); + } + static void NotEqual(DataParser& parser) + { + parser.Match('!', false); + parser.Match('='); + parser.Push(); + Additive(parser); + parser.Pop(Register::L); + parser.Emit(Instruction::NotEqual); + } + + static void Ternary(DataParser& parser) + { + parser.Match('?'); + parser.Push(); + Expression(parser); + parser.Push(); + parser.Match(':'); + Expression(parser); + parser.Pop(Register::C); + parser.Pop(Register::L); + parser.Emit(Instruction::Ternary); + } + static void Function(DataParser& parser, Instruction function_type, const String& func_name) + { + RMLUI_ASSERT(function_type == Instruction::TransformFnc || function_type == Instruction::EventFnc); + + // We already matched the variable name (and '|' for transform functions) + if (parser.Look() == '(') + { + int num_arguments = 0; + bool looping = true; + + parser.Match('('); + if (parser.Look() == ')') { + parser.Match(')'); + looping = false; + } + else + parser.Push(); + + while (looping) + { + num_arguments += 1; + Expression(parser); + parser.Push(); + + switch (parser.Look()) { + case ')': parser.Match(')'); looping = false; break; + case ',': parser.Match(','); break; + default: + parser.Expected("one of ')' or ','"); + looping = false; + } + } + + if (num_arguments > 0) { + parser.Arguments(num_arguments); + parser.Pop(Register::R); + } + } + else { + parser.SkipWhitespace(); + } + + parser.Emit(function_type, Variant(func_name)); + } + + +} // + + + +class DataInterpreter { +public: + DataInterpreter(const Program& program, const AddressList& addresses, DataExpressionInterface expression_interface) + : program(program), addresses(addresses), expression_interface(expression_interface) {} + + bool Error(String message) const + { + message = "Error during execution. " + message; + Log::Message(Log::LT_WARNING, message.c_str()); + RMLUI_ERROR; + return false; + } + + bool Run() + { + bool success = true; + for (size_t i = 0; i < program.size(); i++) + { + if (!Execute(program[i].instruction, program[i].data)) + { + success = false; + break; + } + } + + if(success && !stack.empty()) + Log::Message(Log::LT_WARNING, "Possible data interpreter stack corruption. Stack size is %d at end of execution (should be zero).", stack.size()); + + if(!success) + { + String program_str = DumpProgram(); + Log::Message(Log::LT_WARNING, "Failed to execute program with %d instructions:", program.size()); + Log::Message(Log::LT_WARNING, program_str.c_str()); + } + + return success; + } + + String DumpProgram() const + { + String str; + for (size_t i = 0; i < program.size(); i++) + { + String instruction_str = program[i].data.Get(); + str += CreateString(50 + instruction_str.size(), " %4d '%c' %s\n", i, char(program[i].instruction), instruction_str.c_str()); + } + return str; + } + + Variant Result() const { + return R; + } + + +private: + Variant R, L, C; + std::stack stack; + std::vector arguments; + + const Program& program; + const AddressList& addresses; + DataExpressionInterface expression_interface; + + bool Execute(const Instruction instruction, const Variant& data) + { + auto AnyString = [](const Variant& v1, const Variant& v2) { + return v1.GetType() == Variant::STRING || v2.GetType() == Variant::STRING; + }; + + switch (instruction) + { + case Instruction::Push: + { + stack.push(std::move(R)); + R.Clear(); + } + break; + case Instruction::Pop: + { + if (stack.empty()) + return Error("Cannot pop stack, it is empty."); + + Register reg = Register(data.Get(-1)); + switch (reg) { + case Register::R: R = stack.top(); stack.pop(); break; + case Register::L: L = stack.top(); stack.pop(); break; + case Register::C: C = stack.top(); stack.pop(); break; + default: + return Error(CreateString(50, "Invalid register %d.", int(reg))); + } + } + break; + case Instruction::Literal: + { + R = data; + } + break; + case Instruction::Variable: + { + size_t variable_index = size_t(data.Get(-1)); + if (variable_index < addresses.size()) + R = expression_interface.GetValue(addresses[variable_index]); + else + return Error("Variable address not found."); + } + break; + case Instruction::Add: + { + if (AnyString(L, R)) + R = Variant(L.Get() + R.Get()); + else + R = Variant(L.Get() + R.Get()); + } + break; + case Instruction::Subtract: R = Variant(L.Get() - R.Get()); break; + case Instruction::Multiply: R = Variant(L.Get() * R.Get()); break; + case Instruction::Divide: R = Variant(L.Get() / R.Get()); break; + case Instruction::Not: R = Variant(!R.Get()); break; + case Instruction::And: R = Variant(L.Get() && R.Get()); break; + case Instruction::Or: R = Variant(L.Get() || R.Get()); break; + case Instruction::Less: R = Variant(L.Get() < R.Get()); break; + case Instruction::LessEq: R = Variant(L.Get() <= R.Get()); break; + case Instruction::Greater: R = Variant(L.Get() > R.Get()); break; + case Instruction::GreaterEq: R = Variant(L.Get() >= R.Get()); break; + case Instruction::Equal: + { + if (AnyString(L, R)) + R = Variant(L.Get() == R.Get()); + else + R = Variant(L.Get() == R.Get()); + } + break; + case Instruction::NotEqual: + { + if (AnyString(L, R)) + R = Variant(L.Get() != R.Get()); + else + R = Variant(L.Get() != R.Get()); + } + break; + case Instruction::Ternary: + { + if (L.Get()) + R = C; + } + break; + case Instruction::Arguments: + { + if (!arguments.empty()) + return Error("Argument stack is not empty."); + + int num_arguments = data.Get(-1); + if (num_arguments < 0) + return Error("Invalid number of arguments."); + if (stack.size() < size_t(num_arguments)) + return Error(CreateString(100, "Cannot pop %d arguments, stack contains only %d elements.", num_arguments, stack.size())); + + arguments.resize(num_arguments); + for (int i = num_arguments - 1; i >= 0; i--) + { + arguments[i] = std::move(stack.top()); + stack.pop(); + } + } + break; + case Instruction::TransformFnc: + { + const String function_name = data.Get(); + + if (!expression_interface.CallTransform(function_name, R, arguments)) + { + String arguments_str; + for (size_t i = 0; i < arguments.size(); i++) + { + arguments_str += arguments[i].Get(); + if (i < arguments.size() - 1) + arguments_str += ", "; + } + Error(CreateString(50 + function_name.size() + arguments_str.size(), "Failed to execute data function: %s(%s)", function_name.c_str(), arguments_str.c_str())); + } + + arguments.clear(); + } + break; + case Instruction::EventFnc: + { + const String function_name = data.Get(); + + if (!expression_interface.EventCallback(function_name, arguments)) + { + String arguments_str; + for (size_t i = 0; i < arguments.size(); i++) + { + arguments_str += arguments[i].Get(); + if (i < arguments.size() - 1) + arguments_str += ", "; + } + Error(CreateString(50 + function_name.size() + arguments_str.size(), "Failed to execute event callback: %s(%s)", function_name.c_str(), arguments_str.c_str())); + } + + arguments.clear(); + } + break; + case Instruction::Assign: + { + size_t variable_index = size_t(data.Get(-1)); + if (variable_index < addresses.size()) + { + if (!expression_interface.SetValue(addresses[variable_index], R)) + return Error("Could not assign to variable."); + } + else + return Error("Variable address not found."); + } + break; + default: + RMLUI_ERRORMSG("Instruction not implemented."); break; + } + return true; + } +}; + + + +#ifdef RMLUI_TESTS_ENABLED + +struct TestParser { + TestParser() : model(type_register.GetTransformFuncRegister()) + { + DataModelConstructor handle(&model, &type_register); + handle.Bind("radius", &radius); + handle.Bind("color_name", &color_name); + handle.BindFunc("color_value", [this](Rml::Core::Variant& variant) { + variant = ToString(color_value); + }); + + String result; + result = TestExpression("!!10 - 1 ? 'hello' : 'world' | to_upper", "WORLD"); + result = TestExpression("(color_name) + (': rgba(' + color_value + ')')", "color: rgba(180, 100, 255, 255)"); + result = TestExpression("'hello world' | to_upper(5 + 12 == 17 ? 'yes' : 'no', 9*2)", "HELLO WORLD"); + result = TestExpression("true == false", "0"); + result = TestExpression("true != false", "1"); + result = TestExpression("true", "1"); + + result = TestExpression("true || false ? true && 3==1+2 ? 'Absolutely!' : 'well..' : 'no'", "Absolutely!"); + result = TestExpression(R"('It\'s a fit')", R"(It's a fit)"); + result = TestExpression("2 * 2", "4"); + result = TestExpression("50000 / 1500", "33.333"); + result = TestExpression("5*1+2", "7"); + result = TestExpression("5*(1+2)", "15"); + result = TestExpression("2*(-2)/4", "-1"); + result = TestExpression("5.2 + 19 + 'px'", "24.2px"); + + result = TestExpression("(radius | format(2)) + 'm'", "8.70m"); + result = TestExpression("radius < 10.5 ? 'smaller' : 'larger'", "smaller"); + TestAssignment("radius = 15"); + result = TestExpression("radius < 10.5 ? 'smaller' : 'larger'", "larger"); + TestAssignment("radius = 4; color_name = 'image-color'"); + result = TestExpression("radius == 4 && color_name == 'image-color'", "1"); + + result = TestExpression("5 == 1 + 2*2 || 8 == 1 + 4 ? 'yes' : 'no'", "yes"); + result = TestExpression("!!('fa' + 'lse')", "0"); + result = TestExpression("!!('tr' + 'ue')", "1"); + result = TestExpression("'fox' + 'dog' ? 'FoxyDog' : 'hot' + 'dog' | to_upper", "HOTDOG"); + + result = TestExpression("3.62345 | round", "4"); + result = TestExpression("3.62345 | format(0)", "4"); + result = TestExpression("3.62345 | format(2)", "3.62"); + result = TestExpression("3.62345 | format(10)", "3.6234500000"); + result = TestExpression("3.62345 | format(10, true)", "3.62345"); + result = TestExpression("3.62345 | round | format(2)", "4.00"); + result = TestExpression("3.0001 | format(2, false)", "3.00"); + result = TestExpression("3.0001 | format(2, true)", "3"); + + result = TestExpression("0.2 + 3.42345 | round", "4"); + result = TestExpression("(3.42345 | round) + 0.2", "3.2"); + result = TestExpression("(3.42345 | format(0)) + 0.2", "30.2"); // Here, format(0) returns a string, so the + means string concatenation. + } + + String TestExpression(String expression, String expected = String()) + { + String result; + DataExpressionInterface interface(&model, nullptr); + DataParser parser(expression, interface); + if (parser.Parse(false)) + { + Program program = parser.ReleaseProgram(); + AddressList addresses = parser.ReleaseAddresses(); + + DataInterpreter interpreter(program, addresses, interface); + if (interpreter.Run()) + result = interpreter.Result().Get(); + + if (!expected.empty() && result != expected) + { + String program_str = interpreter.DumpProgram(); + Log::Message(Log::LT_WARNING, "%s", program_str.c_str()); + RMLUI_ERRORMSG("Got unexpected data parser result."); + } + } + else + { + RMLUI_ERRORMSG("Could not parse expression."); + } + + return result; + }; + + bool TestAssignment(String expression) + { + bool result = false; + DataExpressionInterface interface(&model, nullptr); + DataParser parser(expression, interface); + if (parser.Parse(true)) + { + Program program = parser.ReleaseProgram(); + AddressList addresses = parser.ReleaseAddresses(); + + DataInterpreter interpreter(program, addresses, interface); + result = interpreter.Run(); + } + RMLUI_ASSERT(result); + return result; + }; + + DataTypeRegister type_register; + DataModel model; + + float radius = 8.7f; + String color_name = "color"; + Colourb color_value = Colourb(180, 100, 255); +}; + +static TestParser test_parser; + +#endif + + +DataExpression::DataExpression(String expression) : expression(expression) {} + +DataExpression::~DataExpression() +{ +} + +bool DataExpression::Parse(const DataExpressionInterface& expression_interface, bool is_assignment_expression) +{ + DataParser parser(expression, expression_interface); + if (!parser.Parse(is_assignment_expression)) + return false; + + program = parser.ReleaseProgram(); + addresses = parser.ReleaseAddresses(); + + return true; +} + +bool DataExpression::Run(const DataExpressionInterface& expression_interface, Variant& out_value) +{ + DataInterpreter interpreter(program, addresses, expression_interface); + + if (!interpreter.Run()) + return false; + + out_value = interpreter.Result(); + return true; +} + +StringList DataExpression::GetVariableNameList() const +{ + StringList list; + list.reserve(addresses.size()); + for (const DataAddress& address : addresses) + { + if (!address.empty()) + list.push_back(address[0].name); + } + return list; +} + +DataExpressionInterface::DataExpressionInterface(DataModel* data_model, Element* element, Event* event) : data_model(data_model), element(element), event(event) +{} + +DataAddress DataExpressionInterface::ParseAddress(const String& address_str) const +{ + if (address_str.size() >= 4 && address_str[0] == 'e' && address_str[1] == 'v' && address_str[2] == '.') + return DataAddress{ DataAddressEntry("ev"), DataAddressEntry(address_str.substr(3)) }; + + return data_model ? data_model->ResolveAddress(address_str, element) : DataAddress(); +} +Variant DataExpressionInterface::GetValue(const DataAddress& address) const +{ + Variant result; + if(event && address.size() == 2 && address.front().name == "ev") + { + auto& parameters = event->GetParameters(); + auto it = parameters.find(address.back().name); + if (it != parameters.end()) + result = it->second; + } + else if (data_model) + { + data_model->GetVariableInto(address, result); + } + return result; +} + +bool DataExpressionInterface::SetValue(const DataAddress& address, const Variant& value) const +{ + bool result = false; + if (data_model && !address.empty()) + { + if (DataVariable variable = data_model->GetVariable(address)) + result = variable.Set(value); + + if (result) + data_model->DirtyVariable(address.front().name); + } + return result; +} + +bool DataExpressionInterface::CallTransform(const String& name, Variant& inout_variant, const VariantList& arguments) +{ + return data_model ? data_model->CallTransform(name, inout_variant, arguments) : false; +} + +bool DataExpressionInterface::EventCallback(const String& name, const VariantList& arguments) +{ + if (!data_model || !event) + return false; + + const DataEventFunc* func = data_model->GetEventCallback(name); + if (!func || !*func) + return false; + + DataModelHandle handle(data_model); + func->operator()(handle, *event, arguments); + return true; +} + +} +} diff --git a/Source/Core/DataExpression.h b/Source/Core/DataExpression.h new file mode 100644 index 000000000..1d914f215 --- /dev/null +++ b/Source/Core/DataExpression.h @@ -0,0 +1,85 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATAEXPRESSION_H +#define RMLUICOREDATAEXPRESSION_H + +#include "../../Include/RmlUi/Core/Header.h" +#include "../../Include/RmlUi/Core/Types.h" +#include "../../Include/RmlUi/Core/DataTypes.h" + +namespace Rml { +namespace Core { + +class Element; +class DataModel; +struct InstructionData; +using Program = std::vector; +using AddressList = std::vector; + +class DataExpressionInterface { +public: + DataExpressionInterface() = default; + DataExpressionInterface(DataModel* data_model, Element* element, Event* event = nullptr); + + DataAddress ParseAddress(const String& address_str) const; + Variant GetValue(const DataAddress& address) const; + bool SetValue(const DataAddress& address, const Variant& value) const; + bool CallTransform(const String& name, Variant& inout_result, const VariantList& arguments); + bool EventCallback(const String& name, const VariantList& arguments); + +private: + DataModel* data_model = nullptr; + Element* element = nullptr; + Event* event = nullptr; +}; + + +class DataExpression { +public: + DataExpression(String expression); + ~DataExpression(); + + bool Parse(const DataExpressionInterface& expression_interface, bool is_assignment_expression); + + bool Run(const DataExpressionInterface& expression_interface, Variant& out_value); + + // Available after Parse() + StringList GetVariableNameList() const; + +private: + String expression; + + Program program; + AddressList addresses; +}; + +} +} + +#endif diff --git a/Source/Core/DataModel.cpp b/Source/Core/DataModel.cpp new file mode 100644 index 000000000..aeb18ed69 --- /dev/null +++ b/Source/Core/DataModel.cpp @@ -0,0 +1,470 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "../../Include/RmlUi/Core/DataModel.h" +#include "../../Include/RmlUi/Core/DataController.h" +#include "../../Include/RmlUi/Core/DataView.h" +#include "../../Include/RmlUi/Core/Element.h" + +namespace Rml { +namespace Core { + + +static DataAddress ParseAddress(const String& address_str) +{ + StringList list; + StringUtilities::ExpandString(list, address_str, '.'); + + DataAddress address; + address.reserve(list.size() * 2); + + for (const auto& item : list) + { + if (item.empty()) + return DataAddress(); + + size_t i_open = item.find('[', 0); + if (i_open == 0) + return DataAddress(); + + address.emplace_back(item.substr(0, i_open)); + + while (i_open != String::npos) + { + size_t i_close = item.find(']', i_open + 1); + if (i_close == String::npos) + return DataAddress(); + + int index = FromString(item.substr(i_open + 1, i_close - i_open), -1); + if (index < 0) + return DataAddress(); + + address.emplace_back(index); + + i_open = item.find('[', i_close + 1); + } + // TODO: Abort on invalid characters among [ ] and after the last found bracket? + } + + RMLUI_ASSERT(!address.empty() && !address[0].name.empty()); + + return address; +} + +// Returns an error string on error, or nullptr on success. +static const char* LegalVariableName(const String& name) +{ + static SmallUnorderedSet reserved_names{ "it", "ev", "true", "false", "size", "literal" }; + + if (name.empty()) + return "Name cannot be empty."; + + const String name_lower = StringUtilities::ToLower(name); + + const char first = name_lower.front(); + if (!(first >= 'a' && first <= 'z')) + return "First character must be 'a-z' or 'A-Z'."; + + for (const char c : name_lower) + { + if (!(c == '_' || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) + return "Name must strictly contain characters a-z, A-Z, 0-9 and under_score."; + } + + if (reserved_names.count(name_lower) == 1) + return "Name is reserved."; + + return nullptr; +} + +static String DataAddressToString(const DataAddress& address) +{ + String result; + bool is_first = true; + for (auto& entry : address) + { + if (entry.index >= 0) + result += '[' + ToString(entry.index) + ']'; + else + { + if (!is_first) + result += "."; + result += entry.name; + } + is_first = false; + } + return result; +} + +DataModel::DataModel(const TransformFuncRegister* transform_register) : transform_register(transform_register) +{ + views = std::make_unique(); + controllers = std::make_unique(); +} + +DataModel::~DataModel() +{ + RMLUI_ASSERT(attached_elements.empty()); +} + +void DataModel::AddView(DataViewPtr view) { + views->Add(std::move(view)); +} + +void DataModel::AddController(DataControllerPtr controller) { + controllers->Add(std::move(controller)); +} + +bool DataModel::BindVariable(const String& name, DataVariable variable) +{ + const char* name_error_str = LegalVariableName(name); + if (name_error_str) + { + Log::Message(Log::LT_WARNING, "Could not bind data variable '%s'. %s", name.c_str(), name_error_str); + return false; + } + + if (!variable) + { + Log::Message(Log::LT_WARNING, "Could not bind variable '%s' to data model, data type not registered.", name.c_str()); + return false; + } + + bool inserted = variables.emplace(name, variable).second; + if (!inserted) + { + Log::Message(Log::LT_WARNING, "Data model variable with name '%s' already exists.", name.c_str()); + return false; + } + + return true; +} + +bool DataModel::BindFunc(const String& name, DataGetFunc get_func, DataSetFunc set_func) +{ + auto result = function_variable_definitions.emplace(name, nullptr); + auto& it = result.first; + bool inserted = result.second; + if (!inserted) + { + Log::Message(Log::LT_ERROR, "Data get/set function with name %s already exists in model", name.c_str()); + return false; + } + auto& func_definition_ptr = it->second; + func_definition_ptr = std::make_unique(std::move(get_func), std::move(set_func)); + + return BindVariable(name, DataVariable(func_definition_ptr.get(), nullptr)); +} + +bool DataModel::BindEventCallback(const String& name, DataEventFunc event_func) +{ + const char* name_error_str = LegalVariableName(name); + if (name_error_str) + { + Log::Message(Log::LT_WARNING, "Could not bind data event callback '%s'. %s", name.c_str(), name_error_str); + return false; + } + + if (!event_func) + { + Log::Message(Log::LT_WARNING, "Could not bind data event callback '%s' to data model, empty function provided.", name.c_str()); + return false; + } + + bool inserted = event_callbacks.emplace(name, std::move(event_func)).second; + if (!inserted) + { + Log::Message(Log::LT_WARNING, "Data event callback with name '%s' already exists.", name.c_str()); + return false; + } + + return true; +} + +bool DataModel::InsertAlias(Element* element, const String& alias_name, DataAddress replace_with_address) +{ + if (replace_with_address.empty() || replace_with_address.front().name.empty()) + { + Log::Message(Log::LT_WARNING, "Could not add alias variable '%s' to data model, replacement address invalid.", alias_name.c_str()); + return false; + } + + if (variables.count(alias_name) == 1) + Log::Message(Log::LT_WARNING, "Alias variable '%s' is shadowed by a global variable.", alias_name.c_str()); + + auto& map = aliases.emplace(element, SmallUnorderedMap()).first->second; + + auto it = map.find(alias_name); + if (it != map.end()) + Log::Message(Log::LT_WARNING, "Alias name '%s' in data model already exists, replaced.", alias_name.c_str()); + + map[alias_name] = std::move(replace_with_address); + + return true; +} + +bool DataModel::EraseAliases(Element* element) +{ + return aliases.erase(element) == 1; +} + +DataAddress DataModel::ResolveAddress(const String& address_str, Element* element) const +{ + DataAddress address = ParseAddress(address_str); + + if (address.empty()) + return address; + + const String& first_name = address.front().name; + + auto it = variables.find(first_name); + if (it != variables.end()) + return address; + + // Look for a variable alias for the first name. + + Element* ancestor = element; + while (ancestor && ancestor->GetDataModel() == this) + { + auto it_element = aliases.find(ancestor); + if (it_element != aliases.end()) + { + const auto& alias_names = it_element->second; + auto it_alias_name = alias_names.find(first_name); + if (it_alias_name != alias_names.end()) + { + const DataAddress& replace_address = it_alias_name->second; + if (replace_address.empty() || replace_address.front().name.empty()) + { + // Variable alias is invalid + return DataAddress(); + } + + // Insert the full alias address, replacing the first element. + address[0] = replace_address[0]; + address.insert(address.begin() + 1, replace_address.begin() + 1, replace_address.end()); + return address; + } + } + + ancestor = ancestor->GetParentNode(); + } + + Log::Message(Log::LT_WARNING, "Could not find variable name '%s' in data model.", address_str.c_str()); + + return DataAddress(); +} + +DataVariable DataModel::GetVariable(const DataAddress& address) const +{ + if (address.empty()) + return DataVariable(); + + auto it = variables.find(address.front().name); + if (it != variables.end()) + { + DataVariable variable = it->second; + + for (int i = 1; i < (int)address.size() && variable; i++) + { + variable = variable.Child(address[i]); + if (!variable) + return DataVariable(); + } + + return variable; + } + + if (address[0].name == "literal") + { + if (address.size() > 2 && address[1].name == "int") + return MakeLiteralIntVariable(address[2].index); + } + + return DataVariable(); +} + +const DataEventFunc* DataModel::GetEventCallback(const String& name) +{ + auto it = event_callbacks.find(name); + if (it == event_callbacks.end()) + { + Log::Message(Log::LT_WARNING, "Could not find data event callback '%s' in data model.", name.c_str()); + return nullptr; + } + + return &it->second; +} + +bool DataModel::GetVariableInto(const DataAddress& address, Variant& out_value) const { + DataVariable variable = GetVariable(address); + bool result = (variable && variable.Get(out_value)); + if (!result) + Log::Message(Log::LT_WARNING, "Could not get value from data variable '%s'.", DataAddressToString(address).c_str()); + return result; +} + +void DataModel::DirtyVariable(const String& variable_name) +{ + RMLUI_ASSERTMSG(LegalVariableName(variable_name) == nullptr, "Illegal variable name provided. Only top-level variables can be dirtied."); + RMLUI_ASSERTMSG(variables.count(variable_name) == 1, "In DirtyVariable: Variable name not found among added variables."); + dirty_variables.emplace(variable_name); +} + +bool DataModel::IsVariableDirty(const String& variable_name) const +{ + RMLUI_ASSERTMSG(LegalVariableName(variable_name) == nullptr, "Illegal variable name provided. Only top-level variables can be dirtied."); + return dirty_variables.count(variable_name) == 1; +} + +bool DataModel::CallTransform(const String& name, Variant& inout_result, const VariantList& arguments) const +{ + if (transform_register) + return transform_register->Call(name, inout_result, arguments); + return false; +} + +void DataModel::AttachModelRootElement(Element* element) +{ + attached_elements.insert(element); +} + +ElementList DataModel::GetAttachedModelRootElements() const +{ + return ElementList(attached_elements.begin(), attached_elements.end()); +} + +void DataModel::OnElementRemove(Element* element) +{ + EraseAliases(element); + views->OnElementRemove(element); + controllers->OnElementRemove(element); + attached_elements.erase(element); +} + +bool DataModel::Update() +{ + bool result = views->Update(*this, dirty_variables); + dirty_variables.clear(); + return result; +} + + + +#ifdef RMLUI_DEBUG + +static struct TestDataVariables { + TestDataVariables() + { + using IntVector = std::vector; + + struct FunData { + int i = 99; + String x = "hello"; + IntVector magic = { 3, 5, 7, 11, 13 }; + }; + + using FunArray = std::array; + + struct SmartData { + bool valid = true; + FunData fun; + FunArray more_fun; + }; + + DataModel model; + DataTypeRegister types; + + DataModelConstructor handle(&model, &types); + + { + handle.RegisterArray(); + + if (auto fun_handle = handle.RegisterStruct()) + { + fun_handle.RegisterMember("i", &FunData::i); + fun_handle.RegisterMember("x", &FunData::x); + fun_handle.RegisterMember("magic", &FunData::magic); + } + + handle.RegisterArray(); + + if (auto smart_handle = handle.RegisterStruct()) + { + smart_handle.RegisterMember("valid", &SmartData::valid); + smart_handle.RegisterMember("fun", &SmartData::fun); + smart_handle.RegisterMember("more_fun", &SmartData::more_fun); + } + } + + SmartData data; + data.fun.x = "Hello, we're in SmartData!"; + + handle.Bind("data", &data); + + { + std::vector test_addresses = { "data.more_fun[1].magic[3]", "data.more_fun[1].magic.size", "data.fun.x", "data.valid" }; + std::vector expected_results = { ToString(data.more_fun[1].magic[3]), ToString(int(data.more_fun[1].magic.size())), ToString(data.fun.x), ToString(data.valid) }; + + std::vector results; + + for (auto& str_address : test_addresses) + { + DataAddress address = ParseAddress(str_address); + + Variant result; + if(model.GetVariableInto(address, result)) + results.push_back(result.Get()); + } + + RMLUI_ASSERT(results == expected_results); + + bool success = true; + success &= model.GetVariable(ParseAddress("data.more_fun[1].magic[1]")).Set(Variant(String("199"))); + RMLUI_ASSERT(success && data.more_fun[1].magic[1] == 199); + + data.fun.magic = { 99, 190, 55, 2000, 50, 60, 70, 80, 90 }; + + Variant get_result; + + const int magic_size = int(data.fun.magic.size()); + success &= model.GetVariable(ParseAddress("data.fun.magic.size")).Get(get_result); + RMLUI_ASSERT(success && get_result.Get() == ToString(magic_size)); + RMLUI_ASSERT(model.GetVariable(ParseAddress("data.fun.magic")).Size() == magic_size); + + success &= model.GetVariable(ParseAddress("data.fun.magic[8]")).Get(get_result); + RMLUI_ASSERT(success && get_result.Get() == "90"); + } + } +} test_data_variables; + + +#endif + +} +} diff --git a/Source/Core/DataTypeRegister.cpp b/Source/Core/DataTypeRegister.cpp new file mode 100644 index 000000000..03f8eb708 --- /dev/null +++ b/Source/Core/DataTypeRegister.cpp @@ -0,0 +1,126 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "../../Include/RmlUi/Core/DataTypeRegister.h" + +namespace Rml { +namespace Core { + + +DataTypeRegister::DataTypeRegister() +{ + // Add default transform functions. + + transform_register.Register("to_lower", [](Variant& variant, const VariantList& /*arguments*/) -> bool { + String value; + if (!variant.GetInto(value)) + return false; + variant = StringUtilities::ToLower(value); + return true; + }); + + transform_register.Register("to_upper", [](Variant& variant, const VariantList& /*arguments*/) -> bool { + String value; + if (!variant.GetInto(value)) + return false; + variant = StringUtilities::ToUpper(value); + return true; + }); + + transform_register.Register("format", [](Variant& variant, const VariantList& arguments) -> bool { + // Arguments in: + // 0 : int[0,32] Precision. Number of digits after the decimal point. + // [1]: bool True to remove trailing zeros (default = false). + if (arguments.size() < 1 || arguments.size() > 2) { + Log::Message(Log::LT_WARNING, "Transform function 'format' requires at least one argument, at most two arguments."); + return false; + } + int precision = 0; + if (!arguments[0].GetInto(precision) || precision < 0 || precision > 32) { + Log::Message(Log::LT_WARNING, "Transform function 'format': First argument must be an integer in [0, 32]."); + return false; + } + bool remove_trailing_zeros = false; + if (arguments.size() >= 2) { + if (!arguments[1].GetInto(remove_trailing_zeros)) + return false; + } + + double value = 0; + if (!variant.GetInto(value)) + return false; + + String format_specifier = String(remove_trailing_zeros ? "%#." : "%.") + ToString(precision) + 'f'; + String result; + if (FormatString(result, 64, format_specifier.c_str(), value) == 0) + return false; + + if (remove_trailing_zeros) + StringUtilities::TrimTrailingDotZeros(result); + + variant = result; + return true; + }); + + transform_register.Register("round", [](Variant& variant, const VariantList& /*arguments*/) -> bool { + double value = 0; + if (!variant.GetInto(value)) + return false; + variant = Math::RoundFloat(value); + return true; + }); +} + +DataTypeRegister::~DataTypeRegister() +{} + +void TransformFuncRegister::Register(const String& name, DataTransformFunc transform_func) +{ + RMLUI_ASSERT(transform_func); + bool inserted = transform_functions.emplace(name, std::move(transform_func)).second; + if (!inserted) + { + Log::Message(Log::LT_ERROR, "Transform function '%s' already exists.", name.c_str()); + RMLUI_ERROR; + } +} + +bool TransformFuncRegister::Call(const String& name, Variant& inout_result, const VariantList& arguments) const +{ + auto it = transform_functions.find(name); + if (it == transform_functions.end()) + return false; + + const DataTransformFunc& transform_func = it->second; + RMLUI_ASSERT(transform_func); + + return transform_func(inout_result, arguments); +} + +} +} diff --git a/Source/Core/DataVariable.cpp b/Source/Core/DataVariable.cpp new file mode 100644 index 000000000..dfea84cf0 --- /dev/null +++ b/Source/Core/DataVariable.cpp @@ -0,0 +1,90 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "../../Include/RmlUi/Core/DataVariable.h" + +namespace Rml { +namespace Core { + +bool DataVariable::Get(Variant& variant) { + return definition->Get(ptr, variant); +} + +bool DataVariable::Set(const Variant& variant) { + return definition->Set(ptr, variant); +} + +int DataVariable::Size() { + return definition->Size(ptr); +} + +DataVariable DataVariable::Child(const DataAddressEntry& address) { + return definition->Child(ptr, address); +} + +DataVariableType DataVariable::Type() { + return definition->Type(); +} + + +bool VariableDefinition::Get(void* /*ptr*/, Variant& /*variant*/) { + Log::Message(Log::LT_WARNING, "Values can only be retrieved from scalar data types."); + return false; +} +bool VariableDefinition::Set(void* /*ptr*/, const Variant& /*variant*/) { + Log::Message(Log::LT_WARNING, "Values can only be assigned to scalar data types."); + return false; +} +int VariableDefinition::Size(void* /*ptr*/) { + Log::Message(Log::LT_WARNING, "Tried to get the size from a non-array data type."); + return 0; +} +DataVariable VariableDefinition::Child(void* /*ptr*/, const DataAddressEntry& /*address*/) { + Log::Message(Log::LT_WARNING, "Tried to get the child of a scalar type."); + return DataVariable(); +} + +class LiteralIntDefinition final : public VariableDefinition { +public: + LiteralIntDefinition() : VariableDefinition(DataVariableType::Scalar) {} + + bool Get(void* ptr, Variant& variant) override + { + variant = static_cast(reinterpret_cast(ptr)); + return true; + } +}; + +DataVariable MakeLiteralIntVariable(int value) +{ + static LiteralIntDefinition literal_int_definition; + return DataVariable(&literal_int_definition, reinterpret_cast(static_cast(value))); +} + +} +} diff --git a/Source/Core/DataView.cpp b/Source/Core/DataView.cpp new file mode 100644 index 000000000..a835bbb26 --- /dev/null +++ b/Source/Core/DataView.cpp @@ -0,0 +1,162 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "../../Include/RmlUi/Core/DataView.h" +#include "../../Include/RmlUi/Core/Element.h" +#include + +namespace Rml { +namespace Core { + +DataView::~DataView() +{} + +Element* DataView::GetElement() const +{ + Element* result = attached_element.get(); + if (!result) + Log::Message(Log::LT_WARNING, "Could not retrieve element in view, was it destroyed?"); + return result; +} + +int DataView::GetElementDepth() const { + return element_depth; +} + +bool DataView::IsValid() const { + return static_cast(attached_element); +} + +DataView::DataView(Element* element) : attached_element(element->GetObserverPtr()), element_depth(0) { + if (element) + { + for (Element* parent = element->GetParentNode(); parent; parent = parent->GetParentNode()) + element_depth += 1; + } +} + + +DataViews::DataViews() +{} + +DataViews::~DataViews() +{} + +void DataViews::Add(DataViewPtr view) { + views_to_add.push_back(std::move(view)); +} + +void DataViews::OnElementRemove(Element* element) +{ + for (auto it = views.begin(); it != views.end();) + { + auto& view = *it; + if (view && view->GetElement() == element) + { + views_to_remove.push_back(std::move(view)); + it = views.erase(it); + } + else + ++it; + } +} + +bool DataViews::Update(DataModel& model, const DirtyVariables& dirty_variables) +{ + bool result = false; + + // View updates may result in newly added views, thus we do it recursively but with an upper limit. + // Without the loop, newly added views won't be updated until the next Update() call. + for(int i = 0; i == 0 || (!views_to_add.empty() && i < 10); i++) + { + std::vector dirty_views; + + if (!views_to_add.empty()) + { + views.reserve(views.size() + views_to_add.size()); + for (auto&& view : views_to_add) + { + dirty_views.push_back(view.get()); + for (const String& variable_name : view->GetVariableNameList()) + name_view_map.emplace(variable_name, view.get()); + + views.push_back(std::move(view)); + } + views_to_add.clear(); + } + + for (const String& variable_name : dirty_variables) + { + auto pair = name_view_map.equal_range(variable_name); + for (auto it = pair.first; it != pair.second; ++it) + dirty_views.push_back(it->second); + } + + // Remove duplicate entries + std::sort(dirty_views.begin(), dirty_views.end()); + auto it_remove = std::unique(dirty_views.begin(), dirty_views.end()); + dirty_views.erase(it_remove, dirty_views.end()); + + // Sort by the element's depth in the document tree so that any structural changes due to a changed variable are reflected in the element's children. + // Eg. the 'data-for' view will remove children if any of its data variable array size is reduced. + std::sort(dirty_views.begin(), dirty_views.end(), [](auto&& left, auto&& right) { return left->GetElementDepth() < right->GetElementDepth(); }); + + for (DataView* view : dirty_views) + { + RMLUI_ASSERT(view); + if (!view) + continue; + + if (view->IsValid()) + result |= view->Update(model); + } + + // Destroy views marked for destruction + // @performance: Horrible... + if (!views_to_remove.empty()) + { + for (const auto& view : views_to_remove) + { + for (auto it = name_view_map.begin(); it != name_view_map.end(); ) + { + if (it->second == view.get()) + it = name_view_map.erase(it); + else + ++it; + } + } + + views_to_remove.clear(); + } + } + + return result; +} + +} +} diff --git a/Source/Core/DataViewDefault.cpp b/Source/Core/DataViewDefault.cpp new file mode 100644 index 000000000..6b8d6b3f3 --- /dev/null +++ b/Source/Core/DataViewDefault.cpp @@ -0,0 +1,496 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "DataViewDefault.h" +#include "DataExpression.h" +#include "../../Include/RmlUi/Core/DataModel.h" +#include "../../Include/RmlUi/Core/Element.h" +#include "../../Include/RmlUi/Core/ElementText.h" +#include "../../Include/RmlUi/Core/Factory.h" +#include "../../Include/RmlUi/Core/Variant.h" + +namespace Rml { +namespace Core { + + +DataViewCommon::DataViewCommon(Element* element, String override_modifier) : DataView(element), modifier(override_modifier) +{} + +bool DataViewCommon::Initialize(DataModel& model, Element* element, const String& expression_str, const String& in_modifier) +{ + // The modifier can be overriden in the constructor + if (modifier.empty()) + modifier = in_modifier; + + expression = std::make_unique(expression_str); + DataExpressionInterface interface(&model, element); + + bool result = expression->Parse(interface, false); + return result; +} + +StringList DataViewCommon::GetVariableNameList() const { + RMLUI_ASSERT(expression); + return expression->GetVariableNameList(); +} + +const String& DataViewCommon::GetModifier() const { + return modifier; +} + +DataExpression& DataViewCommon::GetExpression() { + RMLUI_ASSERT(expression); + return *expression; +} + +void DataViewCommon::Release() +{ + delete this; +} + + +DataViewAttribute::DataViewAttribute(Element* element) : DataViewCommon(element) +{} + +DataViewAttribute::DataViewAttribute(Element * element, String override_attribute) : DataViewCommon(element, std::move(override_attribute)) +{} + +bool DataViewAttribute::Update(DataModel& model) +{ + const String& attribute_name = GetModifier(); + bool result = false; + Variant variant; + Element* element = GetElement(); + DataExpressionInterface interface(&model, element); + + if (element && GetExpression().Run(interface, variant)) + { + const String value = variant.Get(); + const Variant* attribute = element->GetAttribute(attribute_name); + + if (!attribute || (attribute && attribute->Get() != value)) + { + element->SetAttribute(attribute_name, value); + result = true; + } + } + return result; +} + + +DataViewValue::DataViewValue(Element* element) : DataViewAttribute(element, "value") +{} + + +DataViewStyle::DataViewStyle(Element* element) : DataViewCommon(element) +{} + +bool DataViewStyle::Update(DataModel& model) +{ + const String& property_name = GetModifier(); + bool result = false; + Variant variant; + Element* element = GetElement(); + DataExpressionInterface interface(&model, element); + + if (element && GetExpression().Run(interface, variant)) + { + const String value = variant.Get(); + const Property* p = element->GetLocalProperty(property_name); + if (!p || p->Get() != value) + { + element->SetProperty(property_name, value); + result = true; + } + } + return result; +} + + +DataViewClass::DataViewClass(Element* element) : DataViewCommon(element) +{} + +bool DataViewClass::Update(DataModel& model) +{ + const String& class_name = GetModifier(); + bool result = false; + Variant variant; + Element* element = GetElement(); + DataExpressionInterface interface(&model, element); + + if (element && GetExpression().Run(interface, variant)) + { + const bool activate = variant.Get(); + const bool is_set = element->IsClassSet(class_name); + if (activate != is_set) + { + element->SetClass(class_name, activate); + result = true; + } + } + return result; +} + + +DataViewRml::DataViewRml(Element* element) : DataViewCommon(element) +{} + +bool DataViewRml::Update(DataModel & model) +{ + bool result = false; + Variant variant; + Element* element = GetElement(); + DataExpressionInterface interface(&model, element); + + if (element && GetExpression().Run(interface, variant)) + { + String new_rml = variant.Get(); + if (new_rml != previous_rml) + { + element->SetInnerRML(new_rml); + previous_rml = std::move(new_rml); + result = true; + } + } + return result; +} + + +DataViewIf::DataViewIf(Element* element) : DataViewCommon(element) +{} + +bool DataViewIf::Update(DataModel& model) +{ + bool result = false; + Variant variant; + Element* element = GetElement(); + DataExpressionInterface interface(&model, element); + + if (element && GetExpression().Run(interface, variant)) + { + const bool value = variant.Get(); + const bool is_visible = (element->GetLocalStyleProperties().count(PropertyId::Display) == 0); + if(is_visible != value) + { + if (value) + element->RemoveProperty(PropertyId::Display); + else + element->SetProperty(PropertyId::Display, Property(Style::Display::None)); + result = true; + } + } + return result; +} + + +DataViewVisible::DataViewVisible(Element* element) : DataViewCommon(element) +{} + +bool DataViewVisible::Update(DataModel& model) +{ + bool result = false; + Variant variant; + Element* element = GetElement(); + DataExpressionInterface interface(&model, element); + + if (element && GetExpression().Run(interface, variant)) + { + const bool value = variant.Get(); + const bool is_visible = (element->GetLocalStyleProperties().count(PropertyId::Visibility) == 0); + if (is_visible != value) + { + if (value) + element->RemoveProperty(PropertyId::Visibility); + else + element->SetProperty(PropertyId::Visibility, Property(Style::Visibility::Hidden)); + result = true; + } + } + return result; +} + + +DataViewText::DataViewText(Element* element) : DataView(element) +{} + +bool DataViewText::Initialize(DataModel& model, Element* element, const String& RMLUI_UNUSED_PARAMETER(expression), const String& RMLUI_UNUSED_PARAMETER(modifier)) +{ + RMLUI_UNUSED(expression); + RMLUI_UNUSED(modifier); + + ElementText* element_text = rmlui_dynamic_cast(element); + if (!element_text) + return false; + + const String& in_text = element_text->GetText(); + + text.reserve(in_text.size()); + + DataExpressionInterface expression_interface(&model, element); + + size_t previous_close_brackets = 0; + size_t begin_brackets = 0; + while ((begin_brackets = in_text.find("{{", begin_brackets)) != String::npos) + { + text.insert(text.end(), in_text.begin() + previous_close_brackets, in_text.begin() + begin_brackets); + + const size_t begin_name = begin_brackets + 2; + const size_t end_name = in_text.find("}}", begin_name); + + if (end_name == String::npos) + return false; + + DataEntry entry; + entry.index = text.size(); + entry.data_expression = std::make_unique(String(in_text.begin() + begin_name, in_text.begin() + end_name)); + + if (entry.data_expression->Parse(expression_interface, false)) + data_entries.push_back(std::move(entry)); + + previous_close_brackets = end_name + 2; + begin_brackets = previous_close_brackets; + } + + if (data_entries.empty()) + return false; + + if (previous_close_brackets < in_text.size()) + text.insert(text.end(), in_text.begin() + previous_close_brackets, in_text.end()); + + return true; +} + +bool DataViewText::Update(DataModel& model) +{ + bool entries_modified = false; + { + Element* element = GetElement(); + DataExpressionInterface expression_interface(&model, element); + + for (DataEntry& entry : data_entries) + { + RMLUI_ASSERT(entry.data_expression); + Variant variant; + bool result = entry.data_expression->Run(expression_interface, variant); + const String value = variant.Get(); + if (result && entry.value != value) + { + entry.value = value; + entries_modified = true; + } + } + } + + if (entries_modified) + { + if (Element* element = GetElement()) + { + RMLUI_ASSERTMSG(rmlui_dynamic_cast(element), "Somehow the element type was changed from ElementText since construction of the view. Should not be possible?"); + + if (ElementText* text_element = static_cast(element)) + { + String new_text = BuildText(); + text_element->SetText(new_text); + } + } + else + { + Log::Message(Log::LT_WARNING, "Could not update data view text, element no longer valid. Was it destroyed?"); + } + } + + return entries_modified; +} + +StringList DataViewText::GetVariableNameList() const +{ + StringList full_list; + full_list.reserve(data_entries.size()); + + for (const DataEntry& entry : data_entries) + { + RMLUI_ASSERT(entry.data_expression); + + StringList entry_list = entry.data_expression->GetVariableNameList(); + full_list.insert(full_list.end(), + std::make_move_iterator(entry_list.begin()), + std::make_move_iterator(entry_list.end()) + ); + } + + return full_list; +} + +void DataViewText::Release() +{ + delete this; +} + +String DataViewText::BuildText() const +{ + size_t reserve_size = text.size(); + + for (const DataEntry& entry : data_entries) + reserve_size += entry.value.size(); + + String result; + result.reserve(reserve_size); + + size_t previous_index = 0; + for (const DataEntry& entry : data_entries) + { + result += text.substr(previous_index, entry.index - previous_index); + result += entry.value; + previous_index = entry.index; + } + + if (previous_index < text.size()) + result += text.substr(previous_index); + + return result; +} + + + +DataViewFor::DataViewFor(Element* element) : DataView(element) +{} + +bool DataViewFor::Initialize(DataModel& model, Element* element, const String& in_expression, const String& in_rml_content) +{ + rml_contents = in_rml_content; + + StringList iterator_container_pair; + StringUtilities::ExpandString(iterator_container_pair, in_expression, ':'); + + if (iterator_container_pair.empty() || iterator_container_pair.size() > 2 || iterator_container_pair.front().empty() || iterator_container_pair.back().empty()) + { + Log::Message(Log::LT_WARNING, "Invalid syntax in data-for '%s'", in_expression.c_str()); + return false; + } + + if (iterator_container_pair.size() == 2) + { + StringList iterator_index_pair; + StringUtilities::ExpandString(iterator_index_pair, iterator_container_pair.front(), ','); + + if (iterator_index_pair.empty()) + { + Log::Message(Log::LT_WARNING, "Invalid syntax in data-for '%s'", in_expression.c_str()); + return false; + } + else if (iterator_index_pair.size() == 1) + { + iterator_name = iterator_index_pair.front(); + } + else if (iterator_index_pair.size() == 2) + { + iterator_name = iterator_index_pair.front(); + iterator_index_name = iterator_index_pair.back(); + } + } + + if (iterator_name.empty()) + iterator_name = "it"; + + if (iterator_index_name.empty()) + iterator_index_name = "it_index"; + + const String& container_name = iterator_container_pair.back(); + + container_address = model.ResolveAddress(container_name, element); + if (container_address.empty()) + return false; + + element->SetProperty(PropertyId::Display, Property(Style::Display::None)); + + return true; +} + + +bool DataViewFor::Update(DataModel& model) +{ + DataVariable variable = model.GetVariable(container_address); + if (!variable) + return false; + + bool result = false; + const int size = variable.Size(); + const int num_elements = (int)elements.size(); + Element* element = GetElement(); + + for (int i = 0; i < Math::Max(size, num_elements); i++) + { + if (i >= num_elements) + { + ElementPtr new_element_ptr = Factory::InstanceElement(nullptr, element->GetTagName(), element->GetTagName(), attributes); + + DataAddress iterator_address; + iterator_address.reserve(container_address.size() + 1); + iterator_address = container_address; + iterator_address.push_back(DataAddressEntry(i)); + + DataAddress iterator_index_address = { + {"literal"}, {"int"}, {i} + }; + + model.InsertAlias(new_element_ptr.get(), iterator_name, std::move(iterator_address)); + model.InsertAlias(new_element_ptr.get(), iterator_index_name, std::move(iterator_index_address)); + + Element* new_element = element->GetParentNode()->InsertBefore(std::move(new_element_ptr), element); + elements.push_back(new_element); + + elements[i]->SetInnerRML(rml_contents); + + RMLUI_ASSERT(i < (int)elements.size()); + } + if (i >= size) + { + model.EraseAliases(elements[i]); + elements[i]->GetParentNode()->RemoveChild(elements[i]).reset(); + elements[i] = nullptr; + } + } + + if (num_elements > size) + elements.resize(size); + + return result; +} + +StringList DataViewFor::GetVariableNameList() const { + RMLUI_ASSERT(!container_address.empty()); + return StringList{ container_address.front().name }; +} + +void DataViewFor::Release() +{ + delete this; +} + +} +} diff --git a/Source/Core/DataViewDefault.h b/Source/Core/DataViewDefault.h new file mode 100644 index 000000000..8e8f94c4e --- /dev/null +++ b/Source/Core/DataViewDefault.h @@ -0,0 +1,176 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RMLUICOREDATAVIEWDEFAULT_H +#define RMLUICOREDATAVIEWDEFAULT_H + +#include "../../Include/RmlUi/Core/Header.h" +#include "../../Include/RmlUi/Core/Types.h" +#include "../../Include/RmlUi/Core/DataView.h" +#include "../../Include/RmlUi/Core/Variant.h" + +namespace Rml { +namespace Core { + +class Element; +class DataExpression; +using DataExpressionPtr = UniquePtr; + + +class DataViewCommon : public DataView { +public: + DataViewCommon(Element* element, String override_modifier = String()); + + bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override; + + StringList GetVariableNameList() const override; + +protected: + const String& GetModifier() const; + DataExpression& GetExpression(); + + // Delete this + void Release() override; + +private: + String modifier; + DataExpressionPtr expression; +}; + + +class DataViewAttribute : public DataViewCommon { +public: + DataViewAttribute(Element* element); + DataViewAttribute(Element* element, String override_attribute); + + bool Update(DataModel& model) override; +}; + + +class DataViewValue final : public DataViewAttribute { +public: + DataViewValue(Element* element); +}; + + +class DataViewStyle final : public DataViewCommon { +public: + DataViewStyle(Element* element); + + bool Update(DataModel& model) override; +}; + + +class DataViewClass final : public DataViewCommon { +public: + DataViewClass(Element* element); + + bool Update(DataModel& model) override; +}; + + +class DataViewRml final : public DataViewCommon { +public: + DataViewRml(Element* element); + + bool Update(DataModel& model) override; + +private: + String previous_rml; +}; + + +class DataViewIf final : public DataViewCommon { +public: + DataViewIf(Element* element); + + bool Update(DataModel& model) override; +}; + + +class DataViewVisible final : public DataViewCommon { +public: + DataViewVisible(Element* element); + + bool Update(DataModel& model) override; +}; + + +class DataViewText final : public DataView { +public: + DataViewText(Element* in_element); + + bool Initialize(DataModel& model, Element* element, const String& expression, const String& modifier) override; + + bool Update(DataModel& model) override; + StringList GetVariableNameList() const override; + +protected: + void Release() override; + +private: + String BuildText() const; + + struct DataEntry { + size_t index = 0; // Index into 'text' + DataExpressionPtr data_expression; + String value; + }; + + String text; + std::vector data_entries; +}; + + +class DataViewFor final : public DataView { +public: + DataViewFor(Element* element); + + bool Initialize(DataModel& model, Element* element, const String& expression, const String& inner_rml) override; + + bool Update(DataModel& model) override; + + StringList GetVariableNameList() const override; + +protected: + void Release() override; + +private: + DataAddress container_address; + String iterator_name; + String iterator_index_name; + String rml_contents; + ElementAttributes attributes; + + ElementList elements; +}; + +} +} + +#endif diff --git a/Source/Core/Element.cpp b/Source/Core/Element.cpp index 87a01f43a..f4e36c585 100644 --- a/Source/Core/Element.cpp +++ b/Source/Core/Element.cpp @@ -30,6 +30,7 @@ #include "../../Include/RmlUi/Core/Element.h" #include "../../Include/RmlUi/Core/Context.h" #include "../../Include/RmlUi/Core/Core.h" +#include "../../Include/RmlUi/Core/DataModel.h" #include "../../Include/RmlUi/Core/ElementDocument.h" #include "../../Include/RmlUi/Core/ElementInstancer.h" #include "../../Include/RmlUi/Core/ElementScroll.h" @@ -147,6 +148,7 @@ transform_state(), dirty_transform(false), dirty_perspective(false), dirty_anima clipping_state_dirty = true; meta = element_meta_chunk_pool.AllocateAndConstruct(this); + data_model = nullptr; } Element::~Element() @@ -1282,7 +1284,6 @@ Element* Element::AppendChild(ElementPtr child, bool dom_element) { RMLUI_ASSERT(child); Element* child_ptr = child.get(); - child_ptr->SetParent(this); if (dom_element) children.insert(children.end() - num_non_dom_children, std::move(child)); else @@ -1290,6 +1291,8 @@ Element* Element::AppendChild(ElementPtr child, bool dom_element) children.push_back(std::move(child)); num_non_dom_children++; } + // Set parent just after inserting into children. This allows us to eg. get our previous sibling in SetParent. + child_ptr->SetParent(this); Element* ancestor = child_ptr; for (int i = 0; i <= ChildNotifyLevels && ancestor; i++, ancestor = ancestor->GetParentNode()) @@ -1331,7 +1334,6 @@ Element* Element::InsertBefore(ElementPtr child, Element* adjacent_element) if (found_child) { child_ptr = child.get(); - child_ptr->SetParent(this); if ((int) child_index >= GetNumChildren()) num_non_dom_children++; @@ -1339,6 +1341,7 @@ Element* Element::InsertBefore(ElementPtr child, Element* adjacent_element) DirtyLayout(); children.insert(children.begin() + child_index, std::move(child)); + child_ptr->SetParent(this); Element* ancestor = child_ptr; for (int i = 0; i <= ChildNotifyLevels && ancestor; i++, ancestor = ancestor->GetParentNode()) @@ -1373,9 +1376,9 @@ ElementPtr Element::ReplaceChild(ElementPtr inserted_element, Element* replaced_ return nullptr; } + children.insert(insertion_point, std::move(inserted_element)); inserted_element_ptr->SetParent(this); - children.insert(insertion_point, std::move(inserted_element)); ElementPtr result = RemoveChild(replaced_element); Element* ancestor = inserted_element_ptr; @@ -1513,6 +1516,11 @@ ElementScroll* Element::GetElementScroll() const { return &meta->scroll; } + +DataModel* Element::GetDataModel() const +{ + return data_model; +} int Element::GetClippingIgnoreDepth() { @@ -1606,7 +1614,6 @@ void Element::OnAttributeChange(const ElementAttributes& changed_attributes) meta->style.SetClassNames(it->second.Get()); } - // Add any inline style declarations. it = changed_attributes.find("style"); if (it != changed_attributes.end()) { @@ -1933,6 +1940,25 @@ void Element::SetOwnerDocument(ElementDocument* document) } } +void Element::SetDataModel(DataModel* new_data_model) +{ + RMLUI_ASSERTMSG(!data_model || !new_data_model, "We must either attach a new data model, or detach the old one."); + + if (data_model == new_data_model) + return; + + if (data_model) + data_model->OnElementRemove(this); + + data_model = new_data_model; + + if (data_model) + ElementUtilities::ApplyDataViewsControllers(this); + + for (ElementPtr& child : children) + child->SetDataModel(new_data_model); +} + void Element::Release() { if (instancer) @@ -1960,6 +1986,36 @@ void Element::SetParent(Element* _parent) DirtyTransformState(true, true); SetOwnerDocument(parent ? parent->GetOwnerDocument() : nullptr); + + if (!parent) + { + if (data_model) + SetDataModel(nullptr); + } + else + { + auto it = attributes.find("data-model"); + if (it == attributes.end()) + { + SetDataModel(parent->data_model); + } + else if (parent->data_model) + { + String name = it->second.Get(); + Log::Message(Log::LT_ERROR, "Nested data models are not allowed. Data model '%s' given in element %s.", name.c_str(), GetAddress().c_str()); + } + else if (Context* context = GetContext()) + { + String name = it->second.Get(); + if (DataModel* model = context->GetDataModelPtr(name)) + { + model->AttachModelRootElement(this); + SetDataModel(model); + } + else + Log::Message(Log::LT_ERROR, "Could not locate data model '%s' in element %s.", name.c_str(), GetAddress().c_str()); + } + } } void Element::DirtyOffset() diff --git a/Source/Core/ElementUtilities.cpp b/Source/Core/ElementUtilities.cpp index da3cabb4e..c17596d39 100644 --- a/Source/Core/ElementUtilities.cpp +++ b/Source/Core/ElementUtilities.cpp @@ -27,18 +27,21 @@ */ #include "../../Include/RmlUi/Core/ElementUtilities.h" -#include "../../Include/RmlUi/Core/TransformState.h" +#include "../../Include/RmlUi/Core/Context.h" +#include "../../Include/RmlUi/Core/Core.h" +#include "../../Include/RmlUi/Core/DataController.h" +#include "../../Include/RmlUi/Core/DataModel.h" +#include "../../Include/RmlUi/Core/DataView.h" #include "../../Include/RmlUi/Core/Element.h" #include "../../Include/RmlUi/Core/ElementScroll.h" -#include "../../Include/RmlUi/Core/Context.h" +#include "../../Include/RmlUi/Core/Factory.h" #include "../../Include/RmlUi/Core/FontEngineInterface.h" #include "../../Include/RmlUi/Core/RenderInterface.h" -#include "../../Include/RmlUi/Core/Core.h" -#include "../../Include/RmlUi/Core/Factory.h" +#include "../../Include/RmlUi/Core/TransformState.h" +#include "ElementStyle.h" +#include "LayoutEngine.h" #include #include -#include "LayoutEngine.h" -#include "ElementStyle.h" namespace Rml { namespace Core { @@ -381,5 +384,121 @@ bool ElementUtilities::ApplyTransform(Element &element) return true; } + +static bool ApplyDataViewsControllersInternal(Element* element, const bool construct_structural_view, const String& structural_view_inner_rml) +{ + RMLUI_ASSERT(element); + bool result = false; + + // If we have an active data model, check the attributes for any data bindings + if (DataModel* data_model = element->GetDataModel()) + { + struct ViewControllerInitializer { + String type; + String modifier_or_inner_rml; + String expression; + DataViewPtr view; + DataControllerPtr controller; + explicit operator bool() const { return view || controller; } + }; + + // Since data views and controllers may modify the element's attributes during initialization, we + // need to iterate over all the attributes _before_ initializing any views or controllers. We store + // the information needed to initialize them in the following container. + std::vector initializer_list; + + for (auto& attribute : element->GetAttributes()) + { + // Data views and controllers are declared by the following element attribute: + // data-[type]-[modifier]="[expression]" + + constexpr size_t data_str_length = sizeof("data-") - 1; + + const String& name = attribute.first; + + if (name.size() > data_str_length && name[0] == 'd' && name[1] == 'a' && name[2] == 't' && name[3] == 'a' && name[4] == '-') + { + const size_t type_end = name.find('-', data_str_length); + const size_t type_size = (type_end == String::npos ? String::npos : type_end - data_str_length); + String type_name = name.substr(data_str_length, type_size); + + ViewControllerInitializer initializer; + + // Structural data views are applied in a separate step from the normal views and controllers. + if (construct_structural_view) + { + if (DataViewPtr view = Factory::InstanceDataView(type_name, element, true)) + { + initializer.modifier_or_inner_rml = structural_view_inner_rml; + initializer.view = std::move(view); + } + } + else + { + const size_t modifier_offset = data_str_length + type_name.size() + 1; + if (modifier_offset < name.size()) + initializer.modifier_or_inner_rml = name.substr(modifier_offset); + + if (DataViewPtr view = Factory::InstanceDataView(type_name, element, false)) + initializer.view = std::move(view); + + if (DataControllerPtr controller = Factory::InstanceDataController(type_name, element)) + initializer.controller = std::move(controller); + } + + if (initializer) + { + initializer.type = std::move(type_name); + initializer.expression = attribute.second.Get(); + + initializer_list.push_back(std::move(initializer)); + } + } + } + + // Now, we can safely initialize the data views and controllers, even modifying the element's attributes when desired. + for (ViewControllerInitializer& initializer : initializer_list) + { + DataViewPtr& view = initializer.view; + DataControllerPtr& controller = initializer.controller; + + if (view) + { + if (view->Initialize(*data_model, element, initializer.expression, initializer.modifier_or_inner_rml)) + { + data_model->AddView(std::move(view)); + result = true; + } + else + Log::Message(Log::LT_WARNING, "Could not add data-%s view to element: %s", initializer.type.c_str(), element->GetAddress().c_str()); + } + + if (controller) + { + if (controller->Initialize(*data_model, element, initializer.expression, initializer.modifier_or_inner_rml)) + { + data_model->AddController(std::move(controller)); + result = true; + } + else + Log::Message(Log::LT_WARNING, "Could not add data-%s controller to element: %s", initializer.type.c_str(), element->GetAddress().c_str()); + } + } + } + + return result; +} + + +bool ElementUtilities::ApplyDataViewsControllers(Element* element) +{ + return ApplyDataViewsControllersInternal(element, false, String()); +} + +bool ElementUtilities::ApplyStructuralDataViews(Element* element, const String& inner_rml) +{ + return ApplyDataViewsControllersInternal(element, true, inner_rml); +} + } } diff --git a/Source/Core/Factory.cpp b/Source/Core/Factory.cpp index fdd6abcfe..ba9c33932 100644 --- a/Source/Core/Factory.cpp +++ b/Source/Core/Factory.cpp @@ -39,6 +39,8 @@ #include "../../Include/RmlUi/Core/SystemInterface.h" #include "ContextInstancerDefault.h" +#include "DataControllerDefault.h" +#include "DataViewDefault.h" #include "DecoratorTiledBoxInstancer.h" #include "DecoratorTiledHorizontalInstancer.h" #include "DecoratorTiledImageInstancer.h" @@ -63,22 +65,38 @@ #include "XMLNodeHandlerHead.h" #include "XMLNodeHandlerTemplate.h" #include "XMLParseTools.h" +#include namespace Rml { namespace Core { // Element instancers. -typedef UnorderedMap< String, ElementInstancer* > ElementInstancerMap; +using ElementInstancerMap = UnorderedMap< String, ElementInstancer* >; static ElementInstancerMap element_instancers; // Decorator instancers. -typedef UnorderedMap< String, DecoratorInstancer* > DecoratorInstancerMap; +using DecoratorInstancerMap = UnorderedMap< String, DecoratorInstancer* >; static DecoratorInstancerMap decorator_instancers; // Font effect instancers. -typedef UnorderedMap< String, FontEffectInstancer* > FontEffectInstancerMap; +using FontEffectInstancerMap = UnorderedMap< String, FontEffectInstancer* >; static FontEffectInstancerMap font_effect_instancers; +// Data view instancers. +using DataViewInstancerMap = UnorderedMap< String, DataViewInstancer* >; +static DataViewInstancerMap data_view_instancers; + +// Data controller instancers. +using DataControllerInstancerMap = UnorderedMap< String, DataControllerInstancer* >; +static DataControllerInstancerMap data_controller_instancers; + +// Structural data view instancers. +using StructuralDataViewInstancerMap = SmallUnorderedMap< String, DataViewInstancer* >; +static StructuralDataViewInstancerMap structural_data_view_instancers; + +// Structural data view names. +static StringList structural_data_view_attribute_names; + // The context instancer. static ContextInstancer* context_instancer = nullptr; @@ -113,6 +131,21 @@ struct DefaultInstancers { Ptr font_effect_glow = std::make_unique(); Ptr font_effect_outline = std::make_unique(); Ptr font_effect_shadow = std::make_unique(); + + Ptr data_view_attribute = std::make_unique>(); + Ptr data_view_class = std::make_unique>(); + Ptr data_view_if = std::make_unique>(); + Ptr data_view_visible = std::make_unique>(); + Ptr data_view_rml = std::make_unique>(); + Ptr data_view_style = std::make_unique>(); + Ptr data_view_text = std::make_unique>(); + Ptr data_view_value = std::make_unique>(); + + Ptr structural_data_view_for = std::make_unique>(); + + Ptr data_controller_value = std::make_unique>(); + Ptr data_controller_event = std::make_unique>(); + }; static UniquePtr default_instancers; @@ -175,6 +208,21 @@ bool Factory::Initialise() XMLParser::RegisterNodeHandler("head", std::make_shared()); XMLParser::RegisterNodeHandler("template", std::make_shared()); + // Register the default data views + RegisterDataViewInstancer(default_instancers->data_view_attribute.get(), "attr", false); + RegisterDataViewInstancer(default_instancers->data_view_class.get(), "class", false); + RegisterDataViewInstancer(default_instancers->data_view_if.get(), "if", false); + RegisterDataViewInstancer(default_instancers->data_view_visible.get(), "visible", false); + RegisterDataViewInstancer(default_instancers->data_view_rml.get(), "rml", false); + RegisterDataViewInstancer(default_instancers->data_view_style.get(), "style", false); + RegisterDataViewInstancer(default_instancers->data_view_text.get(), "text", false); + RegisterDataViewInstancer(default_instancers->data_view_value.get(), "value", false); + RegisterDataViewInstancer(default_instancers->structural_data_view_for.get(), "for", true ); + + RegisterDataControllerInstancer(default_instancers->data_controller_value.get(), "value"); + RegisterDataControllerInstancer(default_instancers->data_controller_event.get(), "event"); + + return true; } @@ -186,6 +234,10 @@ void Factory::Shutdown() font_effect_instancers.clear(); + data_view_instancers.clear(); + structural_data_view_instancers.clear(); + structural_data_view_attribute_names.clear(); + context_instancer = nullptr; event_listener_instancer = nullptr; @@ -257,21 +309,49 @@ ElementPtr Factory::InstanceElement(Element* parent, const String& instancer_nam } // Instances a single text element containing a string. -bool Factory::InstanceElementText(Element* parent, const String& text) +bool Factory::InstanceElementText(Element* parent, const String& in_text) { - SystemInterface* system_interface = GetSystemInterface(); - - // Do any necessary translation. If any substitutions were made then new XML may have been introduced, so we'll - // have to run the data through the XML parser again. - String translated_data; - if (system_interface != nullptr && - (system_interface->TranslateString(translated_data, text) > 0 || - translated_data.find("<") != String::npos)) + RMLUI_ASSERT(parent); + + String text; + if (SystemInterface* system_interface = GetSystemInterface()) + system_interface->TranslateString(text, in_text); + + // If this text node only contains white-space we don't want to construct it. + const bool only_white_space = std::all_of(text.begin(), text.end(), &StringUtilities::IsWhitespace); + if (only_white_space) + return true; + + // See if we need to parse it as RML, and whether the text contains data expressions (curly brackets). + bool parse_as_rml = false; + bool has_data_expression = false; + + bool inside_brackets = false; + char previous = 0; + for (const char c : text) + { + const char* error_str = XMLParseTools::ParseDataBrackets(inside_brackets, c, previous); + if (error_str) + { + Log::Message(Log::LT_WARNING, "Failed to instance text element '%s'. %s", text.c_str(), error_str); + return false; + } + + if (inside_brackets) + has_data_expression = true; + else if (c == '<') + parse_as_rml = true; + + previous = c; + } + + // If the text contains RML elements then run it through the XML parser again. + if (parse_as_rml) { RMLUI_ZoneScopedNC("InstanceStream", 0xDC143C); - auto stream = std::make_unique(translated_data.size() + 32); + auto stream = std::make_unique(text.size() + 32); stream->Write("", 6); - stream->Write(translated_data); + stream->Write(text); stream->Write("", 7); stream->Seek(0, SEEK_SET); @@ -280,26 +360,18 @@ bool Factory::InstanceElementText(Element* parent, const String& text) else { RMLUI_ZoneScopedNC("InstanceText", 0x8FBC8F); - // Check if this text node contains only white-space; if so, we don't want to construct it. - bool only_white_space = true; - for (size_t i = 0; i < translated_data.size(); ++i) - { - if (!StringUtilities::IsWhitespace(translated_data[i])) - { - only_white_space = false; - break; - } - } - - if (only_white_space) - return true; - + // Attempt to instance the element. XMLAttributes attributes; + + // If we have curly brackets in the text, we tag the element so that the appropriate data view (DataViewText) is constructed. + if (has_data_expression) + attributes.emplace("data-text", Variant()); + ElementPtr element = Factory::InstanceElement(parent, "#text", "#text", attributes); if (!element) { - Log::Message(Log::LT_ERROR, "Failed to instance text element '%s', instancer returned nullptr.", translated_data.c_str()); + Log::Message(Log::LT_ERROR, "Failed to instance text element '%s', instancer returned nullptr.", text.c_str()); return false; } @@ -307,11 +379,11 @@ bool Factory::InstanceElementText(Element* parent, const String& text) ElementText* text_element = rmlui_dynamic_cast< ElementText* >(element.get()); if (!text_element) { - Log::Message(Log::LT_ERROR, "Failed to instance text element '%s'. Found type '%s', was expecting a derivative of ElementText.", translated_data.c_str(), rmlui_type_name(*element)); + Log::Message(Log::LT_ERROR, "Failed to instance text element '%s'. Found type '%s', was expecting a derivative of ElementText.", text.c_str(), rmlui_type_name(*element)); return false; } - text_element->SetText(translated_data); + text_element->SetText(text); // Add to active node. parent->AppendChild(std::move(element)); @@ -459,5 +531,62 @@ EventListener* Factory::InstanceEventListener(const String& value, Element* elem return nullptr; } +void Factory::RegisterDataViewInstancer(DataViewInstancer* instancer, const String& name, bool is_structural_view) +{ + bool inserted = false; + if (is_structural_view) + { + inserted = structural_data_view_instancers.emplace(name, instancer).second; + if (inserted) + structural_data_view_attribute_names.push_back(String("data-") + name); + } + else + { + inserted = data_view_instancers.emplace(name, instancer).second; + } + + if (!inserted) + Log::Message(Log::LT_WARNING, "Could not register data view instancer '%s'. The given name is already registered.", name.c_str()); +} + +void Factory::RegisterDataControllerInstancer(DataControllerInstancer* instancer, const String& name) +{ + bool inserted = data_controller_instancers.emplace(name, instancer).second; + if (!inserted) + Log::Message(Log::LT_WARNING, "Could not register data controller instancer '%s'. The given name is already registered.", name.c_str()); +} + +DataViewPtr Factory::InstanceDataView(const String& type_name, Element* element, bool is_structural_view) +{ + RMLUI_ASSERT(element); + + if (is_structural_view) + { + auto it = structural_data_view_instancers.find(type_name); + if (it != structural_data_view_instancers.end()) + return it->second->InstanceView(element); + } + else + { + auto it = data_view_instancers.find(type_name); + if (it != data_view_instancers.end()) + return it->second->InstanceView(element); + } + return nullptr; +} + +DataControllerPtr Factory::InstanceDataController(const String& type_name, Element* element) +{ + auto it = data_controller_instancers.find(type_name); + if (it != data_controller_instancers.end()) + return it->second->InstanceController(element); + return DataControllerPtr(); +} + +const StringList& Factory::GetStructuralDataViewAttributeNames() +{ + return structural_data_view_attribute_names; +} + } } diff --git a/Source/Core/Math.cpp b/Source/Core/Math.cpp index c9bc39239..4a1b59038 100644 --- a/Source/Core/Math.cpp +++ b/Source/Core/Math.cpp @@ -130,6 +130,12 @@ RMLUICORE_API float RoundFloat(float value) return roundf(value); } +// Rounds a floating-point value to the nearest integer. +RMLUICORE_API double RoundFloat(double value) +{ + return round(value); +} + // Rounds a floating-point value to the nearest integer. RMLUICORE_API int RoundToInteger(float value) { diff --git a/Source/Core/Property.cpp b/Source/Core/Property.cpp index c15254922..71531c35b 100644 --- a/Source/Core/Property.cpp +++ b/Source/Core/Property.cpp @@ -40,7 +40,7 @@ Property::Property() : unit(UNKNOWN), specificity(-1) String Property::ToString() const { - if (definition == nullptr) + if (!definition) return value.Get< String >(); String string; diff --git a/Source/Core/StringUtilities.cpp b/Source/Core/StringUtilities.cpp index 41c1077e2..6d24e2948 100644 --- a/Source/Core/StringUtilities.cpp +++ b/Source/Core/StringUtilities.cpp @@ -94,6 +94,18 @@ String StringUtilities::ToLower(const String& string) { return str_lower; } +String StringUtilities::ToUpper(const String& string) +{ + String str_upper = string; + std::transform(str_upper.begin(), str_upper.end(), str_upper.begin(), [](char c) { + if (c >= 'a' && c <= 'z') + c -= char('a' - 'A'); + return c; + } + ); + return str_upper; +} + RMLUICORE_API String StringUtilities::EncodeRml(const String& string) { String result; @@ -270,6 +282,61 @@ RMLUICORE_API String StringUtilities::StripWhitespace(StringView string) return String(); } +void StringUtilities::TrimTrailingDotZeros(String& string) +{ + RMLUI_ASSERTMSG(string.find('.') != String::npos, "This function probably does not do what you want if the string is not a number with a decimal point.") + + size_t new_size = string.size(); + for (size_t i = string.size() - 1; i < string.size(); i--) + { + if (string[i] == '.') + { + new_size = i; + break; + } + else if (string[i] == '0') + new_size = i; + else + break; + } + + if (new_size < string.size()) + string.resize(new_size); +} + +#ifdef RMLUI_DEBUG +static struct TestTrimTrailingDotZeros { + TestTrimTrailingDotZeros() { + auto test = [](const String test_string, const String expected) { + String result = test_string; + StringUtilities::TrimTrailingDotZeros(result); + RMLUI_ASSERT(result == expected); + }; + + test("0.1", "0.1"); + test("0.10", "0.1"); + test("0.1000", "0.1"); + test("0.01", "0.01"); + test("0.", "0"); + test("5.", "5"); + test("5.5", "5.5"); + test("5.50", "5.5"); + test("5.501", "5.501"); + test("10.0", "10"); + test("11.0", "11"); + + // Some test cases for behavior that are probably not what you want. + //test("test0", "test"); + //test("1000", "1"); + //test(".", ""); + //test("0", ""); + //test(".0", ""); + //test(" 11 2121 3.00", " 11 2121 3"); + //test("11", "11"); + } +} test_trim_trailing_dot_zeros; +#endif + bool StringUtilities::StringCompareCaseInsensitive(const StringView lhs, const StringView rhs) { if (lhs.size() != rhs.size()) @@ -520,19 +587,19 @@ StringIteratorU8::StringIteratorU8(const String& string, size_t offset) : view(s StringIteratorU8::StringIteratorU8(const String& string, size_t offset, size_t count) : view(string, 0, offset + count), p(string.data() + offset) {} StringIteratorU8& StringIteratorU8::operator++() { - RMLUI_ASSERT(p != view.end()); + RMLUI_ASSERT(p < view.end()); ++p; SeekForward(); return *this; } StringIteratorU8& StringIteratorU8::operator--() { - RMLUI_ASSERT(p - 1 != view.begin()); + RMLUI_ASSERT(p >= view.begin()); --p; SeekBack(); return *this; } inline void StringIteratorU8::SeekBack() { - p = StringUtilities::SeekBackwardUTF8(p, view.end()); + p = StringUtilities::SeekBackwardUTF8(p, view.begin()); } inline void StringIteratorU8::SeekForward() { diff --git a/Source/Core/Variant.cpp b/Source/Core/Variant.cpp index 6874a71df..c978eb4c2 100644 --- a/Source/Core/Variant.cpp +++ b/Source/Core/Variant.cpp @@ -192,6 +192,12 @@ void Variant::Set(Variant&& other) RMLUI_ASSERT(type == other.type); } +void Variant::Set(const bool value) +{ + type = BOOL; + SET_VARIANT(bool); +} + void Variant::Set(const byte value) { type = BYTE; @@ -210,15 +216,22 @@ void Variant::Set(const float value) SET_VARIANT(float); } +void Variant::Set(const double value) +{ + type = DOUBLE; + SET_VARIANT(double); +} + void Variant::Set(const int value) { type = INT; SET_VARIANT(int); } -void Variant::Set(const Character value) + +void Variant::Set(const int64_t value) { - type = WORD; - SET_VARIANT(Character); + type = INT64; + SET_VARIANT(int64_t); } void Variant::Set(const char* value) @@ -444,18 +457,22 @@ bool Variant::operator==(const Variant & other) const switch (type) { + case BOOL: + return DEFAULT_VARIANT_COMPARE(bool); case BYTE: return DEFAULT_VARIANT_COMPARE(byte); case CHAR: return DEFAULT_VARIANT_COMPARE(char); case FLOAT: return DEFAULT_VARIANT_COMPARE(float); + case DOUBLE: + return DEFAULT_VARIANT_COMPARE(double); case INT: return DEFAULT_VARIANT_COMPARE(int); + case INT64: + return DEFAULT_VARIANT_COMPARE(int64_t); case STRING: return DEFAULT_VARIANT_COMPARE(String); - case WORD: - return DEFAULT_VARIANT_COMPARE(Character); case VECTOR2: return DEFAULT_VARIANT_COMPARE(Vector2f); case VECTOR3: diff --git a/Source/Core/XMLNodeHandlerBody.cpp b/Source/Core/XMLNodeHandlerBody.cpp index 362c63d91..68005fe4c 100644 --- a/Source/Core/XMLNodeHandlerBody.cpp +++ b/Source/Core/XMLNodeHandlerBody.cpp @@ -76,8 +76,9 @@ bool XMLNodeHandlerBody::ElementEnd(XMLParser* RMLUI_UNUSED_PARAMETER(parser), c return true; } -bool XMLNodeHandlerBody::ElementData(XMLParser* parser, const String& data) +bool XMLNodeHandlerBody::ElementData(XMLParser* parser, const String& data, XMLDataType RMLUI_UNUSED_PARAMETER(type)) { + RMLUI_UNUSED(type); return Factory::InstanceElementText(parser->GetParseFrame()->element, data); } diff --git a/Source/Core/XMLNodeHandlerBody.h b/Source/Core/XMLNodeHandlerBody.h index 6840b7459..a59d9d54f 100644 --- a/Source/Core/XMLNodeHandlerBody.h +++ b/Source/Core/XMLNodeHandlerBody.h @@ -51,7 +51,7 @@ class XMLNodeHandlerBody : public XMLNodeHandler /// Called when an element is closed bool ElementEnd(XMLParser* parser, const String& name) override; /// Called for element data - bool ElementData(XMLParser* parser, const String& data) override; + bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override; }; } diff --git a/Source/Core/XMLNodeHandlerDefault.cpp b/Source/Core/XMLNodeHandlerDefault.cpp index 37f49ccad..e9af08da1 100644 --- a/Source/Core/XMLNodeHandlerDefault.cpp +++ b/Source/Core/XMLNodeHandlerDefault.cpp @@ -33,6 +33,7 @@ #include "../../Include/RmlUi/Core/Factory.h" #include "../../Include/RmlUi/Core/Profiling.h" #include "../../Include/RmlUi/Core/XMLParser.h" +#include "../../Include/RmlUi/Core/ElementUtilities.h" namespace Rml { @@ -61,7 +62,7 @@ Element* XMLNodeHandlerDefault::ElementStart(XMLParser* parser, const String& na return nullptr; } - // Add the element to its parent and remove the reference + // Move and append the element to the parent Element* result = parent->AppendChild(std::move(element)); return result; @@ -75,12 +76,20 @@ bool XMLNodeHandlerDefault::ElementEnd(XMLParser* RMLUI_UNUSED_PARAMETER(parser) return true; } -bool XMLNodeHandlerDefault::ElementData(XMLParser* parser, const String& data) +bool XMLNodeHandlerDefault::ElementData(XMLParser* parser, const String& data, XMLDataType type) { RMLUI_ZoneScopedC(0x006400); // Determine the parent Element* parent = parser->GetParseFrame()->element; + RMLUI_ASSERT(parent); + + if (type == XMLDataType::InnerXML) + { + // Structural data views use the raw inner xml contents of the node, submit them now. + if (ElementUtilities::ApplyStructuralDataViews(parent, data)) + return true; + } // Parse the text into the element return Factory::InstanceElementText(parent, data); diff --git a/Source/Core/XMLNodeHandlerDefault.h b/Source/Core/XMLNodeHandlerDefault.h index 8f11ea796..eae8b1436 100644 --- a/Source/Core/XMLNodeHandlerDefault.h +++ b/Source/Core/XMLNodeHandlerDefault.h @@ -52,7 +52,7 @@ class XMLNodeHandlerDefault : public XMLNodeHandler /// Called when an element is closed bool ElementEnd(XMLParser* parser, const String& name) override; /// Called for element data - bool ElementData(XMLParser* parser, const String& data) override; + bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override; }; } diff --git a/Source/Core/XMLNodeHandlerHead.cpp b/Source/Core/XMLNodeHandlerHead.cpp index d016088b6..0231f0e66 100644 --- a/Source/Core/XMLNodeHandlerHead.cpp +++ b/Source/Core/XMLNodeHandlerHead.cpp @@ -120,8 +120,9 @@ bool XMLNodeHandlerHead::ElementEnd(XMLParser* parser, const String& name) return true; } -bool XMLNodeHandlerHead::ElementData(XMLParser* parser, const String& data) +bool XMLNodeHandlerHead::ElementData(XMLParser* parser, const String& data, XMLDataType RMLUI_UNUSED_PARAMETER(type)) { + RMLUI_UNUSED(type); const String& tag = parser->GetParseFrame()->tag; // Store the title diff --git a/Source/Core/XMLNodeHandlerHead.h b/Source/Core/XMLNodeHandlerHead.h index b2263597a..3eeee3bcb 100644 --- a/Source/Core/XMLNodeHandlerHead.h +++ b/Source/Core/XMLNodeHandlerHead.h @@ -51,7 +51,7 @@ class XMLNodeHandlerHead : public XMLNodeHandler /// Called when an element is closed bool ElementEnd(XMLParser* parser, const String& name) override; /// Called for element data - bool ElementData(XMLParser* parser, const String& data) override; + bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override; }; } diff --git a/Source/Core/XMLNodeHandlerTemplate.cpp b/Source/Core/XMLNodeHandlerTemplate.cpp index 65e751c33..096f2c402 100644 --- a/Source/Core/XMLNodeHandlerTemplate.cpp +++ b/Source/Core/XMLNodeHandlerTemplate.cpp @@ -66,8 +66,9 @@ bool XMLNodeHandlerTemplate::ElementEnd(XMLParser* RMLUI_UNUSED_PARAMETER(parser return true; } -bool XMLNodeHandlerTemplate::ElementData(XMLParser* parser, const String& data) +bool XMLNodeHandlerTemplate::ElementData(XMLParser* parser, const String& data, XMLDataType RMLUI_UNUSED_PARAMETER(type)) { + RMLUI_UNUSED(type); return Factory::InstanceElementText(parser->GetParseFrame()->element, data); } diff --git a/Source/Core/XMLNodeHandlerTemplate.h b/Source/Core/XMLNodeHandlerTemplate.h index 22783edad..8a98df2d0 100644 --- a/Source/Core/XMLNodeHandlerTemplate.h +++ b/Source/Core/XMLNodeHandlerTemplate.h @@ -51,7 +51,7 @@ class XMLNodeHandlerTemplate : public XMLNodeHandler /// Called when an element is closed bool ElementEnd(XMLParser* parser, const String& name) override; /// Called for element data - bool ElementData(XMLParser* parser, const String& data) override; + bool ElementData(XMLParser* parser, const String& data, XMLDataType type) override; }; } diff --git a/Source/Core/XMLParseTools.cpp b/Source/Core/XMLParseTools.cpp index 2c883502c..3b5d3d292 100644 --- a/Source/Core/XMLParseTools.cpp +++ b/Source/Core/XMLParseTools.cpp @@ -154,5 +154,39 @@ Element* XMLParseTools::ParseTemplate(Element* element, const String& template_n return parse_template->ParseTemplate(element); } +const char* XMLParseTools::ParseDataBrackets(bool& inside_brackets, char c, char previous) +{ + if (inside_brackets) + { + if (c == '}' && previous == '}') + inside_brackets = false; + + else if (c == '{' && previous == '{') + return "Nested double curly brackets are illegal."; + + else if (previous == '}' && c != '}') + return "Single closing curly bracket encountered, use double curly brackets to close an expression."; + + else if (previous == '/' && c == '>') + return "Closing double curly brackets not found, XML end node encountered first."; + + else if (previous == '<' && c == '/') + return "Closing double curly brackets not found, XML end node encountered first."; + } + else + { + if (c == '{' && previous == '{') + { + inside_brackets = true; + } + else if (c == '}' && previous == '}') + { + return "Closing double curly brackets encountered outside an expression."; + } + } + + return nullptr; +} + } } diff --git a/Source/Core/XMLParseTools.h b/Source/Core/XMLParseTools.h index 0c005ebf0..4329921ac 100644 --- a/Source/Core/XMLParseTools.h +++ b/Source/Core/XMLParseTools.h @@ -61,6 +61,12 @@ class XMLParseTools /// @param template_name Name of the template to apply, in TEMPLATE:ELEMENT_ID form /// @returns Element to continue the parse from static Element* ParseTemplate(Element* element, const String& template_name); + + /// Determine the presence of data expression brackets inside XML data. + /// Call this for each iteration through the data string. + /// 'inside_brackets' should be initialized to false. + /// Returns nullptr on success, or an error string on failure. + static const char* ParseDataBrackets(bool& inside_brackets, char c, char previous); }; } diff --git a/Source/Core/XMLParser.cpp b/Source/Core/XMLParser.cpp index 128c80909..b5c0c457f 100644 --- a/Source/Core/XMLParser.cpp +++ b/Source/Core/XMLParser.cpp @@ -34,11 +34,12 @@ #include "../../Include/RmlUi/Core/XMLNodeHandler.h" #include "../../Include/RmlUi/Core/URL.h" #include "../../Include/RmlUi/Core/XMLParser.h" +#include "../../Include/RmlUi/Core/Factory.h" namespace Rml { namespace Core { -using NodeHandlers = UnorderedMap< String, SharedPtr > ; +using NodeHandlers = UnorderedMap< String, SharedPtr >; static NodeHandlers node_handlers; static SharedPtr default_node_handler; @@ -46,23 +47,21 @@ XMLParser::XMLParser(Element* root) { RegisterCDATATag("script"); + for (const String& name : Factory::GetStructuralDataViewAttributeNames()) + RegisterInnerXMLAttribute(name); + // Add the first frame. ParseFrame frame; - frame.node_handler = nullptr; - frame.child_handler = nullptr; frame.element = root; - frame.tag = ""; stack.push(frame); active_handler = nullptr; - header = new DocumentHeader(); + header = std::make_unique(); } XMLParser::~XMLParser() -{ - delete header; -} +{} // Registers a custom node handler to be used to a given tag. XMLNodeHandler* XMLParser::RegisterNodeHandler(const String& _tag, SharedPtr handler) @@ -90,12 +89,7 @@ void XMLParser::ReleaseHandlers() DocumentHeader* XMLParser::GetDocumentHeader() { - return header; -} - -const URL& XMLParser::GetSourceURL() const -{ - return xml_source->GetSourceURL(); + return header.get(); } // Pushes the default element handler onto the parse stack. @@ -120,6 +114,12 @@ const XMLParser::ParseFrame* XMLParser::GetParseFrame() const return &stack.top(); } +const URL& XMLParser::GetSourceURL() const +{ + RMLUI_ASSERT(GetSourceURLPtr()); + return *GetSourceURLPtr(); +} + /// Called when the parser finds the beginning of an element tag. void XMLParser::HandleElementStart(const String& _name, const XMLAttributes& attributes) { @@ -178,11 +178,11 @@ void XMLParser::HandleElementEnd(const String& _name) } /// Called when the parser encounters data. -void XMLParser::HandleData(const String& data) +void XMLParser::HandleData(const String& data, XMLDataType type) { RMLUI_ZoneScoped; if (stack.top().node_handler) - stack.top().node_handler->ElementData(this, data); + stack.top().node_handler->ElementData(this, data, type); } } diff --git a/Source/Debugger/ElementInfo.cpp b/Source/Debugger/ElementInfo.cpp index ea498137f..82704b163 100644 --- a/Source/Debugger/ElementInfo.cpp +++ b/Source/Debugger/ElementInfo.cpp @@ -29,6 +29,7 @@ #include "ElementInfo.h" #include "../../Include/RmlUi/Core/Core.h" #include "../../Include/RmlUi/Core/ElementUtilities.h" +#include "../../Include/RmlUi/Core/ElementText.h" #include "../../Include/RmlUi/Core/Factory.h" #include "../../Include/RmlUi/Core/Property.h" #include "../../Include/RmlUi/Core/PropertiesIteratorView.h" @@ -43,70 +44,6 @@ namespace Rml { namespace Debugger { -static Core::String PrettyFormatNumbers(const Core::String& in_string) -{ - // Removes trailing zeros and truncates decimal digits to the specified number of significant digits. - constexpr int num_significant_digits = 4; - - Core::String string = in_string; - - if (string.empty()) - return string; - - // First, check for a decimal point. No point, no chance of trailing zeroes! - size_t decimal_point_position = 0; - - while ((decimal_point_position = string.find('.', decimal_point_position + 1)) != Core::String::npos) - { - // Find the left-most digit. - int pos_left = (int)decimal_point_position - 1; // non-inclusive - while (pos_left >= 0 && string[pos_left] >= '0' && string[pos_left] <= '9') - pos_left--; - - // Significant digits left of the decimal point. We also consider all zero digits significant on the left side. - const int significant_left = (int)decimal_point_position - (pos_left + 1); - - // Let's not touch numbers that don't start with a digit before the decimal. - if (significant_left == 0) - continue; - - const int max_significant_right = std::max(num_significant_digits - significant_left, 0); - - // Find the right-most digit and number of non-zero digits less than our maximum. - int pos_right = (int)decimal_point_position + 1; // non-inclusive - int significant_right = 0; - while (pos_right < (int)string.size() && string[pos_right] >= '0' && string[pos_right] <= '9') - { - const int current_digit_right = pos_right - (int)decimal_point_position; - if (string[pos_right] != '0' && current_digit_right <= max_significant_right) - significant_right = current_digit_right; - pos_right++; - } - - size_t pos_cut_start = decimal_point_position + (size_t)(significant_right + 1); - size_t pos_cut_end = (size_t)pos_right; - - // Remove the decimal point if we don't have any right digits. - if (pos_cut_start == decimal_point_position + 1) - pos_cut_start = decimal_point_position; - - string.erase(string.begin() + pos_cut_start, string.begin() + pos_cut_end); - } - - return string; -} - -#ifdef RMLUI_DEBUG -static bool TestPrettyFormat(Core::String original, Core::String should_be) -{ - Core::String formatted = PrettyFormatNumbers(original); - bool result = (formatted == should_be); - if (!result) - Core::Log::Message(Core::Log::LT_ERROR, "Remove trailing string failed. PrettyFormatNumbers('%s') == '%s' != '%s'", original.c_str(), formatted.c_str(), should_be.c_str()); - return result; -} -#endif - ElementInfo::ElementInfo(const Core::String& tag) : Core::ElementDocument(tag) { hover_element = nullptr; @@ -117,25 +54,6 @@ ElementInfo::ElementInfo(const Core::String& tag) : Core::ElementDocument(tag) force_update_once = false; title_dirty = true; previous_update_time = 0.0; - - RMLUI_ASSERT(TestPrettyFormat("0.15", "0.15")); - RMLUI_ASSERT(TestPrettyFormat("0.150", "0.15")); - RMLUI_ASSERT(TestPrettyFormat("1.15", "1.15")); - RMLUI_ASSERT(TestPrettyFormat("1.150", "1.15")); - RMLUI_ASSERT(TestPrettyFormat("123.15", "123.1")); - RMLUI_ASSERT(TestPrettyFormat("1234.5", "1234")); - RMLUI_ASSERT(TestPrettyFormat("12.15", "12.15")); - RMLUI_ASSERT(TestPrettyFormat("12.154", "12.15")); - RMLUI_ASSERT(TestPrettyFormat("12.154666", "12.15")); - RMLUI_ASSERT(TestPrettyFormat("15889", "15889")); - RMLUI_ASSERT(TestPrettyFormat("15889.1", "15889")); - RMLUI_ASSERT(TestPrettyFormat("0.00660", "0.006")); - RMLUI_ASSERT(TestPrettyFormat("0.000001", "0")); - RMLUI_ASSERT(TestPrettyFormat("0.00000100", "0")); - RMLUI_ASSERT(TestPrettyFormat("a .", "a .")); - RMLUI_ASSERT(TestPrettyFormat("a .0", "a .0")); - RMLUI_ASSERT(TestPrettyFormat("a 0.0", "a 0")); - RMLUI_ASSERT(TestPrettyFormat("hello.world: 14.5600 1.1 0.55623 more.values: 0.1544 0.", "hello.world: 14.56 1.1 0.556 more.values: 0.154 0")); } ElementInfo::~ElementInfo() @@ -502,6 +420,13 @@ void ElementInfo::UpdateSourceElement() if(name != "class" && name != "style" && name != "id") attributes += Core::CreateString(name.size() + value.size() + 32, "%s: %s
", name.c_str(), value.c_str()); } + + // Text is not an attribute but useful nonetheless + if (auto text_element = rmlui_dynamic_cast(source_element)) + { + const Core::String& text_content = text_element->GetText(); + attributes += Core::CreateString(text_content.size() + 32, "Text: %s
", text_content.c_str()); + } } if (attributes.empty()) @@ -566,18 +491,16 @@ void ElementInfo::UpdateSourceElement() // left, top, width, height. if (source_element != nullptr) { - Core::Vector2f element_offset = source_element->GetRelativeOffset(Core::Box::BORDER); - Core::Vector2f element_size = source_element->GetBox().GetSize(Core::Box::BORDER); - - Core::String positions = Core::CreateString(400, R"( - left: %fpx
- top: %fpx
- width: %fpx
- height: %fpx
)", - element_offset.x, element_offset.y, element_size.x, element_size.y - ); + const Core::Vector2f element_offset = source_element->GetRelativeOffset(Core::Box::BORDER); + const Core::Vector2f element_size = source_element->GetBox().GetSize(Core::Box::BORDER); + + const Core::String positions = + "left: " + Core::ToString(element_offset.x) + "px
" + + "top: " + Core::ToString(element_offset.y) + "px
" + + "width: " + Core::ToString(element_size.x) + "px
" + + "height: " + Core::ToString(element_size.y) + "px
"; - position_content->SetInnerRML( PrettyFormatNumbers(positions) ); + position_content->SetInnerRML( positions ); } else { @@ -727,7 +650,7 @@ void ElementInfo::BuildElementPropertiesRML(Core::String& property_rml, Core::El void ElementInfo::BuildPropertyRML(Core::String& property_rml, const Core::String& name, const Core::Property* property) { - Core::String property_value = PrettyFormatNumbers(property->ToString()); + const Core::String property_value = property->ToString(); property_rml += "" + name + ": " + property_value + "
"; } diff --git a/Source/Debugger/ElementLog.cpp b/Source/Debugger/ElementLog.cpp index bf1b83f4e..c045d9e99 100644 --- a/Source/Debugger/ElementLog.cpp +++ b/Source/Debugger/ElementLog.cpp @@ -131,12 +131,10 @@ bool ElementLog::Initialise() // Adds a log message to the debug log. void ElementLog::AddLogMessage(Core::Log::Type type, const Core::String& message) { - using Core::StringUtilities::Replace; - // Add the message to the list of messages for the specified log type. LogMessage log_message; log_message.index = current_index++; - log_message.message = Replace(Replace(Core::String(message), "<", "<"), ">", ">"); + log_message.message = Core::StringUtilities::EncodeRml(message); log_types[type].log_messages.push_back(log_message); if (log_types[type].log_messages.size() >= MAX_LOG_MESSAGES) { @@ -159,21 +157,18 @@ void ElementLog::AddLogMessage(Core::Log::Type type, const Core::String& message // Trigger the beacon if we're hidden. Override any lower-level log type if it is already visible. else { - if (!IsVisible()) + if (beacon != nullptr) { - if (beacon != nullptr) + if (type < current_beacon_level) { - if (type < current_beacon_level) - { - beacon->SetProperty(Core::PropertyId::Visibility, Core::Property(Core::Style::Visibility::Visible)); + beacon->SetProperty(Core::PropertyId::Visibility, Core::Property(Core::Style::Visibility::Visible)); - current_beacon_level = type; - Rml::Core::Element* beacon_button = beacon->GetFirstChild(); - if (beacon_button) - { - beacon_button->SetClassNames(log_types[type].class_name); - beacon_button->SetInnerRML(log_types[type].alert_contents); - } + current_beacon_level = type; + Rml::Core::Element* beacon_button = beacon->GetFirstChild(); + if (beacon_button) + { + beacon_button->SetClassNames(log_types[type].class_name); + beacon_button->SetInnerRML(log_types[type].alert_contents); } } } diff --git a/Source/Debugger/LogSource.h b/Source/Debugger/LogSource.h index 9e3c8bebf..826e6406b 100644 --- a/Source/Debugger/LogSource.h +++ b/Source/Debugger/LogSource.h @@ -85,6 +85,7 @@ div.button.last div.log-entry p.message { display: block; + white-space: pre-wrap; margin-left: 20dp; } )RCSS"; diff --git a/changelog.md b/changelog.md index a213c5324..031153282 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,19 @@ * [RmlUi 2.0](#rmlui-20) +## RmlUi 4.0 (WIP) + +### Model-view-controller (MVC) implementation + +RmlUi now supports a model-view-controller (MVC) approach through data bindings. This is a powerful approach for making documents respond to data changes, or in reverse, updating data based on user actions. + +For now, this is considered an experimental feature. + +- See the work-in-progress [documentation for this feature](https://gist.github.com/mikke89/030cca078e36749580c975692d03cbee). +- Have a look at the 'databinding' sample for usage examples. +- See discussion in [#83](https://github.com/mikke89/RmlUi/pull/83) and [#25](https://github.com/mikke89/RmlUi/issues/25). + + ## RmlUi 3.3 ### Rml `select` element improvements