imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Fractional font width cannot be aligned with itself

Open SlNPacifist opened this issue 9 years ago • 3 comments

When working with monospace font it is common to align different parts using spaces. For example, when one wants to align text "abcd" and "efg", he would usually add a separate space before "efg" like this:

Text("abcd");
Text(" ");
SameLine(0, 0);
Text("efg");

Which will lead to the next output:

abcd
 efg

However, dear imgui rounds up calculated text width to the pixel border so after rendering space cursor will be at rounded position and "efg" will not be aligned with "bcd". This makes using monospace font pretty awkward.

Originally issue started at https://github.com/emoon/ProDBG/issues/304.

SlNPacifist avatar Aug 23 '16 08:08 SlNPacifist

I think I'm hitting this issue too. Calling GetCharacterAdvance slightly undershoots a monospace font's width, calling CalcTextSize slightly overshoots it. When creating a custom text input widget, this makes cursor placement nearly impossible as it d r i f t s from its correct location the longer your text line gets. (even though it works on the default font).

The font I'm running into issues with specifically is Fantasque Sans Mono https://github.com/belluzj/fantasque-sans. Maybe the font's messed up, switching to Fira Code works great.

But I just wanted to flag this in case it helps nailing down the issue in the future!

SamNChiet avatar Oct 09 '21 13:10 SamNChiet

I've bumped into a similar issue. If text is pretty long, CalcTextSize() may add an extra pixel to its width - for me, it was happening on texts 280+ pixels wide. Whenever ImFont::CalcTextSizeA() was computing width to be 280.0 pixels, ImGui::CalcTextSize() was returning x=281.0 instead.

PixelSnapH=true in the font config didn't help since characters were already 8.0 pixels wide.

The issue was caused by the following line within CalcTextSize:

    // FIXME: This has been here since Dec 2015 (7b0bf230) but down the line we want this out.
    // FIXME: Investigate using ceilf or e.g.
    // - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c
    // - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html
    text_size.x = IM_FLOOR(text_size.x + 0.99999f);

Apparently for text_size.x==280.0f, the addition of 0.99999f resulted in a number above 281.0f, which doesn't look like proper rounding :).

I've fixed it in my copy of ImGui by replacing that cumbersome rounding with a call to ceilf(); however, I'm extremely curious to know why wasn't ceil() or ceilf() used in the first place.

Neither is floor() used in this code - here's the definition of IM_FLOOR():

#define IM_FLOOR(_VAL)                  ((float)(int)(_VAL))                                    // ImFloor() is not inlined in MSVC debug builds

Does ImGui avoid those standard functions for performance reasons? This thread on StackOverflow hints that floor() implementation may be suboptimal:

https://stackoverflow.com/questions/824118/why-is-floor-so-slow

One of the answers there also gives a better implementation of ceil() based on conversion to int (I mean, better than adding 0.99999f):

int c(double x)
{
    return (int) x + (x > (int) x);
}

Is it time to finally fix this issue?

v-ein avatar Feb 06 '23 16:02 v-ein

I also encountered IM_TRUNC(text_size.x + 0.99999f) by accident. Initially I planned to make an issue, but post here since it's already talked about here.

In a special use case, I tried to calculate text size by piecing up CalcTextSize of substrings. Though the font has integral size, I found sometimes the final result doesn't always equal CalcTextSize of the entire string.

https://github.com/ocornut/imgui/blob/4ab86e1d61c58688e1c7df90e842687c7a49bfdf/imgui.cpp#L6067-L6071

This will "ceil" large integral values, so due to this, the substrings are short enough (in pixel size), so IM_TRUNC(text_size.x + 0.99999f) doesn't ceil up, but the entire string is long enough to trigger the ceiling.


The following behavior is reproduced in msvc, gcc and clang (in their default settings). The threshold for this behavior is not too large to be rare. Godbolt test: https://godbolt.org/z/PnK5zn8dj

assert(int(255.0f + 0.99999f) == 255.0f);
assert(int(256.0f + 0.99999f) == 257.0f); // This threshold is observed in all 3 compilers.
assert(int(257.0f + 0.99999f) == 258.0f);

The following test demonstrates the irregularties caused by the rounding behavior (requires the default proggyclean font & no manual scaling):

static void issue() {
    auto calc_width = [](int len) { return ImGui::CalcTextSize(std::string(len, ' ').c_str()).x; };
    // Assuming the default proggyclean font & no manual scaling (set `main_scale` to 1 in main()).
    assert(calc_width(1) == 7.0f); // Integral size.

    // The strange rounding behavior causes irregularities:
    assert(calc_width(20) == 140.0f); // Not spuriously ceiled.
    assert(calc_width(40) == 281.0f); // Spuriously ceiled.
    // As a result:
    assert(calc_width(10) + calc_width(10) == calc_width(20));
    assert(calc_width(20) + calc_width(20) < calc_width(40));
    assert(calc_width(40) + calc_width(40) > calc_width(80));
    // So for example:
    // The size of Text(x * " ") cannot be calculated by x * CalcTextSize(" ")
    // Text(str) can be either wider or shorter than Text(substrs) (connected by SameLine(0, 0)), depending on how the substrs are splitted.

    // As to the benefit of changing to `ceilf` - I think it can ensure the following assumptions:
    // assert(calc_width(A) + calc_width(B) >= calc_width(A + B));
    // And if the font has strict integral size, the following holds true.
    // assert(calc_width(A) + calc_width(B) == calc_width(A + B));
}

achabense avatar Nov 20 '25 15:11 achabense