diff --git a/doc/changelog.dox b/doc/changelog.dox index cd2033401..41a99da30 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -1252,6 +1252,12 @@ See also: it's short enough to not warrant the existence of a dedicated overload - The @ref Text library API was reworked for more features, better efficiency and no dependencies on the STL: + - @cpp Text::AbstractFont::layout() @ce and the + @cpp Text::AbstractLayouter @ce class is deprecated in favor of + @ref Text::AbstractFont::createShaper() and the + @ref Text::AbstractShaper class, which allows for reusing allocated + resources, is independent on a glyph cache and has a batch-oriented + interface for retrieving shaped glyph data - @cpp Text::GlyphCacheFeature::ImageDownload @ce is deprecated in favor of @ref Text::GlyphCacheFeature::ProcessedImageDownload, as there's now a possibility to get the image directly for glyph caches that don't @@ -1631,6 +1637,13 @@ See also: more verbose and less clear - The @ref Text library API was reworked for more features, better efficiency and no dependencies on the STL: + - The @cpp Text::AbstractFont::doLayout() @ce and + @cpp Text::AbstractLayouter::doRenderGlyph() @ce virtual interfaces + no longer exist, plugins are expected to implement + @ref Text::AbstractFont::doCreateShaper() and a + @ref Text::AbstractShaper subclass instead. Only public APIs of the old + interface are preserved for backward compatibility with existing + application code. - The @cpp Text::AbstractGlyphCache::begin() @ce / @cpp end() @ce access via @ref std::unordered_map iterators is removed. Use @ref Text::AbstractGlyphCache::glyph() for accessing properties of a diff --git a/doc/snippets/MagnumText.cpp b/doc/snippets/MagnumText.cpp index f47a74cb3..2239e2c47 100644 --- a/doc/snippets/MagnumText.cpp +++ b/doc/snippets/MagnumText.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,9 @@ #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractFontConverter.h" #include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/AbstractShaper.h" +#include "Magnum/Text/Feature.h" +#include "Magnum/Text/Script.h" #include "Magnum/TextureTools/Atlas.h" #define DOXYGEN_ELLIPSIS(...) __VA_ARGS__ @@ -67,7 +71,7 @@ struct MyFont: Text::AbstractFont { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const Text::AbstractGlyphCache&, Float, Containers::StringView) override { return {}; } + Containers::Pointer doCreateShaper() override { return nullptr; } }; struct MyFontConverter: Text::AbstractFontConverter { explicit MyFontConverter(PluginManager::AbstractManager& manager, Containers::StringView plugin): Text::AbstractFontConverter{manager, plugin} {} @@ -255,4 +259,79 @@ for(std::size_t i = 0; i != fontGlyphIds.size(); ++i) { /* [AbstractGlyphCache-querying-batch] */ } +{ +/* [AbstractShaper-shape] */ +Containers::Pointer font = DOXYGEN_ELLIPSIS({}); +Containers::Pointer shaper = font->createShaper(); + +/* Set text properties and shape it */ +shaper->setScript(Text::Script::Latin); +shaper->setDirection(Text::Direction::LeftToRight); +shaper->setLanguage("en"); +shaper->shape("Hello, world!"); + +/* Get the glyph info back */ +struct GlyphInfo { + UnsignedInt id; + Vector2 offset; + Vector2 advance; +}; +Containers::Array glyphs{NoInit, shaper->glyphCount()}; +shaper->glyphsInto(stridedArrayView(glyphs).slice(&GlyphInfo::id), + stridedArrayView(glyphs).slice(&GlyphInfo::offset), + stridedArrayView(glyphs).slice(&GlyphInfo::advance)); +/* [AbstractShaper-shape] */ +} + +{ +Containers::Pointer font; +Containers::Pointer shaper = font->createShaper(); +/* [AbstractShaper-shape-features] */ +shaper->shape("Hello, world!", { + {Text::Feature::SmallCapitals, 7, 12} +}); +/* [AbstractShaper-shape-features] */ +} + +{ +struct GlyphInfo { + UnsignedInt id; + Vector2 offset; + Vector2 advance; +}; +/* [AbstractShaper-shape-multiple] */ +Containers::Pointer font = DOXYGEN_ELLIPSIS({}); +Containers::Pointer boldFont = DOXYGEN_ELLIPSIS({}); +Containers::Pointer shaper = font->createShaper(); +Containers::Pointer boldShaper = boldFont->createShaper(); +DOXYGEN_ELLIPSIS() + +Containers::Array glyphs; + +/* Shape "Hello, " with a regular font */ +shaper->shape("Hello, world!", 0, 7); +Containers::StridedArrayView1D glyphs1 = + arrayAppend(glyphs, NoInit, shaper->glyphCount()); +shaper->glyphsInto(glyphs1.slice(&GlyphInfo::id), + glyphs1.slice(&GlyphInfo::offset), + glyphs1.slice(&GlyphInfo::advance)); + +/* Append "world" shaped with a bold font */ +boldShaper->shape("Hello, world!", 7, 12); +Containers::StridedArrayView1D glyphs2 = + arrayAppend(glyphs, NoInit, boldShaper->glyphCount()); +boldShaper->glyphsInto(glyphs2.slice(&GlyphInfo::id), + glyphs2.slice(&GlyphInfo::offset), + glyphs2.slice(&GlyphInfo::advance)); + +/* Finally shape "!" shaped with a regular font again */ +shaper->shape("Hello, world!", 12, 13); +Containers::StridedArrayView1D glyphs3 = + arrayAppend(glyphs, NoInit, shaper->glyphCount()); +shaper->glyphsInto(glyphs3.slice(&GlyphInfo::id), + glyphs3.slice(&GlyphInfo::offset), + glyphs3.slice(&GlyphInfo::advance)); +/* [AbstractShaper-shape-multiple] */ +} + } diff --git a/src/Magnum/Text/AbstractFont.cpp b/src/Magnum/Text/AbstractFont.cpp index f8f66975e..39cf0b29e 100644 --- a/src/Magnum/Text/AbstractFont.cpp +++ b/src/Magnum/Text/AbstractFont.cpp @@ -29,18 +29,25 @@ #include #include #include -#include #include #include /** @todo remove once file callbacks are -free */ -#include #include #include #include #include "Magnum/FileCallback.h" +#include "Magnum/Math/Vector2.h" +#include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/AbstractShaper.h" + +#ifdef MAGNUM_BUILD_DEPRECATED +#include +#include +#include + #include "Magnum/Math/Functions.h" #include "Magnum/Math/Range.h" -#include "Magnum/Text/AbstractGlyphCache.h" +#endif #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT #include "Magnum/Text/configure.h" @@ -295,11 +302,85 @@ Containers::Pointer AbstractFont::doCreateGlyphCache() { CORRADE_ASSERT_UNREACHABLE("Text::AbstractFont::createGlyphCache(): feature advertised but not implemented", nullptr); } +Containers::Pointer AbstractFont::createShaper() { + CORRADE_ASSERT(isOpened(), + "Text::AbstractFont::createShaper(): no font opened", {}); + Containers::Pointer out = doCreateShaper(); + CORRADE_ASSERT(out, + "Text::AbstractFont::createShaper(): implementation returned nullptr", {}); + return out; +} + +#ifdef MAGNUM_BUILD_DEPRECATED +CORRADE_IGNORE_DEPRECATED_PUSH Containers::Pointer AbstractFont::layout(const AbstractGlyphCache& cache, const Float size, const Containers::StringView text) { - CORRADE_ASSERT(isOpened(), "Text::AbstractFont::layout(): no font opened", nullptr); + CORRADE_ASSERT(isOpened(), + "Text::AbstractFont::layout(): no font opened", {}); + /* This was originally added as a runtime error into plugin + implementations during the transition period for the new + AbstractGlyphCache API, now it's an assert. Shouldn't get triggered by + existing code in practice. */ + CORRADE_ASSERT(cache.size().z() == 1, + "Text::AbstractFont::layout(): array glyph caches are not supported", {}); + + /* Find this font in the cache. This is kept as a runtime error however. */ + Containers::Optional fontId = cache.findFont(this); + if(!fontId) { + Error{} << "Text::AbstractFont::layout(): font not found among" << cache.fontCount() << "fonts in passed glyph cache"; + return {}; + } - return doLayout(cache, size, text); + /* Ignoring the failures in this case, as the old API was never failing + also -- it'll simply return an empty AbstractLayouter */ + Containers::Pointer shaper = createShaper(); + shaper->shape(text); + + /* Scaling factor */ + const Float scale = size/this->size(); + + /* Get the glyph data. Yes, this is one extra temporary allocation which + could be aliased with the output array, but for the deprecated API implementation should be as unsurprising and unclever as possible. */ + struct Glyph { + UnsignedInt id; + Vector2 offset; + Vector2 advance; + }; + Containers::Array glyphs{NoInit, shaper->glyphCount()}; + shaper->glyphsInto(stridedArrayView(glyphs).slice(&Glyph::id), + stridedArrayView(glyphs).slice(&Glyph::offset), + stridedArrayView(glyphs).slice(&Glyph::advance)); + + /* Create the data to return from AbstractLayouter::renderGlyph(). Most of + this used to be copypasted in various *Layouter::doRenderGlyph() + implementations, ugh. */ + Containers::Array> out{NoInit, glyphs.size()}; + for(std::size_t i = 0; i != glyphs.size(); ++i) { + /* Offset of the glyph rectangle relative to the cursor, layer, texture + coordinates. We checked that the glyph cache is 2D above so the + layer can be ignored. */ + const Containers::Triple cacheGlyph = cache.glyph(*fontId, glyphs[i].id); + CORRADE_INTERNAL_ASSERT(cacheGlyph.second() == 0); + + out[i] = { + /* Quad rectangle, created from cache and shaper offset and the + texture rectangle, scaled to requested text size */ + Range2D::fromSize(Vector2{cacheGlyph.first()} + glyphs[i].offset, + Vector2{cacheGlyph.third().size()}) + .scaled(Vector2{scale}), + + /* Normalized texture coordinates */ + Range2D{cacheGlyph.third()} + .scaled(1.0f/Vector2{cache.size().xy()}), + + /* Advance from the font, again scaled */ + glyphs[i].advance*scale + }; + } + + return Containers::pointer(Utility::move(out)); } +CORRADE_IGNORE_DEPRECATED_POP +#endif Debug& operator<<(Debug& debug, const FontFeature value) { const bool packed = debug.immediateFlags() >= Debug::Flag::Packed; @@ -327,28 +408,28 @@ Debug& operator<<(Debug& debug, const FontFeatures value) { FontFeature::PreparedGlyphCache}); } -AbstractLayouter::AbstractLayouter(UnsignedInt glyphCount): _glyphCount(glyphCount) {} +#ifdef MAGNUM_BUILD_DEPRECATED +AbstractLayouter::AbstractLayouter(Containers::Array>&& glyphs): _glyphs{Utility::move(glyphs)} {} AbstractLayouter::~AbstractLayouter() = default; Containers::Pair AbstractLayouter::renderGlyph(const UnsignedInt i, Vector2& cursorPosition, Range2D& rectangle) { - CORRADE_ASSERT(i < glyphCount(), "Text::AbstractLayouter::renderGlyph(): index" << i << "out of range for" << glyphCount() << "glyphs", {}); - - /* Render the glyph */ - const Containers::Triple quadPositionTextureCoordinatesAdvance = doRenderGlyph(i); + CORRADE_ASSERT(i < _glyphs.size(), + "Text::AbstractLayouter::renderGlyph(): index" << i << "out of range for" << _glyphs.size() << "glyphs", {}); /* Move the quad to cursor */ - const Range2D quadPosition = quadPositionTextureCoordinatesAdvance.first().translated(cursorPosition); + const Range2D quadPosition = _glyphs[i].first().translated(cursorPosition); /* Extend the rectangle with current quad bounds. If the original is zero size, it gets replaced. */ rectangle = Math::join(rectangle, quadPosition); /* Advance cursor position to next character */ - cursorPosition += quadPositionTextureCoordinatesAdvance.third(); + cursorPosition += _glyphs[i].third(); /* Return moved quad and unchanged texture coordinates */ - return {quadPosition, quadPositionTextureCoordinatesAdvance.second()}; + return {quadPosition, _glyphs[i].second()}; } +#endif }} diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index b81838c97..847b31664 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -37,6 +37,8 @@ #include "Magnum/Text/visibility.h" #ifdef MAGNUM_BUILD_DEPRECATED +/* Used by deprecated AbstractLayouter */ +#include /* For APIs that used to take or return a std::string */ #include /* renderGlyph() used to return a std::pair */ @@ -182,7 +184,7 @@ instance is destroyed. @section Text-AbstractFont-subclassing Subclassing The plugin needs to implement the @ref doFeatures(), @ref doClose(), -@ref doLayout() functions, either @ref doCreateGlyphCache() or +@ref doCreateShaper() functions, either @ref doCreateGlyphCache() or @ref doFillGlyphCache() and one or more of `doOpen*()` functions. See also @ref AbstractLayouter for more information. @@ -512,18 +514,35 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { */ Containers::Pointer createGlyphCache(); + /** + * @brief Create an instance of this font shaper implementation + * @m_since_latest + * + * The returned class can be used to shape text using this font. See + * its documentation for more information. Note that the font has to + * stay in scope for as long as any @ref AbstractShaper instances + * originating from the font exist. Expects that a font is opened. The + * returned instance is never @cpp nullptr @ce. + */ + Containers::Pointer createShaper(); + + #ifdef MAGNUM_BUILD_DEPRECATED /** * @brief Layout the text using font's own layouter * @param cache Glyph cache * @param size Size to layout the text in, in pooints * @param text Text to layout * + * @m_deprecated_since_latest Use @ref createShaper() and the + * @ref AbstractShaper class instead. + * * Note that the layouters support rendering of single-line text only. * See @ref Renderer class for more advanced text layouting. Expects * that a font is opened. * @see @ref fillGlyphCache(), @ref createGlyphCache() */ - Containers::Pointer layout(const AbstractGlyphCache& cache, Float size, Containers::StringView text); + CORRADE_DEPRECATED("use createShaper() instead") Containers::Pointer layout(const AbstractGlyphCache& cache, Float size, Containers::StringView text); + #endif protected: /** @@ -647,8 +666,14 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** @brief Implementation for @ref createGlyphCache() */ virtual Containers::Pointer doCreateGlyphCache(); - /** @brief Implementation for @ref layout() */ - virtual Containers::Pointer doLayout(const AbstractGlyphCache& cache, Float size, Containers::StringView text) = 0; + /** + * @brief Implementation for @ref createShaper() + * @m_since_latest + * + * This function is only called if the font is opened. The + * implementation is not allowed to return @cpp nullptr @ce. + */ + virtual Containers::Pointer doCreateShaper() = 0; Containers::Optional>(*_fileCallback)(const std::string&, InputFileCallbackPolicy, void*){}; void* _fileCallbackUserData{}; @@ -664,20 +689,14 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { UnsignedInt _glyphCount{}; }; +#ifdef MAGNUM_BUILD_DEPRECATED /** @brief Base for text layouters -Returned by @ref AbstractFont::layout(). - -@section Text-AbstractLayouter-subclassing Subclassing - -The @ref AbstractFont plugin creates a local @ref AbstractLayouter subclass and -implements @ref doRenderGlyph(). You don't need to do most of the redundant -sanity checks, these things are checked by the implementation: - -- The @ref doRenderGlyph() is called only if `i` is from valid range +@m_deprecated_since_latest Use @ref AbstractShaper returned from + @ref AbstractFont::createShaper() instead. */ -class MAGNUM_TEXT_EXPORT AbstractLayouter { +class MAGNUM_TEXT_EXPORT CORRADE_DEPRECATED("use AbstractShaper instead") AbstractLayouter { public: /** @brief Copying is not allowed */ AbstractLayouter(const AbstractLayouter&) = delete; @@ -694,7 +713,7 @@ class MAGNUM_TEXT_EXPORT AbstractLayouter { AbstractLayouter& operator=(const AbstractLayouter&&) = delete; /** @brief Count of glyphs in the laid out text */ - UnsignedInt glyphCount() const { return _glyphCount; } + UnsignedInt glyphCount() const { return _glyphs.size(); } /** * @brief Render a glyph @@ -709,25 +728,19 @@ class MAGNUM_TEXT_EXPORT AbstractLayouter { */ Containers::Pair renderGlyph(UnsignedInt i, Vector2& cursorPosition, Range2D& rectangle); - protected: - /** - * @brief Constructor - * @param glyphCount Count of glyphs in laid out text - */ - explicit AbstractLayouter(UnsignedInt glyphCount); - + #ifdef DOXYGEN_GENERATING_OUTPUT private: - /** - * @brief Implementation for @ref renderGlyph() - * @param i Glyph index - * - * Returns quad position (relative to current cursor position), texture - * coordinates and advance to the next glyph. - */ - virtual Containers::Triple doRenderGlyph(UnsignedInt i) = 0; + #else + public: + #endif + /* Can't just friend AbstractFont as this is actually called from + Pointer internals */ + explicit AbstractLayouter(Containers::Array>&& glyphs); - UnsignedInt _glyphCount; + private: + Containers::Array> _glyphs; }; +#endif /** @brief Font plugin interface @@ -747,7 +760,7 @@ updated interface string. */ /* Silly indentation to make the string appear in pluginInterface() docs */ #define MAGNUM_TEXT_ABSTRACTFONT_PLUGIN_INTERFACE /* [interface] */ \ -"cz.mosra.magnum.Text.AbstractFont/0.3.3" +"cz.mosra.magnum.Text.AbstractFont/0.3.4" /* [interface] */ #ifndef DOXYGEN_GENERATING_OUTPUT diff --git a/src/Magnum/Text/AbstractShaper.cpp b/src/Magnum/Text/AbstractShaper.cpp new file mode 100644 index 000000000..0712787a6 --- /dev/null +++ b/src/Magnum/Text/AbstractShaper.cpp @@ -0,0 +1,145 @@ +/* + 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 "AbstractShaper.h" + +#include +#include + +#include "Magnum/Text/Script.h" + +namespace Magnum { namespace Text { + +Debug& operator<<(Debug& debug, const Direction value) { + debug << "Text::Direction" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case Direction::v: return debug << "::" #v; + _c(Unspecified) + _c(LeftToRight) + _c(RightToLeft) + _c(TopToBottom) + _c(BottomToTop) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedByte(value)) << Debug::nospace << ")"; +} + +AbstractShaper::AbstractShaper(AbstractFont& font): _font(font), _glyphCount{0} {} + +AbstractShaper::AbstractShaper(AbstractShaper&&) noexcept = default; + +AbstractShaper::~AbstractShaper() = default; + +AbstractShaper& AbstractShaper::operator=(AbstractShaper&&) noexcept = default; + +bool AbstractShaper::setScript(const Script script) { + return doSetScript(script); +} + +bool AbstractShaper::doSetScript(Script) { return false; } + +bool AbstractShaper::setLanguage(const Containers::StringView language) { + return doSetLanguage(language); +} + +bool AbstractShaper::doSetLanguage(Containers::StringView) { return false; } + +bool AbstractShaper::setDirection(const Direction direction) { + return doSetDirection(direction); +} + +bool AbstractShaper::doSetDirection(Direction) { return false; } + +UnsignedInt AbstractShaper::shape(const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const Containers::ArrayView features) { + CORRADE_ASSERT((end == ~UnsignedInt{} && begin <= text.size()) || + (begin <= end && end <= text.size()), + "Text::AbstractShaper::shape(): begin" << begin << "and end" << end << "out of range for a text of" << text.size() << "bytes", {}); + CORRADE_ASSERT(begin < text.size() && begin != end, + "Text::AbstractShaper::shape(): shaped text at begin" << begin << "is empty", {}); + #ifndef CORRADE_NO_ASSERT + for(std::size_t i = 0; i != features.size(); ++i) { + const FeatureRange& feature = features[i]; + CORRADE_ASSERT( + (feature._end == ~UnsignedInt{} && feature._begin <= text.size()) || + (feature._begin <= feature._end && feature._end <= text.size()), + "Text::AbstractShaper::shape(): feature" << i << "begin" << feature._begin << "and end" << feature._end << "out of range for a text of" << text.size() << "bytes", {}); + /** @todo catch empty feature ranges too? or not important */ + } + #endif + return _glyphCount = doShape(text, begin, end, features); +} + +UnsignedInt AbstractShaper::shape(const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const std::initializer_list features) { + return shape(text, begin, end, Containers::arrayView(features)); +} + +UnsignedInt AbstractShaper::shape(const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end) { + return shape(text, begin, end, nullptr); +} + +UnsignedInt AbstractShaper::shape(const Containers::StringView text, const Containers::ArrayView features) { + return shape(text, 0, ~UnsignedInt{}, features); +} + +UnsignedInt AbstractShaper::shape(const Containers::StringView text, const std::initializer_list features) { + return shape(text, Containers::arrayView(features)); +} + +UnsignedInt AbstractShaper::shape(const Containers::StringView text) { + return shape(text, nullptr); +} + +Script AbstractShaper::script() const { + return _glyphCount ? doScript() : Script::Unspecified; +} + +Script AbstractShaper::doScript() const { return Script::Unspecified; } + +Containers::StringView AbstractShaper::language() const { + return _glyphCount ? doLanguage() : Containers::StringView{}; +} + +Containers::StringView AbstractShaper::doLanguage() const { return {}; } + +Direction AbstractShaper::direction() const { + return _glyphCount ? doDirection() : Direction::Unspecified; +} + +Direction AbstractShaper::doDirection() const { return Direction::Unspecified; } + +void AbstractShaper::glyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const { + CORRADE_ASSERT(ids.size() == _glyphCount && offsets.size() == _glyphCount && advances.size() == _glyphCount, + "Text::AbstractShaper::glyphsInto(): expected the ids, offsets and advanced views to have a size of" << _glyphCount << "but got" << ids.size() << Debug::nospace << "," << offsets.size() << "and" << advances.size(), ); + /* Call into the implementation only if there's actually anything shaped, + otherwise it might not yet have everything properly set up */ + if(_glyphCount) + doGlyphsInto(ids, offsets, advances); +} + +}} diff --git a/src/Magnum/Text/AbstractShaper.h b/src/Magnum/Text/AbstractShaper.h new file mode 100644 index 000000000..779158956 --- /dev/null +++ b/src/Magnum/Text/AbstractShaper.h @@ -0,0 +1,543 @@ +#ifndef Magnum_Text_AbstractShaper_h +#define Magnum_Text_AbstractShaper_h +/* + 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. +*/ + +/** @file + * @brief Class @ref Magnum::Text::AbstractShaper, @ref Magnum::Text::FeatureRange, enum @ref Magnum::Text::Direction + * @m_since_latest + */ + +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/Text/Text.h" +#include "Magnum/Text/visibility.h" + +namespace Magnum { namespace Text { + +/** +@brief Direction a text is shaped in +@m_since_latest + +@see @ref AbstractShaper::setDirection(), @ref AbstractShaper::direction() +*/ +enum class Direction: UnsignedByte { + /** + * Unspecified. When set in @ref AbstractShaper::setDirection(), makes the + * shaping rely on direction autodetection implemented in a particular + * @ref AbstractFont plugin (if any). When returned from + * @ref AbstractShaper::direction() after a successful + * @ref AbstractShaper::shape() call, it means a particular + * @ref AbstractFont plugin doesn't implement any script-specific behavior. + */ + Unspecified = 0, + + /** + * Left to right. When returned from @ref AbstractShaper::direction(), + * the @p advances filled by @ref AbstractShaper::glyphsInto() are + * guaranteed to have their Y components @cpp 0.0f @ce. + */ + LeftToRight = 1, + + /** + * Right to left. When returned from @ref AbstractShaper::direction(), + * the @p advances filled by @ref AbstractShaper::glyphsInto() are + * guaranteed to have their Y components @cpp 0.0f @ce. + */ + RightToLeft, + + /** + * Top to bottom. When returned from @ref AbstractShaper::direction(), + * the @p advances filled by @ref AbstractShaper::glyphsInto() are + * guaranteed to have their X components @cpp 0.0f @ce. + */ + TopToBottom, + + /** + * Bottom to top. When returned from @ref AbstractShaper::direction(), + * the @p advances filled by @ref AbstractShaper::glyphsInto() are + * guaranteed to have their X components @cpp 0.0f @ce. + */ + BottomToTop +}; + +/** @debugoperatorenum{Direction} */ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& debug, Direction value); + +/** +@brief OpenType feature for a text range +@m_since_latest + +@see @ref AbstractShaper::shape() +*/ +class FeatureRange { + public: + /** + * @brief Constructor + * @param feature Feature to control + * @param begin Beginning byte in the input text + * @param end (One byte after) the end byte in the input text + * @param value Feature value to set + */ + constexpr /*implicit*/ FeatureRange(Feature feature, UnsignedInt begin, UnsignedInt end, UnsignedInt value = true): _feature{feature}, _value{value}, _begin{begin}, _end{end} {} + + /** + * @brief Construct for the whole text + * + * Equivalent to calling @ref FeatureRange(Feature, UnsignedInt, UnsignedInt, UnsignedInt) + * with @p begin set to @cpp 0 @ce and @p end to @cpp 0xffffffffu @ce. + */ + constexpr /*implicit*/ FeatureRange(Feature feature, UnsignedInt value = true): _feature{feature}, _value{value}, _begin{0}, _end{~UnsignedInt{}} {} + + /** @brief Feature to control */ + constexpr Feature feature() const { return _feature; } + + /** + * @brief Whether to enable the feature + * + * Returns @cpp false @ce if @ref value() is @cpp 0 @ce, @cpp true @ce + * otherwise. + */ + constexpr bool isEnabled() const { return _value; } + + /** @brief Feature value to set */ + constexpr UnsignedInt value() const { return _value; } + + /** + * @brief Beginning byte in the input text + * + * If the feature is set for the whole text, this is @cpp 0 @ce. + */ + constexpr UnsignedInt begin() const { return _begin; } + + /** + * @brief (One byte after) the end byte in the input text + * + * If the feature is set for the whole text, this is + * @cpp 0xffffffffu @ce. + */ + constexpr UnsignedInt end() const { return _end; } + + private: + friend AbstractShaper; + + Feature _feature; + UnsignedInt _value; + UnsignedInt _begin; + UnsignedInt _end; +}; + +/** +@brief Base for text shapers +@m_since_latest + +Returned from @ref AbstractFont::createShaper(), provides an interface for +* *shaping* text with the @ref AbstractFont it originated from. Meant to be +(privately) subclassed by @ref AbstractFont plugin implementations. + +* *Shaping* is a process of converting a sequence of Unicode codepoints to a +visual form, i.e. a list of glyphs of a particular font, their offsets and horizontal or vertical advances. Shaping is often not a 1:1 mapping from +codepoints to glyphs, but involves merging, subdividing and reordering as well. + +@section Text-AbstractShaper-usage Usage + +Call @ref AbstractFont::createShaper() to get a shaper instance. The plugin +will always return a valid instance so it's not needed to check for the pointer +beging @cpp nullptr @ce, however note that the originating @ref AbstractFont +instance has to stay in scope for at least as long as the @ref AbstractShaper +is alive. + +A text is shaped by calling @ref shape(), retrieving the shaped glyph count +with @ref glyphCount() and then getting the glyph data with @ref glyphsInto(). +Glyph IDs can be then queried in (or inserted into) an @ref AbstractGlyphCache, +and the rendered glyphs positioned at @p offsets with the cursor moving by +@p advances is what makes up the final shaped text. + +@snippet MagnumText.cpp AbstractShaper-shape + +For best results, it's recommended to call (a subset of) @ref setScript(), +@ref setLanguage() and @ref setDirection() if at least some properties of the +input text are known, as shown above. Without these, the font plugin may +attempt to autodetect the properties, which might not always give a correct +result. If a particular font plugin doesn't implement given script, language or +direction or if it doesn't have any special handling for it, given function +will return @cpp false @ce. The @ref script() const, @ref language() const and +@ref direction() const can be used to inspect results of autodetection after +@ref shape() has been called. The set of supported scripts, languages and +directions and exact behavior for unsupported values is plugin-specific --- it +may for example choose a fallback instead, or it may ignore the setting +altogeter. See documentation of particular @ref AbstractFont subclasses for +more information. + +@subsection Text-AbstractShaper-usage-features Enabling and disabling typographic features + +In the above snippet, the whole text is shaped using typographic features that +are default in the font. For example, assuming the font would support small +capitals (and the particular @ref AbstractFont plugin would recognize and use +the feature), we could render the "world" part with small caps, resulting in +"Hello, ᴡᴏʀʟᴅ!". + +@snippet MagnumText.cpp AbstractShaper-shape-features + +Similarly, features can be enabled for the whole text by omitting the begin and +end parameters, or for example a feature that a particular @ref AbstractFont +plugin uses by default can be disabled by passing an explicit @cpp false @ce +argument. The range, if present, is always given in *bytes* of the UTF-8 input. +Capabilities of typographic features are rather broad, see the @ref Feature +enum and documentation linked from it for exhaustive information. + +@subsection Text-AbstractShaper-usage-multiple Combining different shapers + +Sometimes it's desirable to render different parts of the text with different +fonts, not just different features of the same font. A variation of the above +example could be rendering the "world" part with a bold font: + +@snippet MagnumText.cpp AbstractShaper-shape-multiple + +The resulting `glyphs` array is usable the same way as in the above case, with +a difference that the glyph IDs have to be looked up in an +@ref AbstractGlyphCache with a font ID corresponding to the range they're in. +Also note that the whole text is passed every time and a begin & end is +specified for it instead of passing just the slice alone. While possibly not +having any visible effect in this particular case, in general it allows the +shaper to make additional decisions based on surrounding context, for example +picking glyphs that are better connected to their neighbors in handwriting +fonts. + +@subsection Text-AbstractShaper-usage-instances Managing multiple instances + +As shown above, a particular @ref AbstractShaper instance is reusable, i.e. +it's possible to call @ref shape() (and potentially also @ref setScript(), +@ref setLanguage() and @ref setDirection()) several times to shape multiple +pieces of text with it. Doing so allows the @ref AbstractFont plugin +implementation to reuse allocated buffers and other state compared to a fresh +instance from @ref AbstractFont::createShaper() having to be initialized every +time. + +The application may choose several strategies, for example have a single +@ref AbstractShaper instance and shape all texts with it, resetting its state +every time. Or for example have a few persistent @ref AbstractShaper instances +for dynamic text that changes every frame, or have dedicated preconfigured +per-font, per-script or per-language instances. + +@section Text-AbstractShaper-subclassing Subclassing + +The @ref AbstractFont plugin is meant to create a local @ref AbstractShaper +subclass. It implements at least @ref doShape() and @ref doGlyphsInto(), and +potentially also (a subset of) @ref doSetScript(), @ref doScript(), +@ref doSetLanguage(),@ref doLanguage(), @ref doSetDirection() and +@ref doDirection(). The public API does most sanity checks on its own, see +documentation of particular `do*()` functions for more information about the +guarantees. +*/ +class MAGNUM_TEXT_EXPORT AbstractShaper { + public: + /** + * @brief Constructor + * @param font Font the shaper is originating from + */ + explicit AbstractShaper(AbstractFont& font); + + /** @brief Copying is not allowed */ + AbstractShaper(AbstractShaper&) = delete; + + /** @brief Move constructor */ + AbstractShaper(AbstractShaper&&) noexcept; + + /** @brief Copying is not allowed */ + AbstractShaper& operator=(AbstractShaper&) = delete; + + /** @brief Move assignment */ + AbstractShaper& operator=(AbstractShaper&&) noexcept; + + virtual ~AbstractShaper(); + + /** @brief Font the shaper is originating from */ + AbstractFont& font() { return _font; } + const AbstractFont& font() const { return _font; } /**< @overload */ + + /** + * @brief Set text script + * + * The script is used for all following @ref shape() calls. If not + * called at all or if explicitly set to @ref Script::Unspecified, the + * @ref AbstractFont plugin may attempt to guess the script from the + * input text. The actual script used for shaping (if any) is queryable + * with @ref script() const after @ref shape() has been called. + * + * Returns @cpp true @ce if the plugin supports setting a + * script and the script is supported, @cpp false @ce otherwise, in + * which case the shaping falls back to a generic behavior. See + * documentation of a particular plugin for more information. + * @see @ref setLanguage(), @ref setDirection() + */ + bool setScript(Script script); + + /** + * @brief Set text language + * + * The language is expected to be a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag), + * either just the base tag such as @cpp "en" @ce or @cpp "cs" @ce + * alone, or further differentiating with a region subtag like for + * example @cpp "en-US" @ce vs @cpp "en-GB" @ce. + * + * The language is used for all following @ref shape() calls. If not + * called at all or if explicitly set to an empty string, the + * @ref AbstractFont plugin may attempt to guess the language from the + * input text or the execution environment, such as current locale. The + * actual language used for shaping (if any) is queryable with + * @ref language() const after @ref shape() has been called. + * + * Returns @cpp true @ce if the plugin supports setting a language and + * the language is supported, @cpp false @ce otherwise, in which case + * the shaping falls back to a generic behavior. See documentation of a + * particular plugin for more information. + * @see @ref setScript(), @ref setDirection() + */ + bool setLanguage(Containers::StringView language); + + /** + * @brief Set text direction + * + * The direction is used for all following @ref shape() calls. If not + * called at all or if explicitly set to @ref Direction::Unspecified, + * the @ref AbstractFont plugin may attempt to guess the direction from + * the input text. The actual direction used for shaping (if any) is + * queryable with @ref direction() const after @ref shape() has been + * called. + * + * Returns @cpp true @ce if the plugin supports setting a language and + * the language is supported, @cpp false @ce otherwise, in which case + * the shaping falls back to a generic behavior. See documentation of a + * particular font plugin for more information. + * @see @ref setScript(), @ref setLanguage() + */ + bool setDirection(Direction direction); + + /** + * @brief Shape a text + * @param text Text in UTF-8 + * @param features OpenType features to apply for the whole text or + * its subranges + * + * Expects that @p text is non-empty, that both @p begin and all + * @ref FeatureRange::begin() are contained within @p text, and that + * @p end and all @ref FeatureRange::end() are either contained within + * @p text or have a value of @cpp 0xffffffffu @ce. On success returns + * the number of shaped glyphs (which is also subsequently available + * through @ref glyphCount() const) and updates the @ref script() const, + * @ref language() const and @ref direction() const values. + * + * On failure, such as when the input is not valid UTF-8 or when + * specified combination or script, language and direction is + * unsupported, prints a message to @relativeref{Magnum,Error} and + * returns @cpp 0 @ce. + * + * Whether @p features are used depends on a particular + * @ref AbstractFont plugin implementation and the font file itself as + * well --- for example, a plugin may enable @ref Feature::Kerning + * by default but the font may not even have appropriate tables for it + * included, in which case no kerning is performed. See documentation + * of a particular font plugin for more information. + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + UnsignedInt shape(Containers::StringView text, Containers::ArrayView features = {}); + #else + /* To not have to include ArrayView */ + UnsignedInt shape(Containers::StringView text, Containers::ArrayView features); + UnsignedInt shape(Containers::StringView text); + #endif + + /** @overload */ + UnsignedInt shape(Containers::StringView text, std::initializer_list features); + + /** + * @brief Shape a slice of text + * @param text Text in UTF-8 + * @param begin Beginning byte in the input text + * @param end (One byte after) the end byte in the input text + * @param features OpenType features to apply for the whole text or + * its subranges + * + * A variant of @ref shape(Containers::StringView, Containers::ArrayView) + * to be used when passing pieces of larger text with different + * shapers, for example when the script, language or direction changes + * in each piece or when the pieces are using a different font + * entirely. Compared to passing just the actually shaped slice of + * @p text this allows the implementation to perform shaping aware of + * surrounding context, such as picking correct glyphs for beginning, + * middle or end of a word or a paragraph. + * @see @ref Text-AbstractShaper-usage-multiple + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + UnsignedInt shape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features = {}); + #else + /* To not have to include ArrayView */ + UnsignedInt shape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features); + UnsignedInt shape(Containers::StringView text, UnsignedInt begin, UnsignedInt end); + #endif + + /** @overload */ + UnsignedInt shape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, std::initializer_list features); + + /** + * @brief Count of glyphs produced by the last @ref shape() call + * + * If the last @ref shape() call failed or it hasn't been called yet, + * returns @cpp 0 @ce. + */ + UnsignedInt glyphCount() const { return _glyphCount; } + + /** + * @brief Script used for the last @ref shape() call + * + * If the last @ref shape() call failed, it hasn't been called yet or + * if the @ref AbstractFont doesn't implement any script-specific + * behavior, returns @ref Script::Unspecified. + * @see @ref setScript(), @ref language(), @ref direction() + */ + Script script() const; + + /** + * @brief Language used for the last @ref shape() call + * + * If the last @ref shape() call failed, it hasn't been called yet or + * if the @ref AbstractFont doesn't implement any language-specific + * behavior, returns an empty string. + * + * The returned view is generally neither + * @relativeref{Corrade,Containers::StringViewFlag::Global} nor + * @relativeref{Corrade,Containers::StringViewFlag::NullTerminated} and + * is only guaranteed to stay valid until the next @ref setLanguage() + * or @ref shape() call. Particular @ref AbstractFont implementations + * may give better guarantees, see their documentation for more + * information. + * @see @ref setLanguage(), @ref script(), @ref direction() + */ + Containers::StringView language() const; + + /** + * @brief Language used for the last @ref shape() call + * + * If the last @ref shape() call failed, it hasn't been called yet or + * if the @ref AbstractFont doesn't implement any direction-specific + * behavior, returns @ref Direction::Unspecified. + * @see @ref setDirection(), @ref script(), @ref language() + */ + Direction direction() const; + + /** + * @brief Retrieve glyph information + * @param[out] ids Where to put glyph IDs + * @param[out] offsets Where to put glyph offsets + * @param[out] advances Where to put glyph advances + * + * The @p ids, @p offsets and @p advances views are all expected to + * have a size of @ref glyphCount(). After calling this function, the + * @p ids are commonly looked up in or inserted into an + * @ref AbstractGlyphCache, @p offsets specify where to put the glyph + * relative to current cursor (which is then further offset for the + * particular glyph rectangle returned from the glyph cache) and + * @p advances specify in which direction to move the cursor for the + * next glyph. For @ref direction() being @ref Direction::LeftToRight + * or @relativeref{Direction,RightToLeft} Y components of @p advances + * are @cpp 0.0f @ce, for @relativeref{Direction,TopToBottom} or + * @relativeref{Direction,BottomToTop} X components of @p advances are + * @cpp 0.0f @ce. + * @see @ref direction() + */ + void glyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const; + + private: + /** + * @brief Implemenation for @ref setScript() + * + * Default implementation does nothing and returns @cpp false @ce. + */ + virtual bool doSetScript(Script script); + + /** + * @brief Implemenation for @ref setLanguage() + * + * Default implementation does nothing and returns @cpp false @ce. + */ + virtual bool doSetLanguage(Containers::StringView language); + + /** + * @brief Implemenation for @ref setDirection() + * + * Default implementation does nothing and returns @cpp false @ce. + */ + virtual bool doSetDirection(Direction direction); + + /** + * @brief Implemenation for @ref shape() + * + * The @p begin as well as all @ref FeatureRange::begin() values + * are guaranteed to be within @p text, @p end as well as all + * @ref FeatureRange::end() values are guaranteed to be either within + * @p text or have a value of @cpp 0xffffffffu @ce. + */ + virtual UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features) = 0; + + /** + * @brief Implemenation for @ref script() + * + * Default implementation returns @ref Script::Unspecified. + */ + virtual Script doScript() const; + + /** + * @brief Implemenation for @ref language() + * + * Default implementation returns an empty string. + */ + virtual Containers::StringView doLanguage() const; + + /** + * @brief Implemenation for @ref direction() + * + * Default implementation returns @ref Direction::Unspecified. + */ + virtual Direction doDirection() const; + + /** + * @brief Implemenation for @ref glyphsInto() + * + * The @p ids, @p offsets and @p advances are guaranteed to have a size + * of @ref glyphCount(). Called only if @ref glyphCount() is not + * @cpp 0 @ce. + */ + virtual void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const = 0; + + Containers::Reference _font; + UnsignedInt _glyphCount; +}; + +}} + +#endif diff --git a/src/Magnum/Text/CMakeLists.txt b/src/Magnum/Text/CMakeLists.txt index c8ab7d919..154d17d55 100644 --- a/src/Magnum/Text/CMakeLists.txt +++ b/src/Magnum/Text/CMakeLists.txt @@ -37,6 +37,7 @@ set(MagnumText_GracefulAssert_SRCS AbstractFont.cpp AbstractFontConverter.cpp AbstractGlyphCache.cpp + AbstractShaper.cpp Feature.cpp Script.cpp) @@ -44,6 +45,7 @@ set(MagnumText_HEADERS AbstractFont.h AbstractFontConverter.h AbstractGlyphCache.h + AbstractShaper.h Alignment.h Feature.h Script.h diff --git a/src/Magnum/Text/Feature.h b/src/Magnum/Text/Feature.h index 89e826699..26493d851 100644 --- a/src/Magnum/Text/Feature.h +++ b/src/Magnum/Text/Feature.h @@ -51,6 +51,7 @@ for creating values not listed in the enum. Currently, there's no corresponding feature list for [Apple Advanced Typography](https://en.wikipedia.org/wiki/Apple_Advanced_Typography). Mapping from OpenType features to AAT features is possible but nontrivial, and is the responsibility of a particular font plugin. +@see @ref FeatureRange, @ref AbstractShaper::shape() */ enum class Feature: UnsignedInt { /** diff --git a/src/Magnum/Text/Script.h b/src/Magnum/Text/Script.h index 1201166ec..c75b9de64 100644 --- a/src/Magnum/Text/Script.h +++ b/src/Magnum/Text/Script.h @@ -45,6 +45,7 @@ The values are [FourCC](https://en.wikipedia.org/wiki/FourCC) codes according to [ISO 15924](https://en.wikipedia.org/wiki/ISO_15924). Use @ref script(char, char, char, char) or @ref script(Containers::StringView) for creating values not listed in the enum. +@see @ref AbstractShaper::setScript(), @ref AbstractShaper::script() */ enum class Script: UnsignedInt { /* List taken from https://en.wikipedia.org/wiki/ISO_15924, ordered by diff --git a/src/Magnum/Text/Test/AbstractFontConverterTest.cpp b/src/Magnum/Text/Test/AbstractFontConverterTest.cpp index a9b54ea63..19713d817 100644 --- a/src/Magnum/Text/Test/AbstractFontConverterTest.cpp +++ b/src/Magnum/Text/Test/AbstractFontConverterTest.cpp @@ -39,6 +39,7 @@ #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractFontConverter.h" #include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/AbstractShaper.h" #include "configure.h" @@ -163,9 +164,7 @@ struct DummyFont: AbstractFont { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return nullptr; } } dummyFont; struct DummyGlyphCache: AbstractGlyphCache { diff --git a/src/Magnum/Text/Test/AbstractFontTest.cpp b/src/Magnum/Text/Test/AbstractFontTest.cpp index 9ebee5509..6cf7f219c 100644 --- a/src/Magnum/Text/Test/AbstractFontTest.cpp +++ b/src/Magnum/Text/Test/AbstractFontTest.cpp @@ -41,9 +41,14 @@ #include "Magnum/Math/Vector2.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/AbstractShaper.h" #include "configure.h" +#ifdef MAGNUM_BUILD_DEPRECATED +#include +#endif + namespace Magnum { namespace Text { namespace Test { namespace { struct AbstractFontTest: TestSuite::Tester { @@ -82,9 +87,6 @@ struct AbstractFontTest: TestSuite::Tester { void glyphSizeAdvanceNoFont(); void glyphSizeAdvanceOutOfRange(); - void layout(); - void layoutNoFont(); - void fillGlyphCache(); void fillGlyphCacheNotSupported(); void fillGlyphCacheNotImplemented(); @@ -96,6 +98,18 @@ struct AbstractFontTest: TestSuite::Tester { void createGlyphCacheNotImplemented(); void createGlyphCacheNoFont(); + void createShaper(); + void createShaperNoFont(); + void createShaperNullptr(); + + #ifdef MAGNUM_BUILD_DEPRECATED + void layout(); + void layoutArrayGlyphCache(); + void layoutGlyphCacheFontNotFound(); + void layoutGlyphOutOfRange(); + void layoutNoFont(); + #endif + void debugFeature(); void debugFeaturePacked(); void debugFeatures(); @@ -136,9 +150,6 @@ AbstractFontTest::AbstractFontTest() { &AbstractFontTest::glyphSizeAdvanceNoFont, &AbstractFontTest::glyphSizeAdvanceOutOfRange, - &AbstractFontTest::layout, - &AbstractFontTest::layoutNoFont, - &AbstractFontTest::fillGlyphCache, &AbstractFontTest::fillGlyphCacheNotSupported, &AbstractFontTest::fillGlyphCacheNotImplemented, @@ -150,6 +161,18 @@ AbstractFontTest::AbstractFontTest() { &AbstractFontTest::createGlyphCacheNotImplemented, &AbstractFontTest::createGlyphCacheNoFont, + &AbstractFontTest::createShaper, + &AbstractFontTest::createShaperNoFont, + &AbstractFontTest::createShaperNullptr, + + #ifdef MAGNUM_BUILD_DEPRECATED + &AbstractFontTest::layout, + &AbstractFontTest::layoutArrayGlyphCache, + &AbstractFontTest::layoutGlyphCacheFontNotFound, + &AbstractFontTest::layoutGlyphOutOfRange, + &AbstractFontTest::layoutNoFont, + #endif + &AbstractFontTest::debugFeature, &AbstractFontTest::debugFeaturePacked, &AbstractFontTest::debugFeatures, @@ -167,9 +190,7 @@ void AbstractFontTest::construct() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; CORRADE_COMPARE(font.features(), FontFeatures{}); @@ -193,9 +214,7 @@ void AbstractFontTest::openData() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; } font; @@ -225,9 +244,7 @@ void AbstractFontTest::openFileAsData() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; } font; @@ -252,9 +269,7 @@ void AbstractFontTest::openFileAsDataNotFound() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -278,9 +293,7 @@ void AbstractFontTest::openFileNotImplemented() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -301,9 +314,7 @@ void AbstractFontTest::openDataNotSupported() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -323,9 +334,7 @@ void AbstractFontTest::openDataNotImplemented() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -346,9 +355,7 @@ void AbstractFontTest::setFileCallback() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; int a = 0; @@ -373,9 +380,7 @@ void AbstractFontTest::setFileCallbackTemplate() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool called = false; } font; @@ -406,9 +411,7 @@ void AbstractFontTest::setFileCallbackTemplateNull() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool called = false; } font; @@ -432,9 +435,7 @@ void AbstractFontTest::setFileCallbackTemplateConst() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool called = false; } font; @@ -460,9 +461,7 @@ void AbstractFontTest::setFileCallbackFileOpened() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -483,9 +482,7 @@ void AbstractFontTest::setFileCallbackNotImplemented() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; int a; @@ -509,9 +506,7 @@ void AbstractFontTest::setFileCallbackNotSupported() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -545,9 +540,7 @@ void AbstractFontTest::setFileCallbackOpenFileDirectly() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; bool openDataCalledNotSureWhy = false; @@ -588,9 +581,7 @@ void AbstractFontTest::setFileCallbackOpenFileThroughBaseImplementation() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; bool openFileCalled = false; @@ -643,9 +634,7 @@ void AbstractFontTest::setFileCallbackOpenFileThroughBaseImplementationFailed() UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool openFileCalled = false; } font; @@ -681,9 +670,7 @@ void AbstractFontTest::setFileCallbackOpenFileAsData() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; bool openFileCalled = false; @@ -737,9 +724,7 @@ void AbstractFontTest::setFileCallbackOpenFileAsDataFailed() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool openFileCalled = false; } font; @@ -770,9 +755,7 @@ void AbstractFontTest::properties() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; } font; @@ -796,9 +779,7 @@ void AbstractFontTest::propertiesNoFont() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -825,9 +806,7 @@ void AbstractFontTest::glyphId() { UnsignedInt doGlyphId(char32_t a) override { return a*10; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; CORRADE_COMPARE(font.glyphId(U'a'), 970); @@ -844,9 +823,7 @@ void AbstractFontTest::glyphIdNoFont() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -868,9 +845,7 @@ void AbstractFontTest::glyphSizeAdvance() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt a) override { return {a*2.0f, a/3.0f}; } Vector2 doGlyphAdvance(UnsignedInt a) override { return {a*10.0f, -Float(a)/10.0f}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; } font; @@ -892,9 +867,7 @@ void AbstractFontTest::glyphSizeAdvanceNoFont() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -921,9 +894,7 @@ void AbstractFontTest::glyphSizeAdvanceOutOfRange() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return {}; } bool _opened = false; } font; @@ -947,51 +918,6 @@ struct DummyGlyphCache: AbstractGlyphCache { void doSetImage(const Vector2i&, const ImageView2D&) override {} }; -void AbstractFontTest::layout() { - struct Layouter: AbstractLayouter { - explicit Layouter(UnsignedInt count): AbstractLayouter{count} {} - Containers::Triple doRenderGlyph(UnsignedInt) override { return {}; } - }; - - struct MyFont: AbstractFont { - FontFeatures doFeatures() const override { return {}; } - bool doIsOpened() const override { return true; } - void doClose() override {} - - UnsignedInt doGlyphId(char32_t) override { return {}; } - Vector2 doGlyphSize(UnsignedInt) override { return {}; } - Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache& cache, Float size, Containers::StringView str) override { - return Containers::pointer(UnsignedInt(cache.size().x()*str.size()*size)); - } - } font; - - DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 200}}; - Containers::Pointer layouter = font.layout(cache, 0.25f, "hello"); - CORRADE_COMPARE(layouter->glyphCount(), 100*5/4); -} - -void AbstractFontTest::layoutNoFont() { - CORRADE_SKIP_IF_NO_ASSERT(); - - struct MyFont: AbstractFont { - FontFeatures doFeatures() const override { return {}; } - bool doIsOpened() const override { return false; } - void doClose() override {} - - UnsignedInt doGlyphId(char32_t) override { return {}; } - Vector2 doGlyphSize(UnsignedInt) override { return {}; } - Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } - } font; - - std::ostringstream out; - Error redirectError{&out}; - DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 200}}; - font.layout(cache, 0.25f, "hello"); - CORRADE_COMPARE(out.str(), "Text::AbstractFont::layout(): no font opened\n"); -} - void AbstractFontTest::fillGlyphCache() { struct MyFont: AbstractFont { FontFeatures doFeatures() const override { return {}; } @@ -1001,7 +927,7 @@ void AbstractFontTest::fillGlyphCache() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } void doFillGlyphCache(AbstractGlyphCache& cache, Containers::ArrayView characters) override { CORRADE_COMPARE(cache.size(), (Vector3i{100, 100, 1})); @@ -1034,7 +960,7 @@ void AbstractFontTest::fillGlyphCacheNotSupported() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -1055,7 +981,7 @@ void AbstractFontTest::fillGlyphCacheNotImplemented() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -1076,7 +1002,7 @@ void AbstractFontTest::fillGlyphCacheNoFont() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -1097,7 +1023,7 @@ void AbstractFontTest::fillGlyphCacheInvalidUtf8() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -1116,7 +1042,7 @@ void AbstractFontTest::createGlyphCache() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } Containers::Pointer doCreateGlyphCache() override { return Containers::pointer(PixelFormat::R8Unorm, Vector2i{123, 345}); @@ -1140,7 +1066,7 @@ void AbstractFontTest::createGlyphCacheNotSupported() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -1160,7 +1086,7 @@ void AbstractFontTest::createGlyphCacheNotImplemented() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -1180,7 +1106,7 @@ void AbstractFontTest::createGlyphCacheNoFont() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return {}; } } font; std::ostringstream out; @@ -1189,6 +1115,300 @@ void AbstractFontTest::createGlyphCacheNoFont() { CORRADE_COMPARE(out.str(), "Text::AbstractFont::createGlyphCache(): no font opened\n"); } +void AbstractFontTest::createShaper() { + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 37; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + }; + + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return Containers::pointer(*this); } + } font; + + Containers::Pointer shaper = font.createShaper(); + CORRADE_COMPARE(shaper->shape("eh"), 37); +} + +void AbstractFontTest::createShaperNoFont() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return false; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + + std::ostringstream out; + Error redirectError{&out}; + font.createShaper(); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::createShaper(): no font opened\n"); +} + +void AbstractFontTest::createShaperNullptr() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return nullptr; } + } font; + + std::ostringstream out; + Error redirectError{&out}; + font.createShaper(); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::createShaper(): implementation returned nullptr\n"); +} + +#ifdef MAGNUM_BUILD_DEPRECATED +void AbstractFontTest::layout() { + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 3; + } + + void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + Utility::copy({3, 7, 3}, ids); + Utility::copy({{0.5f, 1.0f}, + {1.0f, 0.5f}, + {2.0f, 2.0f}}, offsets); + Utility::copy({{50.0f, 0.0f}, + {10.0f, 0.0f}, + {20.0f, 0.0f}}, advances); + } + }; + + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override {} + + Properties doOpenFile(Containers::StringView, Float) override { + _opened = true; + return {0.5f, 0.0f, 0.0f, 0.0f, 666}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { + return Containers::pointer(*this); + } + + bool _opened = false; + } font; + + /* Have to open the font to fill the font size */ + font.openFile({}, {}); + CORRADE_COMPARE(font.size(), 0.5f); + + DummyGlyphCache cache{PixelFormat::R8Unorm, {10, 20}}; + + UnsignedInt fontId = cache.addFont(15, &font); + + cache.addGlyph(fontId, 3, {1, 2}, {{3, 4}, {6, 5}}); + cache.addGlyph(fontId, 7, {3, 4}, {{5, 6}, {9, 8}}); + + CORRADE_IGNORE_DEPRECATED_PUSH + Containers::Pointer layouter = font.layout(cache, 0.25f, "hello"); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_VERIFY(layouter); + CORRADE_IGNORE_DEPRECATED_PUSH /* MSVC warns here too */ + CORRADE_COMPARE(layouter->glyphCount(), 3); + CORRADE_IGNORE_DEPRECATED_POP + + /* Positions are scaled by 0.25/0.5, texture coordinates by {0.1, 0.05} */ + Vector2 cursor{100.0f, 10.0f}; + Range2D rect{{70.0f, 10.0f}, {70.0f, 10.0f}}; + + /* Glyph 3 at initial cursor position, offset by scaled {0.5, 1.0} from + the shaper and scaled {1, 2} from the glyph cache */ + CORRADE_IGNORE_DEPRECATED_PUSH /* MSVC warns here too */ + CORRADE_COMPARE(layouter->renderGlyph(0, cursor, rect), Containers::pair( + Range2D::fromSize({100.75f, 11.5f}, {1.5f, 0.5f}), + Range2D{{0.3f, 0.2f}, {0.6f, 0.25f}} + )); + CORRADE_IGNORE_DEPRECATED_POP + /* Moving the cursor by scaled {50, 0} */ + CORRADE_COMPARE(cursor, (Vector2{125.0f, 10.0f})); + /* The initial rect is empty, so this replaces it */ + CORRADE_COMPARE(rect, (Range2D{{100.75f, 11.5f}, {102.25f, 12.0f}})); + + /* Glyph 7 at the next cursor position, offset by scaled {1.0, 0.5} from + the shaper and scaled {3, 4} from the glyph cache */ + CORRADE_IGNORE_DEPRECATED_PUSH /* MSVC warns here too */ + CORRADE_COMPARE(layouter->renderGlyph(1, cursor, rect), Containers::pair( + Range2D::fromSize({127.0f, 12.25f}, {2.0f, 1.0f}), + Range2D{{0.5f, 0.3f}, {0.9f, 0.4f}} + )); + CORRADE_IGNORE_DEPRECATED_POP + /* Moving the cursor by scaled {10, 0} */ + CORRADE_COMPARE(cursor, (Vector2{130.0f, 10.0f})); + /* Union of the two rectangles */ + CORRADE_COMPARE(rect, (Range2D{{100.75f, 11.5f}, {129.0f, 13.25f}})); + + /* Glyph 3 again, offset by scaled {2.0, 2.0} from the shaper and scaled + {1, 2} from the glyph cache */ + CORRADE_IGNORE_DEPRECATED_PUSH /* MSVC warns here too */ + CORRADE_COMPARE(layouter->renderGlyph(2, cursor, rect), Containers::pair( + Range2D::fromSize({131.5f, 12.0f}, {1.5f, 0.5f}), + Range2D{{0.3f, 0.2f}, {0.6f, 0.25f}} + )); + CORRADE_IGNORE_DEPRECATED_POP + /* Moving the cursor by scaled {20, 0} */ + CORRADE_COMPARE(cursor, (Vector2{140.0f, 10.0f})); + /* Union of the three rectangles */ + CORRADE_COMPARE(rect, (Range2D{{100.75f, 11.5f}, {133.0f, 13.25f}})); +} + +void AbstractFontTest::layoutArrayGlyphCache() { + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + + DummyGlyphCache cache{PixelFormat::R8Unorm, {1, 2, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + font.layout(cache, 0.25f, "hello"); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractFont::layout(): array glyph caches are not supported\n"); +} + +void AbstractFontTest::layoutGlyphCacheFontNotFound() { + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + + DummyGlyphCache cache{PixelFormat::R8Unorm, {1, 2}}; + + cache.addFont(3); + cache.addFont(17); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + Containers::Pointer layouter = font.layout(cache, 0.25f, "hello"); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_VERIFY(!layouter); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::layout(): font not found among 2 fonts in passed glyph cache\n"); +} + +void AbstractFontTest::layoutGlyphOutOfRange() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 3; + } + + void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { + /* Clear the IDs as otherwise it'd result in OOB calls into the + glyph cache */ + for(UnsignedInt& i: ids) i = 0; + } + }; + + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { + return Containers::pointer(*this); + } + } font; + + DummyGlyphCache cache{PixelFormat::R8Unorm, {10, 20}}; + + cache.addFont(15, &font); + + CORRADE_IGNORE_DEPRECATED_PUSH + Containers::Pointer layouter = font.layout(cache, 0.25f, "hello"); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_VERIFY(layouter); + CORRADE_IGNORE_DEPRECATED_PUSH /* MSVC warns here too */ + CORRADE_COMPARE(layouter->glyphCount(), 3); + CORRADE_IGNORE_DEPRECATED_POP + + Range2D rectangle; + Vector2 cursorPosition; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH /* MSVC warns here too */ + layouter->renderGlyph(3, cursorPosition, rectangle); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractLayouter::renderGlyph(): index 3 out of range for 3 glyphs\n"); +} + +void AbstractFontTest::layoutNoFont() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return false; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + + std::ostringstream out; + Error redirectError{&out}; + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 200}}; + CORRADE_IGNORE_DEPRECATED_PUSH + font.layout(cache, 0.25f, "hello"); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractFont::layout(): no font opened\n"); +} +#endif + void AbstractFontTest::debugFeature() { std::ostringstream out; diff --git a/src/Magnum/Text/Test/AbstractLayouterTest.cpp b/src/Magnum/Text/Test/AbstractLayouterTest.cpp deleted file mode 100644 index cd20730ab..000000000 --- a/src/Magnum/Text/Test/AbstractLayouterTest.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - 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 /** @todo remove once Debug is stream-free */ - -#include "Magnum/Math/Range.h" -#include "Magnum/Text/AbstractFont.h" - -namespace Magnum { namespace Text { namespace Test { namespace { - -struct AbstractLayouterTest: TestSuite::Tester { - explicit AbstractLayouterTest(); - - void renderGlyph(); - void renderGlyphOutOfRange(); -}; - -AbstractLayouterTest::AbstractLayouterTest() { - addTests({&AbstractLayouterTest::renderGlyph, - &AbstractLayouterTest::renderGlyphOutOfRange}); -} - -void AbstractLayouterTest::renderGlyph() { - struct Layouter: AbstractLayouter { - explicit Layouter(): AbstractLayouter{3} {} - - Containers::Triple doRenderGlyph(UnsignedInt) override { - return {{{1.0f, 0.5f}, {1.1f, 1.0f}}, - {{0.3f, 1.1f}, {-0.5f, 0.7f}}, - {2.0f, -1.0f}}; - } - }; - - /* Rectangle of zero size shouldn't be merged, but replaced */ - Range2D rectangle({-1.0f, -1.0f}, {-1.0f, -1.0f}); - Vector2 cursorPosition(1.0f, 2.0f); - - Layouter l; - CORRADE_COMPARE(l.renderGlyph(0, cursorPosition, rectangle), - Containers::pair(Range2D{{2.0f, 2.5f}, {2.1f, 3.0f}}, - Range2D{{0.3f, 1.1f}, {-0.5f, 0.7f}})); - CORRADE_COMPARE(cursorPosition, Vector2(3.0f, 1.0f)); - CORRADE_COMPARE(rectangle, Range2D({2.0f, 2.5f}, {2.1f, 3.0f})); - - CORRADE_COMPARE(l.renderGlyph(1, cursorPosition, rectangle), - Containers::pair(Range2D{{4.0f, 1.5f}, {4.1f, 2.0f}}, - Range2D{{0.3f, 1.1f}, {-0.5f, 0.7f}})); - CORRADE_COMPARE(cursorPosition, Vector2(5.0f, 0.0f)); - CORRADE_COMPARE(rectangle, Range2D({2.0f, 1.5f}, {4.1f, 3.0f})); - - CORRADE_COMPARE(l.renderGlyph(2, cursorPosition, rectangle), - Containers::pair(Range2D{{6.0f, 0.5f}, {6.1f, 1.0f}}, - Range2D{{0.3f, 1.1f}, {-0.5f, 0.7f}})); - CORRADE_COMPARE(cursorPosition, Vector2(7.0f, -1.0f)); - CORRADE_COMPARE(rectangle, Range2D({2.0f, 0.5f}, {6.1f, 3.0f})); -} - -void AbstractLayouterTest::renderGlyphOutOfRange() { - CORRADE_SKIP_IF_NO_ASSERT(); - - struct Layouter: AbstractLayouter { - explicit Layouter(): AbstractLayouter{3} {} - - Containers::Triple doRenderGlyph(UnsignedInt) override { return {}; } - } layouter; - - Range2D rectangle; - Vector2 cursorPosition; - - std::ostringstream out; - Error redirectError{&out}; - layouter.renderGlyph(3, cursorPosition, rectangle); - CORRADE_COMPARE(out.str(), "Text::AbstractLayouter::renderGlyph(): index 3 out of range for 3 glyphs\n"); -} - -}}}} - -CORRADE_TEST_MAIN(Magnum::Text::Test::AbstractLayouterTest) diff --git a/src/Magnum/Text/Test/AbstractShaperTest.cpp b/src/Magnum/Text/Test/AbstractShaperTest.cpp new file mode 100644 index 000000000..7a0eef195 --- /dev/null +++ b/src/Magnum/Text/Test/AbstractShaperTest.cpp @@ -0,0 +1,675 @@ +/* + 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 /** @todo remove once Debug is stream-free */ +#include +#include +#include +#include /** @todo remove once Debug is stream-free */ + +#include "Magnum/Math/Vector2.h" +#include "Magnum/Text/AbstractShaper.h" +#include "Magnum/Text/Feature.h" +#include "Magnum/Text/Script.h" + +namespace Magnum { namespace Text { namespace Test { namespace { + +struct AbstractShaperTest: TestSuite::Tester { + explicit AbstractShaperTest(); + + void debugDirection(); + + void featureRangeConstruct(); + void featureRangeConstructBeginEnd(); + + void construct(); + void constructCopy(); + void constructMove(); + + void setScript(); + void setScriptNotImplemented(); + + void setLanguage(); + void setLanguageNotImplemented(); + + void setDirection(); + void setDirectionNotImplemented(); + + void shape(); + void shapeNoFeatures(); + void shapeNoBeginEnd(); + void shapeNoBeginEndFeatures(); + void shapeScriptLanguageDirectionNotImplemented(); + void shapeFailed(); + void shapeBeginEndOutOfRange(); + void shapeEmptyText(); + + /* glyphsInto() tested in shape() already */ + void glyphsIntoEmpty(); + void glyphsIntoInvalidViewSizes(); +}; + +AbstractShaperTest::AbstractShaperTest() { + addTests({&AbstractShaperTest::debugDirection, + + &AbstractShaperTest::featureRangeConstruct, + &AbstractShaperTest::featureRangeConstructBeginEnd, + + &AbstractShaperTest::construct, + &AbstractShaperTest::constructCopy, + &AbstractShaperTest::constructMove, + + &AbstractShaperTest::setScript, + &AbstractShaperTest::setScriptNotImplemented, + + &AbstractShaperTest::setLanguage, + &AbstractShaperTest::setLanguageNotImplemented, + + &AbstractShaperTest::setDirection, + &AbstractShaperTest::setDirectionNotImplemented, + + &AbstractShaperTest::shape, + &AbstractShaperTest::shapeNoFeatures, + &AbstractShaperTest::shapeNoBeginEnd, + &AbstractShaperTest::shapeNoBeginEndFeatures, + &AbstractShaperTest::shapeScriptLanguageDirectionNotImplemented, + &AbstractShaperTest::shapeFailed, + &AbstractShaperTest::shapeBeginEndOutOfRange, + &AbstractShaperTest::shapeEmptyText, + + &AbstractShaperTest::glyphsIntoEmpty, + &AbstractShaperTest::glyphsIntoInvalidViewSizes}); +} + +void AbstractShaperTest::debugDirection() { + std::ostringstream out; + Debug{&out} << Direction::RightToLeft << Direction(0xab); + CORRADE_COMPARE(out.str(), "Text::Direction::RightToLeft Text::Direction(0xab)\n"); +} + +void AbstractShaperTest::featureRangeConstruct() { + FeatureRange a{Feature::Kerning}; + FeatureRange b{Feature::StandardLigatures, false}; + FeatureRange c{Feature::AccessAllAlternates, 13}; + CORRADE_COMPARE(a.feature(), Feature::Kerning); + CORRADE_COMPARE(b.feature(), Feature::StandardLigatures); + CORRADE_COMPARE(c.feature(), Feature::AccessAllAlternates); + CORRADE_VERIFY(a.isEnabled()); + CORRADE_VERIFY(!b.isEnabled()); + CORRADE_COMPARE(a.value(), 1); + CORRADE_COMPARE(b.value(), 0); + CORRADE_COMPARE(c.value(), 13); + CORRADE_COMPARE(a.begin(), 0); + CORRADE_COMPARE(b.begin(), 0); + CORRADE_COMPARE(c.begin(), 0); + CORRADE_COMPARE(a.end(), ~UnsignedInt{}); + CORRADE_COMPARE(b.end(), ~UnsignedInt{}); + CORRADE_COMPARE(c.end(), ~UnsignedInt{}); + + constexpr FeatureRange ca{Feature::Kerning}; + constexpr FeatureRange cb{Feature::StandardLigatures, false}; + constexpr FeatureRange cc{Feature::AccessAllAlternates, 13}; + CORRADE_COMPARE(ca.feature(), Feature::Kerning); + CORRADE_COMPARE(cb.feature(), Feature::StandardLigatures); + CORRADE_COMPARE(cc.feature(), Feature::AccessAllAlternates); + CORRADE_VERIFY(ca.isEnabled()); + CORRADE_VERIFY(!cb.isEnabled()); + CORRADE_COMPARE(ca.value(), 1); + CORRADE_COMPARE(cb.value(), 0); + CORRADE_COMPARE(cc.value(), 13); + CORRADE_COMPARE(ca.begin(), 0); + CORRADE_COMPARE(cb.begin(), 0); + CORRADE_COMPARE(cc.begin(), 0); + CORRADE_COMPARE(ca.end(), ~UnsignedInt{}); + CORRADE_COMPARE(cb.end(), ~UnsignedInt{}); + CORRADE_COMPARE(cc.end(), ~UnsignedInt{}); +} + +void AbstractShaperTest::featureRangeConstructBeginEnd() { + FeatureRange a{Feature::Kerning, 7, 26}; + FeatureRange b{Feature::StandardLigatures, 7, 26, false}; + FeatureRange c{Feature::AccessAllAlternates, 7, 26, 13}; + CORRADE_COMPARE(a.feature(), Feature::Kerning); + CORRADE_COMPARE(b.feature(), Feature::StandardLigatures); + CORRADE_COMPARE(c.feature(), Feature::AccessAllAlternates); + CORRADE_VERIFY(a.isEnabled()); + CORRADE_VERIFY(!b.isEnabled()); + CORRADE_COMPARE(a.value(), 1); + CORRADE_COMPARE(b.value(), 0); + CORRADE_COMPARE(c.value(), 13); + CORRADE_COMPARE(a.begin(), 7); + CORRADE_COMPARE(b.begin(), 7); + CORRADE_COMPARE(c.begin(), 7); + CORRADE_COMPARE(a.end(), 26); + CORRADE_COMPARE(b.end(), 26); + CORRADE_COMPARE(c.end(), 26); + + constexpr FeatureRange ca{Feature::Kerning, 7, 26}; + constexpr FeatureRange cb{Feature::StandardLigatures, 7, 26, false}; + constexpr FeatureRange cc{Feature::AccessAllAlternates, 7, 26, 13}; + CORRADE_COMPARE(ca.feature(), Feature::Kerning); + CORRADE_COMPARE(cb.feature(), Feature::StandardLigatures); + CORRADE_COMPARE(cc.feature(), Feature::AccessAllAlternates); + CORRADE_VERIFY(ca.isEnabled()); + CORRADE_VERIFY(!cb.isEnabled()); + CORRADE_COMPARE(ca.value(), 1); + CORRADE_COMPARE(cb.value(), 0); + CORRADE_COMPARE(cc.value(), 13); + CORRADE_COMPARE(ca.begin(), 7); + CORRADE_COMPARE(cb.begin(), 7); + CORRADE_COMPARE(cc.begin(), 7); + CORRADE_COMPARE(ca.end(), 26); + CORRADE_COMPARE(cb.end(), 26); + CORRADE_COMPARE(cc.end(), 26); +} + +AbstractFont& FakeFont = *reinterpret_cast(0xdeadbeef); + +struct DummyShaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} +}; + +void AbstractShaperTest::construct() { + DummyShaper shaper{FakeFont}; + CORRADE_COMPARE(&shaper.font(), &FakeFont); + CORRADE_COMPARE(shaper.glyphCount(), 0); + + /* Initial state of script() etc getters verified in the shape() test */ + + /* Const overloads */ + const DummyShaper& cshaper = shaper; + CORRADE_COMPARE(&cshaper.font(), &FakeFont); +} + +void AbstractShaperTest::constructCopy() { + CORRADE_VERIFY(!std::is_copy_constructible{}); + CORRADE_VERIFY(!std::is_copy_assignable{}); +} + +void AbstractShaperTest::constructMove() { + DummyShaper a{FakeFont}; + + DummyShaper b = Utility::move(a); + CORRADE_COMPARE(&b.font(), &FakeFont); + CORRADE_COMPARE(b.glyphCount(), 0); + + DummyShaper c{*reinterpret_cast(0xcafebabe)}; + c = Utility::move(b); + CORRADE_COMPARE(&b.font(), &FakeFont); + CORRADE_COMPARE(b.glyphCount(), 0); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + +void AbstractShaperTest::setScript() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + bool doSetScript(Script script) override { + CORRADE_COMPARE(script, Script::Math); + called = true; + return true; + } + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + + bool called = false; + } shaper{FakeFont}; + + CORRADE_VERIFY(shaper.setScript(Script::Math)); + CORRADE_VERIFY(shaper.called); +} + +void AbstractShaperTest::setScriptNotImplemented() { + DummyShaper shaper{FakeFont}; + CORRADE_VERIFY(!shaper.setScript(Script::Math)); +} + +void AbstractShaperTest::setLanguage() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + bool doSetLanguage(Containers::StringView language) override { + CORRADE_COMPARE(language, "cs"); + called = true; + return true; + } + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + + bool called = false; + } shaper{FakeFont}; + + CORRADE_VERIFY(shaper.setLanguage("cs")); + CORRADE_VERIFY(shaper.called); +} + +void AbstractShaperTest::setLanguageNotImplemented() { + DummyShaper shaper{FakeFont}; + CORRADE_VERIFY(!shaper.setLanguage("cs")); +} + +void AbstractShaperTest::setDirection() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + bool doSetDirection(Direction direction) override { + CORRADE_COMPARE(direction, Direction::BottomToTop); + called = true; + return true; + } + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + + bool called = false; + } shaper{FakeFont}; + + CORRADE_VERIFY(shaper.setDirection(Direction::BottomToTop)); + CORRADE_VERIFY(shaper.called); +} + +void AbstractShaperTest::setDirectionNotImplemented() { + DummyShaper shaper{FakeFont}; + CORRADE_VERIFY(!shaper.setDirection(Direction::BottomToTop)); +} + +void AbstractShaperTest::shape() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features) override { + CORRADE_COMPARE(text, "some text"); + CORRADE_COMPARE(begin, 3); + CORRADE_COMPARE(end, 8); + CORRADE_COMPARE(features.size(), 2); + CORRADE_COMPARE(features[0].feature(), Feature::ContextualLigatures); + CORRADE_VERIFY(features[0].isEnabled()); + CORRADE_COMPARE(features[0].begin(), 0); + CORRADE_COMPARE(features[0].end(), ~UnsignedInt{}); + CORRADE_COMPARE(features[1].feature(), Feature::Kerning); + CORRADE_VERIFY(!features[1].isEnabled()); + CORRADE_COMPARE(features[1].begin(), 2); + CORRADE_COMPARE(features[1].end(), 5); + shapeCalled = true; + return 24; + } + + Script doScript() const override { + return Script::LinearA; + } + + Containers::StringView doLanguage() const override { + return "eh-UH"; + } + + Direction doDirection() const override { + return Direction::BottomToTop; + } + + void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + CORRADE_COMPARE(ids.size(), 24); + CORRADE_COMPARE(ids[0], 1337); + CORRADE_COMPARE(offsets.size(), 24); + CORRADE_COMPARE(offsets[0], (Vector2{13.0f, 37.0f})); + CORRADE_COMPARE(advances.size(), 24); + CORRADE_COMPARE(advances[0], (Vector2{42.0f, 69.0f})); + ids[1] = 666; + offsets[1] = {-4.0f, -5.0f}; + advances[1] = {12.0f, 23.0f}; + } + + bool shapeCalled = false; + } shaper{FakeFont}; + + /* Initially it shouldn't call into any of the implementations */ + CORRADE_COMPARE(shaper.glyphCount(), 0); + CORRADE_COMPARE(shaper.script(), Script::Unspecified); + CORRADE_COMPARE(shaper.language(), ""); + CORRADE_COMPARE(shaper.direction(), Direction::Unspecified); + + /* Shaping fills glyph count and allows calling into the implementations */ + CORRADE_COMPARE(shaper.shape("some text", 3, 8, { + Feature::ContextualLigatures, + {Feature::Kerning, 2, 5, false} + }), 24); + CORRADE_VERIFY(shaper.shapeCalled); + CORRADE_COMPARE(shaper.glyphCount(), 24); + CORRADE_COMPARE(shaper.script(), Script::LinearA); + CORRADE_COMPARE(shaper.language(), "eh-UH"); + CORRADE_COMPARE(shaper.direction(), Direction::BottomToTop); + + UnsignedInt ids[24]; + Vector2 offsets[24]; + Vector2 advances[24]; + ids[0] = 1337; + offsets[0] = {13.0f, 37.0f}; + advances[0] = {42.0f, 69.0f}; + shaper.glyphsInto(ids, offsets, advances); + CORRADE_COMPARE(ids[1], 666); + CORRADE_COMPARE(offsets[1], (Vector2{-4.0f, -5.0f})); + CORRADE_COMPARE(advances[1], (Vector2{12.0f, 23.0f})); +} + +void AbstractShaperTest::shapeNoFeatures() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features) override { + CORRADE_COMPARE(text, "some text"); + CORRADE_COMPARE(begin, 3); + CORRADE_COMPARE(end, 8); + CORRADE_COMPARE(features.size(), 0); + shapeCalled = true; + return 24; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + + bool shapeCalled = false; + } shaper{FakeFont}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + CORRADE_COMPARE(shaper.shape("some text", 3, 8), 24); + CORRADE_VERIFY(shaper.shapeCalled); + CORRADE_COMPARE(shaper.glyphCount(), 24); +} + +void AbstractShaperTest::shapeNoBeginEnd() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features) override { + CORRADE_COMPARE(text, "some text"); + CORRADE_COMPARE(begin, 0); + CORRADE_COMPARE(end, ~UnsignedInt{}); + CORRADE_COMPARE(features.size(), 2); + CORRADE_COMPARE(features[0].feature(), Feature::ContextualLigatures); + CORRADE_VERIFY(features[0].isEnabled()); + CORRADE_COMPARE(features[0].begin(), 0); + CORRADE_COMPARE(features[0].end(), ~UnsignedInt{}); + CORRADE_COMPARE(features[1].feature(), Feature::Kerning); + CORRADE_VERIFY(!features[1].isEnabled()); + CORRADE_COMPARE(features[1].begin(), 2); + CORRADE_COMPARE(features[1].end(), 5); + shapeCalled = true; + return 24; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + + bool shapeCalled = false; + } shaper{FakeFont}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* Shaping fills glyph count and allows calling into the implementations */ + CORRADE_COMPARE(shaper.shape("some text", { + Feature::ContextualLigatures, + {Feature::Kerning, 2, 5, false} + }), 24); + CORRADE_VERIFY(shaper.shapeCalled); + CORRADE_COMPARE(shaper.glyphCount(), 24); +} + +void AbstractShaperTest::shapeNoBeginEndFeatures() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features) override { + CORRADE_COMPARE(text, "some text"); + CORRADE_COMPARE(begin, 0); + CORRADE_COMPARE(end, ~UnsignedInt{}); + CORRADE_COMPARE(features.size(), 0); + shapeCalled = true; + return 24; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + + bool shapeCalled = false; + } shaper{FakeFont}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + CORRADE_COMPARE(shaper.shape("some text"), 24); + CORRADE_VERIFY(shaper.shapeCalled); + CORRADE_COMPARE(shaper.glyphCount(), 24); +} + +void AbstractShaperTest::shapeScriptLanguageDirectionNotImplemented() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 24; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + } shaper{FakeFont}; + + /* Initially it won't call into any of the implementations */ + CORRADE_COMPARE(shaper.script(), Script::Unspecified); + CORRADE_COMPARE(shaper.language(), ""); + CORRADE_COMPARE(shaper.direction(), Direction::Unspecified); + + CORRADE_COMPARE(shaper.shape("some text"), 24); + + /* It should delegate to the default implementations, which return the same + values as if shape() wouldn't be called at all */ + CORRADE_COMPARE(shaper.script(), Script::Unspecified); + CORRADE_COMPARE(shaper.language(), ""); + CORRADE_COMPARE(shaper.direction(), Direction::Unspecified); +} + +void AbstractShaperTest::shapeFailed() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 0; + } + + Script doScript() const override { + return Script::LinearA; + } + + Containers::StringView doLanguage() const override { + return "eh-UH"; + } + + Direction doDirection() const override { + return Direction::BottomToTop; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + } shaper{FakeFont}; + + /* The implementation is responsible for printing a message, the base class + doesn't */ + CORRADE_COMPARE(shaper.shape("some text", 3, 8), 0); + + /* After a failure it shouldn't call into any of the implementations + either */ + CORRADE_COMPARE(shaper.glyphCount(), 0); + CORRADE_COMPARE(shaper.script(), Script::Unspecified); + CORRADE_COMPARE(shaper.language(), ""); + CORRADE_COMPARE(shaper.direction(), Direction::Unspecified); +} + +void AbstractShaperTest::shapeBeginEndOutOfRange() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + CORRADE_FAIL("This shouldn't be called"); + return 5; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + } shaper{FakeFont}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + std::ostringstream out; + Error redirectError{&out}; + /* Begin out of range, end unbounded */ + shaper.shape("hello", 6, ~UnsignedInt{}); + shaper.shape("hello", { + Feature::AccessAllAlternates, + {Feature::Kerning, 6, ~UnsignedInt{}}, + }); + /* Begin and end out of range */ + shaper.shape("hello", 6, 7); + shaper.shape("hello", { + Feature::AccessAllAlternates, + {Feature::Kerning, 6, 7}, + }); + /* End out of range */ + shaper.shape("hello", 4, 6); + shaper.shape("hello", { + Feature::AccessAllAlternates, + {Feature::Kerning, 4, 6}, + }); + /* Begin larger than end */ + shaper.shape("hello", 4, 3); + shaper.shape("hello", { + Feature::AccessAllAlternates, + {Feature::Kerning, 4, 3}, + }); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractShaper::shape(): begin 6 and end 4294967295 out of range for a text of 5 bytes\n" + "Text::AbstractShaper::shape(): feature 1 begin 6 and end 4294967295 out of range for a text of 5 bytes\n" + "Text::AbstractShaper::shape(): begin 6 and end 7 out of range for a text of 5 bytes\n" + "Text::AbstractShaper::shape(): feature 1 begin 6 and end 7 out of range for a text of 5 bytes\n" + "Text::AbstractShaper::shape(): begin 4 and end 6 out of range for a text of 5 bytes\n" + "Text::AbstractShaper::shape(): feature 1 begin 4 and end 6 out of range for a text of 5 bytes\n" + "Text::AbstractShaper::shape(): begin 4 and end 3 out of range for a text of 5 bytes\n" + "Text::AbstractShaper::shape(): feature 1 begin 4 and end 3 out of range for a text of 5 bytes\n", + TestSuite::Compare::String); +} + +void AbstractShaperTest::shapeEmptyText() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + CORRADE_FAIL("This shouldn't be called"); + return 5; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + } shaper{FakeFont}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + std::ostringstream out; + Error redirectError{&out}; + shaper.shape(""); + shaper.shape("hello", 3, 3); + shaper.shape("hello", 5, ~UnsignedInt{}); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractShaper::shape(): shaped text at begin 0 is empty\n" + "Text::AbstractShaper::shape(): shaped text at begin 3 is empty\n" + "Text::AbstractShaper::shape(): shaped text at begin 5 is empty\n", + TestSuite::Compare::String); +} + +void AbstractShaperTest::glyphsIntoEmpty() { + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 0; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { + CORRADE_FAIL("This shouldn't be called"); + } + } shaper{FakeFont}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* This should not assert but also not call anywhere */ + shaper.glyphsInto(nullptr, nullptr, nullptr); +} + +void AbstractShaperTest::glyphsIntoInvalidViewSizes() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 5; + } + + void doGlyphsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { + CORRADE_FAIL("This shouldn't be called"); + } + } shaper{FakeFont}; + + CORRADE_COMPARE(shaper.shape("yey"), 5); + + UnsignedInt ids[5]; + UnsignedInt idsWrong[6]; + Vector2 offsets[5]; + Vector2 offsetsWrong[6]; + Vector2 advances[5]; + Vector2 advancesWrong[6]; + + std::ostringstream out; + Error redirectError{&out}; + shaper.glyphsInto(idsWrong, offsets, advances); + shaper.glyphsInto(ids, offsetsWrong, advances); + shaper.glyphsInto(ids, offsets, advancesWrong); + CORRADE_COMPARE(out.str(), + "Text::AbstractShaper::glyphsInto(): expected the ids, offsets and advanced views to have a size of 5 but got 6, 5 and 5\n" + "Text::AbstractShaper::glyphsInto(): expected the ids, offsets and advanced views to have a size of 5 but got 5, 6 and 5\n" + "Text::AbstractShaper::glyphsInto(): expected the ids, offsets and advanced views to have a size of 5 but got 5, 5 and 6\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Text::Test::AbstractShaperTest) diff --git a/src/Magnum/Text/Test/CMakeLists.txt b/src/Magnum/Text/Test/CMakeLists.txt index 8c7b56bcf..9427240d0 100644 --- a/src/Magnum/Text/Test/CMakeLists.txt +++ b/src/Magnum/Text/Test/CMakeLists.txt @@ -74,7 +74,8 @@ if(CORRADE_TARGET_EMSCRIPTEN) target_link_options(TextAbstractGlyphCacheTest PRIVATE "SHELL:-s ALLOW_MEMORY_GROWTH=1") endif() -corrade_add_test(TextAbstractLayouterTest AbstractLayouterTest.cpp LIBRARIES Magnum MagnumTextTestLib) +corrade_add_test(TextAbstractShaperTest AbstractShaperTest.cpp + LIBRARIES MagnumTextTestLib) corrade_add_test(TextFeatureTest FeatureTest.cpp LIBRARIES MagnumTextTestLib) diff --git a/src/Magnum/Text/Test/RendererGLTest.cpp b/src/Magnum/Text/Test/RendererGLTest.cpp index f9f686f17..404a656eb 100644 --- a/src/Magnum/Text/Test/RendererGLTest.cpp +++ b/src/Magnum/Text/Test/RendererGLTest.cpp @@ -24,6 +24,7 @@ */ #include +#include #include #include #include @@ -32,6 +33,7 @@ #include "Magnum/GL/Extensions.h" #include "Magnum/GL/OpenGLTester.h" #include "Magnum/Text/AbstractFont.h" +#include "Magnum/Text/AbstractShaper.h" #include "Magnum/Text/GlyphCache.h" #include "Magnum/Text/Renderer.h" @@ -86,33 +88,29 @@ RendererGLTest::RendererGLTest() { &RendererGLTest::multiline}); } -struct TestLayouter: AbstractLayouter { - explicit TestLayouter(const AbstractGlyphCache& cache, UnsignedInt fontId, Float scale, UnsignedInt glyphCount): AbstractLayouter{glyphCount}, _cache(cache), _fontId{fontId}, _scale{scale} {} - - Containers::Triple doRenderGlyph(UnsignedInt i) override { - /* It just rotates between the three glyphs */ - UnsignedInt glyphId; - if(i % 3 == 0) - glyphId = 3; - else if(i % 3 == 1) - glyphId = 7; - else - glyphId = 9; - - Containers::Triple glyph = _cache.glyph(_fontId, glyphId); - CORRADE_VERIFY(glyph.second() == 0); - - /* Offset Y and advance X is getting larger with every glyph */ - return { - Range2D::fromSize(Vector2{glyph.first() + Vector2i::yAxis(i + 1)}*_scale, Vector2{glyph.third().size()}*_scale), - Range2D{glyph.third()}.scaled(1.0f/Vector2{_cache.size().xy()}), - Vector2{Float(i + 1), i % 2 ? -0.5f : +0.5f}*_scale - }; +struct TestShaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); } - const AbstractGlyphCache& _cache; - UnsignedInt _fontId; - Float _scale; + 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 { @@ -130,10 +128,8 @@ struct TestFont: AbstractFont { Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache& cache, Float size, Containers::StringView text) override { - /* The final rendered size should be a ratio of the font and layout - size */ - return Containers::pointer(cache, *cache.findFont(this), size/this->size(), UnsignedInt(text.size())); + Containers::Pointer doCreateShaper() override { + return Containers::pointer(*this); } bool _opened = false; @@ -430,29 +426,24 @@ void RendererGLTest::mutableText() { } void RendererGLTest::multiline() { - struct Layouter: AbstractLayouter { - explicit Layouter(const AbstractGlyphCache& cache, UnsignedInt fontId, Float scale, UnsignedInt glyphCount): AbstractLayouter{glyphCount}, _cache(cache), _fontId{fontId}, _scale{scale} {} - - Containers::Triple doRenderGlyph(UnsignedInt) override { - Containers::Triple glyph = _cache.glyph(_fontId, 0); - CORRADE_VERIFY(glyph.second() == 0); - - return { - Range2D::fromSize(Vector2{glyph.first()}*_scale, - Vector2{glyph.third().size()}*_scale), - Range2D{glyph.third()}.scaled(1.0f/Vector2{_cache.size().xy()}), - Vector2::xAxis(4.0f)*_scale - }; + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); } - const AbstractGlyphCache& _cache; - UnsignedInt _fontId; - Float _scale; + 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; } @@ -467,10 +458,8 @@ void RendererGLTest::multiline() { Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache& cache, Float size, Containers::StringView text) override { - /* The final rendered size should be a ratio of the font and layout - size */ - return Containers::pointer(cache, *cache.findFont(this), size/this->size(), UnsignedInt(text.size())); + Containers::Pointer doCreateShaper() override { + return Containers::pointer(*this); } bool _opened = false; diff --git a/src/Magnum/Text/Text.h b/src/Magnum/Text/Text.h index 02365d148..397b00ed9 100644 --- a/src/Magnum/Text/Text.h +++ b/src/Magnum/Text/Text.h @@ -39,6 +39,7 @@ class AbstractFont; class AbstractFontConverter; class AbstractGlyphCache; class AbstractLayouter; +class AbstractShaper; enum class Alignment: UnsignedByte; diff --git a/src/MagnumPlugins/MagnumFont/MagnumFont.cpp b/src/MagnumPlugins/MagnumFont/MagnumFont.cpp index a2572c395..93913b5b8 100644 --- a/src/MagnumPlugins/MagnumFont/MagnumFont.cpp +++ b/src/MagnumPlugins/MagnumFont/MagnumFont.cpp @@ -33,12 +33,14 @@ #include #include #include +#include #include #include #include #include "Magnum/ImageView.h" #include "Magnum/Math/ConfigurationValue.h" +#include "Magnum/Text/AbstractShaper.h" #include "Magnum/Text/GlyphCache.h" #include "Magnum/Trade/ImageData.h" #include "MagnumPlugins/TgaImporter/TgaImporter.h" @@ -62,22 +64,6 @@ struct MagnumFont::Data { Containers::Array glyphs; }; -namespace { - class MagnumFontLayouter: public AbstractLayouter { - public: - explicit MagnumFontLayouter(const Containers::StridedArrayView1D& glyphAdvance, const AbstractGlyphCache& cache, UnsignedInt fontId, Float fontSize, Float textSize, Containers::Array&& glyphs); - - private: - Containers::Triple doRenderGlyph(UnsignedInt i) override; - - const Containers::StridedArrayView1D _glyphAdvance; - const AbstractGlyphCache& _cache; - const UnsignedInt _fontId; - const Float _fontSize, _textSize; - const Containers::Array _glyphs; - }; -} - MagnumFont::MagnumFont(): _opened(nullptr) {} MagnumFont::MagnumFont(PluginManager::AbstractManager& manager, const Containers::StringView& plugin): AbstractFont{manager, plugin}, _opened(nullptr) {} @@ -201,57 +187,43 @@ Containers::Pointer MagnumFont::doCreateGlyphCache() { return Containers::Pointer{Utility::move(cache)}; } -Containers::Pointer MagnumFont::doLayout(const AbstractGlyphCache& cache, const Float size, const Containers::StringView text) { - /* Not yet, at least */ - if(cache.size().z() != 1) { - Error{} << "Text::MagnumFont::layout(): array glyph caches are not supported"; - return {}; - } - - /* Find this font in the cache */ - Containers::Optional fontId = cache.findFont(this); - if(!fontId) { - Error{} << "Text::MagnumFont::layout(): font not found among" << cache.fontCount() << "fonts in passed glyph cache"; - return {}; - } - - /* Get glyph codes from characters */ - Containers::Array glyphs; - arrayReserve(glyphs, text.size()); - for(std::size_t i = 0; i != text.size(); ) { - const Containers::Pair codepointNext = Utility::Unicode::nextChar(text, i); - const auto it = _opened->glyphId.find(codepointNext.first()); - arrayAppend(glyphs, it == _opened->glyphId.end() ? 0 : it->second); - i = codepointNext.second(); - } - - return Containers::pointer(stridedArrayView(_opened->glyphs).slice(&Data::Glyph::advance), cache, *fontId, this->size(), size, Utility::move(glyphs)); -} - -namespace { - -MagnumFontLayouter::MagnumFontLayouter(const Containers::StridedArrayView1D& glyphAdvance, const AbstractGlyphCache& cache, const UnsignedInt fontId, const Float fontSize, const Float textSize, Containers::Array&& glyphs): AbstractLayouter{UnsignedInt(glyphs.size())}, _glyphAdvance{glyphAdvance}, _cache(cache), _fontId{fontId}, _fontSize{fontSize}, _textSize{textSize}, _glyphs{Utility::move(glyphs)} {} - -Containers::Triple MagnumFontLayouter::doRenderGlyph(const UnsignedInt i) { - /* Offset of the glyph rectangle relative to the cursor, layer, texture - coordinates. We checked that the glyph cache is 2D in doLayout() so the - layer can be ignored. */ - const Containers::Triple glyph = _cache.glyph(_fontId, _glyphs[i]); - CORRADE_INTERNAL_ASSERT(glyph.second() == 0); - - /* Normalized texture coordinates */ - const auto textureCoordinates = Range2D{glyph.third()}.scaled(1.0f/Vector2{_cache.size().xy()}); - - /* Quad rectangle, computed from texture rectangle, denormalized to - requested text size */ - const auto quadRectangle = Range2D{Range2Di::fromSize(glyph.first(), glyph.third().size())}.scaled(Vector2{_textSize/_fontSize}); - - /* Advance for given glyph, denormalized to requested text size */ - const Vector2 advance = _glyphAdvance[_glyphs[i]]*(_textSize/_fontSize); - - return {quadRectangle, textureCoordinates, advance}; -} +Containers::Pointer MagnumFont::doCreateShaper() { + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(const Containers::StringView textFull, const UnsignedInt begin, const UnsignedInt end, Containers::ArrayView) override { + const Data& fontData = *static_cast(font())._opened; + const Containers::StringView text = textFull.slice(begin, end == ~UnsignedInt{} ? textFull.size() : end); + + /* Get glyph codes from characters */ + arrayResize(_glyphs, 0); + arrayReserve(_glyphs, text.size()); + for(std::size_t i = 0; i != text.size(); ) { + const Containers::Pair codepointNext = Utility::Unicode::nextChar(text, i); + const auto it = fontData.glyphId.find(codepointNext.first()); + arrayAppend(_glyphs, it == fontData.glyphId.end() ? 0 : it->second); + i = codepointNext.second(); + } + + return _glyphs.size(); + } + + void doGlyphsInto(const Containers::StridedArrayView1D& ids, const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + const Data& fontData = *static_cast(font())._opened; + + Utility::copy(_glyphs, ids); + for(std::size_t i = 0; i != _glyphs.size(); ++i) { + /* There's no glyph offsets in addition to advances */ + offsets[i] = {}; + advances[i] = fontData.glyphs[_glyphs[i]].advance; + } + } + + Containers::StridedArrayView1D _glyphAdvance; + Containers::Array _glyphs; + }; + return Containers::pointer(*this); } }} diff --git a/src/MagnumPlugins/MagnumFont/MagnumFont.h b/src/MagnumPlugins/MagnumFont/MagnumFont.h index 7b67a39f7..8371c89b0 100644 --- a/src/MagnumPlugins/MagnumFont/MagnumFont.h +++ b/src/MagnumPlugins/MagnumFont/MagnumFont.h @@ -180,7 +180,7 @@ class MAGNUM_MAGNUMFONT_EXPORT MagnumFont: public AbstractFont { MAGNUM_MAGNUMFONT_LOCAL Vector2 doGlyphSize(UnsignedInt glyph) override; MAGNUM_MAGNUMFONT_LOCAL Vector2 doGlyphAdvance(UnsignedInt glyph) override; MAGNUM_MAGNUMFONT_LOCAL Containers::Pointer doCreateGlyphCache() override; - MAGNUM_MAGNUMFONT_LOCAL Containers::Pointer doLayout(const AbstractGlyphCache& cache, Float size, Containers::StringView text) override; + MAGNUM_MAGNUMFONT_LOCAL Containers::Pointer doCreateShaper() override; struct Data; Containers::Pointer _opened; diff --git a/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp b/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp index e77530336..6b2996ab4 100644 --- a/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp +++ b/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp @@ -28,7 +28,9 @@ #include #include #include /** @todo remove once AbstractFont is -free */ +#include #include +#include #include #include #include @@ -38,6 +40,7 @@ #include "Magnum/Math/Range.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/AbstractShaper.h" #include "Magnum/Trade/AbstractImporter.h" #include "configure.h" @@ -51,8 +54,8 @@ struct MagnumFontTest: TestSuite::Tester { void properties(); void layout(); void layoutNoGlyphsInCache(); - void layoutNoFontInCache(); - void layoutArrayCache(); + + void shaperReuse(); void fileCallbackImage(); void fileCallbackImageNotFound(); @@ -78,8 +81,8 @@ MagnumFontTest::MagnumFontTest() { Containers::arraySize(LayoutData)); addTests({&MagnumFontTest::layoutNoGlyphsInCache, - &MagnumFontTest::layoutNoFontInCache, - &MagnumFontTest::layoutArrayCache, + + &MagnumFontTest::shaperReuse, &MagnumFontTest::fileCallbackImage, &MagnumFontTest::fileCallbackImageNotFound}); @@ -242,43 +245,71 @@ void MagnumFontTest::layoutNoGlyphsInCache() { CORRADE_COMPARE(cursorPosition, Vector2(0.375f, 0.0f)); } -void MagnumFontTest::layoutNoFontInCache() { +void MagnumFontTest::shaperReuse() { Containers::Pointer font = _fontManager.instantiate("MagnumFont"); - CORRADE_VERIFY(font->openFile(Utility::Path::join(MAGNUMFONT_TEST_DIR, "font.conf"), 0.0f)); - struct: AbstractGlyphCache { - using AbstractGlyphCache::AbstractGlyphCache; - - GlyphCacheFeatures doFeatures() const override { return {}; } - void doSetImage(const Vector2i&, const ImageView2D&) override {} - } cache{PixelFormat::R8Unorm, Vector2i{256}}; - - /* Add a font that isn't associated with this one */ - cache.addFont(15); - - std::ostringstream out; - Error redirectError{&out}; - CORRADE_VERIFY(!font->layout(cache, 0.5f, "Wave")); - CORRADE_COMPARE(out.str(), "Text::MagnumFont::layout(): font not found among 1 fonts in passed glyph cache\n"); -} - -void MagnumFontTest::layoutArrayCache() { - Containers::Pointer font = _fontManager.instantiate("MagnumFont"); - - CORRADE_VERIFY(font->openFile(Utility::Path::join(MAGNUMFONT_TEST_DIR, "font.conf"), 0.0f)); - - struct: AbstractGlyphCache { - using AbstractGlyphCache::AbstractGlyphCache; - - GlyphCacheFeatures doFeatures() const override { return {}; } - void doSetImage(const Vector2i&, const ImageView2D&) override {} - } cache{PixelFormat::R8Unorm, {256, 128, 3}}; - - std::ostringstream out; - Error redirectError{&out}; - CORRADE_VERIFY(!font->layout(cache, 0.5f, "Wave")); - CORRADE_COMPARE(out.str(), "Text::MagnumFont::layout(): array glyph caches are not supported\n"); + Containers::Pointer shaper = font->createShaper(); + + /* Short text */ + { + CORRADE_COMPARE(shaper->shape("We"), 2); + UnsignedInt ids[2]; + Vector2 offsets[2]; + Vector2 advances[2]; + shaper->glyphsInto(ids, offsets, advances); + CORRADE_COMPARE_AS(Containers::arrayView(ids), Containers::arrayView({ + 2u, /* 'W' */ + 1u /* 'e' */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView({ + {}, {}, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(advances), Containers::arrayView({ + {23.0f, 0.0f}, + {12.f, 0.0f} + }), TestSuite::Compare::Container); + + /* Long text, same as in shape(), should enlarge the array for it */ + } { + CORRADE_COMPARE(shaper->shape("Wave"), 4); + UnsignedInt ids[4]; + Vector2 offsets[4]; + Vector2 advances[4]; + shaper->glyphsInto(ids, offsets, advances); + CORRADE_COMPARE_AS(Containers::arrayView(ids), Containers::arrayView({ + 2u, /* 'W' */ + 0u, /* 'a' (not found) */ + 0u, /* 'v' (not found) */ + 1u /* 'e' or 'ě' */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView({ + {}, {}, {}, {} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(advances), Containers::arrayView({ + {23.0f, 0.0f}, + {8.0f, 0.0f}, + {8.0f, 0.0f}, + {12.f, 0.0f} + }), TestSuite::Compare::Container); + + /* Short text again, should not leave the extra glyphs there */ + } { + CORRADE_COMPARE(shaper->shape("a"), 1); + UnsignedInt ids[1]; + Vector2 offsets[1]; + Vector2 advances[1]; + shaper->glyphsInto(ids, offsets, advances); + CORRADE_COMPARE_AS(Containers::arrayView(ids), Containers::arrayView({ + 0u, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView({ + {}, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(advances), Containers::arrayView({ + {8.0f, 0.0f} + }), TestSuite::Compare::Container); + } } void MagnumFontTest::fileCallbackImage() { diff --git a/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp b/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp index 3c6b97195..413531ef7 100644 --- a/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp +++ b/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp @@ -42,6 +42,7 @@ #include "Magnum/Text/AbstractGlyphCache.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractFontConverter.h" +#include "Magnum/Text/AbstractShaper.h" #include "Magnum/Trade/AbstractImageConverter.h" #include "Magnum/Trade/AbstractImporter.h" #include "Magnum/Trade/ImageData.h" @@ -112,7 +113,7 @@ class MyFont: public Text::AbstractFont { return {16.0f, 25.0f, -10.0f, 39.7333f, 4}; } FontFeatures doFeatures() const override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { return nullptr; } + Containers::Pointer doCreateShaper() override { return nullptr; } UnsignedInt doGlyphId(const char32_t character) override { switch(character) { @@ -424,9 +425,7 @@ void MagnumFontConverterTest::exportFontImageProcessingGlyphCacheNoDownload() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return nullptr; } } font; struct: AbstractGlyphCache { @@ -454,9 +453,7 @@ void MagnumFontConverterTest::exportFontArrayCache() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return nullptr; } } font; struct: AbstractGlyphCache { @@ -486,9 +483,7 @@ void MagnumFontConverterTest::exportFontNotFoundInCache() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return nullptr; } } font1, font2; struct: AbstractGlyphCache { @@ -522,9 +517,7 @@ void MagnumFontConverterTest::exportFontImageConversionFailed() { UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, Containers::StringView) override { - return nullptr; - } + Containers::Pointer doCreateShaper() override { return nullptr; } private: bool _opened;