diff --git a/doc/snippets/Text.cpp b/doc/snippets/Text.cpp index 2e2e78c72..5c9435f4a 100644 --- a/doc/snippets/Text.cpp +++ b/doc/snippets/Text.cpp @@ -357,4 +357,25 @@ shaper->glyphOffsetsAdvancesInto( /* [AbstractShaper-shape-multiple] */ } +{ +/* -Wnonnull in GCC 11+ "helpfully" says "this is null" if I don't initialize + the font pointer. I don't care, I just want you to check compilation errors, + not more! */ +PluginManager::Manager manager; +Containers::Pointer font = manager.loadAndInstantiate("SomethingWhatever"); +Containers::Pointer shaper = font->createShaper(); +/* [AbstractShaper-shape-clusters] */ +Containers::StringView text = DOXYGEN_ELLIPSIS({}); + +shaper->shape(text); +DOXYGEN_ELLIPSIS() + +Containers::Array clusters{NoInit, shaper->glyphCount()}; +shaper->glyphClustersInto(clusters); + +Containers::StringView selection = text.slice(clusters[2], clusters[5]); +/* [AbstractShaper-shape-clusters] */ +static_cast(selection); +} + } diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index 26952a827..39c3eb91f 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -858,7 +858,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.6" +"cz.mosra.magnum.Text.AbstractFont/0.3.7" /* [interface] */ #ifndef DOXYGEN_GENERATING_OUTPUT diff --git a/src/Magnum/Text/AbstractShaper.cpp b/src/Magnum/Text/AbstractShaper.cpp index e50160194..d128a0b7a 100644 --- a/src/Magnum/Text/AbstractShaper.cpp +++ b/src/Magnum/Text/AbstractShaper.cpp @@ -134,4 +134,13 @@ void AbstractShaper::glyphOffsetsAdvancesInto(const Containers::StridedArrayView doGlyphOffsetsAdvancesInto(offsets, advances); } +void AbstractShaper::glyphClustersInto(const Containers::StridedArrayView1D& clusters) const { + CORRADE_ASSERT(clusters.size() == _glyphCount, + "Text::AbstractShaper::glyphClustersInto(): expected the clusters view to have a size of" << _glyphCount << "but got" << clusters.size(), ); + /* Call into the implementation only if there's actually anything shaped, + otherwise it might not yet have everything properly set up */ + if(_glyphCount) + doGlyphClustersInto(clusters); +} + }} diff --git a/src/Magnum/Text/AbstractShaper.h b/src/Magnum/Text/AbstractShaper.h index 493b4521c..602e36431 100644 --- a/src/Magnum/Text/AbstractShaper.h +++ b/src/Magnum/Text/AbstractShaper.h @@ -133,15 +133,38 @@ 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. +@subsection Text-AbstractShaper-usage-clusters Mapping between input text and shaped glyphs + +For implementing text selection or editing, mapping from screen position to +concrete glyphs can be done using the advances returned from +@ref glyphOffsetsAdvancesInto(). From there however, in the general case, the +text can consist of multi-byte UTF-8 characters, the shaper can perform +ligature substitutions, glyph decomposition or reordering, and thus there's +rarely a 1:1 mapping from the shaped glyphs back to the input text. + +The mapping from glyph IDs to bytes of the text passed to @ref shape() can be +retrieved using @ref glyphClustersInto(). In the following example, a range +between glyphs 2 and 5 is mapped to the input text bytes, for example to copy +it as a selection to clipboard: + +@snippet Text.cpp AbstractShaper-shape-clusters + +In the other direction, picking a range of glyphs corresponding to a range of +input bytes, involves finding cluster IDs with a lower and upper bound for +given byte positions. See the documentation of @ref glyphClustersInto() for +concrete examples of how retrieved cluster IDs may look like depending on what +operations the shaper performs. + @section Text-AbstractShaper-subclassing Subclassing The @ref AbstractFont plugin is meant to create a local @ref AbstractShaper -subclass. It implements at least @ref doShape(), @ref doGlyphIdsInto() and -@ref doGlyphOffsetsAdvancesInto(), 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. +subclass. It implements at least @ref doShape(), @ref doGlyphIdsInto(), +@ref doGlyphOffsetsAdvancesInto() and @ref doGlyphClustersInto(), 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: @@ -330,6 +353,11 @@ class MAGNUM_TEXT_EXPORT AbstractShaper { * May return @ref ShapeDirection::Unspecified if @ref shape() hasn't * been called yet or if the @ref AbstractFont doesn't implement any * script-specific behavior. + * + * The direction affects properties of advances coming from + * @ref glyphOffsetsAdvancesInto() and cluster IDs coming from + * @ref glyphClustersInto(), see particular @ref ShapeDirection values + * for more information. * @see @ref setDirection(), @ref script(), @ref language() */ ShapeDirection direction() const; @@ -367,6 +395,54 @@ class MAGNUM_TEXT_EXPORT AbstractShaper { */ void glyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const; + /** + * @brief Retrieve glyph cluster IDs + * @param[out] clusters Where to put glyph clusters + * + * The @p clusters view is expected to have a size of + * @ref glyphCount(). The cluster IDs are used to map shaped glyphs + * back to the text passed to @ref shape(). By default, the cluster ID + * sequence is monotonically non-decreasing or non-increasing based on + * @ref direction(), with the IDs being byte positions in the original + * text corresponding to particular glyphs: + * + * - For plain ASCII text and with the shaper not performing any + * ligature substitutions, glyph decomposition or reordering, the + * @ref glyphCount() will be equal to the shaped text byte count, + * with clusters being a sequence of @cpp {0, 1, 2, 3, …} @ce, or + * additionally shifted if the @p begin parameter passed to + * @ref shape() was non-zero. + * - For UTF-8 text and the shaper again not performing any ligature + * substitutions, glyph decomposition or reordering, the sequence + * will point to start bytes of multi-byte UTF-8 characters. For + * example @cpp {0, 1, 3, 4, 7, …} @ce, assuming a two-byte UTF-8 + * character at byte @cpp 1 @ce and a three-byte character at byte + * @cpp 4 @ce. Similar output will be if the shaper performs a + * ligature substitution (such as `fi` at byte @cpp 1 @ce and + * `ffl` at byte @cpp 4 @ce both turned into a ligature in an + * otherwise ASCII input). + * - If the shaper performs glyph decomposition, one character in + * the input may end up being multiple glyphs. For example + * @cpp {0, 1, 1, 3, 4, …} @ce, assuming a two-byte UTF-8 + * character `ě` at byte @cpp 1 @ce being decomposed into two + * glyphs, `e` and `ˇ`. + * - If the shaper performs glyph reordering, the cluster ID will + * become the whole range of bytes in which the reordering + * happened, to preserve monotonicity. For example + * @cpp {0, 1, 1, 1, 1, 4, …} @ce, assuming glyphs corresponding + * to bytes @cpp 1 @ce to @cpp 3 @ce were swapped during shaping. + * + * Certain shaper implementations may offer behavior where the + * monotonicity is not preserved or the mapping is not to the original + * input bytes. Such behavior is however never the default, always + * opt-in. See documentation of particular font plugins for more + * information. + * @see @ref Feature::StandardLigatures, + * @ref Feature::GlyphCompositionDecomposition, + * @ref HarfBuzzFont + */ + void glyphClustersInto(const Containers::StridedArrayView1D& clusters) const; + private: /** * @brief Implemenation for @ref setScript() @@ -437,6 +513,14 @@ class MAGNUM_TEXT_EXPORT AbstractShaper { */ virtual void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const = 0; + /** + * @brief Implemenation for @ref glyphClustersInto() + * + * The @p clusters are guaranteed to have a size of @ref glyphCount(). + * Called only if @ref glyphCount() is not @cpp 0 @ce. + */ + virtual void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const = 0; + Containers::Reference _font; UnsignedInt _glyphCount; }; diff --git a/src/Magnum/Text/Direction.h b/src/Magnum/Text/Direction.h index 78f238a24..74b74b9b2 100644 --- a/src/Magnum/Text/Direction.h +++ b/src/Magnum/Text/Direction.h @@ -57,28 +57,36 @@ enum class ShapeDirection: UnsignedByte { /** * Left to right. When returned from @ref AbstractShaper::direction(), * the @p advances filled by @ref AbstractShaper::glyphOffsetsAdvancesInto() - * have their Y components @cpp 0.0f @ce. + * have their Y components @cpp 0.0f @ce, and clusters filled by + * @relativeref{AbstractShaper,glyphClustersInto()} are by default + * monotonically non-decreasing. */ LeftToRight = 1, /** * Right to left. When returned from @ref AbstractShaper::direction(), * the @p advances filled by @ref AbstractShaper::glyphOffsetsAdvancesInto() - * have their Y components @cpp 0.0f @ce. + * have their Y components @cpp 0.0f @ce, and clusters filled by + * @relativeref{AbstractShaper,glyphClustersInto()} are by default + * monotonically non-increasing. */ RightToLeft, /** * Top to bottom. When returned from @ref AbstractShaper::direction(), * the @p advances filled by @ref AbstractShaper::glyphOffsetsAdvancesInto() - * have their X components @cpp 0.0f @ce. + * have their X components @cpp 0.0f @ce, and clusters filled by + * @relativeref{AbstractShaper,glyphClustersInto()} are by default + * monotonically non-decreasing. */ TopToBottom, /** * Bottom to top. When returned from @ref AbstractShaper::direction(), * the @p advances filled by @ref AbstractShaper::glyphOffsetsAdvancesInto() - * have their X components @cpp 0.0f @ce. + * have their X components @cpp 0.0f @ce, and clusters filled by + * @relativeref{AbstractShaper,glyphClustersInto()} are by default + * monotonically non-increasing. */ BottomToTop }; diff --git a/src/Magnum/Text/Test/AbstractFontTest.cpp b/src/Magnum/Text/Test/AbstractFontTest.cpp index cc0a948e2..b74d4fa78 100644 --- a/src/Magnum/Text/Test/AbstractFontTest.cpp +++ b/src/Magnum/Text/Test/AbstractFontTest.cpp @@ -1585,6 +1585,7 @@ void AbstractFontTest::createShaper() { void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} }; struct MyFont: AbstractFont { @@ -1662,6 +1663,10 @@ void AbstractFontTest::layout() { {10.0f, 0.0f}, {20.0f, 0.0f}}, advances); } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* Nothing in the old AbstractLayouter uses this */ + CORRADE_FAIL("This shouldn't be called."); + } }; struct MyFont: AbstractFont { @@ -1812,6 +1817,7 @@ void AbstractFontTest::layoutGlyphOutOfRange() { for(UnsignedInt& i: ids) i = 0; } void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} }; struct MyFont: AbstractFont { diff --git a/src/Magnum/Text/Test/AbstractShaperTest.cpp b/src/Magnum/Text/Test/AbstractShaperTest.cpp index e582f9eb7..7639e703e 100644 --- a/src/Magnum/Text/Test/AbstractShaperTest.cpp +++ b/src/Magnum/Text/Test/AbstractShaperTest.cpp @@ -102,6 +102,7 @@ struct DummyShaper: AbstractShaper { UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} }; void AbstractShaperTest::construct() { @@ -150,6 +151,7 @@ void AbstractShaperTest::setScript() { UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} bool called = false; } shaper{FakeFont}; @@ -176,6 +178,7 @@ void AbstractShaperTest::setLanguage() { UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} bool called = false; } shaper{FakeFont}; @@ -201,6 +204,7 @@ void AbstractShaperTest::setDirection() { UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { return {}; } void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} bool called = false; } shaper{FakeFont}; @@ -262,6 +266,12 @@ void AbstractShaperTest::shape() { advances[1] = {12.0f, 23.0f}; } + void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const override { + CORRADE_COMPARE(clusters.size(), 24); + CORRADE_COMPARE(clusters[0], 667); + clusters[1] = 1336; + } + bool shapeCalled = false; } shaper{FakeFont}; @@ -287,14 +297,18 @@ void AbstractShaperTest::shape() { UnsignedInt ids[24]; Vector2 offsets[24]; Vector2 advances[24]; + UnsignedInt clusters[24]; ids[0] = 1337; offsets[0] = {13.0f, 37.0f}; advances[0] = {42.0f, 69.0f}; + clusters[0] = 667; shaper.glyphIdsInto(ids); shaper.glyphOffsetsAdvancesInto(offsets, advances); + shaper.glyphClustersInto(clusters); CORRADE_COMPARE(ids[1], 666); CORRADE_COMPARE(offsets[1], (Vector2{-4.0f, -5.0f})); CORRADE_COMPARE(advances[1], (Vector2{12.0f, 23.0f})); + CORRADE_COMPARE(clusters[1], 1336); } void AbstractShaperTest::shapeNoFeatures() { @@ -312,6 +326,7 @@ void AbstractShaperTest::shapeNoFeatures() { void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} bool shapeCalled = false; } shaper{FakeFont}; @@ -347,6 +362,7 @@ void AbstractShaperTest::shapeNoBeginEnd() { void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} bool shapeCalled = false; } shaper{FakeFont}; @@ -378,6 +394,7 @@ void AbstractShaperTest::shapeNoBeginEndFeatures() { void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} bool shapeCalled = false; } shaper{FakeFont}; @@ -400,6 +417,7 @@ void AbstractShaperTest::shapeScriptLanguageDirectionNotImplemented() { void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} } shaper{FakeFont}; /* Initially it won't call into any of the implementations */ @@ -438,6 +456,7 @@ void AbstractShaperTest::shapeZeroGlyphs() { void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} } shaper{FakeFont}; CORRADE_COMPARE(shaper.shape("some text", 3, 8), 0); @@ -465,6 +484,7 @@ void AbstractShaperTest::shapeBeginEndOutOfRange() { void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} } shaper{FakeFont}; /* Capture correct function name */ @@ -522,6 +542,9 @@ void AbstractShaperTest::glyphsIntoEmpty() { void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { CORRADE_FAIL("This shouldn't be called"); } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + CORRADE_FAIL("This shouldn't be called"); + } } shaper{FakeFont}; /* Capture correct function name */ @@ -530,6 +553,7 @@ void AbstractShaperTest::glyphsIntoEmpty() { /* This should not assert but also not call anywhere */ shaper.glyphIdsInto(nullptr); shaper.glyphOffsetsAdvancesInto(nullptr, nullptr); + shaper.glyphClustersInto(nullptr); } void AbstractShaperTest::glyphsIntoInvalidViewSizes() { @@ -548,6 +572,9 @@ void AbstractShaperTest::glyphsIntoInvalidViewSizes() { void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { CORRADE_FAIL("This shouldn't be called"); } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + CORRADE_FAIL("This shouldn't be called"); + } } shaper{FakeFont}; CORRADE_COMPARE(shaper.shape("yey"), 5); @@ -557,16 +584,19 @@ void AbstractShaperTest::glyphsIntoInvalidViewSizes() { Vector2 offsetsWrong[6]; Vector2 advances[5]; Vector2 advancesWrong[6]; + UnsignedInt clustersWrong[6]; std::ostringstream out; Error redirectError{&out}; shaper.glyphIdsInto(idsWrong); shaper.glyphOffsetsAdvancesInto(offsetsWrong, advances); shaper.glyphOffsetsAdvancesInto(offsets, advancesWrong); + shaper.glyphClustersInto(clustersWrong); CORRADE_COMPARE(out.str(), "Text::AbstractShaper::glyphIdsInto(): expected the ids view to have a size of 5 but got 6\n" "Text::AbstractShaper::glyphOffsetsAdvancesInto(): expected the offsets and advanced views to have a size of 5 but got 6 and 5\n" - "Text::AbstractShaper::glyphOffsetsAdvancesInto(): expected the offsets and advanced views to have a size of 5 but got 5 and 6\n"); + "Text::AbstractShaper::glyphOffsetsAdvancesInto(): expected the offsets and advanced views to have a size of 5 but got 5 and 6\n" + "Text::AbstractShaper::glyphClustersInto(): expected the clusters view to have a size of 5 but got 6\n"); } }}}} diff --git a/src/Magnum/Text/Test/RendererGLTest.cpp b/src/Magnum/Text/Test/RendererGLTest.cpp index fabaafba5..ab5bea76a 100644 --- a/src/Magnum/Text/Test/RendererGLTest.cpp +++ b/src/Magnum/Text/Test/RendererGLTest.cpp @@ -80,6 +80,10 @@ struct TestShaper: AbstractShaper { advances[i] = {Float(i + 1), i % 2 ? -0.5f : +0.5f}; } } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* Nothing in the renderer uses this API */ + CORRADE_FAIL("This shouldn't be called."); + } }; struct TestFont: AbstractFont { diff --git a/src/Magnum/Text/Test/RendererTest.cpp b/src/Magnum/Text/Test/RendererTest.cpp index 5c3b230fb..4bedfc266 100644 --- a/src/Magnum/Text/Test/RendererTest.cpp +++ b/src/Magnum/Text/Test/RendererTest.cpp @@ -377,6 +377,10 @@ struct TestShaper: AbstractShaper { advances[i] = {Float(i + 1), i % 2 ? -0.5f : +0.5f}; } } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* Nothing in the renderer uses this API */ + CORRADE_FAIL("This shouldn't be called."); + } ShapeDirection _direction; }; @@ -1090,6 +1094,10 @@ void RendererTest::multiline() { advances[i] = Vector2::xAxis(4.0f); } } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* Nothing in the renderer uses this API */ + CORRADE_FAIL("This shouldn't be called."); + } }; struct: AbstractFont {