Skip to content

Graphics/Text: add optional rounding for letter and line spacing#3638

Open
Praful-Joshi wants to merge 3 commits into
SFML:masterfrom
Praful-Joshi:TextSpacingRoudingIssue#3595
Open

Graphics/Text: add optional rounding for letter and line spacing#3638
Praful-Joshi wants to merge 3 commits into
SFML:masterfrom
Praful-Joshi:TextSpacingRoudingIssue#3595

Conversation

@Praful-Joshi
Copy link
Copy Markdown

This pull request introduces optional pixel rounding for text letter spacing and line spacing in sf::Text.

When using fractional spacing factors (e.g. setLetterSpacing(1.1f)), glyph positions may fall on sub-pixel coordinates, which can lead to blurred text due to texture filtering and rasterization behavior. This PR adds a way to opt into pixel-aligned spacing without changing the existing default behavior.

What’s new:

  • Added setLetterSpacingRounding(bool) / getLetterSpacingRounding()
  • Added setLineSpacingRounding(bool) / getLineSpacingRounding()
  • When enabled, rounding is applied during geometry generation only
  • Default behavior remains unchanged (rounding disabled)

This allows users to choose between:

  • Exact mathematical spacing (current default)
  • Sharper text rendering when using fractional spacing values

Related issue:
Fixes #3595

Checklist:

  • Has this change been discussed in an issue before: Yes
  • Does the code follow the SFML Code Style Guide: Yes
  • Have you provided example/test code for your changes: Yes

Tasks:

  • Tested on macOS

How to test this PR?
The effect is most visible when using fractional spacing values.

Minimal example:
#include <SFML/Graphics.hpp>

int main()
{
sf::RenderWindow window(sf::VideoMode({800, 600}), "Text spacing rounding test");
window.setFramerateLimit(60);

sf::Font font;
font.openFromFile("font.ttf");

sf::Text text(font);
text.setString("SFML TEXT SPACING TEST\nThe quick brown fox jumps over the lazy dog");
text.setCharacterSize(24);
text.setLetterSpacing(1.1f);
text.setLineSpacing(1.1f);

// New API
text.setLetterSpacingRounding(true);
text.setLineSpacingRounding(true);

text.setPosition({50.f, 50.f});

while (window.isOpen())
{
    while (const auto event = window.pollEvent())
    {
        if (event->is<sf::Event::Closed>())
            window.close();
    }

    window.clear(sf::Color(36, 36, 71));
    window.draw(text);
    window.display();
}

}

Without Rounding:
Screenshot 2026-01-14 at 1 30 11 PM

With Rounding:
Screenshot 2026-01-14 at 1 32 04 PM

Compare the output with rounding enabled vs disabled to clearly see the difference in sharpness.

Comment thread src/SFML/Graphics/Text.cpp Outdated
Comment on lines +821 to +825
const float letterSpacingFactor = m_roundLetterSpacing ? roundToPixel(m_letterSpacingFactor) : m_letterSpacingFactor;
const float letterSpacing = (whitespaceWidth / 3.0f) * (letterSpacingFactor - 1.0f);
const float lineSpacingFactor = m_roundLineSpacing ? roundToPixel(m_lineSpacingFactor) : m_lineSpacingFactor;
const float lineSpacing = m_font->getLineSpacing(m_characterSize) * lineSpacingFactor;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is rounding applied to letterSpacingFactor and lineSpacingFactor instead of letterSpacing and lineSpacing, i.e. the final values used to adjust spacing?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I wasn't sure if I should mess with letterSpacing and lineSpacing as they are also dependent on whitespaceWidth so I just decided to round m_letterSpacingFactor and m_lineSpacingFactor instead. But you're right, rounding should be applied to the final spacing values to avoid changing their semantic meaning. I’ll move the rounding accordingly.

Comment thread include/SFML/Graphics/Text.hpp Outdated
Comment on lines +296 to +307
////////////////////////////////////////////////////////////
/// \brief Round a value to the nearest pixel coordinate
///
/// This function rounds the given floating-point value to
/// the nearest integer value, which corresponds to a pixel
/// boundary in window coordinates.
///
/// \param value Value to round
///
/// \return Rounded value
////////////////////////////////////////////////////////////
float roundToPixel(float value) const;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see why this method is needed as part of the public interface, or even as a helper method. It has nothing to do with an sf::Text instance, being just a wrapper around std::round. You can just use that directly in the implementation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. I was contemplating real hard on this one. roundToPixel doesn’t belong in the public interface and doesn’t add value over std::round. I’ll remove it and apply rounding directly in the implementation.

Comment on lines +466 to +483
////////////////////////////////////////////////////////////
void Text::setLetterSpacingRounding(bool enabled)
{
if(m_roundLetterSpacing != enabled)
{
m_roundLetterSpacing = enabled;
}
}


////////////////////////////////////////////////////////////
void Text::setLineSpacingRounding(bool enabled)
{
if(m_roundLineSpacing != enabled)
{
m_roundLineSpacing = enabled;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changing these settings affect the geometry, so you do need to set m_geometryNeedUpdate = true; if the values have changed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch! this was an oversight. I’ll make sure toggling the rounding mode properly invalidates geometry by setting m_geometryNeedUpdate = true.

Comment thread include/SFML/Graphics/Text.hpp Outdated
Comment on lines +470 to +488
////////////////////////////////////////////////////////////
/// \brief Tell whether letter spacing rounding is enabled
///
/// \return True if letter spacing rounding is enabled, false otherwise
///
/// \see `setLetterSpacingRounding`
///
////////////////////////////////////////////////////////////
[[nodiscard]] bool getLetterSpacingRounding() const;

////////////////////////////////////////////////////////////
/// \brief Tell whether line spacing rounding is enabled
///
/// \return True if line spacing rounding is enabled, false otherwise
///
/// \see `setLineSpacingRounding`
///
////////////////////////////////////////////////////////////
[[nodiscard]] bool getLineSpacingRounding() const;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the use case of separating the rounding mode vertically and horizontally, instead of using a single setting for it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yaah I was wondering this too. I just moved ahead with the separate implementation to be on the safer side. I’ll consolidate this into one spacing-rounding setting applied consistently to both directions.

@Praful-Joshi
Copy link
Copy Markdown
Author

@Marioalexsan Done. I made a commit with the fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text: Add ability to round lineSpacing and letterSpacing to the closest integer

2 participants