imgui icon indicating copy to clipboard operation
imgui copied to clipboard

How do I scale the GUI?

Open JC3 opened this issue 2 years ago • 15 comments

I'm using imgui via pyimgui and GLFW.

How do I scale the entire GUI?

I need to support high DPI displays. I can easily get a scale factor from GLFW, but I can't figure out how to apply the scale factor to imgui.

I've found various discussions on the topic but all of them are pretty confusing. Also a lot of them involve rebuilding imgui's fonts, which seems a little weird, since assuming imgui is drawing fonts on textured quads my gut is that the quads can just be drawn ... bigger.

JC3 avatar Oct 27 '23 19:10 JC3

https://github.com/ocornut/imgui/issues?q=is%3Aissue+is%3Aopen+dpi+label%3Adpi

emoon avatar Oct 27 '23 20:10 emoon

FAQ answer stands: https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-how-should-i-handle-dpi-in-my-application

my gut is that the quads can just be drawn ... bigger.

If you used upscaled texture the quality would be rather low. Loading fonts at the right size is currently the way. I think it will improve in the future but for now it is what it is.

ocornut avatar Oct 27 '23 20:10 ocornut

@ocornut

FAQ answer stands: https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-how-should-i-handle-dpi-in-my-application

Thanks. Ok, couple of questions:

  1. I'm not currently using any sort of "style structure"; how do I obtain one?
  2. I'm not currently loading any fonts I'm just using the default font (my app just uses a menu bar over some other OpenGL stuff). I don't have any particular TTF files available. How do I reload the default font and scale the default font size?

my gut is that the quads can just be drawn ... bigger.

If you used upscaled texture the quality would be rather low. Loading fonts at the right size is currently the way. I think it will improve in the future but for now it is what it is.

Fwiw I'm OK with lower quality scaling, especially if it's the only cost to a simpler scaling API. Perhaps it could be one option for scaling, in situations where you don't mind the quality drop. 🤷‍♂️

JC3 avatar Oct 29 '23 17:10 JC3

@emoon

https://github.com/ocornut/imgui/issues?q=is%3Aissue+is%3Aopen+dpi+label%3Adpi

Thanks. I've seen a good number of those already. They all seem to either be doing things a different way, or describe a failed attempt. But I'll keep looking.

By the way, I noticed one of those issues mentions ImGuiConfigFlags_DpiEnableScaleFonts, which isn't something I've seen mentioned before. What is that?

JC3 avatar Oct 29 '23 17:10 JC3

https://github.com/ocornut/imgui/issues/797 mentions:

The ImGui sample app shows how to use the in-built font scaling

