/*
* 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/ElementUtilities.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/Factory.h"
#include "../../Include/RmlUi/Core/FontEngineInterface.h"
#include "../../Include/RmlUi/Core/RenderInterface.h"
#include "../../Include/RmlUi/Core/TransformState.h"
#include "ElementStyle.h"
#include "LayoutEngine.h"
#include
#include
namespace Rml {
namespace Core {
// Builds and sets the box for an element.
static void SetBox(Element* element)
{
Element* parent = element->GetParentNode();
RMLUI_ASSERT(parent != nullptr);
Vector2f containing_block = parent->GetBox().GetSize();
containing_block.x -= parent->GetElementScroll()->GetScrollbarSize(ElementScroll::VERTICAL);
containing_block.y -= parent->GetElementScroll()->GetScrollbarSize(ElementScroll::HORIZONTAL);
Box box;
LayoutEngine::BuildBox(box, containing_block, element);
if (element->GetComputedValues().height.type != Style::Height::Auto)
box.SetContent(Vector2f(box.GetSize().x, containing_block.y));
element->SetBox(box);
}
// Positions an element relative to an offset parent.
static void SetElementOffset(Element* element, const Vector2f& offset)
{
Vector2f relative_offset = element->GetParentNode()->GetBox().GetPosition(Box::CONTENT);
relative_offset += offset;
relative_offset.x += element->GetBox().GetEdge(Box::MARGIN, Box::LEFT);
relative_offset.y += element->GetBox().GetEdge(Box::MARGIN, Box::TOP);
element->SetOffset(relative_offset, element->GetParentNode());
}
Element* ElementUtilities::GetElementById(Element* root_element, const String& id)
{
// Breadth first search on elements for the corresponding id
typedef std::queue SearchQueue;
SearchQueue search_queue;
search_queue.push(root_element);
while (!search_queue.empty())
{
Element* element = search_queue.front();
search_queue.pop();
if (element->GetId() == id)
{
return element;
}
// Add all children to search
for (int i = 0; i < element->GetNumChildren(); i++)
search_queue.push(element->GetChild(i));
}
return nullptr;
}
void ElementUtilities::GetElementsByTagName(ElementList& elements, Element* root_element, const String& tag)
{
// Breadth first search on elements for the corresponding id
typedef std::queue< Element* > SearchQueue;
SearchQueue search_queue;
for (int i = 0; i < root_element->GetNumChildren(); ++i)
search_queue.push(root_element->GetChild(i));
while (!search_queue.empty())
{
Element* element = search_queue.front();
search_queue.pop();
if (element->GetTagName() == tag)
elements.push_back(element);
// Add all children to search.
for (int i = 0; i < element->GetNumChildren(); i++)
search_queue.push(element->GetChild(i));
}
}
void ElementUtilities::GetElementsByClassName(ElementList& elements, Element* root_element, const String& class_name)
{
// Breadth first search on elements for the corresponding id
typedef std::queue< Element* > SearchQueue;
SearchQueue search_queue;
for (int i = 0; i < root_element->GetNumChildren(); ++i)
search_queue.push(root_element->GetChild(i));
while (!search_queue.empty())
{
Element* element = search_queue.front();
search_queue.pop();
if (element->IsClassSet(class_name))
elements.push_back(element);
// Add all children to search.
for (int i = 0; i < element->GetNumChildren(); i++)
search_queue.push(element->GetChild(i));
}
}
float ElementUtilities::GetDensityIndependentPixelRatio(Element * element)
{
Context* context = element->GetContext();
if (context == nullptr)
return 1.0f;
return context->GetDensityIndependentPixelRatio();
}
// Returns the width of a string rendered within the context of the given element.
int ElementUtilities::GetStringWidth(Element* element, const String& string)
{
FontFaceHandle font_face_handle = element->GetFontFaceHandle();
if (font_face_handle == 0)
return 0;
return GetFontEngineInterface()->GetStringWidth(font_face_handle, string);
}
void ElementUtilities::BindEventAttributes(Element* element)
{
// Check for and instance the on* events
for (const auto& pair: element->GetAttributes())
{
if (pair.first.size() > 2 && pair.first[0] == 'o' && pair.first[1] == 'n')
{
EventListener* listener = Factory::InstanceEventListener(pair.second.Get(), element);
if (listener)
element->AddEventListener(pair.first.substr(2), listener, false);
}
}
}
// Generates the clipping region for an element.
bool ElementUtilities::GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_dimensions, Element* element)
{
clip_origin = Vector2i(-1, -1);
clip_dimensions = Vector2i(-1, -1);
int num_ignored_clips = element->GetClippingIgnoreDepth();
if (num_ignored_clips < 0)
return false;
// Search through the element's ancestors, finding all elements that clip their overflow and have overflow to clip.
// For each that we find, we combine their clipping region with the existing clipping region, and so build up a
// complete clipping region for the element.
Element* clipping_element = element->GetParentNode();
while (clipping_element != nullptr)
{
// Merge the existing clip region with the current clip region if we aren't ignoring clip regions.
if (num_ignored_clips == 0 && clipping_element->IsClippingEnabled())
{
// Ignore nodes that don't clip.
if (clipping_element->GetClientWidth() < clipping_element->GetScrollWidth()
|| clipping_element->GetClientHeight() < clipping_element->GetScrollHeight())
{
Vector2f element_origin_f = clipping_element->GetAbsoluteOffset(Box::CONTENT);
Vector2f element_dimensions_f = clipping_element->GetBox().GetSize(Box::CONTENT);
Vector2i element_origin(Math::RealToInteger(element_origin_f.x), Math::RealToInteger(element_origin_f.y));
Vector2i element_dimensions(Math::RealToInteger(element_dimensions_f.x), Math::RealToInteger(element_dimensions_f.y));
if (clip_origin == Vector2i(-1, -1) && clip_dimensions == Vector2i(-1, -1))
{
clip_origin = element_origin;
clip_dimensions = element_dimensions;
}
else
{
Vector2i top_left(Math::Max(clip_origin.x, element_origin.x),
Math::Max(clip_origin.y, element_origin.y));
Vector2i bottom_right(Math::Min(clip_origin.x + clip_dimensions.x, element_origin.x + element_dimensions.x),
Math::Min(clip_origin.y + clip_dimensions.y, element_origin.y + element_dimensions.y));
clip_origin = top_left;
clip_dimensions.x = Math::Max(0, bottom_right.x - top_left.x);
clip_dimensions.y = Math::Max(0, bottom_right.y - top_left.y);
}
}
}
// If this region is meant to clip and we're skipping regions, update the counter.
if (num_ignored_clips > 0)
{
if (clipping_element->IsClippingEnabled())
num_ignored_clips--;
}
// Determine how many clip regions this ancestor ignores, and inherit the value. If this region ignores all
// clipping regions, then we do too.
int clipping_element_ignore_clips = clipping_element->GetClippingIgnoreDepth();
if (clipping_element_ignore_clips < 0)
break;
num_ignored_clips = Math::Max(num_ignored_clips, clipping_element_ignore_clips);
// Climb the tree to this region's parent.
clipping_element = clipping_element->GetParentNode();
}
return clip_dimensions.x >= 0 && clip_dimensions.y >= 0;
}
// Sets the clipping region from an element and its ancestors.
bool ElementUtilities::SetClippingRegion(Element* element, Context* context)
{
Rml::Core::RenderInterface* render_interface = nullptr;
if (element)
{
render_interface = element->GetRenderInterface();
if (!context)
context = element->GetContext();
}
else if (context)
{
render_interface = context->GetRenderInterface();
if (!render_interface)
render_interface = GetRenderInterface();
}
if (!render_interface || !context)
return false;
Vector2i clip_origin = { -1, -1 };
Vector2i clip_dimensions = { -1, -1 };
bool clip = element && GetClippingRegion(clip_origin, clip_dimensions, element);
Vector2i current_origin = { -1, -1 };
Vector2i current_dimensions = { -1, -1 };
bool current_clip = context->GetActiveClipRegion(current_origin, current_dimensions);
if (current_clip != clip || (clip && (clip_origin != current_origin || clip_dimensions != current_dimensions)))
{
context->SetActiveClipRegion(clip_origin, clip_dimensions);
ApplyActiveClipRegion(context, render_interface);
}
return true;
}
void ElementUtilities::ApplyActiveClipRegion(Context* context, RenderInterface* render_interface)
{
if (render_interface == nullptr)
return;
Vector2i origin;
Vector2i dimensions;
bool clip_enabled = context->GetActiveClipRegion(origin, dimensions);
render_interface->EnableScissorRegion(clip_enabled);
if (clip_enabled)
{
render_interface->SetScissorRegion(origin.x, origin.y, dimensions.x, dimensions.y);
}
}
// Formats the contents of an element.
bool ElementUtilities::FormatElement(Element* element, const Vector2f& containing_block)
{
LayoutEngine layout_engine;
return layout_engine.FormatElement(element, containing_block);
}
// Generates the box for an element.
void ElementUtilities::BuildBox(Box& box, const Vector2f& containing_block, Element* element, bool inline_element)
{
LayoutEngine::BuildBox(box, containing_block, element, inline_element);
}
// Sizes an element, and positions it within its parent offset from the borders of its content area.
bool ElementUtilities::PositionElement(Element* element, const Vector2f& offset, PositionAnchor anchor)
{
Element* parent = element->GetParentNode();
if (parent == nullptr)
return false;
SetBox(element);
Vector2f containing_block = element->GetParentNode()->GetBox().GetSize(Box::CONTENT);
Vector2f element_block = element->GetBox().GetSize(Box::MARGIN);
Vector2f resolved_offset = offset;
if (anchor & RIGHT)
resolved_offset.x = containing_block.x - (element_block.x + offset.x);
if (anchor & BOTTOM)
resolved_offset.y = containing_block.y - (element_block.y + offset.y);
SetElementOffset(element, resolved_offset);
return true;
}
bool ElementUtilities::ApplyTransform(Element &element)
{
RenderInterface *render_interface = element.GetRenderInterface();
if (!render_interface)
return false;
struct PreviousMatrix {
const Matrix4f* pointer; // This may be expired, dereferencing not allowed!
Matrix4f value;
};
static SmallUnorderedMap previous_matrix;
auto it = previous_matrix.find(render_interface);
if (it == previous_matrix.end())
it = previous_matrix.emplace(render_interface, PreviousMatrix{ nullptr, Matrix4f::Identity() }).first;
RMLUI_ASSERT(it != previous_matrix.end());
const Matrix4f*& old_transform = it->second.pointer;
const Matrix4f* new_transform = nullptr;
if (const TransformState* state = element.GetTransformState())
new_transform = state->GetTransform();
// Only changed transforms are submitted.
if (old_transform != new_transform)
{
Matrix4f& old_transform_value = it->second.value;
// Do a deep comparison as well to avoid submitting a new transform which is equal.
if(!old_transform || !new_transform || (old_transform_value != *new_transform))
{
render_interface->SetTransform(new_transform);
if(new_transform)
old_transform_value = *new_transform;
}
old_transform = new_transform;
}
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);
}
}
}