Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add text alignment to sf::Text #2713

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions include/SFML/Graphics/Text.hpp
Expand Up @@ -63,6 +63,17 @@ class SFML_GRAPHICS_API Text : public Drawable, public Transformable
StrikeThrough = 1 << 3 //!< Strike through characters
};

////////////////////////////////////////////////////////////
/// \brief Enumeration of the text alignment options
///
////////////////////////////////////////////////////////////
enum LineAlignment
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enum LineAlignment
enum class LineAlignment

{
Left, //!< Align lines to the left
Center, //!< Align lines centrally
Right //!< Align lines to the right
};

////////////////////////////////////////////////////////////
/// \brief Construct the text from a string, font and size
///
Expand Down Expand Up @@ -237,6 +248,18 @@ class SFML_GRAPHICS_API Text : public Drawable, public Transformable
////////////////////////////////////////////////////////////
void setOutlineThickness(float thickness);

////////////////////////////////////////////////////////////
/// \brief Set the line alignment for a multi-line text
///
/// By default, the line alignment is Left.
///
/// \param lineAlignment New line alignment
///
/// \see getLineAlignment
///
////////////////////////////////////////////////////////////
void setLineAlignment(LineAlignment lineAlignment);

////////////////////////////////////////////////////////////
/// \brief Get the text's string
///
Expand Down Expand Up @@ -339,6 +362,16 @@ class SFML_GRAPHICS_API Text : public Drawable, public Transformable
////////////////////////////////////////////////////////////
float getOutlineThickness() const;

////////////////////////////////////////////////////////////
/// \brief Get the line alignment for a multi-line text
///
/// \return Line alignment
///
/// \see setLineAlignment
///
////////////////////////////////////////////////////////////
LineAlignment getLineAlignment() const;

////////////////////////////////////////////////////////////
/// \brief Return the position of the \a index-th character
///
Expand Down Expand Up @@ -403,6 +436,33 @@ class SFML_GRAPHICS_API Text : public Drawable, public Transformable
////////////////////////////////////////////////////////////
void ensureGeometryUpdate() const;

////////////////////////////////////////////////////////////
/// \brief Calculate and update the line offsets
///
/// This is required before updating geometry or
/// finding the character position as this information
/// is needed in both places.
///
////////////////////////////////////////////////////////////
void updateLineOffsets() const;

////////////////////////////////////////////////////////////
/// \brief Structure storing spacing information
///
////////////////////////////////////////////////////////////
struct Spacing
{
float whitespaceWidth{};
float letterSpacing{};
float lineSpacing{};
};

////////////////////////////////////////////////////////////
/// \brief Calculate various text spacing values
///
////////////////////////////////////////////////////////////
Spacing getSpacing() const;

////////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
Expand All @@ -415,11 +475,13 @@ class SFML_GRAPHICS_API Text : public Drawable, public Transformable
Color m_fillColor{Color::White}; //!< Text fill color
Color m_outlineColor{Color::Black}; //!< Text outline color
float m_outlineThickness{0.f}; //!< Thickness of the text's outline
LineAlignment m_lineAlignment{Left}; //!< Line alignment for a multi-line text
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LineAlignment m_lineAlignment{Left}; //!< Line alignment for a multi-line text
LineAlignment m_lineAlignment{LineAlignment::Left}; //!< Line alignment for a multi-line text

