Browse Source

Text: add AbstractShaper::glyphClustersInto().

Essential for text selection and editing in cases where mapping from the
input text to the actual shaped glyphs is nontrivial. I.e., in case of
HarfBuzzFont; both FreeTypeFont and StbTrueTypeFont perform a 1:1
translation from input (UTF-8) characters to glyphs so there it isn't
as important.
pull/641/head
Vladimír Vondruš 2 years ago
parent
commit
2f672eb7de
  1. 21
      doc/snippets/Text.cpp
  2. 2
      src/Magnum/Text/AbstractFont.h
  3. 9
      src/Magnum/Text/AbstractShaper.cpp
  4. 96
      src/Magnum/Text/AbstractShaper.h
  5. 16
      src/Magnum/Text/Direction.h
  6. 6
      src/Magnum/Text/Test/AbstractFontTest.cpp
  7. 32
      src/Magnum/Text/Test/AbstractShaperTest.cpp
  8. 4
      src/Magnum/Text/Test/RendererGLTest.cpp
  9. 8
      src/Magnum/Text/Test/RendererTest.cpp

21
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<Text::AbstractFont> manager;
Containers::Pointer<Text::AbstractFont> font = manager.loadAndInstantiate("SomethingWhatever");
Containers::Pointer<Text::AbstractShaper> shaper = font->createShaper();
/* [AbstractShaper-shape-clusters] */
Containers::StringView text = DOXYGEN_ELLIPSIS({});
shaper->shape(text);
DOXYGEN_ELLIPSIS()
Containers::Array<UnsignedInt> clusters{NoInit, shaper->glyphCount()};
shaper->glyphClustersInto(clusters);
Containers::StringView selection = text.slice(clusters[2], clusters[5]);
/* [AbstractShaper-shape-clusters] */
static_cast<void>(selection);
}
}

2
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

9
src/Magnum/Text/AbstractShaper.cpp

@ -134,4 +134,13 @@ void AbstractShaper::glyphOffsetsAdvancesInto(const Containers::StridedArrayView
doGlyphOffsetsAdvancesInto(offsets, advances);
}
void AbstractShaper::glyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>& 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);
}
}}

96
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<Vector2>& offsets, const Containers::StridedArrayView1D<Vector2>& 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<UnsignedInt>& clusters) const;
private:
/**
* @brief Implemenation for @ref setScript()
@ -437,6 +513,14 @@ class MAGNUM_TEXT_EXPORT AbstractShaper {
*/
virtual void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>& offsets, const Containers::StridedArrayView1D<Vector2>& 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<UnsignedInt>& clusters) const = 0;
Containers::Reference<AbstractFont> _font;
UnsignedInt _glyphCount;
};

16
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
};

6
src/Magnum/Text/Test/AbstractFontTest.cpp

@ -1585,6 +1585,7 @@ void AbstractFontTest::createShaper() {
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) 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<UnsignedInt>&) 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<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
};
struct MyFont: AbstractFont {

32
src/Magnum/Text/Test/AbstractShaperTest.cpp

@ -102,6 +102,7 @@ struct DummyShaper: AbstractShaper {
UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView<const FeatureRange>) override { return {}; }
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
};
void AbstractShaperTest::construct() {
@ -150,6 +151,7 @@ void AbstractShaperTest::setScript() {
UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView<const FeatureRange>) override { return {}; }
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
bool called = false;
} shaper{FakeFont};
@ -176,6 +178,7 @@ void AbstractShaperTest::setLanguage() {
UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView<const FeatureRange>) override { return {}; }
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
bool called = false;
} shaper{FakeFont};
@ -201,6 +204,7 @@ void AbstractShaperTest::setDirection() {
UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView<const FeatureRange>) override { return {}; } void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) 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<UnsignedInt>& 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<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
bool shapeCalled = false;
} shaper{FakeFont};
@ -347,6 +362,7 @@ void AbstractShaperTest::shapeNoBeginEnd() {
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
bool shapeCalled = false;
} shaper{FakeFont};
@ -378,6 +394,7 @@ void AbstractShaperTest::shapeNoBeginEndFeatures() {
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
bool shapeCalled = false;
} shaper{FakeFont};
@ -400,6 +417,7 @@ void AbstractShaperTest::shapeScriptLanguageDirectionNotImplemented() {
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) 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<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) 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<UnsignedInt>&) const override {}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
} shaper{FakeFont};
/* Capture correct function name */
@ -522,6 +542,9 @@ void AbstractShaperTest::glyphsIntoEmpty() {
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {
CORRADE_FAIL("This shouldn't be called");
}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) 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<Vector2>&, const Containers::StridedArrayView1D<Vector2>&) const override {
CORRADE_FAIL("This shouldn't be called");
}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) 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");
}
}}}}

4
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<UnsignedInt>&) const override {
/* Nothing in the renderer uses this API */
CORRADE_FAIL("This shouldn't be called.");
}
};
struct TestFont: AbstractFont {

8
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<UnsignedInt>&) 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<UnsignedInt>&) const override {
/* Nothing in the renderer uses this API */
CORRADE_FAIL("This shouldn't be called.");
}
};
struct: AbstractFont {

Loading…
Cancel
Save