/*
* 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 "ElementInfo.h"
#include "../../Include/RmlUi/Core.h"
#include "../../Include/RmlUi/Core/Property.h"
#include "../../Include/RmlUi/Core/PropertiesIteratorView.h"
#include "../../Include/RmlUi/Core/Factory.h"
#include "../../Include/RmlUi/Core/StyleSheet.h"
#include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
#include "Geometry.h"
#include "CommonSource.h"
#include "InfoSource.h"
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;
source_element = nullptr;
enable_element_select = true;
show_source_element = true;
update_source_element = true;
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()
{
}
// Initialises the info element.
bool ElementInfo::Initialise()
{
SetInnerRML(info_rml);
SetId("rmlui-debug-info");
AddEventListener(Core::EventId::Click, this);
AddEventListener(Core::EventId::Mouseover, this);
AddEventListener(Core::EventId::Mouseout, this);
Core::SharedPtr style_sheet = Core::Factory::InstanceStyleSheetString(Core::String(common_rcss) + Core::String(info_rcss));
if (!style_sheet)
return false;
SetStyleSheet(std::move(style_sheet));
return true;
}
// Clears the element references.
void ElementInfo::Reset()
{
hover_element = nullptr;
show_source_element = true;
update_source_element = true;
SetSourceElement(nullptr);
}
void ElementInfo::OnUpdate()
{
if (source_element && (update_source_element || force_update_once) && IsVisible())
{
const double t = Core::GetSystemInterface()->GetElapsedTime();
const float dt = (float)(t - previous_update_time);
constexpr float update_interval = 0.3f;
if (dt > update_interval || (force_update_once))
{
if (force_update_once && source_element)
{
// Since an update is being forced, it is possibly because we are reacting to an event and made some changes.
// Update the source element's document to reflect any recent changes.
if (auto document = source_element->GetOwnerDocument())
document->UpdateDocument();
}
force_update_once = false;
UpdateSourceElement();
}
}
if (title_dirty)
{
UpdateTitle();
title_dirty = false;
}
}
// Called when an element is destroyed.
void ElementInfo::OnElementDestroy(Core::Element* element)
{
if (hover_element == element)
hover_element = nullptr;
if (source_element == element)
source_element = nullptr;
}
void ElementInfo::RenderHoverElement()
{
if (hover_element)
{
Core::ElementUtilities::ApplyTransform(*hover_element);
for (int i = 0; i < hover_element->GetNumBoxes(); i++)
{
// Render the content area.
const Core::Box element_box = hover_element->GetBox(i);
Core::Vector2f size = element_box.GetSize(Core::Box::BORDER);
size = Core::Vector2f(std::max(size.x, 2.0f), std::max(size.y, 2.0f));
Geometry::RenderOutline(
hover_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::BORDER),
size,
Core::Colourb(255, 0, 0, 255),
1
);
}
}
}
void ElementInfo::RenderSourceElement()
{
if (source_element && show_source_element)
{
Core::ElementUtilities::ApplyTransform(*source_element);
for (int i = 0; i < source_element->GetNumBoxes(); i++)
{
const Core::Box element_box = source_element->GetBox(i);
// Content area:
Geometry::RenderBox(source_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::CONTENT), element_box.GetSize(), Core::Colourb(158, 214, 237, 128));
// Padding area:
Geometry::RenderBox(source_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::PADDING), element_box.GetSize(Core::Box::PADDING), source_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::CONTENT), element_box.GetSize(), Core::Colourb(135, 122, 214, 128));
// Border area:
Geometry::RenderBox(source_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::BORDER), element_box.GetSize(Core::Box::BORDER), source_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::PADDING), element_box.GetSize(Core::Box::PADDING), Core::Colourb(133, 133, 133, 128));
// Border area:
Geometry::RenderBox(source_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::MARGIN), element_box.GetSize(Core::Box::MARGIN), source_element->GetAbsoluteOffset(Core::Box::BORDER) + element_box.GetPosition(Core::Box::BORDER), element_box.GetSize(Core::Box::BORDER), Core::Colourb(240, 255, 131, 128));
}
}
}
void ElementInfo::ProcessEvent(Core::Event& event)
{
// Only process events if we're visible
if (IsVisible())
{
if (event == Core::EventId::Click)
{
Core::Element* target_element = event.GetTargetElement();
// Deal with clicks on our own elements differently.
if (target_element->GetOwnerDocument() == this)
{
const Core::String& id = event.GetTargetElement()->GetId();
if (id == "close_button")
{
if (IsVisible())
SetProperty(Core::PropertyId::Visibility, Core::Property(Core::Style::Visibility::Hidden));
}
else if (id == "update_source")
{
update_source_element = !update_source_element;
target_element->SetClass("active", update_source_element);
}
else if (id == "show_source")
{
show_source_element = !target_element->IsClassSet("active");;
target_element->SetClass("active", show_source_element);
}
else if (id == "enable_element_select")
{
enable_element_select = !target_element->IsClassSet("active");;
target_element->SetClass("active", enable_element_select);
}
else if (target_element->GetTagName() == "pseudo" && source_element)
{
const Core::String name = target_element->GetAttribute("name", "");
if (!name.empty())
{
bool pseudo_active = target_element->IsClassSet("active");
if (name == "focus")
{
if (!pseudo_active)
source_element->Focus();
else if (auto document = source_element->GetOwnerDocument())
document->Focus();
}
else
{
source_element->SetPseudoClass(name, !pseudo_active);
}
force_update_once = true;
}
}
// Check if the id is in the form "a %d" or "c %d" - these are the ancestor or child labels.
else
{
int element_index;
if (sscanf(target_element->GetId().c_str(), "a %d", &element_index) == 1)
{
Core::Element* new_source_element = source_element;
for (int i = 0; i < element_index; i++)
{
if (new_source_element != nullptr)
new_source_element = new_source_element->GetParentNode();
}
SetSourceElement(new_source_element);
}
else if (sscanf(target_element->GetId().c_str(), "c %d", &element_index) == 1)
{
if (source_element != nullptr)
SetSourceElement(source_element->GetChild(element_index));
}
}
event.StopPropagation();
}
// Otherwise we just want to focus on the clicked element (unless it's on a debug element)
else if (enable_element_select && target_element->GetOwnerDocument() != nullptr && !IsDebuggerElement(target_element))
{
Core::Element* new_source_element = target_element;
if (new_source_element != source_element)
{
SetSourceElement(new_source_element);
event.StopPropagation();
}
}
}
else if (event == Core::EventId::Mouseover)
{
Core::Element* target_element = event.GetTargetElement();
Core::ElementDocument* owner_document = target_element->GetOwnerDocument();
if (owner_document == this)
{
// Check if the id is in the form "a %d" or "c %d" - these are the ancestor or child labels.
const Core::String& id = target_element->GetId();
int element_index;
if (sscanf(id.c_str(), "a %d", &element_index) == 1)
{
hover_element = source_element;
for (int i = 0; i < element_index; i++)
{
if (hover_element != nullptr)
hover_element = hover_element->GetParentNode();
}
}
else if (sscanf(id.c_str(), "c %d", &element_index) == 1)
{
if (source_element != nullptr)
hover_element = source_element->GetChild(element_index);
}
else
{
hover_element = nullptr;
}
if (id == "show_source" && !show_source_element)
{
// Preview the source element view while hovering
show_source_element = true;
}
if (id == "show_source" || id == "update_source" || id == "enable_element_select")
{
title_dirty = true;
}
}
// Otherwise we just want to focus on the clicked element (unless it's on a debug element)
else if (enable_element_select && owner_document != nullptr && owner_document->GetId().find("rmlui-debug-") != 0)
{
hover_element = target_element;
}
}
else if (event == Core::EventId::Mouseout)
{
Core::Element* target_element = event.GetTargetElement();
Core::ElementDocument* owner_document = target_element->GetOwnerDocument();
if (owner_document == this)
{
const Core::String& id = target_element->GetId();
if (id == "show_source")
{
// Disable the preview of the source element view
if (show_source_element && !target_element->IsClassSet("active"))
show_source_element = false;
}
if (id == "show_source" || id == "update_source" || id == "enable_element_select")
{
title_dirty = true;
}
}
}
}
}
void ElementInfo::SetSourceElement(Core::Element* new_source_element)
{
source_element = new_source_element;
force_update_once = true;
}
void ElementInfo::UpdateSourceElement()
{
previous_update_time = Core::GetSystemInterface()->GetElapsedTime();
title_dirty = true;
// Set the pseudo classes
if (Core::Element* pseudo = GetElementById("pseudo"))
{
Core::PseudoClassList list;
if (source_element)
list = source_element->GetActivePseudoClasses();
// There are some fixed pseudo classes that we always show and iterate through to determine if they are set.
// We also want to show other pseudo classes when they are set, they are added under the #extra element last.
for (int i = 0; i < pseudo->GetNumChildren(); i++)
{
Element* child = pseudo->GetChild(i);
const Core::String name = child->GetAttribute("name", "");
if (!name.empty())
{
bool active = (list.erase(name) == 1);
child->SetClass("active", active);
}
else if(child->GetId() == "extra")
{
// First, we iterate through the extra elements and remove those that are no longer active.
for (int j = 0; j < child->GetNumChildren(); j++)
{
Element* grandchild = child->GetChild(j);
const Core::String grandchild_name = grandchild->GetAttribute("name", "");
bool active = (list.erase(grandchild_name) == 1);
if(!active)
child->RemoveChild(grandchild);
}
// Finally, create new pseudo buttons for the rest of the active pseudo classes.
for (auto& extra_pseudo : list)
{
Core::Element* grandchild = child->AppendChild(CreateElement("pseudo"));
grandchild->SetClass("active", true);
grandchild->SetAttribute("name", extra_pseudo);
grandchild->SetInnerRML(":" + extra_pseudo);
}
}
}
}
// Set the attributes
if (Core::Element* attributes_content = GetElementById("attributes-content"))
{
Core::String attributes;
if (source_element != nullptr)
{
{
Core::String name;
Core::String value;
// The element's attribute list is not always synchronized with its internal values, fetch
// them manually here (see e.g. Element::OnAttributeChange for relevant attributes)
{
name = "id";
value = source_element->GetId();
if (!value.empty())
attributes += Core::CreateString(name.size() + value.size() + 32, "%s: %s
", name.c_str(), value.c_str());
}
{
name = "class";
value = source_element->GetClassNames();
if (!value.empty())
attributes += Core::CreateString(name.size() + value.size() + 32, "%s: %s
", name.c_str(), value.c_str());
}
}
for(const auto& pair : source_element->GetAttributes())
{
auto& name = pair.first;
auto& variant = pair.second;
Core::String value = Core::StringUtilities::EncodeRml(variant.Get());
if(name != "class" && name != "style" && name != "id")
attributes += Core::CreateString(name.size() + value.size() + 32, "%s: %s
", name.c_str(), value.c_str());
}
}
if (attributes.empty())
{
while (attributes_content->HasChildNodes())
attributes_content->RemoveChild(attributes_content->GetChild(0));
attributes_rml.clear();
}
else if (attributes != attributes_rml)
{
attributes_content->SetInnerRML(attributes);
attributes_rml = std::move(attributes);
}
}
// Set the properties
if (Core::Element* properties_content = GetElementById("properties-content"))
{
Core::String properties;
if (source_element != nullptr)
BuildElementPropertiesRML(properties, source_element, source_element);
if (properties.empty())
{
while (properties_content->HasChildNodes())
properties_content->RemoveChild(properties_content->GetChild(0));
properties_rml.clear();
}
else if (properties != properties_rml)
{
properties_content->SetInnerRML(properties);
properties_rml = std::move(properties);
}
}
// Set the events
if (Core::Element* events_content = GetElementById("events-content"))
{
Core::String events;
if (source_element != nullptr)
{
events = source_element->GetEventDispatcherSummary();
}
if (events.empty())
{
while (events_content->HasChildNodes())
events_content->RemoveChild(events_content->GetChild(0));
events_rml.clear();
}
else if (events != events_rml)
{
events_content->SetInnerRML(events);
events_rml = std::move(events);
}
}
// Set the position
if (Core::Element* position_content = GetElementById("position-content"))
{
// 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
);
position_content->SetInnerRML( PrettyFormatNumbers(positions) );
}
else
{
while (position_content->HasChildNodes())
position_content->RemoveChild(position_content->GetFirstChild());
}
}
// Set the ancestors
if (Core::Element* ancestors_content = GetElementById("ancestors-content"))
{
Core::String ancestors;
Core::Element* element_ancestor = nullptr;
if (source_element != nullptr)
element_ancestor = source_element->GetParentNode();
int ancestor_depth = 1;
while (element_ancestor)
{
Core::String ancestor_name = element_ancestor->GetAddress(false, false);
ancestors += Core::CreateString(ancestor_name.size() + 32, "%s
", ancestor_depth, ancestor_name.c_str());
element_ancestor = element_ancestor->GetParentNode();
ancestor_depth++;
}
if (ancestors.empty())
{
while (ancestors_content->HasChildNodes())
ancestors_content->RemoveChild(ancestors_content->GetFirstChild());
ancestors_rml.clear();
}
else if (ancestors != ancestors_rml)
{
ancestors_content->SetInnerRML(ancestors);
ancestors_rml = std::move(ancestors);
}
}
// Set the children
if (Core::Element* children_content = GetElementById("children-content"))
{
Core::String children;
if (source_element != nullptr)
{
for (int i = 0; i < source_element->GetNumChildren(true); i++)
{
Core::Element* child = source_element->GetChild(i);
// If this is a debugger document, do not show it.
if (IsDebuggerElement(child))
continue;
Core::String child_name = child->GetTagName();
const Core::String child_id = child->GetId();
if (!child_id.empty())
{
child_name += "#";
child_name += child_id;
}
children += Core::CreateString(child_name.size() + 32, "%s
", i, child_name.c_str());
}
}
if (children.empty())
{
while (children_content->HasChildNodes())
children_content->RemoveChild(children_content->GetChild(0));
children_rml.clear();
}
else if(children != children_rml)
{
children_content->SetInnerRML(children);
children_rml = std::move(children);
}
}
}
void ElementInfo::BuildElementPropertiesRML(Core::String& property_rml, Core::Element* element, Core::Element* primary_element)
{
NamedPropertyList property_list;
for(auto it = element->IterateLocalProperties(); !it.AtEnd(); ++it)
{
Core::PropertyId property_id = it.GetId();
const Core::String& property_name = it.GetName();
const Core::Property* prop = &it.GetProperty();
// Check that this property isn't overridden or just not inherited.
if (primary_element->GetProperty(property_id) != prop)
continue;
property_list.push_back(NamedProperty{ property_name, prop });
}
std::sort(property_list.begin(), property_list.end(),
[](const NamedProperty& a, const NamedProperty& b) {
if (a.second->source && !b.second->source) return false;
if (!a.second->source && b.second->source) return true;
return a.second->specificity > b.second->specificity;
}
);
if (!property_list.empty())
{
// Print the 'inherited from ...' header if we're not the primary element.
if (element != primary_element)
{
property_rml += "inherited from " + element->GetAddress(false, false) + "
";
}
const Core::PropertySource* previous_source = nullptr;
bool first_iteration = true;
for (auto& named_property : property_list)
{
auto& source = named_property.second->source;
if(source.get() != previous_source || first_iteration)
{
previous_source = source.get();
first_iteration = false;
// Print the rule name header.
if(source)
{
Core::String str_line_number;
Core::TypeConverter::Convert(source->line_number, str_line_number);
property_rml += "" + source->rule_name + "
";
property_rml += "" + source->path + " : " + str_line_number + "
";
}
else
{
property_rml += "inline
";
}
}
BuildPropertyRML(property_rml, named_property.first, named_property.second);
}
}
if (element->GetParentNode() != nullptr)
BuildElementPropertiesRML(property_rml, element->GetParentNode(), primary_element);
}
void ElementInfo::BuildPropertyRML(Core::String& property_rml, const Core::String& name, const Core::Property* property)
{
Core::String property_value = PrettyFormatNumbers(property->ToString());
property_rml += "" + name + ": " + property_value + "
";
}
void ElementInfo::UpdateTitle()
{
auto title_content = GetElementById("title-content");
auto enable_select = GetElementById("enable_element_select");
auto show_source = GetElementById("show_source");
auto update_source = GetElementById("update_source");
if (title_content && enable_select && show_source && update_source)
{
if (enable_select->IsPseudoClassSet("hover"))
title_content->SetInnerRML("(select elements)");
else if (show_source->IsPseudoClassSet("hover"))
title_content->SetInnerRML("(draw element dimensions)");
else if (update_source->IsPseudoClassSet("hover"))
title_content->SetInnerRML("(update info continuously)");
else if (source_element)
title_content->SetInnerRML(source_element->GetTagName());
else
title_content->SetInnerRML("Element Information");
}
}
bool ElementInfo::IsDebuggerElement(Core::Element* element)
{
return element->GetOwnerDocument()->GetId().find("rmlui-debug-") == 0;
}
}
}