mutable VertexArray m_vertices{PrimitiveType::Triangles}; //!< Vertex array containing the fill geometry
mutable VertexArray m_outlineVertices{PrimitiveType::Triangles}; //!< Vertex array containing the outline geometry
mutable FloatRect m_bounds; //!< Bounding rectangle of the text (in local coordinates)
mutable bool m_geometryNeedUpdate{}; //!< Does the geometry need to be recomputed?
mutable std::uint64_t m_fontTextureId{}; //!< The font texture id
mutable std::vector<float> m_lineOffsets; //!< The horizontal offsets of each line of a multi-line text
};

} // namespace sf
Expand Down
176 changes: 143 additions & 33 deletions src/SFML/Graphics/Text.cpp
Expand Up @@ -40,7 +40,8 @@ namespace
{
// Add an underline or strikethrough line to the vertex array
void addLine(sf::VertexArray& vertices,
float lineLength,
float lineLeft,
float lineRight,
float lineTop,
const sf::Color& color,
float offset,
Expand All @@ -50,15 +51,18 @@ void addLine(sf::VertexArray& vertices,
const float top = std::floor(lineTop + offset - (thickness / 2) + 0.5f);
const float bottom = top + std::floor(thickness + 0.5f);

vertices.append(sf::Vertex(sf::Vector2f(-outlineThickness, top - outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(
sf::Vertex(sf::Vector2f(lineLength + outlineThickness, top - outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(sf::Vertex(sf::Vector2f(-outlineThickness, bottom + outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(sf::Vertex(sf::Vector2f(-outlineThickness, bottom + outlineThickness), color, sf::Vector2f(1, 1)));
sf::Vertex(sf::Vector2f(lineLeft - outlineThickness, top - outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(
sf::Vertex(sf::Vector2f(lineLength + outlineThickness, top - outlineThickness), color, sf::Vector2f(1, 1)));
sf::Vertex(sf::Vector2f(lineRight + outlineThickness, top - outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(
sf::Vertex(sf::Vector2f(lineLength + outlineThickness, bottom + outlineThickness), color, sf::Vector2f(1, 1)));
sf::Vertex(sf::Vector2f(lineLeft - outlineThickness, bottom + outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(
sf::Vertex(sf::Vector2f(lineLeft - outlineThickness, bottom + outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(
sf::Vertex(sf::Vector2f(lineRight + outlineThickness, top - outlineThickness), color, sf::Vector2f(1, 1)));
vertices.append(
sf::Vertex(sf::Vector2f(lineRight + outlineThickness, bottom + outlineThickness), color, sf::Vector2f(1, 1)));
}

// Add a glyph quad to the vertex array
Expand Down Expand Up @@ -217,6 +221,15 @@ void Text::setOutlineThickness(float thickness)
}


////////////////////////////////////////////////////////////
void Text::setLineAlignment(LineAlignment lineAlignment)
{
if (m_lineAlignment != lineAlignment)
m_geometryNeedUpdate = true;
m_lineAlignment = lineAlignment;
}


////////////////////////////////////////////////////////////
const String& Text::getString() const
{
Expand Down Expand Up @@ -280,22 +293,29 @@ float Text::getOutlineThickness() const
}


////////////////////////////////////////////////////////////
Text::LineAlignment Text::getLineAlignment() const
{
return m_lineAlignment;
}


////////////////////////////////////////////////////////////
Vector2f Text::findCharacterPos(std::size_t index) const
{
// Adjust the index if it's out of range
if (index > m_string.getSize())
index = m_string.getSize();

// Calculate and update the line offsets
updateLineOffsets();

// Precompute the variables needed by the algorithm
const bool isBold = m_style & Bold;
float whitespaceWidth = m_font->getGlyph(U' ', m_characterSize, isBold).advance;
const float letterSpacing = (whitespaceWidth / 3.f) * (m_letterSpacingFactor - 1.f);
whitespaceWidth += letterSpacing;
const float lineSpacing = m_font->getLineSpacing(m_characterSize) * m_lineSpacingFactor;
const bool isBold = m_style & Bold;
const auto [whitespaceWidth, letterSpacing, lineSpacing] = getSpacing();

// Compute the position
Vector2f position;
Vector2f position(m_lineOffsets[0], 0.f); // There will always be at least one line
vittorioromeo marked this conversation as resolved.
Show resolved Hide resolved
std::uint32_t prevChar = 0;
for (std::size_t i = 0; i < index; ++i)
{
Expand Down Expand Up @@ -387,6 +407,9 @@ void Text::ensureGeometryUpdate() const
if (m_string.isEmpty())
return;

// Calculate and update the line offsets
updateLineOffsets();

// Compute values related to the text style
const bool isBold = m_style & Bold;
const bool isUnderlined = m_style & Underlined;
Expand All @@ -402,19 +425,18 @@ void Text::ensureGeometryUpdate() const
const float strikeThroughOffset = xBounds.top + xBounds.height / 2.f;

// Precompute the variables needed by the algorithm
float whitespaceWidth = m_font->getGlyph(U' ', m_characterSize, isBold).advance;
const float letterSpacing = (whitespaceWidth / 3.f) * (m_letterSpacingFactor - 1.f);
whitespaceWidth += letterSpacing;
const float lineSpacing = m_font->getLineSpacing(m_characterSize) * m_lineSpacingFactor;
float x = 0.f;
auto y = static_cast<float>(m_characterSize);
const auto [whitespaceWidth, letterSpacing, lineSpacing] = getSpacing();
float x = m_lineOffsets[0]; // there will always be at least one line
vittorioromeo marked this conversation as resolved.
Show resolved Hide resolved
auto y = static_cast<float>(m_characterSize);

// Create one quad for each character
auto minX = static_cast<float>(m_characterSize);
auto minY = static_cast<float>(m_characterSize);
float maxX = 0.f;
float maxY = 0.f;
std::uint32_t prevChar = 0;
auto minX = static_cast<float>(m_characterSize);
auto minY = static_cast<float>(m_characterSize);
float maxX = 0.f;
float maxY = 0.f;
std::uint32_t prevChar = 0;
std::size_t line = 0;
float horizontalOffset = x;
for (std::size_t i = 0; i < m_string.getSize(); ++i)
{
const std::uint32_t curChar = m_string[i];
Expand All @@ -429,19 +451,26 @@ void Text::ensureGeometryUpdate() const
// If we're using the underlined style and there's a new line, draw a line
if (isUnderlined && (curChar == U'\n' && prevChar != U'\n'))
{
addLine(m_vertices, x, y, m_fillColor, underlineOffset, underlineThickness);
addLine(m_vertices, horizontalOffset, x, y, m_fillColor, underlineOffset, underlineThickness);

if (m_outlineThickness != 0)
addLine(m_outlineVertices, x, y, m_outlineColor, underlineOffset, underlineThickness, m_outlineThickness);
addLine(m_outlineVertices, horizontalOffset, x, y, m_outlineColor, underlineOffset, underlineThickness, m_outlineThickness);
}

// If we're using the strike through style and there's a new line, draw a line across all characters
if (isStrikeThrough && (curChar == U'\n' && prevChar != U'\n'))
{
addLine(m_vertices, x, y, m_fillColor, strikeThroughOffset, underlineThickness);
addLine(m_vertices, horizontalOffset, x, y, m_fillColor, strikeThroughOffset, underlineThickness);

if (m_outlineThickness != 0)
addLine(m_outlineVertices, x, y, m_outlineColor, strikeThroughOffset, underlineThickness, m_outlineThickness);
addLine(m_outlineVertices,
horizontalOffset,
x,
y,
m_outlineColor,
strikeThroughOffset,
underlineThickness,
m_outlineThickness);
}

prevChar = curChar;
Expand All @@ -463,7 +492,9 @@ void Text::ensureGeometryUpdate() const
break;
case U'\n':
y += lineSpacing;
x = 0;
++line;
horizontalOffset = m_lineOffsets[line];
x = horizontalOffset;
break;
}

Expand Down Expand Up @@ -518,19 +549,19 @@ void Text::ensureGeometryUpdate() const
// If we're using the underlined style, add the last line
if (isUnderlined && (x > 0))
{
addLine(m_vertices, x, y, m_fillColor, underlineOffset, underlineThickness);
addLine(m_vertices, horizontalOffset, x, y, m_fillColor, underlineOffset, underlineThickness);

if (m_outlineThickness != 0)
addLine(m_outlineVertices, x, y, m_outlineColor, underlineOffset, underlineThickness, m_outlineThickness);
addLine(m_outlineVertices, horizontalOffset, x, y, m_outlineColor, underlineOffset, underlineThickness, m_outlineThickness);
}

// If we're using the strike through style, add the last line across all characters
if (isStrikeThrough && (x > 0))
{
addLine(m_vertices, x, y, m_fillColor, strikeThroughOffset, underlineThickness);
addLine(m_vertices, horizontalOffset, x, y, m_fillColor, strikeThroughOffset, underlineThickness);

if (m_outlineThickness != 0)
addLine(m_outlineVertices, x, y, m_outlineColor, strikeThroughOffset, underlineThickness, m_outlineThickness);
addLine(m_outlineVertices, horizontalOffset, x, y, m_outlineColor, strikeThroughOffset, underlineThickness, m_outlineThickness);
}

// Update the bounding rectangle
Expand All @@ -540,4 +571,83 @@ void Text::ensureGeometryUpdate() const
m_bounds.height = maxY - minY;
}


////////////////////////////////////////////////////////////
void Text::updateLineOffsets() const
{
// temporary use m_lineOffsets to store widths of each line
m_lineOffsets.clear();

// Precompute the variables needed by the algorithm
const bool isBold = m_style & Bold;
const auto [whitespaceWidth, letterSpacing, lineSpacing] = getSpacing();

Vector2f position;
std::uint32_t prevChar = 0;
float maxWidth = 0.f;
for (std::size_t i = 0; i < m_string.getSize(); ++i)
{
const std::uint32_t curChar = m_string[i];

// Apply the kerning offset
position.x += m_font->getKerning(prevChar, curChar, m_characterSize, isBold);
prevChar = curChar;

// Handle special characters
switch (curChar)
{
case U' ':
position.x += whitespaceWidth;
continue;
case U'\t':
position.x += whitespaceWidth * 4;
continue;
case U'\n':
position.y += lineSpacing;
if (position.x > maxWidth)
maxWidth = position.x;
m_lineOffsets.push_back(position.x);
position.x = 0;
continue;
}

// For regular characters, add the advance offset of the glyph
position.x += m_font->getGlyph(curChar, m_characterSize, isBold).advance + letterSpacing;
}

// add final part of the string since last newline as the final line
// (this is the entire string if text is single line without newlines)
m_lineOffsets.push_back(position.x);

// convert widths into offsets depending on line alignment
for (std::size_t i = 0; i < m_lineOffsets.size(); ++i)
{
switch (m_lineAlignment)
{
case Right:
m_lineOffsets[i] = maxWidth - m_lineOffsets[i];
break;
case Center:
m_lineOffsets[i] = (maxWidth - m_lineOffsets[i]) / 2.f;
break;
case Left:
m_lineOffsets[i] = 0.f;
break;
}
m_lineOffsets[i] = std::round(m_lineOffsets[i]);
}
}


////////////////////////////////////////////////////////////
Text::Spacing Text::getSpacing() const
{
const bool isBold = m_style & Bold;
float whitespaceWidth = m_font->getGlyph(U' ', m_characterSize, isBold).advance;
const float letterSpacing = (whitespaceWidth / 3.f) * (m_letterSpacingFactor - 1.f);
whitespaceWidth += letterSpacing;
const float lineSpacing = m_font->getLineSpacing(m_characterSize) * m_lineSpacingFactor;
return {whitespaceWidth, letterSpacing, lineSpacing};
}

} // namespace sf