From bc33945d3d5e161f9922db259773e76014bc7ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 19 Jun 2024 15:05:15 +0200 Subject: [PATCH] Text: introduce Alignment::*Start and *End. Based on the actual text direction (either explicitly set or detected), these resolve to either *Left or *Right. For the Text::Renderer it's done automatically inside (and there's no way to actually set the direction from outside due to the API being ancient and limited), for the align*() utils the alignment has to be explicitly resolved using a new alignmentForDirection() utility. --- doc/changelog.dox | 5 + src/Magnum/Text/Alignment.cpp | 44 ++++ src/Magnum/Text/Alignment.h | 286 ++++++++++++++++++++++--- src/Magnum/Text/CMakeLists.txt | 2 +- src/Magnum/Text/Renderer.cpp | 36 +++- src/Magnum/Text/Renderer.h | 10 + src/Magnum/Text/Test/AlignmentTest.cpp | 82 ++++++- src/Magnum/Text/Test/CMakeLists.txt | 2 +- src/Magnum/Text/Test/RendererTest.cpp | 108 ++++++++-- 9 files changed, 518 insertions(+), 57 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 6246b0fa8..5975c9f43 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -834,6 +834,11 @@ See also: missing before, and @ref Text::Alignment::TopCenterIntegral, which is also needed in order to prevent aligning pixel-perfect fonts to fractional positions. +- @ref Text::Alignment gained special `*Start` and `*End` values for + alignment based on text direction, and a new + @ref Text::alignmentForDirection() utility that resolves those to concrete + `*Left` and `*Right` values based @ref Text::LayoutDirection and + @ref Text::ShapeDirection - Added a @ref Text::GlyphCache::GlyphCache(NoCreateT) and @ref Text::DistanceFieldGlyphCache::DistanceFieldGlyphCache(NoCreateT) constructor allowing to construct the object without a GL context present diff --git a/src/Magnum/Text/Alignment.cpp b/src/Magnum/Text/Alignment.cpp index 9c621431c..c7bae0204 100644 --- a/src/Magnum/Text/Alignment.cpp +++ b/src/Magnum/Text/Alignment.cpp @@ -25,8 +25,11 @@ #include "Alignment.h" +#include #include +#include "Magnum/Text/Direction.h" + namespace Magnum { namespace Text { Debug& operator<<(Debug& debug, const Alignment value) { @@ -43,6 +46,10 @@ Debug& operator<<(Debug& debug, const Alignment value) { _c(LineCenterGlyphBoundsIntegral) _c(LineRight) _c(LineRightGlyphBounds) + _c(LineStart) + _c(LineStartGlyphBounds) + _c(LineEnd) + _c(LineEndGlyphBounds) _c(BottomLeft) _c(BottomLeftGlyphBounds) _c(BottomCenter) @@ -51,6 +58,10 @@ Debug& operator<<(Debug& debug, const Alignment value) { _c(BottomCenterGlyphBoundsIntegral) _c(BottomRight) _c(BottomRightGlyphBounds) + _c(BottomStart) + _c(BottomStartGlyphBounds) + _c(BottomEnd) + _c(BottomEndGlyphBounds) _c(MiddleLeft) _c(MiddleLeftIntegral) _c(MiddleLeftGlyphBounds) @@ -63,6 +74,14 @@ Debug& operator<<(Debug& debug, const Alignment value) { _c(MiddleRightIntegral) _c(MiddleRightGlyphBounds) _c(MiddleRightGlyphBoundsIntegral) + _c(MiddleStart) + _c(MiddleStartIntegral) + _c(MiddleStartGlyphBounds) + _c(MiddleStartGlyphBoundsIntegral) + _c(MiddleEnd) + _c(MiddleEndIntegral) + _c(MiddleEndGlyphBounds) + _c(MiddleEndGlyphBoundsIntegral) _c(TopLeft) _c(TopLeftGlyphBounds) _c(TopCenter) @@ -71,6 +90,10 @@ Debug& operator<<(Debug& debug, const Alignment value) { _c(TopCenterGlyphBoundsIntegral) _c(TopRight) _c(TopRightGlyphBounds) + _c(TopStart) + _c(TopStartGlyphBounds) + _c(TopEnd) + _c(TopEndGlyphBounds) #undef _c /* LCOV_EXCL_STOP */ } @@ -78,4 +101,25 @@ Debug& operator<<(Debug& debug, const Alignment value) { return debug << "(" << Debug::nospace << Debug::hex << UnsignedByte(value) << Debug::nospace << ")"; } +Alignment alignmentForDirection(const Alignment alignment, const LayoutDirection layoutDirection, const ShapeDirection shapeDirection) { + CORRADE_ASSERT(layoutDirection == LayoutDirection::HorizontalTopToBottom, + "Text::alignmentForDirection(): only" << LayoutDirection::HorizontalTopToBottom << "is supported right now, got" << layoutDirection, {}); + CORRADE_ASSERT(shapeDirection != ShapeDirection::TopToBottom && + shapeDirection != ShapeDirection::BottomToTop, + "Text::alignmentForDirection():" << shapeDirection << "is not supported yet, sorry", {}); + + const UnsignedByte horizontal = UnsignedByte(alignment) & Implementation::AlignmentHorizontal; + const UnsignedByte exceptHorizontal = UnsignedByte(alignment) & ~Implementation::AlignmentHorizontal; + if(horizontal == Implementation::AlignmentStart) + return Alignment((shapeDirection == ShapeDirection::RightToLeft ? + Implementation::AlignmentRight : + Implementation::AlignmentLeft)|exceptHorizontal); + if(horizontal == Implementation::AlignmentEnd) + return Alignment((shapeDirection == ShapeDirection::RightToLeft ? + Implementation::AlignmentLeft : + Implementation::AlignmentRight)|exceptHorizontal); + + return alignment; +} + }} diff --git a/src/Magnum/Text/Alignment.h b/src/Magnum/Text/Alignment.h index 834c664c4..8da3b0aa6 100644 --- a/src/Magnum/Text/Alignment.h +++ b/src/Magnum/Text/Alignment.h @@ -26,10 +26,11 @@ */ /** @file - * @brief Enum @ref Magnum::Text::Alignment + * @brief Enum @ref Magnum::Text::Alignment, function @ref Magnum::Text::alignmentForDirection() */ #include "Magnum/Magnum.h" +#include "Magnum/Text/Text.h" #include "Magnum/Text/visibility.h" namespace Magnum { namespace Text { @@ -42,16 +43,20 @@ namespace Implementation { AlignmentLeft = 0, AlignmentCenter = 1 << 0, AlignmentRight = 2 << 0, - AlignmentHorizontal = AlignmentLeft|AlignmentCenter|AlignmentRight, + /* Start and End is Left or Right based on ShapeDirection, and possibly + also Top / Bottom eventually for vertical text */ + AlignmentStart = 3 << 0, + AlignmentEnd = 4 << 0, + AlignmentHorizontal = AlignmentLeft|AlignmentCenter|AlignmentRight|AlignmentStart|AlignmentEnd, AlignmentLine = 0, - AlignmentBottom = 1 << 2, - AlignmentMiddle = 2 << 2, - AlignmentTop = 3 << 2, + AlignmentBottom = 1 << 4, + AlignmentMiddle = 2 << 4, + AlignmentTop = 3 << 4, AlignmentVertical = AlignmentLine|AlignmentBottom|AlignmentMiddle|AlignmentTop, - AlignmentIntegral = 1 << 4, - AlignmentGlyphBounds = 1 << 5 + AlignmentIntegral = 1 << 6, + AlignmentGlyphBounds = 1 << 7 }; } @@ -71,19 +76,31 @@ positioned on whole pixels as well in order to avoid making them blurry. These are only needed for `*Middle` and `*Center` alignments as they may result in the bounding rectangle to have odd dimensions, which would then result in half-pixel shifts when divided by 2. -@see @ref Renderer::render(), @ref Renderer::Renderer() + +The `*Start` and `*End` values behave the same as `*Left` and `*Right`, +respectively, if @ref ShapeDirection::LeftToRight is passed to +@ref AbstractShaper::setDirection(), or if it's +@ref ShapeDirection::Unspecified and the particular font plugin doesn't detect +@ref ShapeDirection::RightToLeft when shaping. If +@ref ShapeDirection::RightToLeft is set (or detected for +@ref ShapeDirection::Unspecified), they're swapped, i.e. `*Start` becomes +`*Right` and `*End` becomes `*Left`. +@see @ref Renderer::render(), @ref Renderer::Renderer(), + @see @ref alignmentForDirection() */ enum class Alignment: UnsignedByte { /** * Leftmost cursor position and vertical line position is at origin. - * @see @ref Alignment::LineLeftGlyphBounds + * @see @ref Alignment::LineLeftGlyphBounds, @ref Alignment::LineStart, + * @ref Alignment::LineEnd */ LineLeft = Implementation::AlignmentLine|Implementation::AlignmentLeft, /** * Left side of the glyph bounding rectangle and vertical line position is * at origin. - * @see @ref Alignment::LineLeft + * @see @ref Alignment::LineLeft, @ref Alignment::LineStartGlyphBounds, + * @ref Alignment::LineEndGlyphBounds * @m_since_latest */ LineLeftGlyphBounds = LineLeft|Implementation::AlignmentGlyphBounds, @@ -126,28 +143,68 @@ enum class Alignment: UnsignedByte { /** * Rightmost cursor position and vertical line position is at origin. - * @see @ref Alignment::LineRightGlyphBounds + * @see @ref Alignment::LineRightGlyphBounds, @ref Alignment::LineEnd, + * @ref Alignment::LineStart + * @see @ref alignmentForDirection() */ LineRight = Implementation::AlignmentLine|Implementation::AlignmentRight, /** * Right side of the glyph bounding rectangle and vertical line position is * at origin. - * @see @ref Alignment::LineRight + * @see @ref Alignment::LineRight, @ref Alignment::LineEndGlyphBounds, + * @ref Alignment::LineStartGlyphBounds + * @see @ref alignmentForDirection() * @m_since_latest */ LineRightGlyphBounds = LineRight|Implementation::AlignmentGlyphBounds, + /** + * @ref Alignment::LineRight for @ref ShapeDirection::RightToLeft, + * @ref Alignment::LineLeft otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + LineStart = Implementation::AlignmentLine|Implementation::AlignmentStart, + + /** + * @ref Alignment::LineRightGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::LineLeftGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + LineStartGlyphBounds = LineStart|Implementation::AlignmentGlyphBounds, + + /** + * @ref Alignment::LineLeft for @ref ShapeDirection::RightToLeft, + * @ref Alignment::LineRight otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + LineEnd = Implementation::AlignmentLine|Implementation::AlignmentEnd, + + /** + * @ref Alignment::LineLeftGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::LineRightGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + LineEndGlyphBounds = LineEnd|Implementation::AlignmentGlyphBounds, + /** * Leftmost cursor position and bottommost line descent is at origin. - * @see @ref Alignment::BottomLeftGlyphBounds + * @see @ref Alignment::BottomLeftGlyphBounds, @ref Alignment::BottomStart, + * @ref Alignment::BottomEnd * @m_since_latest */ BottomLeft = Implementation::AlignmentBottom|Implementation::AlignmentLeft, /** * Bottom left corner of the glyph bounding rectangle is at origin. - * @see @ref Alignment::BottomLeft + * @see @ref Alignment::BottomLeft, @ref Alignment::BottomStartGlyphBounds, + * @ref Alignment::BottomEndGlyphBounds * @m_since_latest */ BottomLeftGlyphBounds = BottomLeft|Implementation::AlignmentGlyphBounds, @@ -191,23 +248,60 @@ enum class Alignment: UnsignedByte { /** * Rightmost cursor position and bottommost line descent is at origin. - * @see @ref Alignment::BottomRightGlyphBounds + * @see @ref Alignment::BottomRightGlyphBounds, @ref Alignment::BottomEnd, + * @ref Alignment::BottomStart * @m_since_latest */ BottomRight = Implementation::AlignmentBottom|Implementation::AlignmentRight, /** * Bottom right corner of the glyph bounding rectangle is at origin. - * @see @ref Alignment::BottomRight + * @see @ref Alignment::BottomRight, @ref Alignment::BottomEndGlyphBounds, + * @ref Alignment::BottomStartGlyphBounds * @m_since_latest */ BottomRightGlyphBounds = BottomRight|Implementation::AlignmentGlyphBounds, + /** + * @ref Alignment::BottomRight for @ref ShapeDirection::RightToLeft, + * @ref Alignment::BottomLeft otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + BottomStart = Implementation::AlignmentBottom|Implementation::AlignmentStart, + + /** + * @ref Alignment::BottomRightGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::BottomLeftGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + BottomStartGlyphBounds = BottomStart|Implementation::AlignmentGlyphBounds, + + /** + * @ref Alignment::BottomLeft for @ref ShapeDirection::RightToLeft, + * @ref Alignment::BottomRight otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + BottomEnd = Implementation::AlignmentBottom|Implementation::AlignmentEnd, + + /** + * @ref Alignment::BottomLeftGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::BottomRightGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + BottomEndGlyphBounds = BottomEnd|Implementation::AlignmentGlyphBounds, + /** * Leftmost cursor position and a midpoint between topmost line ascent and * bottommost line descent is at origin. * @see @ref Alignment::MiddleLeftGlyphBounds, - * @ref Alignment::MiddleLeftIntegral + * @ref Alignment::MiddleLeftIntegral, @ref Alignment::MiddleStart, + * @ref Alignment::MiddleEnd */ MiddleLeft = Implementation::AlignmentMiddle|Implementation::AlignmentLeft, @@ -216,7 +310,9 @@ enum class Alignment: UnsignedByte { * bottommost line descent is at origin, with the vertical offset rounded * to whole units. * @see @ref Alignment::MiddleLeft, - * @ref Alignment::MiddleLeftGlyphBoundsIntegral + * @ref Alignment::MiddleLeftGlyphBoundsIntegral, + * @ref Alignment::MiddleStartIntegral, + * @ref Alignment::MiddleEndIntegral */ MiddleLeftIntegral = MiddleLeft|Implementation::AlignmentIntegral, @@ -224,7 +320,9 @@ enum class Alignment: UnsignedByte { * Left side and vertical center of the glyph bounding rectangle is at * origin. * @see @ref Alignment::MiddleLeft, - * @ref Alignment::MiddleLeftGlyphBoundsIntegral + * @ref Alignment::MiddleLeftGlyphBoundsIntegral, + * @ref Alignment::MiddleStartGlyphBounds, + * @ref Alignment::MiddleEndGlyphBounds * @m_since_latest */ MiddleLeftGlyphBounds = MiddleLeft|Implementation::AlignmentGlyphBounds, @@ -233,7 +331,9 @@ enum class Alignment: UnsignedByte { * Left side and vertical center of the glyph bounding rectangle is at * origin, with the vertical offset rounded to whole units. * @see @ref Alignment::MiddleLeftGlyphBounds, - * @ref Alignment::MiddleLeftIntegral + * @ref Alignment::MiddleLeftIntegral, + * @ref Alignment::MiddleStartGlyphBoundsIntegral, + * @ref Alignment::MiddleEndGlyphBoundsIntegral * @m_since_latest */ MiddleLeftGlyphBoundsIntegral = MiddleLeftGlyphBounds|Implementation::AlignmentIntegral, @@ -278,7 +378,8 @@ enum class Alignment: UnsignedByte { * Rightmost cursor position and a midpoint between topmost line ascent and * bottommost line descent is at origin. * @see @ref Alignment::MiddleRightGlyphBounds, - * @ref Alignment::MiddleRightIntegral + * @ref Alignment::MiddleRightIntegral, @ref Alignment::MiddleEnd, + * @ref Alignment::MiddleStart */ MiddleRight = Implementation::AlignmentMiddle|Implementation::AlignmentRight, @@ -287,7 +388,9 @@ enum class Alignment: UnsignedByte { * bottommost line descent is at origin, with the vertical offset rounded * to whole units. * @see @ref Alignment::MiddleRight, - * @ref Alignment::MiddleRightGlyphBoundsIntegral + * @ref Alignment::MiddleRightGlyphBoundsIntegral, + * @ref Alignment::MiddleEndIntegral, + * @ref Alignment::MiddleStartIntegral */ MiddleRightIntegral = MiddleRight|Implementation::AlignmentIntegral, @@ -295,7 +398,9 @@ enum class Alignment: UnsignedByte { * Right side and vertical center of the glyph bounding rectangle is at * origin. * @see @ref Alignment::MiddleRight, - * @ref Alignment::MiddleRightGlyphBoundsIntegral + * @ref Alignment::MiddleRightGlyphBoundsIntegral, + * @ref Alignment::MiddleEndGlyphBounds, + * @ref Alignment::MiddleStartGlyphBounds * @m_since_latest */ MiddleRightGlyphBounds = MiddleRight|Implementation::AlignmentGlyphBounds, @@ -304,20 +409,94 @@ enum class Alignment: UnsignedByte { * Right side and vertical center of the glyph bounding rectangle is at * origin, with the vertical offset rounded to whole units. * @see @ref Alignment::MiddleRightGlyphBounds, - * @ref Alignment::MiddleRightIntegral + * @ref Alignment::MiddleRightIntegral, + * @ref Alignment::MiddleEndGlyphBoundsIntegral, + * @ref Alignment::MiddleStartGlyphBoundsIntegral * @m_since_latest */ MiddleRightGlyphBoundsIntegral = MiddleRightGlyphBounds|Implementation::AlignmentIntegral, + /** + * @ref Alignment::MiddleRight for @ref ShapeDirection::RightToLeft, + * @ref Alignment::MiddleLeft otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleStart = Implementation::AlignmentMiddle|Implementation::AlignmentStart, + + /** + * @ref Alignment::MiddleRightIntegral for + * @ref ShapeDirection::RightToLeft, @ref Alignment::MiddleLeftIntegral + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleStartIntegral = MiddleStart|Implementation::AlignmentIntegral, + + /** + * @ref Alignment::MiddleRightGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::MiddleLeftGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleStartGlyphBounds = MiddleStart|Implementation::AlignmentGlyphBounds, + + /** + * @ref Alignment::MiddleRightGlyphBoundsIntegral for + * @ref ShapeDirection::RightToLeft, + * @ref Alignment::MiddleLeftGlyphBoundsIntegral otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleStartGlyphBoundsIntegral = MiddleStartGlyphBounds|Implementation::AlignmentIntegral, + + /** + * @ref Alignment::MiddleLeft for @ref ShapeDirection::RightToLeft, + * @ref Alignment::MiddleRight otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleEnd = Implementation::AlignmentMiddle|Implementation::AlignmentEnd, + + /** + * @ref Alignment::MiddleLeftIntegral for + * @ref ShapeDirection::RightToLeft, @ref Alignment::MiddleRightIntegral + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleEndIntegral = MiddleEnd|Implementation::AlignmentIntegral, + + /** + * @ref Alignment::MiddleLeftGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::MiddleRightGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleEndGlyphBounds = MiddleEnd|Implementation::AlignmentGlyphBounds, + + /** + * @ref Alignment::MiddleLeftGlyphBoundsIntegral for + * @ref ShapeDirection::RightToLeft, + * @ref Alignment::MiddleRightGlyphBoundsIntegral otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + MiddleEndGlyphBoundsIntegral = MiddleEndGlyphBounds|Implementation::AlignmentIntegral, + /** * Leftmost cursor position and topmost line ascent is at origin. - * @see @ref Alignment::TopLeftGlyphBounds + * @see @ref Alignment::TopLeftGlyphBounds, @ref Alignment::TopStart, + * @ref Alignment::TopEnd */ TopLeft = Implementation::AlignmentTop|Implementation::AlignmentLeft, /** * Top left corner of the glyph bounding rectangle is at origin. - * @see @ref Alignment::TopLeft + * @see @ref Alignment::TopLeft, @ref Alignment::TopStartGlyphBounds, + * @ref Alignment::TopEndGlyphBounds * @m_since_latest */ TopLeftGlyphBounds = TopLeft|Implementation::AlignmentGlyphBounds, @@ -359,21 +538,72 @@ enum class Alignment: UnsignedByte { /** * Rightmost cursor position and topmost line ascent is at origin. - * @see @ref Alignment::TopRightGlyphBounds + * @see @ref Alignment::TopRightGlyphBounds, @ref Alignment::TopEnd, + * @ref Alignment::TopStart */ TopRight = Implementation::AlignmentTop|Implementation::AlignmentRight, /** * Top right corner of the glyph bounding rectangle is at origin. - * @see @ref Alignment::TopRight + * @see @ref Alignment::TopRight, @ref Alignment::TopEndGlyphBounds, + * @ref Alignment::TopStartGlyphBounds * @m_since_latest */ TopRightGlyphBounds = TopRight|Implementation::AlignmentGlyphBounds, + + /** + * @ref Alignment::TopRight for @ref ShapeDirection::RightToLeft, + * @ref Alignment::TopLeft otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + TopStart = Implementation::AlignmentTop|Implementation::AlignmentStart, + + /** + * @ref Alignment::TopRightGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::TopLeftGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + TopStartGlyphBounds = TopStart|Implementation::AlignmentGlyphBounds, + + /** + * @ref Alignment::TopLeft for @ref ShapeDirection::RightToLeft, + * @ref Alignment::TopRight otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + TopEnd = Implementation::AlignmentTop|Implementation::AlignmentEnd, + + /** + * @ref Alignment::TopLeftGlyphBounds for + * @ref ShapeDirection::RightToLeft, @ref Alignment::TopRightGlyphBounds + * otherwise. + * @see @ref alignmentForDirection() + * @m_since_latest + */ + TopEndGlyphBounds = TopEnd|Implementation::AlignmentGlyphBounds, }; /** @debugoperatorenum{Alignment} */ MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& debug, Alignment value); +/** +@brief Alignment for layout and shape direction +@m_since_latest + +The @p layoutDirection is currently expected to always be +@ref LayoutDirection::HorizontalTopToBottom and @p shapeDirection never +@ref ShapeDirection::TopToBottom or @ref ShapeDirection::BottomToTop. Then, if +@p alignment is `*Start` or `*End`, it's converted to `*Left` or `*Right`, +respectively, if @p shapeDirection is @ref ShapeDirection::LeftToRight or +@ref ShapeDirection::Unspecified, and `*Right` or `*Left`, respectively, if +@p shapeDirection is @ref ShapeDirection::RightToLeft. +*/ +MAGNUM_TEXT_EXPORT Alignment alignmentForDirection(Alignment alignment, LayoutDirection layoutDirection, ShapeDirection shapeDirection); + + }} #endif diff --git a/src/Magnum/Text/CMakeLists.txt b/src/Magnum/Text/CMakeLists.txt index 914d756a9..adb0ae27d 100644 --- a/src/Magnum/Text/CMakeLists.txt +++ b/src/Magnum/Text/CMakeLists.txt @@ -31,7 +31,6 @@ find_package(Corrade REQUIRED PluginManager) # Files shared between main library and unit test library set(MagnumText_SRCS - Alignment.cpp Direction.cpp) # Files compiled with different flags for main library and unit test library @@ -40,6 +39,7 @@ set(MagnumText_GracefulAssert_SRCS AbstractFontConverter.cpp AbstractGlyphCache.cpp AbstractShaper.cpp + Alignment.cpp Feature.cpp Renderer.cpp Script.cpp) diff --git a/src/Magnum/Text/Renderer.cpp b/src/Magnum/Text/Renderer.cpp index fd76df20c..2b8154fcd 100644 --- a/src/Magnum/Text/Renderer.cpp +++ b/src/Magnum/Text/Renderer.cpp @@ -188,6 +188,10 @@ Range2D renderGlyphQuadsInto(const AbstractGlyphCache& cache, const Float scale, Range2D alignRenderedLine(const Range2D& lineRectangle, const LayoutDirection direction, const Alignment alignment, const Containers::StridedArrayView1D& positions) { CORRADE_ASSERT(direction == LayoutDirection::HorizontalTopToBottom, "Text::alignRenderedLine(): only" << LayoutDirection::HorizontalTopToBottom << "is supported right now, got" << direction, {}); + CORRADE_ASSERT( + (UnsignedByte(alignment) & Implementation::AlignmentHorizontal) != Implementation::AlignmentStart && + (UnsignedByte(alignment) & Implementation::AlignmentHorizontal) != Implementation::AlignmentEnd, + "Text::alignRenderedLine():" << alignment << "has to be resolved to *Left / *Right before being passed to this function", {}); #ifdef CORRADE_NO_ASSERT static_cast(direction); /** @todo drop once implemented */ #endif @@ -218,6 +222,10 @@ Range2D alignRenderedLine(const Range2D& lineRectangle, const LayoutDirection di Range2D alignRenderedBlock(const Range2D& blockRectangle, const LayoutDirection direction, const Alignment alignment, const Containers::StridedArrayView1D& positions) { CORRADE_ASSERT(direction == LayoutDirection::HorizontalTopToBottom, "Text::alignRenderedBlock(): only" << LayoutDirection::HorizontalTopToBottom << "is supported right now, got" << direction, {}); + CORRADE_ASSERT( + (UnsignedByte(alignment) & Implementation::AlignmentHorizontal) != Implementation::AlignmentStart && + (UnsignedByte(alignment) & Implementation::AlignmentHorizontal) != Implementation::AlignmentEnd, + "Text::alignRenderedBlock():" << alignment << "has to be resolved to *Left / *Right before being passed to this function", {}); #ifdef CORRADE_NO_ASSERT static_cast(direction); /** @todo drop once implemented */ #endif @@ -335,6 +343,12 @@ std::tuple, Range2D> renderVerticesInternal(AbstractFont& fo /** @todo even with reusing a shaper this is all horrific, rework!! */ Containers::Pointer shaper = font.createShaper(); + /* Start/End alignment resolved based on what the shaper detects for the + first line. Not great, but can't do much better with this old limited + API. */ + /** @todo rework all this, again */ + Containers::Optional resolvedAlignment; + /* Render each line separately and align it horizontally */ std::size_t pos, prevPos = 0; do { @@ -404,14 +418,30 @@ std::tuple, Range2D> renderVerticesInternal(AbstractFont& fo lineVertices.slice(&Vertex::position), lineVertices.slice(&Vertex::textureCoordinates)); + /* Resolve the alignment based on what the shaper detected (if + anything). Assume there are no font plugins that would produce + vertical shape direction by default. */ + /** @todo drop all this once the shaper instance is configurable from + outside */ + if(!resolvedAlignment) { + const ShapeDirection shapeDirection = shaper->direction(); + CORRADE_INTERNAL_ASSERT( + shapeDirection != ShapeDirection::TopToBottom && + shapeDirection != ShapeDirection::BottomToTop); + resolvedAlignment = alignmentForDirection(alignment, + /** @todo direction hardcoded here */ + LayoutDirection::HorizontalTopToBottom, + shapeDirection); + } + /* Horizontally align the line, using either of the rectangles based on which alignment is desired */ const Range2D alignedLineRectangle = alignRenderedLine( - UnsignedByte(alignment) & Implementation::AlignmentGlyphBounds ? + UnsignedByte(*resolvedAlignment) & Implementation::AlignmentGlyphBounds ? lineQuadRectangle : lineRectangle, /** @todo direction hardcoded here */ LayoutDirection::HorizontalTopToBottom, - alignment, + *resolvedAlignment, lineVertices.slice(&Vertex::position)); /* Extend the rectangle with final line bounds */ @@ -426,7 +456,7 @@ std::tuple, Range2D> renderVerticesInternal(AbstractFont& fo rectangle, /** @todo direction hardcoded here */ LayoutDirection::HorizontalTopToBottom, - alignment, + *resolvedAlignment, Containers::stridedArrayView(vertices).slice(&Vertex::position)); return std::make_tuple(Utility::move(vertices), alignedRectangle); diff --git a/src/Magnum/Text/Renderer.h b/src/Magnum/Text/Renderer.h index 96f7bebd2..e9cea5911 100644 --- a/src/Magnum/Text/Renderer.h +++ b/src/Magnum/Text/Renderer.h @@ -212,6 +212,11 @@ MAGNUM_TEXT_EXPORT Range2D renderGlyphQuadsInto(const AbstractGlyphCache& cache, the alignment. @m_since_latest +Expects that @p alignment isn't `*Start` or `*End`, those values have to be +resolved to `*Left` or `*Right` based on desired or detected +@ref ShapeDirection using @ref alignmentForDirection() before being passed to +this function. + If @p alignment isn't `*GlyphBounds`, this function should get glyph @p positions for the whole line coming from @ref renderLineGlyphPositionsInto() and @p lineRectangle being all rectangles returned by that function combined @@ -244,6 +249,11 @@ MAGNUM_TEXT_EXPORT Range2D alignRenderedLine(const Range2D& lineRectangle, Layou advance based on the alignment. @m_since_latest +Expects that @p alignment isn't `*Start` or `*End`, those values have to be +resolved to `*Left` or `*Right` based on desired or detected +@ref ShapeDirection using @ref alignmentForDirection() before being passed to +this function. + This function should get glyph or vertex @p positions for all lines as aligned by calls to @ref alignRenderedLine(), and @p blockRectangle being all line rectangles returned by that function combined together with @ref Math::join(). diff --git a/src/Magnum/Text/Test/AlignmentTest.cpp b/src/Magnum/Text/Test/AlignmentTest.cpp index 68605cfb6..90282b050 100644 --- a/src/Magnum/Text/Test/AlignmentTest.cpp +++ b/src/Magnum/Text/Test/AlignmentTest.cpp @@ -24,10 +24,13 @@ */ #include +#include /** @todo remove once Debug is stream-free */ #include +#include #include /** @todo remove once Debug is stream-free */ #include "Magnum/Text/Alignment.h" +#include "Magnum/Text/Direction.h" namespace Magnum { namespace Text { namespace Test { namespace { @@ -35,10 +38,16 @@ struct AlignmentTest: TestSuite::Tester { explicit AlignmentTest(); void debug(); + + void forDirection(); + void forDirectionInvalid(); }; AlignmentTest::AlignmentTest() { - addTests({&AlignmentTest::debug}); + addTests({&AlignmentTest::debug, + + &AlignmentTest::forDirection, + &AlignmentTest::forDirectionInvalid}); } void AlignmentTest::debug() { @@ -47,6 +56,77 @@ void AlignmentTest::debug() { CORRADE_COMPARE(out.str(), "Text::Alignment::MiddleRightGlyphBounds Text::Alignment(0xab)\n"); } +void AlignmentTest::forDirection() { + /* For alignment that's neither Start nor End it's just a passthrough */ + CORRADE_COMPARE(alignmentForDirection( + Alignment::BottomRightGlyphBounds, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::RightToLeft + ), Alignment::BottomRightGlyphBounds); + CORRADE_COMPARE(alignmentForDirection( + Alignment::MiddleLeftIntegral, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::RightToLeft + ), Alignment::MiddleLeftIntegral); + CORRADE_COMPARE(alignmentForDirection( + Alignment::TopCenter, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::LeftToRight + ), Alignment::TopCenter); + + /* For Start / End it resolves based on ShapeDirection, keeping all extra + bits as well */ + CORRADE_COMPARE(alignmentForDirection( + Alignment::TopStart, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::LeftToRight + ), Alignment::TopLeft); + CORRADE_COMPARE(alignmentForDirection( + Alignment::MiddleEndIntegral, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::LeftToRight + ), Alignment::MiddleRightIntegral); + CORRADE_COMPARE(alignmentForDirection( + Alignment::MiddleStartGlyphBoundsIntegral, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::RightToLeft + ), Alignment::MiddleRightGlyphBoundsIntegral); + CORRADE_COMPARE(alignmentForDirection( + Alignment::LineEndGlyphBounds, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::RightToLeft + ), Alignment::LineLeftGlyphBounds); + + /* Unspecified ShapeDirection behaves same as LeftToRight */ + CORRADE_COMPARE(alignmentForDirection( + Alignment::BottomStart, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::Unspecified + ), Alignment::BottomLeft); + CORRADE_COMPARE(alignmentForDirection( + Alignment::TopEndGlyphBounds, + LayoutDirection::HorizontalTopToBottom, + ShapeDirection::Unspecified + ), Alignment::TopRightGlyphBounds); +} + +void AlignmentTest::forDirectionInvalid() { + CORRADE_SKIP_IF_NO_ASSERT(); + + std::ostringstream out; + Error redirectError{&out}; + /* It should blow up also for alignments that don't use Start or End, for + consistency */ + alignmentForDirection(Alignment::BottomCenter, LayoutDirection::VerticalRightToLeft, ShapeDirection::Unspecified); + alignmentForDirection(Alignment::MiddleCenterIntegral, LayoutDirection::HorizontalTopToBottom, ShapeDirection::TopToBottom); + alignmentForDirection(Alignment::MiddleCenterIntegral, LayoutDirection::HorizontalTopToBottom, ShapeDirection::BottomToTop); + CORRADE_COMPARE_AS(out.str(), + "Text::alignmentForDirection(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalRightToLeft\n" + "Text::alignmentForDirection(): Text::ShapeDirection::TopToBottom is not supported yet, sorry\n" + "Text::alignmentForDirection(): Text::ShapeDirection::BottomToTop is not supported yet, sorry\n", + TestSuite::Compare::String); +} + }}}} CORRADE_TEST_MAIN(Magnum::Text::Test::AlignmentTest) diff --git a/src/Magnum/Text/Test/CMakeLists.txt b/src/Magnum/Text/Test/CMakeLists.txt index c3c98b6ad..aad686287 100644 --- a/src/Magnum/Text/Test/CMakeLists.txt +++ b/src/Magnum/Text/Test/CMakeLists.txt @@ -77,7 +77,7 @@ endif() corrade_add_test(TextAbstractShaperTest AbstractShaperTest.cpp LIBRARIES MagnumTextTestLib) -corrade_add_test(TextAlignmentTest AlignmentTest.cpp LIBRARIES MagnumText) +corrade_add_test(TextAlignmentTest AlignmentTest.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) diff --git a/src/Magnum/Text/Test/RendererTest.cpp b/src/Magnum/Text/Test/RendererTest.cpp index 07907023e..5c3b230fb 100644 --- a/src/Magnum/Text/Test/RendererTest.cpp +++ b/src/Magnum/Text/Test/RendererTest.cpp @@ -122,62 +122,106 @@ const struct { const struct { TestSuite::TestCaseDescriptionSourceLocation name; Alignment alignment; + ShapeDirection shapeDirection; 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, + {"line left", + Alignment::LineLeft, ShapeDirection::Unspecified, /* This is the default (0) value, thus should result in no shift */ {}}, - {"line left, glyph bounds", Alignment::LineLeftGlyphBounds, + {"line left, glyph bounds", + Alignment::LineLeftGlyphBounds, ShapeDirection::Unspecified, /* The first glyph has X offset of 2.5, which is subtracted */ {-2.5f, 0.0f}}, - {"top left", Alignment::TopLeft, + {"top left", + Alignment::TopLeft, ShapeDirection::Unspecified, /* Ascent is 4.5, scaled by 0.5 */ {0.0f, -2.25f}}, - {"top left, glyph bounds", Alignment::TopLeftGlyphBounds, + {"top left, glyph bounds", + Alignment::TopLeftGlyphBounds, ShapeDirection::Unspecified, /* Largest Y value is 10.5f */ {-2.5f, -10.5f}}, - {"top right", Alignment::TopRight, + {"top right", + Alignment::TopRight, ShapeDirection::Unspecified, /* 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, + {"top right, glyph bounds", + Alignment::TopRightGlyphBounds, ShapeDirection::Unspecified, /* Basically subtracting the largest vertex value */ {-12.5f, -10.5f}}, - {"top center", Alignment::TopCenter, + {"top center", + Alignment::TopCenter, ShapeDirection::Unspecified, /* 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, + {"top center, integral", + Alignment::TopCenterIntegral, ShapeDirection::Unspecified, /* The Y shift isn't whole units but only X is rounded here */ {-2.0f, -2.25f}}, - {"top center, glyph bounds", Alignment::TopCenterGlyphBounds, + {"top center, glyph bounds", + Alignment::TopCenterGlyphBounds, ShapeDirection::Unspecified, {-7.5f, -10.5f}}, - {"top center, glyph bounds, integral", Alignment::TopCenterGlyphBoundsIntegral, + {"top center, glyph bounds, integral", + Alignment::TopCenterGlyphBoundsIntegral, ShapeDirection::Unspecified, /* The Y shift isn't whole units but only X is rounded here */ {-8.0f, -10.5f}}, - {"middle left, glyph bounds", Alignment::MiddleLeftGlyphBounds, + {"middle left, glyph bounds", + Alignment::MiddleLeftGlyphBounds, ShapeDirection::Unspecified, {-2.5f, -7.125f}}, - {"middle left, glyph bounds, integral", Alignment::MiddleLeftGlyphBoundsIntegral, + {"middle left, glyph bounds, integral", + Alignment::MiddleLeftGlyphBoundsIntegral, ShapeDirection::Unspecified, /* The X shift isn't whole units but only Y is rounded here */ {-2.5f, -7.0f}}, - {"middle center", Alignment::MiddleCenter, + {"middle center", + Alignment::MiddleCenter, ShapeDirection::Unspecified, {-1.5f, -0.5f}}, - {"middle center, integral", Alignment::MiddleCenterIntegral, + {"middle center, integral", + Alignment::MiddleCenterIntegral, ShapeDirection::Unspecified, /* Rounding happens on both X and Y in this case */ {-2.0f, -1.0f}}, - {"middle center, glyph bounds", Alignment::MiddleCenterGlyphBounds, + {"middle center, glyph bounds", + Alignment::MiddleCenterGlyphBounds, ShapeDirection::Unspecified, /* Half size of the bounds quad */ {-7.5f, -7.125f}}, - {"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral, + {"middle center, glyph bounds, integral", + Alignment::MiddleCenterGlyphBoundsIntegral, ShapeDirection::Unspecified, {-8.0f, -7.0f}}, - {"bottom left", Alignment::BottomLeft, + {"bottom left", + Alignment::BottomLeft, ShapeDirection::Unspecified, /* Descent is -2.5; scaled by 0.5 */ {0.0f, 1.25f}}, - {"bottom right", Alignment::BottomRight, + {"bottom right", + Alignment::BottomRight, ShapeDirection::Unspecified, {-3.0f, 1.25f}}, - {"bottom right, glyph bounds", Alignment::BottomRightGlyphBounds, + {"bottom right, glyph bounds", + Alignment::BottomRightGlyphBounds, ShapeDirection::Unspecified, {-12.5f, -3.75f}}, + {"line start, direction unspecified", + Alignment::LineStart, ShapeDirection::Unspecified, + {}}, /* Same as line left, thus no shift */ + {"bottom start, LTR", + Alignment::BottomStart, ShapeDirection::LeftToRight, + {0.0f, 1.25f}}, /* Same as bottom left */ + {"line end, RTL", + Alignment::LineEnd, ShapeDirection::RightToLeft, + {}}, /* Again same as line left, thus no shift */ + {"line end, direction unspecified", + Alignment::LineEnd, ShapeDirection::Unspecified, + {-3.0f, 0.0f}}, /* Same as line right */ + {"top end, LTR", + Alignment::TopEnd, ShapeDirection::LeftToRight, + {-3.0f, -2.25f}}, /* Same as top right */ + {"line start, RTL", + Alignment::LineStart, ShapeDirection::RightToLeft, + {-3.0f, 0.0f}}, /* Same as line right */ + {"line left, RTL", + Alignment::LineLeft, ShapeDirection::RightToLeft, + {}}, /* Line left with no change */ + {"middle center, RTL", + Alignment::MiddleCenter, ShapeDirection::RightToLeft, + {-1.5f, -0.5f}}, /* Middle center with no change */ }; const struct { @@ -303,12 +347,16 @@ RendererTest::RendererTest() { } struct TestShaper: AbstractShaper { - using AbstractShaper::AbstractShaper; + explicit TestShaper(AbstractFont& font, ShapeDirection direction): AbstractShaper{font}, _direction{direction} {} UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return text.size(); } + ShapeDirection doDirection() const override { + return _direction; + } + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { for(UnsignedInt i = 0; i != ids.size(); ++i) { /* It just rotates between the three glyphs */ @@ -325,9 +373,12 @@ struct TestShaper: AbstractShaper { /* 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); + /* This is always to the right, independent of ShapeDirection */ advances[i] = {Float(i + 1), i % 2 ? -0.5f : +0.5f}; } } + + ShapeDirection _direction; }; struct TestFont: AbstractFont { @@ -350,9 +401,11 @@ struct TestFont: AbstractFont { Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } Containers::Pointer doCreateShaper() override { - return Containers::pointer(*this); + return Containers::pointer(*this, direction); } + ShapeDirection direction = ShapeDirection::Unspecified; + bool _opened = false; }; @@ -817,7 +870,11 @@ void RendererTest::alignLineInvalidDirection() { std::ostringstream out; Error redirectError{&out}; alignRenderedLine({}, LayoutDirection::VerticalRightToLeft, Alignment::LineLeft, nullptr); - CORRADE_COMPARE(out.str(), "Text::alignRenderedLine(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalRightToLeft\n"); + alignRenderedLine({}, LayoutDirection::HorizontalTopToBottom, Alignment::BottomEnd, nullptr); + CORRADE_COMPARE_AS(out.str(), + "Text::alignRenderedLine(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalRightToLeft\n" + "Text::alignRenderedLine(): Text::Alignment::BottomEnd has to be resolved to *Left / *Right before being passed to this function\n", + TestSuite::Compare::String); } void RendererTest::alignBlock() { @@ -847,7 +904,11 @@ void RendererTest::alignBlockInvalidDirection() { std::ostringstream out; Error redirectError{&out}; alignRenderedBlock({}, LayoutDirection::VerticalRightToLeft, Alignment::LineLeft, nullptr); - CORRADE_COMPARE(out.str(), "Text::alignRenderedBlock(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalRightToLeft\n"); + alignRenderedBlock({}, LayoutDirection::HorizontalTopToBottom, Alignment::LineStartGlyphBounds, nullptr); + CORRADE_COMPARE_AS(out.str(), + "Text::alignRenderedBlock(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalRightToLeft\n" + "Text::alignRenderedBlock(): Text::Alignment::LineStartGlyphBounds has to be resolved to *Left / *Right before being passed to this function\n", + TestSuite::Compare::String); } template void RendererTest::glyphQuadIndices() { @@ -909,6 +970,7 @@ void RendererTest::renderData() { setTestCaseDescription(data.name); TestFont font; + font.direction = data.shapeDirection; font.openFile({}, 0.5f); DummyGlyphCache cache = testGlyphCache(font);