/*
* 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-2023 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 "../Common/Mocks.h"
#include "../Common/TestsInterface.h"
#include "../Common/TestsShell.h"
#include "../Common/TypesToString.h"
#include "RmlUi/Core/DecorationTypes.h"
#include
#include
#include
#include
#include
using namespace Rml;
static bool DictionaryApproximateMatch(const Rml::Dictionary& dict, const Rml::Dictionary& dict_expected)
{
for (auto& pair : dict)
{
const String& name = pair.first;
const Variant& value = pair.second;
auto it = dict_expected.find(name);
if (it == dict_expected.end())
{
FAIL("Unexpected key: ", name, ". Value: ", value);
return false;
}
const Variant& value_expected = it->second;
CAPTURE(name);
REQUIRE(value.GetType() == value_expected.GetType());
if (value.GetType() == Rml::Variant::Type::FLOAT)
{
REQUIRE(value.Get() == doctest::Approx(value_expected.Get()));
}
else if (value_expected.GetType() == Rml::Variant::Type::VECTOR2)
{
auto a = value.Get();
auto b = value_expected.Get();
REQUIRE(a.x == doctest::Approx(b.x));
REQUIRE(a.y == doctest::Approx(b.y));
}
else if (value_expected.GetType() == Rml::Variant::Type::COLORSTOPLIST)
{
const auto& a = value.GetReference();
const auto& b = value_expected.GetReference();
REQUIRE(a.size() == b.size());
for (size_t i = 0; i < Math::Min(a.size(), b.size()); i++)
{
REQUIRE(a[i].color == b[i].color);
REQUIRE(a[i].position.unit == b[i].position.unit);
REQUIRE(a[i].position.number == doctest::Approx(b[i].position.number));
}
}
else
{
REQUIRE(value == value_expected);
}
}
for (auto& pair_expected : dict_expected)
{
if (dict.find(pair_expected.first) == dict.end())
{
FAIL("Missing key: ", pair_expected.first, ". Expected value: ", pair_expected.second);
return false;
}
}
return true;
}
static const String document_decorator_rml = R"(
Test
)";
TEST_CASE("decorator.paint-area")
{
TestsRenderInterface* render_interface = TestsShell::GetTestsRenderInterface();
// This test only works with the dummy renderer.
if (!render_interface)
return;
Context* context = TestsShell::GetContext();
ElementDocument* document = context->LoadDocumentFromMemory(document_decorator_rml, "assets/");
document->Show();
for (const bool set_at_decorator_class : {false, true})
{
document->SetClass("at_decorator", set_at_decorator_class);
const byte blue = (set_at_decorator_class ? 255 : 0);
render_interface->ExpectCompileGeometry({
Mesh{
Vector{
{{50, 50}, {255, 0, blue, 255}, {0, 0}},
{{100, 50}, {255, 255, blue, 255}, {0, 0}},
{{100, 100}, {255, 255, blue, 255}, {0, 0}},
{{50, 100}, {255, 0, blue, 255}, {0, 0}},
},
Vector{0, 2, 1, 0, 3, 2},
},
Mesh{
Vector{
{{20, 20}, {255, 0, blue, 255}, {0, 0}},
{{130, 20}, {255, 255, blue, 255}, {0, 0}},
{{130, 130}, {255, 255, blue, 255}, {0, 0}},
{{20, 130}, {255, 0, blue, 255}, {0, 0}},
},
Vector{0, 2, 1, 0, 3, 2},
},
Mesh{
Vector{
{{20, 20}, {255, 0, blue, 255}, {0, 0}},
{{130, 20}, {255, 255, blue, 255}, {0, 0}},
{{130, 130}, {255, 255, blue, 255}, {0, 0}},
{{20, 130}, {255, 0, blue, 255}, {0, 0}},
},
Vector{0, 2, 1, 0, 3, 2},
},
Mesh{
Vector{
{{0, 0}, {255, 0, blue, 255}, {0, 0}},
{{150, 0}, {255, 255, blue, 255}, {0, 0}},
{{150, 150}, {255, 255, blue, 255}, {0, 0}},
{{0, 150}, {255, 0, blue, 255}, {0, 0}},
},
Vector{0, 2, 1, 0, 3, 2},
},
});
context->Update();
context->Render();
}
document->Close();
TestsShell::ShutdownShell();
}
TEST_CASE("decorator.gradients_and_shader")
{
namespace tl = trompeloeil;
MockRenderInterface mockRenderInterface;
Context* context = TestsShell::GetContext(true, &mockRenderInterface);
REQUIRE(context);
static const String document_gradients_rml = R"(
Test
)";
struct TestCase {
String value;
String expected_name;
Dictionary expected_dictionary;
};
TestCase test_cases[] = {
// -- linear-gradient --
TestCase{
"linear-gradient(to right, #000, #fff)",
"linear-gradient",
Dictionary{
{"length", Variant(200.f)},
{"p0", Variant(Vector2f{0.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"p1", Variant(Vector2f{200.f, 100.f})},
{"repeating", Variant(false)},
},
},
TestCase{
"repeating-linear-gradient(to right, #000, #fff)",
"linear-gradient",
Dictionary{
{"length", Variant(200.f)},
{"p0", Variant(Vector2f{0.f, 100.f})},
{"p1", Variant(Vector2f{200.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(true)},
},
},
TestCase{
"linear-gradient(to right, #000, rgba( 150, 150, 150, 255 ) 25%, #f00)",
"linear-gradient",
Dictionary{
{"length", Variant(200.f)},
{"p0", Variant(Vector2f{0.f, 100.f})},
{"p1", Variant(Vector2f{200.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{150, 150, 150, 255}, {0.25f, Unit::NUMBER}},
ColorStop{{255, 0, 0, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"linear-gradient(to right, #000, #fff 50px)",
"linear-gradient",
Dictionary{
{"length", Variant(200.f)},
{"p0", Variant(Vector2f{0.f, 100.f})},
{"p1", Variant(Vector2f{200.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {0.25f, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"linear-gradient(to right, #000, #f00 100px 75%, #fff)",
"linear-gradient",
Dictionary{
{"length", Variant(200.f)},
{"p0", Variant(Vector2f{0.f, 100.f})},
{"p1", Variant(Vector2f{200.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 0, 0, 255}, {0.5f, Unit::NUMBER}},
ColorStop{{255, 0, 0, 255}, {0.75f, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"linear-gradient(to right, #000, #fff) content-box",
"linear-gradient",
Dictionary{
{"length", Variant(100.f)},
{"p0", Variant(Vector2f{0.f, 50.f})},
{"p1", Variant(Vector2f{100.f, 50.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"linear-gradient(0deg, #000, #fff)",
"linear-gradient",
Dictionary{
{"length", Variant(200.f)},
{"p0", Variant(Vector2f{100.f, 200.f})},
{"p1", Variant(Vector2f{100.f, 0.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"linear-gradient(#000, #fff)",
"linear-gradient",
Dictionary{
{"length", Variant(200.f)},
{"p0", Variant(Vector2f{100.f, 0.f})},
{"p1", Variant(Vector2f{100.f, 200.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"linear-gradient(to top right, #000, #fff)",
"linear-gradient",
Dictionary{
{"length", Variant(282.843f)},
{"p0", Variant(Vector2f{0.f, 200.f})},
{"p1", Variant(Vector2f{200.f, 0.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
// -- radial-gradient --
TestCase{
"radial-gradient(#000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"radius", Variant(Vector2f{Math::SquareRoot(2.f) * 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"repeating-radial-gradient(#000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"radius", Variant(Vector2f{Math::SquareRoot(2.f) * 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(true)},
},
},
TestCase{
"radial-gradient(closest-side, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"radius", Variant(Vector2f{100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(closest-side, #000 25px 50%, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"radius", Variant(Vector2f{100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0.25f, Unit::NUMBER}},
ColorStop{{0, 0, 0, 255}, {0.5f, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(circle closest-side at 75% 50%, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{150.f, 100.f})},
{"radius", Variant(Vector2f{50.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(ellipse closest-side at 75% 50%, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{150.f, 100.f})},
{"radius", Variant(Vector2f{50.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(circle farthest-side at 150px 100px, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{150.f, 100.f})},
{"radius", Variant(Vector2f{150.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(farthest-side at 150px 100px, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{150.f, 100.f})},
{"radius", Variant(Vector2f{150.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(farthest-corner at right, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{200.f, 100.f})},
{"radius", Variant(Math::SquareRoot(2.f) * Vector2f{200.f, 100.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(50px at top right, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{200.f, 0.f})},
{"radius", Variant(Vector2f{50.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"radial-gradient(50% 25% at bottom left, #000, #fff)",
"radial-gradient",
Dictionary{
{"center", Variant(Vector2f{0.f, 200.f})},
{"radius", Variant(Vector2f{100.f, 50.f})},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
// -- conic-gradient --
TestCase{
"conic-gradient(#000, #fff)",
"conic-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"angle", Variant(0.f)},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"repeating-conic-gradient(#000, #fff)",
"conic-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"angle", Variant(0.f)},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(true)},
},
},
TestCase{
"conic-gradient(#000 50%, #fff)",
"conic-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"angle", Variant(0.f)},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0.5f, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"conic-gradient(#000 50% 270deg, #fff)",
"conic-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"angle", Variant(0.f)},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0.5f, Unit::NUMBER}},
ColorStop{{0, 0, 0, 255}, {0.75f, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"conic-gradient(from 90deg, #000, #fff)",
"conic-gradient",
Dictionary{
{"center", Variant(Vector2f{100.f, 100.f})},
{"angle", Variant(1.5708f)},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"conic-gradient(from 90deg at bottom right, #000, #fff)",
"conic-gradient",
Dictionary{
{"center", Variant(Vector2f{200.f, 200.f})},
{"angle", Variant(1.5708f)},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
TestCase{
"conic-gradient(at 180px 25%, #000, #fff)",
"conic-gradient",
Dictionary{
{"center", Variant(Vector2f{180.f, 50.f})},
{"angle", Variant(0.f)},
{"color_stop_list",
Variant(ColorStopList{
ColorStop{{0, 0, 0, 255}, {0, Unit::NUMBER}},
ColorStop{{255, 255, 255, 255}, {1, Unit::NUMBER}},
})},
{"repeating", Variant(false)},
},
},
// -- shader --
TestCase{
"shader(cake)",
"shader",
Dictionary{
{"value", Variant("cake")},
{"dimensions", Variant(Vector2f{200, 200})},
},
},
TestCase{
"shader(animated_radar) content-box",
"shader",
Dictionary{
{"value", Variant("animated_radar")},
{"dimensions", Variant(Vector2f{100, 100})},
},
},
TestCase{
"shader(\"taco party\") border-box",
"shader",
Dictionary{
{"value", Variant("taco party")},
{"dimensions", Variant(Vector2f{220, 220})},
},
},
};
ElementDocument* document = context->LoadDocumentFromMemory(document_gradients_rml);
REQUIRE(document);
auto div = document->GetChild(0);
REQUIRE(div);
document->Show();
CompiledShaderHandle compiled_shader_handle = {1};
CompiledGeometryHandle compiled_geometry_handle = {1001};
for (const TestCase& test_case : test_cases)
{
compiled_shader_handle += 1;
compiled_geometry_handle += 1;
CAPTURE(test_case.value);
REQUIRE_CALL(mockRenderInterface, CompileGeometry(tl::_, tl::_)).RETURN(compiled_geometry_handle);
REQUIRE_CALL(mockRenderInterface, ReleaseGeometry(compiled_geometry_handle));
REQUIRE_CALL(mockRenderInterface, CompileShader(test_case.expected_name, tl::_))
.WITH(DictionaryApproximateMatch(_2, test_case.expected_dictionary))
.RETURN(compiled_shader_handle);
REQUIRE_CALL(mockRenderInterface, RenderShader(compiled_shader_handle, compiled_geometry_handle, tl::_, TextureHandle{}));
REQUIRE_CALL(mockRenderInterface, ReleaseShader(compiled_shader_handle));
div->SetProperty("decorator", test_case.value);
TestsShell::RenderLoop();
div->SetProperty("decorator", "none");
context->Update();
context->Render();
}
document->Close();
TestsShell::ShutdownShell();
}