But the example apps I looked at (https://github.com/ocornut/imgui/blob/master/examples/example_glfw_opengl3/main.cpp and the others) didn't seem to do any font scaling. What is the "in-built font scaling" and which sample app shows how to use it?

JC3 avatar Oct 29 '23 17:10 JC3

https://github.com/ocornut/imgui/issues/6176 mentions io.FontGlobalScale, but the FAQ does not mention it. Is there a reason not to use it? I can't seem to find any documentation for it, either, although I did find it in the header (along with DisplayFramebufferScale).

JC3 avatar Oct 29 '23 17:10 JC3

I seem to be getting decent results by doing this (GLFW + Python, but you get the gist):

scale = glfw.get_window_content_scale(window) # returns (xscale, yscale)
io.font_global_scale = max(scale) # arbitrarily pick max of the two; they're usually identical
io.display_fb_scale = scale # this is ::DisplayFramebufferScale

That said, I'm only really using a menu bar, so I'm not yet sure if this has other layout issues.

JC3 avatar Oct 29 '23 19:10 JC3

We are not recommending io.FontGlobalScale because the visual quality is very poor, but if you are happy with this then its fine.

You can also call GetStyle().ScaleAllSizes(factor) to scale spacings and paddings similarly.

ocornut avatar Oct 30 '23 11:10 ocornut

You can also call GetStyle().ScaleAllSizes(factor) to scale spacings and paddings similarly.

Darn, it seems pyimgui doesn't expose this function; what is the simplest way to approximate it?

JC3 avatar Nov 02 '23 18:11 JC3

@JC3 The method is pretty simple, you could pretty easily reimplement it in Python:

https://github.com/ocornut/imgui/blob/1ab63d925f21e03be7735661500e5b914dd93c19/imgui.cpp#L1226-L1254

You can replace ImTrunc with math.trunc.

PathogenDavid avatar Nov 02 '23 20:11 PathogenDavid

@PathogenDavid Thanks!

If anybody is interested, here is a Python (pyimgui) implementation, it seems to be working very well:

def _imgui_scale_all_sizes (style, hscale: float, vscale: float) -> None:
    """pyimgui is missing ImGuiStyle::ScaleAllSizes(); this is a reimplementation of it."""
    
    scale = max(hscale, vscale)
    
    def scale_it (attrname: str) -> None:
        value = getattr(style, attrname)
        if isinstance(value, imgui.Vec2):
            value = imgui.Vec2(math.trunc(value.x * hscale), math.trunc(value.y * vscale))
            setattr(style, attrname, value)
        else:
            setattr(style, attrname, math.trunc(value * scale))
    
    scale_it("window_padding")
    scale_it("window_rounding")
    scale_it("window_min_size")
    scale_it("child_rounding")
    scale_it("popup_rounding")
    scale_it("frame_padding")
    scale_it("frame_rounding")
    scale_it("item_spacing")
    scale_it("item_inner_spacing")
    scale_it("cell_padding")
    scale_it("touch_extra_padding")
    scale_it("indent_spacing")
    scale_it("columns_min_spacing")
    scale_it("scrollbar_size")
    scale_it("scrollbar_rounding")
    scale_it("grab_min_size")
    scale_it("grab_rounding")
    scale_it("log_slider_deadzone")
    scale_it("tab_rounding")
    scale_it("tab_min_width_for_close_button")
    #scale_it("separator_text_padding")  # not present in current pyimgui
    scale_it("display_window_padding")
    scale_it("display_safe_area_padding")
    scale_it("mouse_cursor_scale")

JC3 avatar Nov 04 '23 14:11 JC3

I think I found a pretty good way for handling High-DPI modes (or scaling in general) with ImGui.

First a short note on High-DPI behavior on different platforms:

  • macOS and Wayland use a high-res framebuffer (in physical pixels) and lower-res window coordinates/sizes (in logical points)
  • Windows and X11 (at least when used with glfw or SDL3), use the same size for pixels and window coordinates, but communicate how much you should scale your UI.

And remember: You'll have to set a custom vector-based (TTF) font, the builtin bitmap font of course doesn't scale well.

Also note that ImGui operates in logical points everywhere and then scales the coordinates up to physical pixels when rendering (with io.DisplayFramebufferScale).

Values from SDL/glfw/whatever we're operating with:

  • framebuffer_size_in_pixels - the size of the framebuffer you draw to in physical pixels, as returned by glfwGetFramebufferSize() or SDL_GetWindowSizeInPixels() or (in SDL2) SDL_GL_GetDrawableSize()
  • window_size_in_points - the size of the window in logical points, as returned by glfwGetWindowSize() or SDL_GetWindowSize(). On some platforms it's the same as the framebuffer size, on others it isn't
  • content_scale - factor by which the content (in physical pixels) should be scaled to have the expected size, for example 1.25 if you configured 125% desktop scaling. glfw's glfwGetWindowContentScale() gives separate values for horizontal and vertical direction, SDL3's SDL_GetWindowDisplayScale() returns just one value for both directions (on most platforms that the factor is the same for both directions anyway). For SDL2 dividing the DPI you get from SDL_GetDisplayDPI() by 96 (160 on iPhone and Android) should give the same value.
  • imgui_coord_scale - the factor (ImVec2) that ImGui multiplies its coordinates (in logical points) with to get physical pixels. It's framebuffer_size_in_pixels / window_size_in_points. ImGui has this value in io.DisplayFramebufferScale but you'll have to calculate it manually because it only gets set in the first frame (in ImGui_ImplGlfw_NewFrame() or equivalents for SDL etc), but you may want to load the your font before that.

On "normal" displays (that are not High-DPI), framebuffer_size_in_pixels == window_size_in_points, so imgui_coord_scale == {1.0, 1.0}, same for content_scale.

On macOS or Wayland High-DPI modes, framebuffer_size_in_pixels and window_size_in_points are quite different, so imgui_coord_scale is for example {1.25, 1.25} for 125% desktop scaling; I think Apples "Retina" modes always use {2.0, 2.0}. Here imgui_coord_scale == content_scale.

On Windows or X11 High-DPI modes, framebuffer_size_in_pixels == window_size_in_points, so imgui_coord_scale == {1.0, 1.0}, but content_scale has a different value indicating how much you should scale, like {1.25, 1.25} for 125% desktop scaling.

So for Windows/X11 you'll have to do the ImGui scaling manually by increasing the font size and scaling the style, while for macOS and Wayland the scaling is already handled by ImGui (unless you want to apply an additional factor).

There is one problem with the automatic scaling on macOS/Wayland: Fonts are still loaded at the configured size, which is in logical points, but when loading the font it's pixels. So the font gets stretched when rendering and thus doesn't look as sharp as it should.
The best solution I found for macOS/Wayland is to set fontCfg.RasterizerDensity to imgui_coord_scale. It will make ImGui load the font in size font_size*fontCfg.RasterizerDensity, but otherwise treat it like it only has font_size.

Anyway, here's how to configure ImGui to scale properly on all platforms, unifying both the needs of Windows/X11 and macOS/Wayland (and additional scaling by the user, if you want) with sharp fonts (as far as possible), in pseudo-code, assuming the values described above are already set:

// how much scaling must be done "manually"
// by scaling up the font and style sizes
float sx = content_scale.x / imgui_coord_scale.x;
float sy = content_scale.y / imgui_coord_scale.y;
// turn this into a single factor
float imgui_additional_scale = Max(sx, sy);
// if you want to allow users to scale even more (optional)
imgui_additional_scale *= user_configured_scale;
// this assumes you'd use font size 16 on "normal" displays
float font_size = 16.0 * imgui_additional_scale;
// ImGui wants the font size to be an integer and > 0
font_size = Max(1.0, roundf(fontSize));

ImFontConfig font_cfg = {};
// RasterizerDensity allows increasing the font "density"
// without changing its logical size. Increasing it by
// the scale ImGui applies automatically compensates
// for ImGui loading the font at a too low resolution
font_cfg.RasterizerDensity = Max(imgui_coord_scale.x, imgui_coord_scale.y);
// load the font (adjust to the way you use to load them,
//  like AddFontFromFileTTF())
io.Fonts->AddFontFromWhatever(loadargs, font_size, &fontCfg);

// scale the style
// this assumes you copied your unscaled (!!)
// style to style_backup on startup
ImGui::GetStyle() = style_backup;
ImGui::GetStyle().ScaleAllSizes(imgui_additional_scale);

style.ScaleAllSizes(val) just multiplies the relevant values (padding sizes etc) in the style with val, so calling it twice would scale twice.. that's why you need a backup of your unscaled style that you restore before scaling. At least if you want to be able to reload the font on runtime (for example to react to changed content_scale or to let the user change the scaling).

Here is the same in real code for glfw, with extensive documentation (comments): https://github.com/DanielGibson/texview/blob/762de2b76200515b6ed785f492003c4b9728a41b/src/main.cpp#L1345

While this works pretty well overall, it still has one problem that would have to be fixed on the ImGui-side: Apparently fonts are best loaded at an integer size (and ImFontAtlas::AddFont() enforces that with new_font_cfg.SizePixels = ImTrunc(new_font_cfg.SizePixels);). But when setting RasterizerDensity, ImGui uses src_tmp.PackRange.font_size = cfg.SizePixels * cfg.RasterizerDensity; which may not be an integer, depending on the values.
I'm not sure what the best solution for this would be - maybe allow setting a non-integer fontsize and only truncate the result of font_size * cfg.RasterizerDensity? Then I could do something like

font_cfg.RasterizerDensity = Max(imgui_coord_scale.x, imgui_coord_scale.y);
float realFontSize = 16.0 * imgui_additional_scale * font_cfg.RasterizerDensity;
realFontSize = Max(1.0, roundf(realFontSize)); // round this to integer
font_size = realFontSize / font_cfg.RasterizerDensity;

and pass that (potentially fractional) font_size to io.Fonts->AddFont...(), and when the font is actually loaded src_tmp.PackRange.font_size = cfg.SizePixels * cfg.RasterizerDensity; gets an integer value (+/- rounding errors).

But maybe there's a better way, and maybe it's handled differently with the new font loading code anyway (I haven't checked that out yet). Also note that this doesn't look super-bad even if the font is loaded with a non-integer size, I just think it could look even better :)

Update: Or maybe ImGui should automatically scale the loaded fontsize by io.DisplayFramebufferScale (and maybe round that to the nearest integer), so one doesn't have to explicitly set font_cfg.RasterizerDensity? As the upscaling of coordinates happens automatically, doing this automatically too would make sense, I think

Update2: Fixed a bug in the pseudo-code (used content_scale where it should've been imgui_coord_scale)

DanielGibson avatar Apr 28 '25 03:04 DanielGibson

I'm in the middle of implementing HiDPI support for my program, and I'm just going to write down my thoughts here.

Since I'm maintaining an SDL3-based backend in C# for my project, I can easily adjust knobs on the backend too. I ended up doing this:

  • Keep DisplayFramebufferScale equal to 1.0f at all times (vertices submitted by imgui are never scaled before rendering)
  • Use SDL_GetWindowSizeInPixels to determine the framebuffer size (should work on both windows and mac, despite platform differences mentioned above)
  • Scale mouse inputs from native coordinates to pixels using SDL_GetWindowPixelDensity
  • Manually scale all elements with the scale factor from "SDL_GetWindowDisplayScale" (this involves rendering larger fonts, using "ScaleAllSizes()", and avoiding hardcoded size/position constants as mentioned in the FAQ).

The result is that I'm effectively bypassing the OS's local coordinate system and handling all coordinates in pixels. This makes things a bit simpler - I can have one global scale factor rather than two separate ones (DisplayFramebufferScale + imgui_additional_scale in the comment above). It seems to work well on both Wayland and X11 (which behave like mac and windows, respectively, regarding their coordinate systems - see: https://wiki.libsdl.org/SDL3/README-highdpi)

I feel like this probably isn't recommended, but it works. I find the platform-specific differences in DPI handling somewhat maddening; this method basically erases those differences so the results should be identical on all platforms (ie. the Mac & Wayland font scaling issues mentioned in the above comment vanish). I'm wondering if there's a reason I should be avoiding this that I'm not accounting for?

One disadvantage to doing it this way is that my ImGui drawing code needs to account for the scale factor. As an alternative, you could just as well set "DisplayFramebufferScale" to the desired value and it should scale up as needed, the problem of course being that fonts lose their crispyness if you don't approach that carefully (see above).

Stewmath avatar May 10 '25 20:05 Stewmath

handling everything in pixels makes editing styles on High-DPI screens basically impossible, at least if the result should also work on a normal-DPI screen. See also https://github.com/ocornut/imgui/issues/5081#issuecomment-2839344822 for more thoughts on approaches to scaling

DanielGibson avatar May 11 '25 16:05 DanielGibson

Interesting point. When the new font API and DPI handling changes are merged, maybe using local coordinates would be preferable. Until then I think using pixel coordinates is the best way to guarantee fonts are sharp.

Either way, I think it would be nice if imgui picked one coordinate system and stuck with it, regardless of the platform! Would save some headaches. (I assume SDL must have their reasons for sticking with platform-native coordinate systems, but I don't know what they are.)

Stewmath avatar May 11 '25 17:05 Stewmath