Skip to content

Commit

Permalink
add text alignment to sf::Text
Browse files Browse the repository at this point in the history
adds ability to align all lines of a Text to the left, centre or the right.
  • Loading branch information
Hapaxia committed Sep 28, 2023
1 parent 3f6403e commit 3283d23
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 18 deletions.
44 changes: 44 additions & 0 deletions include/SFML/Graphics/Text.hpp
Expand Up @@ -62,6 +62,16 @@ class SFML_GRAPHICS_API Text : public Drawable, public Transformable
Underlined = 1 << 2, //!< Underlined characters
StrikeThrough = 1 << 3 //!< Strike through characters
};
////////////////////////////////////////////////////////////
/// \brief Enumeration of the text alignment options
///
////////////////////////////////////////////////////////////
enum LineAlignment
{
Left = -1, //!< Align lines to the left
Center = 0, //!< Align lines centrally
Right = 1 //!< Align lines to the right
};

////////////////////////////////////////////////////////////
/// \brief Construct the text from a string, font and size
Expand Down Expand Up @@ -237,6 +247,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 +361,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 +435,16 @@ 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;

////////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
Expand All @@ -415,11 +457,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
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
132 changes: 114 additions & 18 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,15 @@ 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(lineLeft - 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(lineRight + outlineThickness, top - 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(lineLeft - outlineThickness, bottom + 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(lineRight + outlineThickness, bottom + outlineThickness), color, sf::Vector2f(1, 1)));
}

// Add a glyph quad to the vertex array
Expand Down Expand Up @@ -217,6 +218,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,13 +290,23 @@ 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;
Expand All @@ -295,7 +315,7 @@ Vector2f Text::findCharacterPos(std::size_t index) const
const float lineSpacing = m_font->getLineSpacing(m_characterSize) * m_lineSpacingFactor;

// Compute the position
Vector2f position;
Vector2f position{ m_lineOffsets[0u], 0.f }; // there will always be at least one line
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 @@ -406,7 +429,7 @@ void Text::ensureGeometryUpdate() const
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;
float x = m_lineOffsets[0u]; // there will always be at least one line
auto y = static_cast<float>(m_characterSize);

// Create one quad for each character
Expand All @@ -415,6 +438,8 @@ void Text::ensureGeometryUpdate() const
float maxX = 0.f;
float maxY = 0.f;
std::uint32_t prevChar = 0;
std::size_t line = 0u;
float lineOffset = x;
for (std::size_t i = 0; i < m_string.getSize(); ++i)
{
const std::uint32_t curChar = m_string[i];
Expand All @@ -429,19 +454,19 @@ 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, lineOffset, x, y, m_fillColor, underlineOffset, underlineThickness);

if (m_outlineThickness != 0)
addLine(m_outlineVertices, x, y, m_outlineColor, underlineOffset, underlineThickness, m_outlineThickness);
addLine(m_outlineVertices, lineOffset, 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, lineOffset, x, y, m_fillColor, strikeThroughOffset, underlineThickness);

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

prevChar = curChar;
Expand All @@ -463,7 +488,9 @@ void Text::ensureGeometryUpdate() const
break;
case U'\n':
y += lineSpacing;
x = 0;
++line;
lineOffset = (line < m_lineOffsets.size()) ? m_lineOffsets[line] : 0.f;
x = lineOffset;
break;
}

Expand Down Expand Up @@ -518,19 +545,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, lineOffset, x, y, m_fillColor, underlineOffset, underlineThickness);

if (m_outlineThickness != 0)
addLine(m_outlineVertices, x, y, m_outlineColor, underlineOffset, underlineThickness, m_outlineThickness);
addLine(m_outlineVertices, lineOffset, 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, lineOffset, x, y, m_fillColor, strikeThroughOffset, underlineThickness);

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

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

////////////////////////////////////////////////////////////
void Text::updateLineOffsets() const
{
// widths of each line
std::vector<float> lineWidths;

// 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;

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;
lineWidths.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)
lineWidths.push_back(position.x);

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

} // namespace sf

0 comments on commit 3283d23

Please sign in to comment.