diff --git a/include/CSFML/Graphics/Text.h b/include/CSFML/Graphics/Text.h index ba52b19..75a1619 100644 --- a/include/CSFML/Graphics/Text.h +++ b/include/CSFML/Graphics/Text.h @@ -30,12 +30,15 @@ #include #include +#include #include #include #include +#include #include #include +#include //////////////////////////////////////////////////////////// @@ -50,6 +53,102 @@ typedef enum sfTextStrikeThrough = 1 << 3 ///< Strike through characters } sfTextStyle; +//////////////////////////////////////////////////////////// +/// \brief Line alignment of a multi-line text +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextLineAlignmentDefault, ///< Automatically align lines by script direction, left-align left-to-right text and right-align right-to-left text + sfTextLineAlignmentLeft, ///< Force align all lines to the left, regardless of script direction + sfTextLineAlignmentCenter, ///< Force align all lines centrally + sfTextLineAlignmentRight ///< Force align lines to the right, regardless of script direction +} sfTextLineAlignment; + +//////////////////////////////////////////////////////////// +/// \brief Cluster grouping algorithm +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextClusterGroupingGrapheme, ///< Group clusters by grapheme + sfTextClusterGroupingCharacter, ///< Group clusters by character + sfTextClusterGroupingNone ///< Do not group clusters +} sfTextClusterGrouping; + +//////////////////////////////////////////////////////////// +/// \brief Direction of the text a glyph belongs to +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextDirectionUnspecified, ///< Unspecified + sfTextDirectionLeftToRight, ///< Left-to-right + sfTextDirectionRightToLeft, ///< Right-to-left + sfTextDirectionTopToBottom, ///< Top-to-bottom + sfTextDirectionBottomToTop ///< Bottom-to-top +} sfTextDirection; + +//////////////////////////////////////////////////////////// +/// \brief Text orientation +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextOrientationDefault, ///< Default (left-to-right or right-to-left depending on detected script) + sfTextOrientationTopToBottom, ///< Top-to-bottom + sfTextOrientationBottomToTop ///< Bottom-to-top +} sfTextOrientation; + +//////////////////////////////////////////////////////////// +/// \brief Glyph that has been positioned by the shaper +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + sfGlyph glyph; ///< The glyph + sfVector2f position; ///< Position of the glyph within a text + uint32_t cluster; ///< Cluster ID + sfTextDirection textDirection; ///< Text direction + float baseline; ///< The baseline position of the line this glyph is a part of + size_t vertexOffset; ///< Starting offset of the vertex data belonging to this glyph + size_t vertexCount; ///< Count of vertices belonging to this glyph +} sfShapedGlyph; + +//////////////////////////////////////////////////////////// +/// \brief Callback that is provided with glyph data for pre-processing +/// +/// The callback is called once per glyph whenever the text +/// geometry is regenerated, in the order in which the glyph +/// geometry is generated. The style, fill color, outline color +/// and outline thickness of the glyph can be modified through +/// the given pointers. +/// +/// To map glyphs back to the input string, use the cluster +/// value of the shaped glyph. See sf::Text::GlyphPreProcessor +/// in the SFML documentation for a detailed explanation. +/// +/// Changing the style or outline thickness might lead to +/// slight inconsistencies of the text bounds, changing the +/// fill or outline color is always safe. It is not safe to +/// query the text bounds from within the callback. +/// +/// \param shapedGlyph The shaped glyph to pre-process +/// \param style Style of the glyph (see sfTextStyle enum) +/// \param fillColor Fill color of the glyph +/// \param outlineColor Outline color of the glyph +/// \param outlineThickness Outline thickness of the glyph +/// \param userData User data passed to sfText_setGlyphPreProcessor +/// +//////////////////////////////////////////////////////////// +typedef void (*sfGlyphPreProcessor)( + const sfShapedGlyph* shapedGlyph, + uint32_t* style, + sfColor* fillColor, + sfColor* outlineColor, + float* outlineThickness, + void* userData); + //////////////////////////////////////////////////////////// /// \brief Create a new text @@ -367,6 +466,31 @@ CSFML_GRAPHICS_API void sfText_setOutlineColor(sfText* text, sfColor color); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API void sfText_setOutlineThickness(sfText* text, float thickness); +//////////////////////////////////////////////////////////// +/// \brief Set the line alignment for a multi-line text +/// +/// By default, the line alignment is sfTextLineAlignmentDefault. +/// +/// \param text Text object +/// \param lineAlignment New line alignment +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setLineAlignment(sfText* text, sfTextLineAlignment lineAlignment); + +//////////////////////////////////////////////////////////// +/// \brief Set the text orientation +/// +/// By default, the text orientation is sfTextOrientationDefault. +/// +/// Vertical text orientations require the font to provide +/// vertical metrics, see sfFontInfo. +/// +/// \param text Text object +/// \param textOrientation New text orientation +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setTextOrientation(sfText* text, sfTextOrientation textOrientation); + //////////////////////////////////////////////////////////// /// \brief Get the string of a text (returns an ANSI string) /// @@ -475,6 +599,26 @@ CSFML_GRAPHICS_API sfColor sfText_getOutlineColor(const sfText* text); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API float sfText_getOutlineThickness(const sfText* text); +//////////////////////////////////////////////////////////// +/// \brief Get the line alignment for a multi-line text +/// +/// \param text Text object +/// +/// \return Line alignment +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfTextLineAlignment sfText_getLineAlignment(const sfText* text); + +//////////////////////////////////////////////////////////// +/// \brief Get the text orientation +/// +/// \param text Text object +/// +/// \return Text orientation +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfTextOrientation sfText_getTextOrientation(const sfText* text); + //////////////////////////////////////////////////////////// /// \brief Return the position of the \a index-th character in a text /// @@ -495,6 +639,109 @@ CSFML_GRAPHICS_API float sfText_getOutlineThickness(const sfText* text); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API CSFML_DEPRECATED sfVector2f sfText_findCharacterPos(const sfText* text, size_t index); +//////////////////////////////////////////////////////////// +/// \brief Get the shaped glyphs that make up a text +/// +/// The result of shaping, i.e. positioning individual glyphs +/// based on the properties of the font and the input text, +/// is a sequence of shaped glyphs. In addition to the glyph +/// information that is available by looking up a glyph from +/// a font, the glyph position, glyph cluster ID and direction +/// of the text represented by the glyph is provided. +/// +/// When positioning e.g. a cursor within the text, grapheme +/// clusters can be treated as the basic units of which the +/// text is composed. See sfText_setClusterGrouping. +/// +/// The returned glyph positions are in local coordinates +/// (translation, rotation, scale and origin are not applied). +/// +/// The returned array is owned by the text and stays valid +/// until the next call to this function or until the text +/// is destroyed. +/// +/// \param text Text object +/// \param count Pointer to a variable that will be filled with the number of shaped glyphs +/// +/// \return Pointer to the array of shaped glyphs +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API const sfShapedGlyph* sfText_getShapedGlyphs(const sfText* text, size_t* count); + +//////////////////////////////////////////////////////////// +/// \brief Get the cluster grouping algorithm in use +/// +/// \param text Text object +/// +/// \return The cluster grouping algorithm in use +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfTextClusterGrouping sfText_getClusterGrouping(const sfText* text); + +//////////////////////////////////////////////////////////// +/// \brief Set the cluster grouping algorithm to use +/// +/// By default, character cluster grouping is used. +/// +/// Character cluster grouping is good enough to be able to +/// position cursors in most scenarios. If more coarse-grained +/// grouping is required, grapheme grouping can be selected. +/// +/// Cluster grouping can also be disabled if necessary. +/// +/// \param text Text object +/// \param clusterGrouping The cluster grouping algorithm to use +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setClusterGrouping(sfText* text, sfTextClusterGrouping clusterGrouping); + +//////////////////////////////////////////////////////////// +/// \brief Set the glyph pre-processor to be called per glyph +/// +/// The glyph pre-processor is called with glyph data to be +/// pre-processed whenever the text geometry is regenerated. +/// +/// \param text Text object +/// \param glyphPreProcessor The glyph pre-processor to be called per glyph, pass NULL to disable pre-processing +/// \param userData User data that will be passed to the glyph pre-processor +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setGlyphPreProcessor(sfText* text, sfGlyphPreProcessor glyphPreProcessor, void* userData); + +//////////////////////////////////////////////////////////// +/// \brief Get the vertex data of a text +/// +/// The vertices form triangles (sfTriangles). +/// +/// The vertex data is regenerated by the text whenever it is +/// necessary. Any changes made to the vertex data will be +/// discarded whenever this happens. +/// +/// \param text Text object +/// \param count Pointer to a variable that will be filled with the number of vertices +/// +/// \return Pointer to the vertex data of the text +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfVertex* sfText_getVertexData(const sfText* text, size_t* count); + +//////////////////////////////////////////////////////////// +/// \brief Get the outline vertex data of a text +/// +/// The vertices form triangles (sfTriangles). +/// +/// The outline vertex data is regenerated by the text whenever +/// it is necessary. Any changes made to the outline vertex data +/// will be discarded whenever this happens. +/// +/// \param text Text object +/// \param count Pointer to a variable that will be filled with the number of vertices +/// +/// \return Pointer to the outline vertex data of the text +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfVertex* sfText_getOutlineVertexData(const sfText* text, size_t* count); + //////////////////////////////////////////////////////////// /// \brief Get the local bounding rectangle of a text /// diff --git a/src/CSFML/Graphics/Text.cpp b/src/CSFML/Graphics/Text.cpp index d838219..83e488b 100644 --- a/src/CSFML/Graphics/Text.cpp +++ b/src/CSFML/Graphics/Text.cpp @@ -26,6 +26,7 @@ // Headers //////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -33,6 +34,22 @@ #include #include + +namespace +{ +// Helper function for converting a SFML shaped glyph to a CSFML one +[[nodiscard]] sfShapedGlyph convertShapedGlyph(const sf::Text::ShapedGlyph& shapedGlyph) +{ + return {convertGlyph(shapedGlyph.glyph), + convertVector2(shapedGlyph.position), + shapedGlyph.cluster, + static_cast(shapedGlyph.textDirection), + shapedGlyph.baseline, + shapedGlyph.vertexOffset, + shapedGlyph.vertexCount}; +} +} // namespace + #include @@ -250,6 +267,22 @@ void sfText_setOutlineThickness(sfText* text, float thickness) } +//////////////////////////////////////////////////////////// +void sfText_setLineAlignment(sfText* text, sfTextLineAlignment lineAlignment) +{ + assert(text); + text->setLineAlignment(static_cast(lineAlignment)); +} + + +//////////////////////////////////////////////////////////// +void sfText_setTextOrientation(sfText* text, sfTextOrientation textOrientation) +{ + assert(text); + text->setTextOrientation(static_cast(textOrientation)); +} + + //////////////////////////////////////////////////////////// const char* sfText_getString(const sfText* text) { @@ -333,6 +366,22 @@ float sfText_getOutlineThickness(const sfText* text) } +//////////////////////////////////////////////////////////// +sfTextLineAlignment sfText_getLineAlignment(const sfText* text) +{ + assert(text); + return static_cast(text->getLineAlignment()); +} + + +//////////////////////////////////////////////////////////// +sfTextOrientation sfText_getTextOrientation(const sfText* text) +{ + assert(text); + return static_cast(text->getTextOrientation()); +} + + //////////////////////////////////////////////////////////// sfVector2f sfText_findCharacterPos(const sfText* text, size_t index) { @@ -341,6 +390,101 @@ sfVector2f sfText_findCharacterPos(const sfText* text, size_t index) } +//////////////////////////////////////////////////////////// +const sfShapedGlyph* sfText_getShapedGlyphs(const sfText* text, size_t* count) +{ + assert(text); + assert(count); + + const auto& shapedGlyphs = text->getShapedGlyphs(); + text->ShapedGlyphs.clear(); + text->ShapedGlyphs.reserve(shapedGlyphs.size()); + for (const auto& shapedGlyph : shapedGlyphs) + text->ShapedGlyphs.push_back(convertShapedGlyph(shapedGlyph)); + + *count = text->ShapedGlyphs.size(); + return text->ShapedGlyphs.data(); +} + + +//////////////////////////////////////////////////////////// +sfTextClusterGrouping sfText_getClusterGrouping(const sfText* text) +{ + assert(text); + return static_cast(text->getClusterGrouping()); +} + + +//////////////////////////////////////////////////////////// +void sfText_setClusterGrouping(sfText* text, sfTextClusterGrouping clusterGrouping) +{ + assert(text); + text->setClusterGrouping(static_cast(clusterGrouping)); +} + + +//////////////////////////////////////////////////////////// +void sfText_setGlyphPreProcessor(sfText* text, sfGlyphPreProcessor glyphPreProcessor, void* userData) +{ + assert(text); + + if (!glyphPreProcessor) + { + text->setGlyphPreProcessor({}); + return; + } + + text->setGlyphPreProcessor( + [glyphPreProcessor, userData](const sf::Text::ShapedGlyph& shapedGlyph, + std::uint32_t& style, + sf::Color& fillColor, + sf::Color& outlineColor, + float& outlineThickness) + { + const sfShapedGlyph glyph = convertShapedGlyph(shapedGlyph); + sfColor csfmlFillColor = convertColor(fillColor); + sfColor csfmlOutlineColor = convertColor(outlineColor); + glyphPreProcessor(&glyph, &style, &csfmlFillColor, &csfmlOutlineColor, &outlineThickness, userData); + fillColor = convertColor(csfmlFillColor); + outlineColor = convertColor(csfmlOutlineColor); + }); +} + + +//////////////////////////////////////////////////////////// +sfVertex* sfText_getVertexData(const sfText* text, size_t* count) +{ + assert(text); + assert(count); + + // Make sure the geometry is up to date, sf::Text only updates it when needed + [[maybe_unused]] const auto bounds = text->getLocalBounds(); + + sf::VertexArray& vertices = text->getVertexData(); + *count = vertices.getVertexCount(); + + // the cast is safe, sfVertex has to be binary compatible with sf::Vertex + return *count > 0 ? reinterpret_cast(&vertices[0]) : nullptr; +} + + +//////////////////////////////////////////////////////////// +sfVertex* sfText_getOutlineVertexData(const sfText* text, size_t* count) +{ + assert(text); + assert(count); + + // Make sure the geometry is up to date, sf::Text only updates it when needed + [[maybe_unused]] const auto bounds = text->getLocalBounds(); + + sf::VertexArray& vertices = text->getOutlineVertexData(); + *count = vertices.getVertexCount(); + + // the cast is safe, sfVertex has to be binary compatible with sf::Vertex + return *count > 0 ? reinterpret_cast(&vertices[0]) : nullptr; +} + + //////////////////////////////////////////////////////////// sfFloatRect sfText_getLocalBounds(const sfText* text) { diff --git a/src/CSFML/Graphics/TextStruct.hpp b/src/CSFML/Graphics/TextStruct.hpp index 628504a..7dc7de4 100644 --- a/src/CSFML/Graphics/TextStruct.hpp +++ b/src/CSFML/Graphics/TextStruct.hpp @@ -29,11 +29,13 @@ //////////////////////////////////////////////////////////// #include #include +#include #include #include #include +#include //////////////////////////////////////////////////////////// @@ -42,8 +44,9 @@ struct sfText : sf::Text { using sf::Text::Text; - const sfFont* Font{}; - mutable std::string String; - mutable sfTransform Transform{}; - mutable sfTransform InverseTransform{}; + const sfFont* Font{}; + mutable std::string String; + mutable sfTransform Transform{}; + mutable sfTransform InverseTransform{}; + mutable std::vector ShapedGlyphs; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 24e2890..e613c17 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -56,6 +56,7 @@ add_executable(test-csfml-graphics Graphics/RenderStates.test.cpp Graphics/Shape.test.cpp Graphics/StencilMode.test.cpp + Graphics/Text.test.cpp Graphics/Transform.test.cpp Graphics/VertexArray.test.cpp Graphics/View.test.cpp diff --git a/test/Graphics/Text.test.cpp b/test/Graphics/Text.test.cpp new file mode 100644 index 0000000..d8d3e95 --- /dev/null +++ b/test/Graphics/Text.test.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include + +#include + +TEST_CASE("[Graphics] sfText") +{ + SECTION("sfTextLineAlignment") + { + STATIC_CHECK(sfTextLineAlignmentDefault == static_cast(sf::Text::LineAlignment::Default)); + STATIC_CHECK(sfTextLineAlignmentLeft == static_cast(sf::Text::LineAlignment::Left)); + STATIC_CHECK(sfTextLineAlignmentCenter == static_cast(sf::Text::LineAlignment::Center)); + STATIC_CHECK(sfTextLineAlignmentRight == static_cast(sf::Text::LineAlignment::Right)); + } + + SECTION("sfTextClusterGrouping") + { + STATIC_CHECK(sfTextClusterGroupingGrapheme == static_cast(sf::Text::ClusterGrouping::Grapheme)); + STATIC_CHECK(sfTextClusterGroupingCharacter == static_cast(sf::Text::ClusterGrouping::Character)); + STATIC_CHECK(sfTextClusterGroupingNone == static_cast(sf::Text::ClusterGrouping::None)); + } + + SECTION("sfTextDirection") + { + STATIC_CHECK(sfTextDirectionUnspecified == static_cast(sf::Text::TextDirection::Unspecified)); + STATIC_CHECK(sfTextDirectionLeftToRight == static_cast(sf::Text::TextDirection::LeftToRight)); + STATIC_CHECK(sfTextDirectionRightToLeft == static_cast(sf::Text::TextDirection::RightToLeft)); + STATIC_CHECK(sfTextDirectionTopToBottom == static_cast(sf::Text::TextDirection::TopToBottom)); + STATIC_CHECK(sfTextDirectionBottomToTop == static_cast(sf::Text::TextDirection::BottomToTop)); + } + + SECTION("sfTextOrientation") + { + STATIC_CHECK(sfTextOrientationDefault == static_cast(sf::Text::TextOrientation::Default)); + STATIC_CHECK(sfTextOrientationTopToBottom == static_cast(sf::Text::TextOrientation::TopToBottom)); + STATIC_CHECK(sfTextOrientationBottomToTop == static_cast(sf::Text::TextOrientation::BottomToTop)); + } + + SECTION("Set/get layout properties") + { + sfFont* font = sfFont_createFromFile("Graphics/tuffy.ttf"); + REQUIRE(font != nullptr); + sfText* text = sfText_create(font); + + CHECK(sfText_getLineAlignment(text) == sfTextLineAlignmentDefault); + CHECK(sfText_getTextOrientation(text) == sfTextOrientationDefault); + CHECK(sfText_getClusterGrouping(text) == sfTextClusterGroupingCharacter); + + sfText_setLineAlignment(text, sfTextLineAlignmentCenter); + sfText_setTextOrientation(text, sfTextOrientationTopToBottom); + sfText_setClusterGrouping(text, sfTextClusterGroupingGrapheme); + + CHECK(sfText_getLineAlignment(text) == sfTextLineAlignmentCenter); + CHECK(sfText_getTextOrientation(text) == sfTextOrientationTopToBottom); + CHECK(sfText_getClusterGrouping(text) == sfTextClusterGroupingGrapheme); + + sfText* copy = sfText_copy(text); + CHECK(sfText_getLineAlignment(copy) == sfTextLineAlignmentCenter); + sfText_destroy(copy); + + sfText_destroy(text); + sfFont_destroy(font); + } +}