diff --git a/src/Text/AbstractFont.h b/src/Text/AbstractFont.h index 69820d1fd..4aa3d415b 100644 --- a/src/Text/AbstractFont.h +++ b/src/Text/AbstractFont.h @@ -199,6 +199,8 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { * @param size Font size * @param text %Text to layout * + * Note that the layouters support rendering of single-line text only. + * See @ref Renderer class for more advanced text layouting. * @see @ref fillGlyphCache(), @ref createGlyphCache() */ std::unique_ptr layout(const GlyphCache& cache, Float size, const std::string& text); diff --git a/src/Text/Renderer.cpp b/src/Text/Renderer.cpp index 5aa464e0d..4ccad7675 100644 --- a/src/Text/Renderer.cpp +++ b/src/Text/Renderer.cpp @@ -58,62 +58,114 @@ struct Vertex { Vector2 position, textureCoordinates; }; -std::tuple, Rectangle> renderVerticesInternal(AbstractFont& font, const GlyphCache& cache, Float size, const std::string& text, Alignment alignment) { - const auto layouter = font.layout(cache, size, text); - const UnsignedInt vertexCount = layouter->glyphCount()*4; - - /* Output data */ +std::tuple, Rectangle> renderVerticesInternal(AbstractFont& font, const GlyphCache& cache, Float size, const std::string& text, const Alignment alignment) { + /* Output data, reserve memory as when the text would be ASCII-only. In + reality the actual vertex count will be smaller, but allocating more at + once is better than reallocating many times later. */ std::vector vertices; - vertices.reserve(vertexCount); + vertices.reserve(text.size()*4); - /* Rendered rectangle */ + /* Total rendered bounds, intial line position, last+1 vertex on previous line */ Rectangle rectangle; - - /* Render all glyphs */ - Vector2 cursorPosition; - for(UnsignedInt i = 0; i != layouter->glyphCount(); ++i) { - Rectangle quadPosition, textureCoordinates; - std::tie(quadPosition, textureCoordinates) = layouter->renderGlyph(i, cursorPosition, rectangle); - - /* 0---2 - | | - | | - | | - 1---3 */ - - vertices.insert(vertices.end(), { - {quadPosition.topLeft(), textureCoordinates.topLeft()}, - {quadPosition.bottomLeft(), textureCoordinates.bottomLeft()}, - {quadPosition.topRight(), textureCoordinates.topRight()}, - {quadPosition.bottomRight(), textureCoordinates.bottomRight()} - }); - } - - /* Align the rendered mesh */ - const Vector2 renderedSize = rectangle.size(); - Vector2 alignmentOffset; - - /** @todo What about top-down text? */ - - /* Horizontal alignment */ - if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentCenter) - alignmentOffset -= Vector2::xAxis(renderedSize.x()*0.5f); - else if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentRight) - alignmentOffset -= Vector2::xAxis(renderedSize.x()); - - /* Vertical alignment */ + Vector2 linePosition; + std::size_t lastLineLastVertex = 0; + + /* Temp buffer so we don't allocate for each new line */ + /** + * @todo C++1z: use std::string_view to avoid the one allocation and all + * the copying altogether + */ + std::string line; + line.reserve(text.size()); + + /* Render each line separately and align it horizontally */ + std::size_t pos, prevPos = 0; + do { + /* Empty line, nothing to do (the rest is done below in while expression) */ + if((pos = text.find('\n', prevPos)) == prevPos) continue; + + /* Copy the line into the temp buffer */ + line.assign(text, prevPos, pos-prevPos); + + /* Layout the line */ + const auto layouter = font.layout(cache, size, line); + const UnsignedInt vertexCount = layouter->glyphCount()*4; + + /* Verify that we don't reallocate anything. The only problem might + arise when the layouter decides to compose one character from more + than one glyph (i.e. accents). Will remove the assert when this + issue arises. */ + CORRADE_INTERNAL_ASSERT(vertices.size()+vertexCount <= vertices.capacity()); + + /* Bounds of rendered line */ + Rectangle lineRectangle; + + /* Render all glyphs */ + Vector2 cursorPosition(linePosition); + for(UnsignedInt i = 0; i != layouter->glyphCount(); ++i) { + Rectangle quadPosition, textureCoordinates; + std::tie(quadPosition, textureCoordinates) = layouter->renderGlyph(i, cursorPosition, lineRectangle); + + /* 0---2 + | | + | | + | | + 1---3 */ + + vertices.insert(vertices.end(), { + {quadPosition.topLeft(), textureCoordinates.topLeft()}, + {quadPosition.bottomLeft(), textureCoordinates.bottomLeft()}, + {quadPosition.topRight(), textureCoordinates.topRight()}, + {quadPosition.bottomRight(), textureCoordinates.bottomRight()} + }); + } + + /** @todo What about top-down text? */ + + /* Horizontally align the rendered line */ + const Float renderedWidth = lineRectangle.width(); + Float alignmentOffsetX = 0.0f; + if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentCenter) + alignmentOffsetX = -renderedWidth*0.5f; + else if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentRight) + alignmentOffsetX = -renderedWidth; + + /* Integer alignment */ + if(UnsignedByte(alignment) & Implementation::AlignmentIntegral) + alignmentOffsetX = Math::round(alignmentOffsetX); + + /* Align positions and bounds on current line */ + lineRectangle = lineRectangle.translated(Vector2::xAxis(alignmentOffsetX)); + for(auto it = vertices.begin()+lastLineLastVertex; it != vertices.end(); ++it) + it->position.x() += alignmentOffsetX; + + /* Add final line bounds to total bounds, similarly to AbstractFont::renderGlyph() */ + if(!rectangle.size().isZero()) { + rectangle.bottomLeft() = Math::min(rectangle.bottomLeft(), lineRectangle.bottomLeft()); + rectangle.topRight() = Math::max(rectangle.topRight(), lineRectangle.topRight()); + } else rectangle = lineRectangle; + + /* Move to next line */ + } while(prevPos = pos+1, + linePosition -= Vector2::yAxis(font.lineHeight()), + lastLineLastVertex = vertices.size(), + pos != std::string::npos); + + /* Vertically align the rendered text */ + const Float renderedHeight = rectangle.height(); + Float alignmentOffsetY = 0.0f; if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentMiddle) - alignmentOffset -= Vector2::yAxis(renderedSize.y()*0.5f); + alignmentOffsetY = -renderedHeight*0.5f; else if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentTop) - alignmentOffset -= Vector2::yAxis(renderedSize.y()); + alignmentOffsetY = -renderedHeight; /* Integer alignment */ if(UnsignedByte(alignment) & Implementation::AlignmentIntegral) - alignmentOffset = Math::round(alignmentOffset); + alignmentOffsetY = Math::round(alignmentOffsetY); - /* Update positions and bounds */ - rectangle = rectangle.translated(alignmentOffset); - for(auto& v: vertices) v.position += alignmentOffset; + /* Align positions and bounds */ + rectangle = rectangle.translated(Vector2::yAxis(alignmentOffsetY)); + for(auto& v: vertices) v.position.y() += alignmentOffsetY; return std::make_tuple(std::move(vertices), rectangle); } diff --git a/src/Text/Test/RendererGLTest.cpp b/src/Text/Test/RendererGLTest.cpp index 8e6615489..fef145817 100644 --- a/src/Text/Test/RendererGLTest.cpp +++ b/src/Text/Test/RendererGLTest.cpp @@ -35,12 +35,16 @@ class RendererGLTest: public Magnum::Test::AbstractOpenGLTester { void renderData(); void renderMesh(); void mutableText(); + + void multiline(); }; RendererGLTest::RendererGLTest() { addTests({&RendererGLTest::renderData, &RendererGLTest::renderMesh, - &RendererGLTest::mutableText}); + &RendererGLTest::mutableText, + + &RendererGLTest::multiline}); } namespace { @@ -257,6 +261,106 @@ void RendererGLTest::mutableText() { #endif } +void RendererGLTest::multiline() { + class Layouter: public Text::AbstractLayouter { + public: + explicit Layouter(UnsignedInt glyphs): AbstractLayouter(glyphs) {} + + private: + std::tuple doRenderGlyph(UnsignedInt) override { + return std::make_tuple(Rectangle({}, Vector2(1.0f)), Rectangle({}, Vector2(1.0f)), Vector2::xAxis(2.0f)); + } + }; + + class Font: public Text::AbstractFont { + public: + explicit Font(): _opened(false) {} + + private: + Features doFeatures() const override { return {}; } + + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + std::pair doOpenFile(const std::string&, Float) { + _opened = true; + return {0, 3.0f}; + } + + UnsignedInt doGlyphId(char32_t) override { return 0; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + + std::unique_ptr doLayout(const GlyphCache&, Float, const std::string& text) override { + return std::unique_ptr(new Layouter(text.size())); + } + + bool _opened; + }; + + Font font; + font.openFile({}, 0.0f); + Rectangle rectangle; + std::vector indices; + std::vector positions, textureCoordinates; + std::tie(positions, textureCoordinates, indices, rectangle) = Text::Renderer2D::render(font, + *static_cast(nullptr), 0.0f, "abcd\nef\n\nghi", Alignment::MiddleCenter); + + /* Bounds */ + CORRADE_COMPARE(rectangle, Rectangle({-3.5f, -5.0f}, {3.5f, 5.0f})); + + /* Vertices + [a] [b] [c] [d] + [e] [f] + + [g] [h] [i] */ + CORRADE_COMPARE(positions, (std::vector{ + Vector2{-3.5f, 5.0f}, Vector2{-3.5f, 4.0f}, /* a */ + Vector2{-2.5f, 5.0f}, Vector2{-2.5f, 4.0f}, + + Vector2{-1.5f, 5.0f}, Vector2{-1.5f, 4.0f}, /* b */ + Vector2{-0.5f, 5.0f}, Vector2{-0.5f, 4.0f}, + + Vector2{ 0.5f, 5.0f}, Vector2{ 0.5f, 4.0f}, /* c */ + Vector2{ 1.5f, 5.0f}, Vector2{ 1.5f, 4.0f}, + + Vector2{ 2.5f, 5.0f}, Vector2{ 2.5f, 4.0f}, /* d */ + Vector2{ 3.5f, 5.0f}, Vector2{ 3.5f, 4.0f}, + + Vector2{-1.5f, 2.0f}, Vector2{-1.5f, 1.0f}, /* e */ + Vector2{-0.5f, 2.0f}, Vector2{-0.5f, 1.0f}, + + Vector2{ 0.5f, 2.0f}, Vector2{ 0.5f, 1.0f}, /* f */ + Vector2{ 1.5f, 2.0f}, Vector2{ 1.5f, 1.0f}, + + Vector2{-2.5f, -4.0f}, Vector2{-2.5f, -5.0f}, /* g */ + Vector2{-1.5f, -4.0f}, Vector2{-1.5f, -5.0f}, + + Vector2{-0.5f, -4.0f}, Vector2{-0.5f, -5.0f}, /* h */ + Vector2{ 0.5f, -4.0f}, Vector2{ 0.5f, -5.0f}, + + Vector2{ 1.5f, -4.0f}, Vector2{ 1.5f, -5.0f}, /* i */ + Vector2{ 2.5f, -4.0f}, Vector2{ 2.5f, -5.0f}, + })); + + /* Indices + 0---2 0---2 5 + | | | / /| + | | | / / | + | | |/ / | + 1---3 1 3---4 */ + CORRADE_COMPARE(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 + })); +} + }}} CORRADE_TEST_MAIN(Magnum::Text::Test::RendererGLTest)