cimgui-go icon indicating copy to clipboard operation
cimgui-go copied to clipboard

Add example for using fonts

Open Omustardo opened this issue 5 months ago • 2 comments

I've spent a good bit of time trying to work with fonts recently. First to just trying to load them, and then using them at different sizes and scales. I wanted to add to the examples/ directory, but as I was writing the example I realized that I don't actually understand some of the FontConfig options, and there are a few remaining issues. Instead I'll include what I've done here as a reference for others.

Example code

https://gist.github.com/Omustardo/79db81d8fddf91fa22d8caa4b94bd1ac

References

  • https://github.com/ocornut/imgui/blob/master/docs/FONTS.md
  • https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-fonts-text
  • https://github.com/AllenDang/giu/blob/master/FontAtlasProsessor.go
  • https://github.com/ocornut/imgui/issues/1018#issuecomment-491875324
  • https://github.com/ocornut/imgui/issues/1018#issuecomment-1891041578

Golang <-> C issues

When loading a font, the functions (e.g. AddFontFromMemoryTTF) require a uintptr, but it wasn't clear what this should refer to. Claude eventually got me to a working solution:

var defaultFontTTF []byte // load the content of a .ttf file into this slice
fontDataPtr := uintptr(unsafe.Pointer(&defaultFontTTF[0]))
fontDataLen := int32(len(defaultFontTTF))
imgui.CurrentIO().Fonts().AddFontFromMemoryTTF(fontDataPtr, fontDataLen, float32(DefaultFontSize))

If you crash with the message: "munmap_chunk(): invalid pointer", it's because C code is trying to free memory that's managed by golang. To avoid this, I loaded fonts using FontConfig structs, which let me mark the uintptr as not being owned by imgui's font atlas:

fontDataPtr := uintptr(unsafe.Pointer(&defaultFontTTF[0]))
fontDataLen := int32(len(defaultFontTTF))

cfg := imgui.NewFontConfig()
// This option lets golang manage the memory of the provided data.
// If this is true, imgui will crash when trying to clean up with the error: `munmap_chunk(): invalid pointer`
cfg.SetFontDataOwnedByAtlas(false)

cfg.SetSizePixels(pixelSize)
cfg.SetFontData(fontDataPtr)
cfg.SetFontDataSize(fontDataLen)
cfg.SetOversampleV(1)
cfg.SetOversampleV(1)
cfg.SetPixelSnapH(true)
return imgui.CurrentIO().Fonts().AddFont(cfg)

Compressed fonts

There's also a way to compress fonts before loading them, like imgui.CurrentIO().Fonts().AddFontFromMemoryCompressedBase85TTF()

I didn't try this out myself, but the compressed input can be created with https://github.com/ocornut/imgui/blob/master/misc/fonts/binary_to_compressed_c.cpp

This shouldn't ever be needed in Go, because it's easier to just embed TTF files. See the example section below for an example of this.

When to load

imgui has some information and documentation about loading fonts, but strangely I didn't find anything about how to load them while using an imgui backend.

If I put the font loading within my loop, I got:

panic: Assertion failed!
        File: /home/runner/work/cimgui-go/cimgui-go/cimgui/imgui/imgui_draw.cpp, Line 2177
        
        Expression: !Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"

That was a bit surprising because I never call NewFrame() anywhere - it's handled by the backend.

I tried loading from the main method, before the main loop and got:

panic: Assertion failed!
        File: /home/runner/work/cimgui-go/cimgui-go/cimgui/imgui/imgui.cpp, Line 4386
        
        Expression: GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"

Instead, load fonts within the backend.SetBeforeRenderHook callback. Note that this function is called every frame so you should guard the loading of fonts with a boolean so it isn't done more than once.

Remaining issues

In the example (https://gist.github.com/Omustardo/79db81d8fddf91fa22d8caa4b94bd1ac), I tried to set the current font size and scale:

	oldSize := imgui.CurrentFont().FontSize()
	imgui.CurrentFont().SetFontSize(userConfigurableFontSize)
	imgui.Text(fmt.Sprintf("5. Font with user-configurabled size: %.2f", userConfigurableFontSize))
	imgui.CurrentFont().SetFontSize(oldSize)

	oldScale := imgui.CurrentFont().Scale()
	imgui.CurrentFont().SetScale(userConfigurableFontScale)
	imgui.Text(fmt.Sprintf("6. Font with user-configurabled scale: %.2f", userConfigurableFontScale))
	imgui.CurrentFont().SetScale(oldScale)

I'm almost certainly mis-using these, but I don't think they work in an intuitive manner.

  • SetFontSize: If you create a font with pixelSize 25 and then SetFontSize(25), it looks the same as if you didn't SetFontSize. If you call SetFontSize(50), the text shows up as half of its original size. Similarly, if you use a smaller font size than the initial size, the font actually looks larger! It seems like this sets a ratio that's based on the size that the text was originally loaded at. I'm not sure why it's like this, or what the expected usage is.

  • With SetFontSize, widgets don't adjust around the resized text. This causes larger text to overlaps other content. I'd probably stay away from this option and just load fonts at the desired sizes in the first place, but there may be something that I'm missing that would make this work.

  • Setting the font scale doesn't seem to do anything at all.

Omustardo avatar Sep 15 '24 22:09 Omustardo