From 4383b2fe015a9691937c4851445400113cea5a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 20 Oct 2023 16:52:59 +0200 Subject: [PATCH] Text: can now test most Renderer APIs without a GL context. This is the case as of 96d56c6d2f21d811df2702e33bedfacf1eaf4ee0, where the GPU-independent AbstractGlyphCache can be used. --- src/Magnum/Text/Test/CMakeLists.txt | 3 +- src/Magnum/Text/Test/RendererGLTest.cpp | 431 ------------------ src/Magnum/Text/Test/RendererTest.cpp | 556 ++++++++++++++++++++++++ 3 files changed, 557 insertions(+), 433 deletions(-) create mode 100644 src/Magnum/Text/Test/RendererTest.cpp diff --git a/src/Magnum/Text/Test/CMakeLists.txt b/src/Magnum/Text/Test/CMakeLists.txt index 8b4bd3fa1..6e4986b74 100644 --- a/src/Magnum/Text/Test/CMakeLists.txt +++ b/src/Magnum/Text/Test/CMakeLists.txt @@ -78,9 +78,8 @@ corrade_add_test(TextAbstractShaperTest AbstractShaperTest.cpp LIBRARIES MagnumTextTestLib) corrade_add_test(TextDirectionTest DirectionTest.cpp LIBRARIES MagnumText) - corrade_add_test(TextFeatureTest FeatureTest.cpp LIBRARIES MagnumTextTestLib) - +corrade_add_test(TextRendererTest RendererTest.cpp LIBRARIES MagnumTextTestLib) corrade_add_test(TextScriptTest ScriptTest.cpp LIBRARIES MagnumTextTestLib) if(MAGNUM_TARGET_GL AND MAGNUM_BUILD_GL_TESTS) diff --git a/src/Magnum/Text/Test/RendererGLTest.cpp b/src/Magnum/Text/Test/RendererGLTest.cpp index aa3a68721..ab2f35645 100644 --- a/src/Magnum/Text/Test/RendererGLTest.cpp +++ b/src/Magnum/Text/Test/RendererGLTest.cpp @@ -27,11 +27,9 @@ #include #include #include -#include #include #include /** @todo drop once Debug is stream-free */ -#include "Magnum/PixelFormat.h" #include "Magnum/GL/Context.h" #include "Magnum/GL/Extensions.h" #include "Magnum/GL/OpenGLTester.h" @@ -45,166 +43,15 @@ namespace Magnum { namespace Text { namespace Test { namespace { struct RendererGLTest: GL::OpenGLTester { explicit RendererGLTest(); - void renderData(); void renderMesh(); void renderMeshIndexType(); void mutableText(); - - void multiline(); - - void arrayGlyphCache(); - void fontNotFoundInCache(); -}; - -const struct { - TestSuite::TestCaseDescriptionSourceLocation name; - Alignment alignment; - Vector2 offset; -} RenderDataData[]{ - /* Not testing all combinations, just making sure that each horizontal, - vertical, glyph bounds and integer variant is covered */ - {"line left", Alignment::LineLeft, - /* This is the default (0) value, thus should result in no shift */ - {}}, - {"line left, glyph bounds", Alignment::LineLeftGlyphBounds, - /* The first glyph has X offset of 2.5, which is subtracted */ - {-2.5f, 0.0f}}, - {"top left", Alignment::TopLeft, - /* Ascent is 4.5, scaled by 0.5 */ - {0.0f, -2.25f}}, - {"top left, glyph bounds", Alignment::TopLeftGlyphBounds, - /* Largest Y value is 10.5f */ - {-2.5f, -10.5f}}, - {"top right", Alignment::TopRight, - /* Advances were 1, 2, 3, so 6 in total, ascent is 4.5; scaled by - 0.5 */ - {-3.0f, -2.25f}}, - {"top right, glyph bounds", Alignment::TopRightGlyphBounds, - /* Basically subtracting the largest vertex value */ - {-12.5f, -10.5f}}, - {"top center", Alignment::TopCenter, - /* Advances were 1, 2, 3, so 6 in total, center is 3, scaled by 0.5 */ - {-1.5f, -2.25f}}, - {"top center, integral", Alignment::TopCenterIntegral, - /* The Y shift isn't whole units but only X is rounded here */ - {-2.0f, -2.25f}}, - {"top center, glyph bounds", Alignment::TopCenterGlyphBounds, - {-7.5f, -10.5f}}, - {"top center, glyph bounds, integral", Alignment::TopCenterGlyphBoundsIntegral, - /* The Y shift isn't whole units but only X is rounded here */ - {-8.0f, -10.5f}}, - {"middle left, glyph bounds", Alignment::MiddleLeftGlyphBounds, - {-2.5f, -7.125f}}, - {"middle left, glyph bounds, integral", Alignment::MiddleLeftGlyphBoundsIntegral, - /* The X shift isn't whole units but only Y is rounded here */ - {-2.5f, -7.0f}}, - {"middle center", Alignment::MiddleCenter, - {-1.5f, -0.5f}}, - {"middle center, integral", Alignment::MiddleCenterIntegral, - /* Rounding happens on both X and Y in this case */ - {-2.0f, -1.0f}}, - {"middle center, glyph bounds", Alignment::MiddleCenterGlyphBounds, - /* Half size of the bounds quad */ - {-7.5f, -7.125f}}, - {"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral, - {-8.0f, -7.0f}}, - {"bottom left", Alignment::BottomLeft, - /* Descent is -2.5; scaled by 0.5 */ - {0.0f, 1.25f}}, - {"bottom right", Alignment::BottomRight, - {-3.0f, 1.25f}}, - {"bottom right, glyph bounds", Alignment::BottomRightGlyphBounds, - {-12.5f, -3.75f}}, -}; - -const struct { - const char* name; - Alignment alignment; - /* The Y offset value could be calculated, but this is easier to grasp and - makes it possible to test overrideable line height later, for example */ - Vector2 offset0, offset1, offset2; -} MultilineData[]{ - {"line left", Alignment::LineLeft, - {0.0f, -0.0f}, - {0.0f, -4.0f}, - {0.0f, -12.0f}}, - {"line left, glyph bounds", Alignment::LineLeftGlyphBounds, - {0.0f, 0.0f}, - {0.0f, -4.0f}, - {0.0f, -12.0f}}, - {"middle left", Alignment::MiddleLeft, - {0.0f, 6.0f}, - {0.0f, 2.0f}, - {0.0f, -6.0f}}, - {"middle left, glyph bounds", Alignment::MiddleLeftGlyphBounds, - {0.0f, 5.5f}, - {0.0f, 1.5f}, - {0.0f, -6.5f}}, - {"middle left, glyph bounds, integral", Alignment::MiddleLeftGlyphBoundsIntegral, - {0.0f, 6.0f}, - {0.0f, 2.0f}, - {0.0f, -6.0f}}, - {"middle center", Alignment::MiddleCenter, - /* The advance for the rightmost glyph is one unit larger than the - actual bounds which makes it different */ - {-4.0f, 6.0f}, - {-2.0f, 2.0f}, - {-3.0f, -6.0f}}, - {"middle center, integral", Alignment::MiddleCenterIntegral, - {-4.0f, 6.0f}, - {-2.0f, 2.0f}, - {-3.0f, -6.0f}}, - {"middle center, glyph bounds", Alignment::MiddleCenterGlyphBounds, - {-3.5f, 5.5f}, - {-1.5f, 1.5f}, - {-2.5f, -6.5f}}, - {"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral, - {-4.0f, 6.0f}, - {-2.0f, 2.0f}, - {-3.0f, -6.0f}}, - {"top right", Alignment::TopRight, - {-8.0f, -0.5f}, - {-4.0f, -4.5f}, - {-6.0f, -12.5f}}, - {"top right, glyph bounds", Alignment::TopRightGlyphBounds, - {-7.0f, -1.0f}, - {-3.0f, -5.0f}, - {-5.0f, -13.0f}}, - {"top center", Alignment::TopCenter, - /* The advance for the rightmost glyph is one unit larger than the - actual bounds which makes it different */ - {-4.0f, -0.5f}, - {-2.0f, -4.5f}, - {-3.0f, -12.5f}}, - {"top center, integral", Alignment::TopCenterIntegral, - /* The Y shift isn't whole units but only X (which is already whole - units) would be rounded here */ - {-4.0f, -0.5f}, - {-2.0f, -4.5f}, - {-3.0f, -12.5f}}, - {"top center, glyph bounds", Alignment::TopCenterGlyphBounds, - {-3.5f, -1.0f}, - {-1.5f, -5.0f}, - {-2.5f, -13.0f}}, - {"top center, integral", Alignment::TopCenterGlyphBoundsIntegral, - {-4.0f, -1.0f}, - {-2.0f, -5.0f}, - {-3.0f, -13.0f}}, }; RendererGLTest::RendererGLTest() { - addInstancedTests({&RendererGLTest::renderData}, - Containers::arraySize(RenderDataData)); - addTests({&RendererGLTest::renderMesh, &RendererGLTest::renderMeshIndexType, &RendererGLTest::mutableText}); - - addInstancedTests({&RendererGLTest::multiline}, - Containers::arraySize(MultilineData)); - - addTests({&RendererGLTest::arrayGlyphCache, - &RendererGLTest::fontNotFoundInCache}); } struct TestShaper: AbstractShaper { @@ -270,108 +117,6 @@ GlyphCache testGlyphCache(AbstractFont& font) { return cache; } -void RendererGLTest::renderData() { - auto&& data = RenderDataData[testCaseInstanceId()]; - setTestCaseDescription(data.name); - - TestFont font; - font.openFile({}, 0.5f); - GlyphCache cache = testGlyphCache(font); - - /* Capture the correct function name */ - CORRADE_VERIFY(true); - - std::vector positions; - std::vector textureCoordinates; - std::vector indices; - Range2D bounds; - std::tie(positions, textureCoordinates, indices, bounds) = AbstractRenderer::render(font, cache, 0.25f, "abc", data.alignment); - - /* Three glyphs, three quads -> 12 vertices, 18 indices */ - CORRADE_COMPARE(positions.size(), 12); - CORRADE_COMPARE(textureCoordinates.size(), 12); - CORRADE_COMPARE(indices.size(), 18); - - /* Vertex positions. Rectangles coming from the cache and offsets + - advances from the layouter are scaled by 0.5. First glyph is moved by - (scaled) 1 up and has advance of (scaled) {1, ±0.5}, every next glyph is - moved up and further distanced by (scaled) {1, ±0.5}. First glyph is - wide, the other two are square. - - +-+ - +-+ |c| - 0---2 |b| +-+ - | a | +-+ - 1---3 */ - CORRADE_COMPARE_AS(positions, (std::vector{ - /* Cursor is {0, 0}. Offset from the cache is {5, 10}, offset from the - renderer is {0, 1}, size is {20, 10}; all scaled by 0.5 */ - Vector2{ 2.5f, 10.5f} + data.offset, - Vector2{ 2.5f, 5.5f} + data.offset, - Vector2{12.5f, 10.5f} + data.offset, - Vector2{12.5f, 5.5f} + data.offset, - - /* Advance was {1, 0.5}, cursor is {1, 0.5}. Offset from the cache is - {10, 5}, offset from the renderer is {0, 2}, size is {10, 10}; all - scaled by 0.5 */ - Vector2{ 5.5f, 8.75f} + data.offset, - Vector2{ 5.5f, 3.75f} + data.offset, - Vector2{10.5f, 8.75f} + data.offset, - Vector2{10.5f, 3.75f} + data.offset, - - /* Advance was {2, -0.5}, cursor is {3, 0}. Offset from the cache is - {5, 5}, offset from the renderer is {0, 3}, size is {10, 10}; all - scaled by 0.5 */ - Vector2{ 4.0f, 9.0f} + data.offset, - Vector2{ 4.0f, 4.0f} + data.offset, - Vector2{ 9.0f, 9.0f} + data.offset, - Vector2{ 9.0f, 4.0f} + data.offset - }), TestSuite::Compare::Container); - - /* Bounds. Different depending on whether or not GlyphBounds alignment is - used. */ - if(UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds) - CORRADE_COMPARE(bounds, (Range2D{{2.5f, 3.75f}, {12.5f, 10.5f}}.translated(data.offset))); - else - CORRADE_COMPARE(bounds, (Range2D{{0.0f, -1.25f}, {3.0f, 2.25f}}.translated(data.offset))); - - /* Texture coordinates. First glyph is bottom, second top left, third top - right. - +-+-+ - |b|c| - 0---2 - | a | - 1---3 */ - CORRADE_COMPARE_AS(textureCoordinates, (std::vector{ - {0.0f, 0.5f}, - {0.0f, 0.0f}, - {1.0f, 0.5f}, - {1.0f, 0.0f}, - - {0.0f, 1.0f}, - {0.0f, 0.5f}, - {0.5f, 1.0f}, - {0.5f, 0.5f}, - - {0.5f, 1.0f}, - {0.5f, 0.5f}, - {1.0f, 1.0f}, - {1.0f, 0.5f} - }), TestSuite::Compare::Container); - - /* Indices - 0---2 0---2 5 - | | | / /| - | | | / / | - | | |/ / | - 1---3 1 3---4 */ - CORRADE_COMPARE_AS(indices, (std::vector{ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10 - }), TestSuite::Compare::Container); -} - void RendererGLTest::renderMesh() { /* Like render(middle center), but with a mesh output instead of data */ @@ -545,182 +290,6 @@ void RendererGLTest::mutableText() { #endif } -void RendererGLTest::multiline() { - auto&& data = MultilineData[testCaseInstanceId()]; - setTestCaseDescription(data.name); - - struct Shaper: AbstractShaper { - using AbstractShaper::AbstractShaper; - - UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { - return text.size(); - } - - void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { - for(UnsignedInt i = 0; i != ids.size(); ++i) { - ids[i] = 0; - offsets[i] = {}; - advances[i] = Vector2::xAxis(4.0f); - } - } - }; - - struct: AbstractFont { - FontFeatures doFeatures() const override { return {}; } - bool doIsOpened() const override { return _opened; } - void doClose() override { _opened = false; } - - Properties doOpenFile(Containers::StringView, Float size) override { - _opened = true; - /* Compared to the glyph bounds, which are from 0 to 2, this is - shifted by one unit, thus by 0.5 in the output */ - return {size, 1.0f, -1.0f, 8.0f, 10}; - } - - UnsignedInt doGlyphId(char32_t) override { return 0; } - Vector2 doGlyphSize(UnsignedInt) override { return {}; } - Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - - Containers::Pointer doCreateShaper() override { - return Containers::pointer(*this); - } - - bool _opened = false; - } font; - font.openFile({}, 0.5f); - - /* Just a single glyph that scales to {1, 1} in the end */ - GlyphCache cache{{20, 20}}; - UnsignedInt fontId = cache.addFont(1, &font); - cache.addGlyph(fontId, 0, {}, {{}, {2, 2}}); - - /* Capture the correct function name */ - CORRADE_VERIFY(true); - - Range2D rectangle; - std::vector indices; - std::vector positions, textureCoordinates; - std::tie(positions, textureCoordinates, indices, rectangle) = Renderer2D::render(font, - cache, 0.25f, "abcd\nef\n\nghi", data.alignment); - - /* We're rendering text at 0.25f size and the font is scaled to 0.5f, so - the line advance should be 8.0f*0.25f/0.5f = 4.0f */ - CORRADE_COMPARE(font.size(), 0.5f); - CORRADE_COMPARE(font.lineHeight(), 8.0f); - - /* Bounds. The advance for the rightmost glyph is one unit larger than the - actual bounds so it's different on X between the two variants */ - if(UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds) - CORRADE_COMPARE(rectangle, Range2D({0.0f, -12.0f}, {7.0f, 1.0f}).translated(data.offset0)); - else - CORRADE_COMPARE(rectangle, Range2D({0.0f, -12.5f}, {8.0f, 0.5f}).translated(data.offset0)); - - /* Vertices - [a] [b] [c] [d] - [e] [f] - - [g] [h] [i] */ - CORRADE_COMPARE_AS(positions, (std::vector{ - Vector2{0.0f, 1.0f} + data.offset0, /* a */ - Vector2{0.0f, 0.0f} + data.offset0, - Vector2{1.0f, 1.0f} + data.offset0, - Vector2{1.0f, 0.0f} + data.offset0, - - Vector2{2.0f, 1.0f} + data.offset0, /* b */ - Vector2{2.0f, 0.0f} + data.offset0, - Vector2{3.0f, 1.0f} + data.offset0, - Vector2{3.0f, 0.0f} + data.offset0, - - Vector2{4.0f, 1.0f} + data.offset0, /* c */ - Vector2{4.0f, 0.0f} + data.offset0, - Vector2{5.0f, 1.0f} + data.offset0, - Vector2{5.0f, 0.0f} + data.offset0, - - Vector2{6.0f, 1.0f} + data.offset0, /* d */ - Vector2{6.0f, 0.0f} + data.offset0, - Vector2{7.0f, 1.0f} + data.offset0, - Vector2{7.0f, 0.0f} + data.offset0, - - Vector2{0.0f, 1.0f} + data.offset1, /* e */ - Vector2{0.0f, 0.0f} + data.offset1, - Vector2{1.0f, 1.0f} + data.offset1, - Vector2{1.0f, 0.0f} + data.offset1, - - Vector2{2.0f, 1.0f} + data.offset1, /* f */ - Vector2{2.0f, 0.0f} + data.offset1, - Vector2{3.0f, 1.0f} + data.offset1, - Vector2{3.0f, 0.0f} + data.offset1, - - /* Two linebreaks here */ - - Vector2{0.0f, 1.0f} + data.offset2, /* g */ - Vector2{0.0f, 0.0f} + data.offset2, - Vector2{1.0f, 1.0f} + data.offset2, - Vector2{1.0f, 0.0f} + data.offset2, - - Vector2{2.0f, 1.0f} + data.offset2, /* h */ - Vector2{2.0f, 0.0f} + data.offset2, - Vector2{3.0f, 1.0f} + data.offset2, - Vector2{3.0f, 0.0f} + data.offset2, - - Vector2{4.0f, 1.0f} + data.offset2, /* i */ - Vector2{4.0f, 0.0f} + data.offset2, - Vector2{5.0f, 1.0f} + data.offset2, - Vector2{5.0f, 0.0f} + data.offset2, - }), TestSuite::Compare::Container); - - /* Indices - 0---2 0---2 5 - | | | / /| - | | | / / | - | | |/ / | - 1---3 1 3---4 */ - CORRADE_COMPARE_AS(indices, (std::vector{ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10, - 12, 13, 14, 13, 15, 14, - 16, 17, 18, 17, 19, 18, - 20, 21, 22, 21, 23, 22, - 24, 25, 26, 25, 27, 26, - 28, 29, 30, 29, 31, 30, - 32, 33, 34, 33, 35, 34 - }), TestSuite::Compare::Container); -} - -void RendererGLTest::arrayGlyphCache() { - CORRADE_SKIP_IF_NO_ASSERT(); - - TestFont font; - font.openFile({}, 0.5f); - struct: AbstractGlyphCache { - using AbstractGlyphCache::AbstractGlyphCache; - - GlyphCacheFeatures doFeatures() const override { return {}; } - } cache{PixelFormat::R8Unorm, {100, 100, 3}}; - - std::ostringstream out; - Error redirectError{&out}; - AbstractRenderer::render(font, cache, 0.25f, "abc"); - CORRADE_COMPARE(out.str(), "Text::Renderer: array glyph caches are not supported\n"); -} - -void RendererGLTest::fontNotFoundInCache() { - CORRADE_SKIP_IF_NO_ASSERT(); - - TestFont font; - font.openFile({}, 0.5f); - GlyphCache cache{{100, 100}}; - - cache.addFont(34); - cache.addFont(25); - - std::ostringstream out; - Error redirectError{&out}; - AbstractRenderer::render(font, cache, 0.25f, "abc"); - CORRADE_COMPARE(out.str(), "Text::Renderer: font not found among 2 fonts in passed glyph cache\n"); -} - }}}} CORRADE_TEST_MAIN(Magnum::Text::Test::RendererGLTest) diff --git a/src/Magnum/Text/Test/RendererTest.cpp b/src/Magnum/Text/Test/RendererTest.cpp new file mode 100644 index 000000000..5aeea60f3 --- /dev/null +++ b/src/Magnum/Text/Test/RendererTest.cpp @@ -0,0 +1,556 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + 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 /** @todo drop once Debug is stream-free */ + +#include "Magnum/PixelFormat.h" +#include "Magnum/Text/AbstractFont.h" +#include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/AbstractShaper.h" +#include "Magnum/Text/Renderer.h" + +namespace Magnum { namespace Text { namespace Test { namespace { + +struct RendererTest: TestSuite::Tester { + explicit RendererTest(); + + void renderData(); + + void multiline(); + + #ifdef MAGNUM_TARGET_GL + void arrayGlyphCache(); + void fontNotFoundInCache(); + #endif +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + Alignment alignment; + Vector2 offset; +} RenderDataData[]{ + /* Not testing all combinations, just making sure that each horizontal, + vertical, glyph bounds and integer variant is covered */ + {"line left", Alignment::LineLeft, + /* This is the default (0) value, thus should result in no shift */ + {}}, + {"line left, glyph bounds", Alignment::LineLeftGlyphBounds, + /* The first glyph has X offset of 2.5, which is subtracted */ + {-2.5f, 0.0f}}, + {"top left", Alignment::TopLeft, + /* Ascent is 4.5, scaled by 0.5 */ + {0.0f, -2.25f}}, + {"top left, glyph bounds", Alignment::TopLeftGlyphBounds, + /* Largest Y value is 10.5f */ + {-2.5f, -10.5f}}, + {"top right", Alignment::TopRight, + /* Advances were 1, 2, 3, so 6 in total, ascent is 4.5; scaled by + 0.5 */ + {-3.0f, -2.25f}}, + {"top right, glyph bounds", Alignment::TopRightGlyphBounds, + /* Basically subtracting the largest vertex value */ + {-12.5f, -10.5f}}, + {"top center", Alignment::TopCenter, + /* Advances were 1, 2, 3, so 6 in total, center is 3, scaled by 0.5 */ + {-1.5f, -2.25f}}, + {"top center, integral", Alignment::TopCenterIntegral, + /* The Y shift isn't whole units but only X is rounded here */ + {-2.0f, -2.25f}}, + {"top center, glyph bounds", Alignment::TopCenterGlyphBounds, + {-7.5f, -10.5f}}, + {"top center, glyph bounds, integral", Alignment::TopCenterGlyphBoundsIntegral, + /* The Y shift isn't whole units but only X is rounded here */ + {-8.0f, -10.5f}}, + {"middle left, glyph bounds", Alignment::MiddleLeftGlyphBounds, + {-2.5f, -7.125f}}, + {"middle left, glyph bounds, integral", Alignment::MiddleLeftGlyphBoundsIntegral, + /* The X shift isn't whole units but only Y is rounded here */ + {-2.5f, -7.0f}}, + {"middle center", Alignment::MiddleCenter, + {-1.5f, -0.5f}}, + {"middle center, integral", Alignment::MiddleCenterIntegral, + /* Rounding happens on both X and Y in this case */ + {-2.0f, -1.0f}}, + {"middle center, glyph bounds", Alignment::MiddleCenterGlyphBounds, + /* Half size of the bounds quad */ + {-7.5f, -7.125f}}, + {"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral, + {-8.0f, -7.0f}}, + {"bottom left", Alignment::BottomLeft, + /* Descent is -2.5; scaled by 0.5 */ + {0.0f, 1.25f}}, + {"bottom right", Alignment::BottomRight, + {-3.0f, 1.25f}}, + {"bottom right, glyph bounds", Alignment::BottomRightGlyphBounds, + {-12.5f, -3.75f}}, +}; + +const struct { + const char* name; + Alignment alignment; + /* The Y offset value could be calculated, but this is easier to grasp and + makes it possible to test overrideable line height later, for example */ + Vector2 offset0, offset1, offset2; +} MultilineData[]{ + {"line left", Alignment::LineLeft, + {0.0f, -0.0f}, + {0.0f, -4.0f}, + {0.0f, -12.0f}}, + {"line left, glyph bounds", Alignment::LineLeftGlyphBounds, + {0.0f, 0.0f}, + {0.0f, -4.0f}, + {0.0f, -12.0f}}, + {"middle left", Alignment::MiddleLeft, + {0.0f, 6.0f}, + {0.0f, 2.0f}, + {0.0f, -6.0f}}, + {"middle left, glyph bounds", Alignment::MiddleLeftGlyphBounds, + {0.0f, 5.5f}, + {0.0f, 1.5f}, + {0.0f, -6.5f}}, + {"middle left, glyph bounds, integral", Alignment::MiddleLeftGlyphBoundsIntegral, + {0.0f, 6.0f}, + {0.0f, 2.0f}, + {0.0f, -6.0f}}, + {"middle center", Alignment::MiddleCenter, + /* The advance for the rightmost glyph is one unit larger than the + actual bounds which makes it different */ + {-4.0f, 6.0f}, + {-2.0f, 2.0f}, + {-3.0f, -6.0f}}, + {"middle center, integral", Alignment::MiddleCenterIntegral, + {-4.0f, 6.0f}, + {-2.0f, 2.0f}, + {-3.0f, -6.0f}}, + {"middle center, glyph bounds", Alignment::MiddleCenterGlyphBounds, + {-3.5f, 5.5f}, + {-1.5f, 1.5f}, + {-2.5f, -6.5f}}, + {"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral, + {-4.0f, 6.0f}, + {-2.0f, 2.0f}, + {-3.0f, -6.0f}}, + {"top right", Alignment::TopRight, + {-8.0f, -0.5f}, + {-4.0f, -4.5f}, + {-6.0f, -12.5f}}, + {"top right, glyph bounds", Alignment::TopRightGlyphBounds, + {-7.0f, -1.0f}, + {-3.0f, -5.0f}, + {-5.0f, -13.0f}}, + {"top center", Alignment::TopCenter, + /* The advance for the rightmost glyph is one unit larger than the + actual bounds which makes it different */ + {-4.0f, -0.5f}, + {-2.0f, -4.5f}, + {-3.0f, -12.5f}}, + {"top center, integral", Alignment::TopCenterIntegral, + /* The Y shift isn't whole units but only X (which is already whole + units) would be rounded here */ + {-4.0f, -0.5f}, + {-2.0f, -4.5f}, + {-3.0f, -12.5f}}, + {"top center, glyph bounds", Alignment::TopCenterGlyphBounds, + {-3.5f, -1.0f}, + {-1.5f, -5.0f}, + {-2.5f, -13.0f}}, + {"top center, integral", Alignment::TopCenterGlyphBoundsIntegral, + {-4.0f, -1.0f}, + {-2.0f, -5.0f}, + {-3.0f, -13.0f}}, +}; + +RendererTest::RendererTest() { + addInstancedTests({&RendererTest::renderData}, + Containers::arraySize(RenderDataData)); + + addInstancedTests({&RendererTest::multiline}, + Containers::arraySize(MultilineData)); + + #ifdef MAGNUM_TARGET_GL + addTests({&RendererTest::arrayGlyphCache, + &RendererTest::fontNotFoundInCache}); + #endif +} + +struct TestShaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) { + /* It just rotates between the three glyphs */ + if(i % 3 == 0) + ids[i] = 3; + else if(i % 3 == 1) + ids[i] = 7; + else + ids[i] = 9; + + /* Offset Y and advance X is getting larger with every glyph, + advance Y is flipping its sign with every glyph */ + offsets[i] = Vector2::yAxis(i + 1); + advances[i] = {Float(i + 1), i % 2 ? -0.5f : +0.5f}; + } + } +}; + +struct TestFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + /* Line height isn't used for anything here so can be arbitrary */ + return {size, 4.5f, -2.5f, 10000.0f, 10}; + } + + UnsignedInt doGlyphId(char32_t) override { return 0; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + + Containers::Pointer doCreateShaper() override { + return Containers::pointer(*this); + } + + bool _opened = false; +}; + +struct DummyGlyphCache: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector2i&, const ImageView2D&) override {} +}; + +DummyGlyphCache testGlyphCache(AbstractFont& font) { + DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20}}; + + /* Add one more font to verify the right one gets picked */ + cache.addFont(96); + UnsignedInt fontId = cache.addFont(font.glyphCount(), &font); + + /* Three glyphs, covering bottom, top left and top right of the cache */ + cache.addGlyph(fontId, 3, {5, 10}, {{}, {20, 10}}); + cache.addGlyph(fontId, 7, {10, 5}, {{0, 10}, {10, 20}}); + cache.addGlyph(fontId, 9, {5, 5}, {{10, 10}, {20, 20}}); + + return cache; +} + +void RendererTest::renderData() { + auto&& data = RenderDataData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + TestFont font; + font.openFile({}, 0.5f); + DummyGlyphCache cache = testGlyphCache(font); + + /* Capture the correct function name */ + CORRADE_VERIFY(true); + + std::vector positions; + std::vector textureCoordinates; + std::vector indices; + Range2D bounds; + std::tie(positions, textureCoordinates, indices, bounds) = AbstractRenderer::render(font, cache, 0.25f, "abc", data.alignment); + + /* Three glyphs, three quads -> 12 vertices, 18 indices */ + CORRADE_COMPARE(positions.size(), 12); + CORRADE_COMPARE(textureCoordinates.size(), 12); + CORRADE_COMPARE(indices.size(), 18); + + /* Vertex positions. Rectangles coming from the cache and offsets + + advances from the layouter are scaled by 0.5. First glyph is moved by + (scaled) 1 up and has advance of (scaled) {1, ±0.5}, every next glyph is + moved up and further distanced by (scaled) {1, ±0.5}. First glyph is + wide, the other two are square. + + +-+ + +-+ |c| + 0---2 |b| +-+ + | a | +-+ + 1---3 */ + CORRADE_COMPARE_AS(positions, (std::vector{ + /* Cursor is {0, 0}. Offset from the cache is {5, 10}, offset from the + renderer is {0, 1}, size is {20, 10}; all scaled by 0.5 */ + Vector2{ 2.5f, 10.5f} + data.offset, + Vector2{ 2.5f, 5.5f} + data.offset, + Vector2{12.5f, 10.5f} + data.offset, + Vector2{12.5f, 5.5f} + data.offset, + + /* Advance was {1, 0.5}, cursor is {1, 0.5}. Offset from the cache is + {10, 5}, offset from the renderer is {0, 2}, size is {10, 10}; all + scaled by 0.5 */ + Vector2{ 5.5f, 8.75f} + data.offset, + Vector2{ 5.5f, 3.75f} + data.offset, + Vector2{10.5f, 8.75f} + data.offset, + Vector2{10.5f, 3.75f} + data.offset, + + /* Advance was {2, -0.5}, cursor is {3, 0}. Offset from the cache is + {5, 5}, offset from the renderer is {0, 3}, size is {10, 10}; all + scaled by 0.5 */ + Vector2{ 4.0f, 9.0f} + data.offset, + Vector2{ 4.0f, 4.0f} + data.offset, + Vector2{ 9.0f, 9.0f} + data.offset, + Vector2{ 9.0f, 4.0f} + data.offset + }), TestSuite::Compare::Container); + + /* Bounds. Different depending on whether or not GlyphBounds alignment is + used. */ + if(UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds) + CORRADE_COMPARE(bounds, (Range2D{{2.5f, 3.75f}, {12.5f, 10.5f}}.translated(data.offset))); + else + CORRADE_COMPARE(bounds, (Range2D{{0.0f, -1.25f}, {3.0f, 2.25f}}.translated(data.offset))); + + /* Texture coordinates. First glyph is bottom, second top left, third top + right. + +-+-+ + |b|c| + 0---2 + | a | + 1---3 */ + CORRADE_COMPARE_AS(textureCoordinates, (std::vector{ + {0.0f, 0.5f}, + {0.0f, 0.0f}, + {1.0f, 0.5f}, + {1.0f, 0.0f}, + + {0.0f, 1.0f}, + {0.0f, 0.5f}, + {0.5f, 1.0f}, + {0.5f, 0.5f}, + + {0.5f, 1.0f}, + {0.5f, 0.5f}, + {1.0f, 1.0f}, + {1.0f, 0.5f} + }), TestSuite::Compare::Container); + + /* Indices + 0---2 0---2 5 + | | | / /| + | | | / / | + | | |/ / | + 1---3 1 3---4 */ + CORRADE_COMPARE_AS(indices, (std::vector{ + 0, 1, 2, 1, 3, 2, + 4, 5, 6, 5, 7, 6, + 8, 9, 10, 9, 11, 10 + }), TestSuite::Compare::Container); +} + +void RendererTest::multiline() { + auto&& data = MultilineData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) { + ids[i] = 0; + offsets[i] = {}; + advances[i] = Vector2::xAxis(4.0f); + } + } + }; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + /* Compared to the glyph bounds, which are from 0 to 2, this is + shifted by one unit, thus by 0.5 in the output */ + return {size, 1.0f, -1.0f, 8.0f, 10}; + } + + UnsignedInt doGlyphId(char32_t) override { return 0; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + + Containers::Pointer doCreateShaper() override { + return Containers::pointer(*this); + } + + bool _opened = false; + } font; + font.openFile({}, 0.5f); + + /* Just a single glyph that scales to {1, 1} in the end */ + DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20}}; + UnsignedInt fontId = cache.addFont(1, &font); + cache.addGlyph(fontId, 0, {}, {{}, {2, 2}}); + + /* Capture the correct function name */ + CORRADE_VERIFY(true); + + Range2D rectangle; + std::vector indices; + std::vector positions, textureCoordinates; + std::tie(positions, textureCoordinates, indices, rectangle) = Renderer2D::render(font, + cache, 0.25f, "abcd\nef\n\nghi", data.alignment); + + /* We're rendering text at 0.25f size and the font is scaled to 0.5f, so + the line advance should be 8.0f*0.25f/0.5f = 4.0f */ + CORRADE_COMPARE(font.size(), 0.5f); + CORRADE_COMPARE(font.lineHeight(), 8.0f); + + /* Bounds. The advance for the rightmost glyph is one unit larger than the + actual bounds so it's different on X between the two variants */ + if(UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds) + CORRADE_COMPARE(rectangle, Range2D({0.0f, -12.0f}, {7.0f, 1.0f}).translated(data.offset0)); + else + CORRADE_COMPARE(rectangle, Range2D({0.0f, -12.5f}, {8.0f, 0.5f}).translated(data.offset0)); + + /* Vertices + [a] [b] [c] [d] + [e] [f] + + [g] [h] [i] */ + CORRADE_COMPARE_AS(positions, (std::vector{ + Vector2{0.0f, 1.0f} + data.offset0, /* a */ + Vector2{0.0f, 0.0f} + data.offset0, + Vector2{1.0f, 1.0f} + data.offset0, + Vector2{1.0f, 0.0f} + data.offset0, + + Vector2{2.0f, 1.0f} + data.offset0, /* b */ + Vector2{2.0f, 0.0f} + data.offset0, + Vector2{3.0f, 1.0f} + data.offset0, + Vector2{3.0f, 0.0f} + data.offset0, + + Vector2{4.0f, 1.0f} + data.offset0, /* c */ + Vector2{4.0f, 0.0f} + data.offset0, + Vector2{5.0f, 1.0f} + data.offset0, + Vector2{5.0f, 0.0f} + data.offset0, + + Vector2{6.0f, 1.0f} + data.offset0, /* d */ + Vector2{6.0f, 0.0f} + data.offset0, + Vector2{7.0f, 1.0f} + data.offset0, + Vector2{7.0f, 0.0f} + data.offset0, + + Vector2{0.0f, 1.0f} + data.offset1, /* e */ + Vector2{0.0f, 0.0f} + data.offset1, + Vector2{1.0f, 1.0f} + data.offset1, + Vector2{1.0f, 0.0f} + data.offset1, + + Vector2{2.0f, 1.0f} + data.offset1, /* f */ + Vector2{2.0f, 0.0f} + data.offset1, + Vector2{3.0f, 1.0f} + data.offset1, + Vector2{3.0f, 0.0f} + data.offset1, + + /* Two linebreaks here */ + + Vector2{0.0f, 1.0f} + data.offset2, /* g */ + Vector2{0.0f, 0.0f} + data.offset2, + Vector2{1.0f, 1.0f} + data.offset2, + Vector2{1.0f, 0.0f} + data.offset2, + + Vector2{2.0f, 1.0f} + data.offset2, /* h */ + Vector2{2.0f, 0.0f} + data.offset2, + Vector2{3.0f, 1.0f} + data.offset2, + Vector2{3.0f, 0.0f} + data.offset2, + + Vector2{4.0f, 1.0f} + data.offset2, /* i */ + Vector2{4.0f, 0.0f} + data.offset2, + Vector2{5.0f, 1.0f} + data.offset2, + Vector2{5.0f, 0.0f} + data.offset2, + }), TestSuite::Compare::Container); + + /* Indices + 0---2 0---2 5 + | | | / /| + | | | / / | + | | |/ / | + 1---3 1 3---4 */ + CORRADE_COMPARE_AS(indices, (std::vector{ + 0, 1, 2, 1, 3, 2, + 4, 5, 6, 5, 7, 6, + 8, 9, 10, 9, 11, 10, + 12, 13, 14, 13, 15, 14, + 16, 17, 18, 17, 19, 18, + 20, 21, 22, 21, 23, 22, + 24, 25, 26, 25, 27, 26, + 28, 29, 30, 29, 31, 30, + 32, 33, 34, 33, 35, 34 + }), TestSuite::Compare::Container); +} + +#ifdef MAGNUM_TARGET_GL +void RendererTest::arrayGlyphCache() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + font.openFile({}, 0.5f); + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } cache{PixelFormat::R8Unorm, {100, 100, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + AbstractRenderer::render(font, cache, 0.25f, "abc"); + CORRADE_COMPARE(out.str(), "Text::Renderer: array glyph caches are not supported\n"); +} + +void RendererTest::fontNotFoundInCache() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + font.openFile({}, 0.5f); + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + + cache.addFont(34); + cache.addFont(25); + + std::ostringstream out; + Error redirectError{&out}; + AbstractRenderer::render(font, cache, 0.25f, "abc"); + CORRADE_COMPARE(out.str(), "Text::Renderer: font not found among 2 fonts in passed glyph cache\n"); +} +#endif + +}}}} + +CORRADE_TEST_MAIN(Magnum::Text::Test::RendererTest)