From 371b894c310e2ff3c84e485ccc04c689a4668f32 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 10 Sep 2026 01:43:37 +0200 Subject: [PATCH 1/2] imgui_dwrite: add a DirectWrite font loader --- imconfig.h | 6 +- imgui.cpp | 22 ++ imgui.h | 3 +- imgui_draw.cpp | 29 +- imgui_internal.h | 8 +- misc/README.txt | 4 + misc/dwrite/README.md | 34 ++ misc/dwrite/imgui_dwrite.cpp | 471 +++++++++++++++++++++++++++ misc/dwrite/imgui_dwrite.h | 13 + misc/freetype/imgui_freetype.cpp | 6 + misc/single_file/imgui_single_file.h | 6 + 11 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 misc/dwrite/README.md create mode 100644 misc/dwrite/imgui_dwrite.cpp create mode 100644 misc/dwrite/imgui_dwrite.h diff --git a/imconfig.h b/imconfig.h index d9c05692f..0ce03162a 100644 --- a/imconfig.h +++ b/imconfig.h @@ -100,8 +100,12 @@ //#define IMGUI_ENABLE_FREETYPE_PLUTOSVG //#define IMGUI_ENABLE_FREETYPE_LUNASVG +//---- Use DirectWrite to rasterize fonts (Windows only). +// Requires misc/dwrite/imgui_dwrite.cpp. Takes precedence over IMGUI_ENABLE_FREETYPE. +//#define IMGUI_ENABLE_DWRITE + //---- Use stb_truetype to build and rasterize the font atlas (default) -// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. +// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType or DirectWrite backend. //#define IMGUI_ENABLE_STB_TRUETYPE //#define IMGUI_DISABLE_STB_TRUETYPE diff --git a/imgui.cpp b/imgui.cpp index 5bf2ec239..5add6e1ec 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -16741,6 +16741,9 @@ static const char* FormatTextureRefForDebugDisplay(char* buf, int buf_size, ImTe return buf; } +#ifdef IMGUI_ENABLE_DWRITE +namespace ImGuiDWrite { IMGUI_API const ImFontLoader* GetFontLoader(); } +#endif #ifdef IMGUI_ENABLE_FREETYPE namespace ImGuiFreeType { IMGUI_API const ImFontLoader* GetFontLoader(); IMGUI_API bool DebugEditFontLoaderFlags(unsigned int* p_font_builder_flags); } #endif @@ -16790,7 +16793,14 @@ void ImGui::ShowFontAtlas(ImFontAtlas* atlas) { const ImFontLoader* loader_current = atlas->FontLoader; IM_UNUSED(loader_current); + + bool has_font_family = false; + for (const ImFontConfig& src : atlas->Sources) + if (src.FontLoader == NULL) + has_font_family |= (src.FontData == NULL); + BeginDisabled(!atlas->RendererHasTextures); + BeginDisabled(has_font_family); #ifdef IMGUI_ENABLE_STB_TRUETYPE const ImFontLoader* loader_stbtruetype = ImFontAtlasGetFontLoaderForStbTruetype(); if (RadioButton("stb_truetype", loader_current == loader_stbtruetype)) @@ -16825,6 +16835,18 @@ void ImGui::ShowFontAtlas(ImFontAtlas* atlas) SetItemTooltip("Requires #define IMGUI_ENABLE_FREETYPE + imgui_freetype.cpp."); EndDisabled(); #endif + EndDisabled(); + SameLine(); + #ifdef IMGUI_ENABLE_DWRITE + const ImFontLoader* loader_dwrite = ImGuiDWrite::GetFontLoader(); + if (RadioButton("DirectWrite", loader_current == loader_dwrite)) + atlas->SetFontLoader(loader_dwrite); + #else + BeginDisabled(); + RadioButton("DirectWrite", false); + SetItemTooltip("Requires #define IMGUI_ENABLE_DWRITE + imgui_dwrite.cpp."); + EndDisabled(); + #endif EndDisabled(); TreePop(); } diff --git a/imgui.h b/imgui.h index 22a8b6da1..c2013de4e 100644 --- a/imgui.h +++ b/imgui.h @@ -3701,7 +3701,7 @@ struct ImTextureData struct ImFontConfig { // Data Source - char Name[40]; // // Name (strictly to ease debugging, hence limited size buffer) + char Name[64]; // // Debug name, or UTF-8 font family name when FontData is NULL. void* FontData; // // TTF/OTF data int FontDataSize; // // TTF/OTF data size bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the owner ImFontAtlas (will delete memory itself). SINCE 1.92, THE DATA NEEDS TO PERSIST FOR WHOLE DURATION OF ATLAS. @@ -3825,6 +3825,7 @@ struct ImFontAtlas IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); // Selects between AddFontDefaultVector() and AddFontDefaultBitmap(). IMGUI_API ImFont* AddFontDefaultVector(const ImFontConfig* font_cfg = NULL); // Embedded scalable font. Recommended at any higher size. IMGUI_API ImFont* AddFontDefaultBitmap(const ImFontConfig* font_cfg = NULL); // Embedded classic pixel-clean font. Recommended at Size 13px with no scaling. + IMGUI_API ImFont* AddFontFromFamily(const char* family, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL); // Requires a loader supporting font families (e.g. DirectWrite). IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 83ceaddc0..5f78ec7f7 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -36,6 +36,9 @@ Index of this file: #include "imgui.h" #ifndef IMGUI_DISABLE #include "imgui_internal.h" +#ifdef IMGUI_ENABLE_DWRITE +#include "misc/dwrite/imgui_dwrite.h" +#endif #ifdef IMGUI_ENABLE_FREETYPE #include "misc/freetype/imgui_freetype.h" #endif @@ -4287,7 +4290,6 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg_in) { // Sanity Checks IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!"); - IM_ASSERT((font_cfg_in->FontData != NULL && font_cfg_in->FontDataSize > 0) || (font_cfg_in->FontLoader != NULL)); //IM_ASSERT(font_cfg_in->SizePixels > 0.0f && "Is ImFontConfig struct correctly initialized?"); IM_ASSERT(font_cfg_in->RasterizerDensity > 0.0f && "Is ImFontConfig struct correctly initialized?"); if (font_cfg_in->GlyphOffset.x != 0.0f || font_cfg_in->GlyphOffset.y != 0.0f || font_cfg_in->GlyphMinAdvanceX != 0.0f || font_cfg_in->GlyphMaxAdvanceX != FLT_MAX) @@ -4504,6 +4506,21 @@ ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* font_data, int font_data_size, f return AddFont(&font_cfg); } +ImFont* ImFontAtlas::AddFontFromFamily(const char* family, float size_pixels, const ImFontConfig* font_cfg_template) +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!"); + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL && font_cfg.FontDataSize == 0); + if (family == NULL || family[0] == '\0' || ImStrlen(family) >= IM_COUNTOF(font_cfg.Name)) + { + IM_ASSERT_USER_ERROR(0, "Invalid font family name."); + return NULL; + } + ImStrncpy(font_cfg.Name, family, IM_COUNTOF(font_cfg.Name)); + font_cfg.SizePixels = size_pixels > 0.0f ? size_pixels : font_cfg.SizePixels; + return AddFont(&font_cfg); +} + ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) { const unsigned int buf_decompressed_size = stb_decompress_length((const unsigned char*)compressed_ttf_data); @@ -5801,7 +5818,9 @@ void ImFontAtlasBuildInit(ImFontAtlas* atlas) // and point to it instead of pointing directly to return value of the GetFontLoaderXXX functions. if (atlas->FontLoader == NULL) { -#ifdef IMGUI_ENABLE_FREETYPE + #ifdef IMGUI_ENABLE_DWRITE + atlas->SetFontLoader(ImGuiDWrite::GetFontLoader()); + #elif defined(IMGUI_ENABLE_FREETYPE) atlas->SetFontLoader(ImGuiFreeType::GetFontLoader()); #elif defined(IMGUI_ENABLE_STB_TRUETYPE) atlas->SetFontLoader(ImFontAtlasGetFontLoaderForStbTruetype()); @@ -6225,6 +6244,12 @@ struct ImGui_ImplStbTrueType_FontSrcData static bool ImGui_ImplStbTrueType_FontSrcInit(ImFontAtlas*, ImFontConfig* src) { + if (src->FontData == NULL || src->FontDataSize <= 0) + { + IM_ASSERT_USER_ERROR(0, "stb_truetype requires font data; font families are not supported."); + return false; + } + ImGui_ImplStbTrueType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplStbTrueType_FontSrcData); IM_ASSERT(src->FontLoaderData == NULL); diff --git a/imgui_internal.h b/imgui_internal.h index 74d91a7d4..93222faf1 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -124,9 +124,9 @@ Index of this file: #error Use IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS #endif -// Enable stb_truetype by default unless FreeType is enabled. -// You can compile with both by defining both IMGUI_ENABLE_FREETYPE and IMGUI_ENABLE_STB_TRUETYPE together. -#if !defined(IMGUI_ENABLE_FREETYPE) && !defined(IMGUI_ENABLE_STB_TRUETYPE) && !defined(IMGUI_DISABLE_STB_TRUETYPE) +// Enable stb_truetype by default unless FreeType or DirectWrite are enabled. +// You can re-enable stb_truetype by explicitly defining IMGUI_ENABLE_STB_TRUETYPE before including imgui.h. +#if !defined(IMGUI_ENABLE_FREETYPE) && !defined(IMGUI_ENABLE_STB_TRUETYPE) && !defined(IMGUI_ENABLE_DWRITE) && !defined(IMGUI_DISABLE_STB_TRUETYPE) #define IMGUI_ENABLE_STB_TRUETYPE #endif @@ -1088,7 +1088,7 @@ enum ImGuiItemStatusFlags_ ImGuiItemStatusFlags_HasShortcut = 1 << 10, // g.LastItemData.Shortcut valid. Set by SetNextItemShortcut() -> ItemAdd(). //ImGuiItemStatusFlags_FocusedByTabbing = 1 << 8, // Removed IN 1.90.1 (Dec 2023). The trigger is part of g.NavActivateId. See commit 54c1bdeceb. ImGuiItemStatusFlags_EditedInternal = 1 << 11, // Similar to ImGuiItemStatusFlags_Edited but bypassing ImGuiItemFlags_NoMarkEdited. - + // Additional status + semantic for ImGuiTestEngine #ifdef IMGUI_ENABLE_TEST_ENGINE ImGuiItemStatusFlags_Openable = 1 << 20, // Item is an openable (e.g. TreeNode) diff --git a/misc/README.txt b/misc/README.txt index 58c00ee8c..59a66cd2f 100644 --- a/misc/README.txt +++ b/misc/README.txt @@ -7,6 +7,10 @@ misc/debuggers/ Helper files for popular debuggers (Visual Studio, GDB, LLDB). e.g. With the .natvis file, types like ImVector<> will be displayed nicely in Visual Studio debugger. +misc/dwrite/ + Font atlas builder/rasterizer using DirectWrite on Windows. + It features an improved rasterization quality over stb_truetype, the ability to load system font families, and system font fallback. + misc/fonts/ Fonts loading/merging instructions (e.g. How to handle glyph ranges, how to merge icons fonts). Command line tool "binary_to_compressed_c" to create compressed arrays to embed data in source code. diff --git a/misc/dwrite/README.md b/misc/dwrite/README.md new file mode 100644 index 000000000..33265b964 --- /dev/null +++ b/misc/dwrite/README.md @@ -0,0 +1,34 @@ +# imgui_dwrite + +This implements font rasterization using DirectWrite instead of stb_truetype (otherwise the default). +
by @lhecker. + +The minimum requirement is Windows 10 1709 (build 16299). + +## Usage + +1. Add imgui_dwrite.h/cpp alongside your project files. +2. Either set `#define IMGUI_ENABLE_DWRITE` in your [imconfig.h](https://github.com/ocornut/imgui/blob/master/imconfig.h) file or call `ImGui::GetIO().Fonts->SetFontLoader(ImGuiDWrite::GetFontLoader())`. +3. Add fonts using `ImGui::GetIO().Fonts->AddFontFromFamily("Segoe UI")`. + +## The Good + +- An improved rasterization over stb_truetype (particularly at small font sizes). +- You can use system fonts. +- It supports font fallback. + +## The Bad + +- Loads font families using regular weight, normal stretch, and normal style only. +- Not all `ImFontConfig` rasterizer options are implemented. +- `IMGUI_USE_WCHAR32` is required to render codepoints above U+FFFF, such as emoji. + Multi-codepoint glyphs are not supported. +- `ProggyForever.ttf` will not look good. + If you're curious: This is because the font does not set `USE_TYPO_METRICS`, which causes DirectWrite to correctly use + the `OS/2` `usWinAscent`/`Descent` fields instead of `sTypoAscender`/`Descender`. stb_truetype on the other hand uses + the `hhea` table (OpenType is an old standard and has competing metrics tables) whose values coincide with the latter. + This causes the glyphs to be nice and large with stb_truetype and small and squished with DirectWrite. + +...and most importantly: **Gamma correct text blending is not supported**. +DirectWrite relies on gamma correction being applied by a shader during composition. +A variant of this can be found at https://github.com/lhecker/dwrite-hlsl but this cannot be trivially integrated into ImGui. diff --git a/misc/dwrite/imgui_dwrite.cpp b/misc/dwrite/imgui_dwrite.cpp new file mode 100644 index 000000000..6decb9c68 --- /dev/null +++ b/misc/dwrite/imgui_dwrite.cpp @@ -0,0 +1,471 @@ +#include "imgui_dwrite.h" +#ifndef IMGUI_DISABLE +#include "imgui_internal.h" + +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#ifdef _MSC_VER +#pragma comment(lib, "dwrite") +#pragma comment(lib, "user32") +#endif + +#define RETURN_FALSE_IF(cond) \ + do { \ + if (cond) { \ + IM_ASSERT(false); \ + return false; \ + } \ + } while (0) + +#define RETURN_FALSE_IF_FAILED(hr) \ + RETURN_FALSE_IF(FAILED(hr)) + +template +struct com_ptr +{ + ~com_ptr() + { + if (m_ptr) + { + m_ptr->Release(); + } + } + + T* get() const + { + return m_ptr; + } + + T* operator->() const + { + return m_ptr; + } + + T** addressof() + { + return &m_ptr; + } + + T* detach() + { + T* ptr = m_ptr; + m_ptr = nullptr; + return ptr; + } + +private: + T* m_ptr = nullptr; +}; + +template +struct unique_im +{ + unique_im(T* ptr) : m_ptr(ptr) + {} + + ~unique_im() + { + IM_DELETE(m_ptr); + } + + T* operator->() + { + return m_ptr; + } + + T* detach() + { + T* ptr = m_ptr; + m_ptr = nullptr; + return ptr; + } + +private: + T* m_ptr; +}; + +struct TextAnalysisSource final : IDWriteTextAnalysisSource +{ + TextAnalysisSource(const wchar_t* locale, const wchar_t* text, UINT32 text_length) : m_locale(locale), m_text(text), m_text_length(text_length) {} + + ULONG STDMETHODCALLTYPE AddRef() override + { + return 1; + } + + ULONG STDMETHODCALLTYPE Release() override + { + return 1; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) override + { + if (IsEqualGUID(riid, __uuidof(IDWriteTextAnalysisSource))) + { + *ppvObject = this; + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + HRESULT STDMETHODCALLTYPE GetTextAtPosition(UINT32 textPosition, const WCHAR** textString, UINT32* textLength) override + { + textPosition = ImMin(textPosition, m_text_length); + *textString = m_text + textPosition; + *textLength = m_text_length - textPosition; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetTextBeforePosition(UINT32 textPosition, const WCHAR** textString, UINT32* textLength) override + { + textPosition = ImMin(textPosition, m_text_length); + *textString = m_text; + *textLength = textPosition; + return S_OK; + } + + DWRITE_READING_DIRECTION STDMETHODCALLTYPE GetParagraphReadingDirection() override + { + return DWRITE_READING_DIRECTION_LEFT_TO_RIGHT; + } + + HRESULT STDMETHODCALLTYPE GetLocaleName(UINT32 textPosition, UINT32* textLength, const WCHAR** localeName) override + { + *textLength = m_text_length - textPosition; + *localeName = m_locale; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetNumberSubstitution(UINT32, UINT32*, IDWriteNumberSubstitution**) override + { + return E_NOTIMPL; + } + +private: + const wchar_t* m_locale; + const wchar_t* m_text; + UINT32 m_text_length; +}; + +struct DWriteFontSource +{ + com_ptr factory; + com_ptr memory_loader; + com_ptr rendering_params; + com_ptr font_face; + com_ptr font_collection; + com_ptr font_fallback; + wchar_t family_name[IM_COUNTOF(ImFontConfig::Name)]; + float em_per_unit; + float ascent; + float descent; + + ~DWriteFontSource() + { + if (memory_loader.get()) + factory->UnregisterFontFileLoader(memory_loader.get()); + } +}; + +static bool font_src_init(ImFontAtlas*, ImFontConfig* src) +{ + unique_im data(IM_NEW(DWriteFontSource)); + + RETURN_FALSE_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory5), (IUnknown**)data->factory.addressof())); + RETURN_FALSE_IF_FAILED(data->factory->CreateRenderingParams(data->rendering_params.addressof())); + RETURN_FALSE_IF_FAILED(data->factory->GetSystemFontCollection(data->font_collection.addressof(), FALSE)); + + com_ptr font_fallback; + RETURN_FALSE_IF_FAILED(data->factory->GetSystemFontFallback(font_fallback.addressof())); + RETURN_FALSE_IF_FAILED(font_fallback->QueryInterface(data->font_fallback.addressof())); + + if (src->FontData || src->FontDataSize) + { + IM_ASSERT_USER_ERROR(src->FontData != nullptr && src->FontDataSize > 0, "Invalid font data"); + + // DWriteFontSource assumes that memory_loader != null means it needs to be unregistered. + // As such, we create and register the loader first and then transfer ownership into `data`. + com_ptr memory_loader; + RETURN_FALSE_IF_FAILED(data->factory->CreateInMemoryFontFileLoader(memory_loader.addressof())); + RETURN_FALSE_IF_FAILED(data->factory->RegisterFontFileLoader(memory_loader.get())); + *data->memory_loader.addressof() = memory_loader.detach(); + + com_ptr font_file; + com_ptr font_reference; + RETURN_FALSE_IF_FAILED(data->memory_loader->CreateInMemoryFontFileReference(data->factory.get(), src->FontData, (UINT32)src->FontDataSize, nullptr, font_file.addressof())); + RETURN_FALSE_IF_FAILED(data->factory->CreateFontFaceReference(font_file.get(), src->FontNo, DWRITE_FONT_SIMULATIONS_NONE, font_reference.addressof())); + RETURN_FALSE_IF_FAILED(font_reference->CreateFontFace(data->font_face.addressof())); + } + else + { + // If no font data is present, we assume the Name field contains the family name of the font. + // With DirectWrite, we generally deal with `IDWriteFontFace`s which represent a single stylistic variant. + // As such: Name -> Name, but wchar -> Find font family -> Get any variant out of it + + const int name_utf8_len = (int)strnlen(src->Name, IM_COUNTOF(src->Name)); + const int name_utf16_len = MultiByteToWideChar(CP_UTF8, 0, src->Name, name_utf8_len, &data->family_name[0], IM_COUNTOF(data->family_name) - 1); + RETURN_FALSE_IF(name_utf16_len <= 0); + data->family_name[name_utf16_len] = L'\0'; + + UINT32 family_index; + BOOL family_exists; + RETURN_FALSE_IF_FAILED(data->font_collection->FindFamilyName(&data->family_name[0], &family_index, &family_exists)); + RETURN_FALSE_IF(!family_exists); + + com_ptr font_family; + com_ptr font; + com_ptr font_face; + RETURN_FALSE_IF_FAILED(data->font_collection->GetFontFamily(family_index, font_family.addressof())); + RETURN_FALSE_IF_FAILED(font_family->GetFirstMatchingFont(DWRITE_FONT_WEIGHT_REGULAR, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof())); + RETURN_FALSE_IF_FAILED(font->CreateFontFace(font_face.addressof())); + RETURN_FALSE_IF_FAILED(font_face->QueryInterface(data->font_face.addressof())); + } + + DWRITE_FONT_METRICS metrics; + data->font_face->GetMetrics(&metrics); + + const float ascent = metrics.ascent; + const float descent = metrics.descent; + const float half_line_gap = (float)metrics.lineGap * 0.5f; + const float em_per_unit = 1.0f / (float)metrics.designUnitsPerEm; + data->em_per_unit = em_per_unit; + data->ascent = (ascent + half_line_gap) * em_per_unit; + data->descent = (descent + half_line_gap) * em_per_unit; + + src->FontLoaderData = data.detach(); + return true; +} + +static void font_src_destroy(ImFontAtlas*, ImFontConfig* src) +{ + IM_DELETE((DWriteFontSource*)src->FontLoaderData); + src->FontLoaderData = nullptr; +} + +static UINT16 map_codepoint_with_fallback(DWriteFontSource* data, ImWchar codepoint, IDWriteFontFace3** font_face) +{ + const UINT32 codepoint32 = codepoint; + + // "Fast" path: Try the primary font. DirectWrite implements GetGlyphIndices as O(1). + UINT16 glyph_index; + if (FAILED(data->font_face->GetGlyphIndices(&codepoint32, 1, &glyph_index))) + return 0; + if (glyph_index != 0) + { + data->font_face->AddRef(); + *font_face = data->font_face.get(); + return glyph_index; + } + +#ifdef IMGUI_USE_WCHAR32 + wchar_t text[2]; + UINT32 text_length; + if (codepoint > 0xFFFF) + { + text[0] = (wchar_t)(0xD800 + ((codepoint - 0x10000) >> 10)); + text[1] = (wchar_t)(0xDC00 + ((codepoint - 0x10000) & 0x3FF)); + text_length = 2; + } + else + { + text[0] = (wchar_t)codepoint; + text_length = 1; + } +#else + const wchar_t* text = (const wchar_t*)&codepoint; + const UINT32 text_length = 1; +#endif + + TextAnalysisSource analysis_source(ImGuiDWrite::GetUserDefaultLocaleName(), text, text_length); + UINT32 mapped_length; + float scale; + IDWriteFontFace5* font_face5; + if (FAILED(data->font_fallback->MapCharacters( + /* analysisSource */ &analysis_source, + /* textPosition */ 0, + /* textLength */ text_length, + /* baseFontCollection */ data->font_collection.get(), + /* baseFamilyName */ data->family_name, + /* fontAxisValues */ nullptr, + /* fontAxisValueCount */ 0, + /* mappedLength */ &mapped_length, + /* scale */ &scale, + /* mappedFontFace */ &font_face5 + ))) + return 0; + + // NOTE: MapCharacters() can return S_OK and yet mappedFontFace can be null. + // This indicates that no font contains the requested codepoint. + if (!font_face5 || FAILED(font_face5->GetGlyphIndices(&codepoint32, 1, &glyph_index))) + glyph_index = 0; + + *font_face = font_face5; + return glyph_index; +} + +static bool font_src_contains_glyph(ImFontAtlas*, ImFontConfig* src, ImWchar codepoint) +{ + DWriteFontSource* data = (DWriteFontSource*)src->FontLoaderData; + com_ptr font_face; + return map_codepoint_with_fallback(data, codepoint, font_face.addressof()) != 0; +} + +static bool font_baked_init(ImFontAtlas*, ImFontConfig* src, ImFontBaked* baked, void*) +{ + if (!src->MergeMode) + { + DWriteFontSource* data = (DWriteFontSource*)src->FontLoaderData; + const float rasterizer_scale = src->RasterizerDensity * baked->RasterizerDensity * src->ExtraSizeScale; + const float font_size = baked->Size * rasterizer_scale / (data->ascent + data->descent); + baked->Ascent = ImCeil(font_size * data->ascent) / rasterizer_scale; + baked->Descent = -ImCeil(font_size * data->descent) / rasterizer_scale; + } + return true; +} + +static bool font_baked_load_glyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void*, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x) +{ + DWriteFontSource* data = (DWriteFontSource*)src->FontLoaderData; + + com_ptr font_face; + const UINT16 glyph_index = map_codepoint_with_fallback(data, codepoint, font_face.addressof()); + if (glyph_index == 0) + return false; + + DWRITE_GLYPH_METRICS glyph_metrics; + RETURN_FALSE_IF_FAILED(font_face->GetDesignGlyphMetrics(&glyph_index, 1, &glyph_metrics, FALSE)); + + const float ref_size = baked->OwnerFont->Sources[0]->SizePixels; + float font_size = baked->Size * src->ExtraSizeScale / (data->ascent + data->descent); + if (src->MergeMode && src->SizePixels != 0.0f) + font_size *= src->SizePixels / ref_size; + + // DirectWrite yields fractional advance widths, but simultaneously we don't implement oversampling. + // To prevent ugly smearing, we need to ensure to only return integer advances. + const float advance_x = IM_ROUND((float)glyph_metrics.advanceWidth * data->em_per_unit * font_size); + + if (out_advance_x) + { + IM_ASSERT(out_glyph == nullptr); + *out_advance_x = advance_x; + return true; + } + + const DWRITE_GLYPH_OFFSET zero_offset = {}; + const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity; + const DWRITE_GLYPH_RUN glyph_run = { font_face.get(), font_size * rasterizer_density, 1, &glyph_index, nullptr, &zero_offset, FALSE, 0 }; + + // If you read SDK headers you may notice the very enticing IDWriteBitmapRenderTarget3::DrawGlyphRunWithColorSupport + // function, but (and these words are written in anger) that one is exclusive to the Windows App SDK. + // + // In the future, it may be reasonable to call IDWriteFactory4::TranslateColorGlyphRun here, + // and manually compose COLRv0 layers into a single bitmap on the CPU side. Using Direct2D + // would not be a good idea as it does not interop well with various graphics debuggers. + + // To properly support GASP, a call to GetRecommendedRenderingMode is required. + DWRITE_RENDERING_MODE1 rendering_mode; + DWRITE_GRID_FIT_MODE grid_fit_mode; + RETURN_FALSE_IF_FAILED(font_face->GetRecommendedRenderingMode( + /* fontEmSize */ glyph_run.fontEmSize, + /* dpiX */ 96.0f, // fontEmSize is already in display pixels + /* dpiY */ 96.0f, + /* transform */ nullptr, + /* isSideways */ FALSE, + /* outlineThreshold */ DWRITE_OUTLINE_THRESHOLD_ANTIALIASED, + /* measuringMode */ DWRITE_MEASURING_MODE_NATURAL, + /* renderingParams */ data->rendering_params.get(), + /* renderingMode */ &rendering_mode, + /* gridFitMode */ &grid_fit_mode + )); + + com_ptr analysis; + RETURN_FALSE_IF_FAILED(data->factory->CreateGlyphRunAnalysis( + /* glyphRun */ &glyph_run, + /* transform */ nullptr, + /* renderingMode */ rendering_mode, + /* measuringMode */ DWRITE_MEASURING_MODE_NATURAL, + /* gridFitMode */ grid_fit_mode, + /* antialiasMode */ DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE, + /* baselineOriginX */ 0, + /* baselineOriginY */ 0, + /* glyphRunAnalysis */ analysis.addressof() + )); + + // Even if it says "aliased", it actually produces an antialiased 8-bit grayscale texture. + // I don't get it either. I don't think anyone at Microsoft does. + RECT bounds; + RETURN_FALSE_IF_FAILED(analysis->GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1, &bounds)); + + out_glyph->Codepoint = codepoint; + out_glyph->AdvanceX = advance_x; + + // Invisible glyphs (whitespace) produce empty bounds. + // out_glyph is initialized with Visible = false, so we can just return here. + if (IsRectEmpty(&bounds)) + return true; + + const int width = bounds.right - bounds.left; + const int height = bounds.bottom - bounds.top; + + ImVector pixels; + pixels.resize(width * height); + RETURN_FALSE_IF_FAILED(analysis->CreateAlphaTexture(DWRITE_TEXTURE_ALIASED_1x1, &bounds, pixels.Data, pixels.Size)); + + const ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, width, height); + RETURN_FALSE_IF(pack_id == ImFontAtlasRectId_Invalid); + + const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f; + const float offset_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f); + const float offset_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent; + const float inv_density = 1.0f / rasterizer_density; + out_glyph->X0 = (float)bounds.left * inv_density + offset_x; + out_glyph->Y0 = (float)bounds.top * inv_density + offset_y; + out_glyph->X1 = (float)bounds.right * inv_density + offset_x; + out_glyph->Y1 = (float)bounds.bottom * inv_density + offset_y; + out_glyph->Visible = true; + out_glyph->PackId = pack_id; + + ImTextureRect* rect = ImFontAtlasPackGetRect(atlas, pack_id); + ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, rect, pixels.Data, ImTextureFormat_Alpha8, width); + return true; +} + +const wchar_t* ImGuiDWrite::GetUserDefaultLocaleName() +{ + static wchar_t locale_name[LOCALE_NAME_MAX_LENGTH]; + + if (locale_name[0] == L'\0') + { + if (!::GetUserDefaultLocaleName(locale_name, LOCALE_NAME_MAX_LENGTH)) + { + static const wchar_t fallback_locale[] = L"en-US"; + memcpy(&locale_name[0], &fallback_locale[0], sizeof(fallback_locale)); + } + } + + return &locale_name[0]; +} + +const ImFontLoader* ImGuiDWrite::GetFontLoader() +{ + static ImFontLoader loader; + loader.Name = "DirectWrite"; + loader.FontSrcInit = font_src_init; + loader.FontSrcDestroy = font_src_destroy; + loader.FontSrcContainsGlyph = font_src_contains_glyph; + loader.FontBakedInit = font_baked_init; + loader.FontBakedDestroy = nullptr; + loader.FontBakedLoadGlyph = font_baked_load_glyph; + return &loader; +} + +#endif diff --git a/misc/dwrite/imgui_dwrite.h b/misc/dwrite/imgui_dwrite.h new file mode 100644 index 000000000..a1744b0d3 --- /dev/null +++ b/misc/dwrite/imgui_dwrite.h @@ -0,0 +1,13 @@ +#pragma once +#include "imgui.h" +#ifndef IMGUI_DISABLE + +struct ImFontLoader; + +namespace ImGuiDWrite +{ + IMGUI_API const wchar_t* GetUserDefaultLocaleName(); + IMGUI_API const ImFontLoader* GetFontLoader(); +} + +#endif diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index 375863fdf..2aebe2061 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -399,6 +399,12 @@ static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas) static bool ImGui_ImplFreeType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src) { + if (src->FontData == NULL || src->FontDataSize <= 0) + { + IM_ASSERT_USER_ERROR(0, "FreeType requires font data; font families are not supported."); + return false; + } + ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData; ImGui_ImplFreeType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplFreeType_FontSrcData); IM_ASSERT(src->FontLoaderData == nullptr); diff --git a/misc/single_file/imgui_single_file.h b/misc/single_file/imgui_single_file.h index bea1cb6b6..57addfc4a 100644 --- a/misc/single_file/imgui_single_file.h +++ b/misc/single_file/imgui_single_file.h @@ -13,6 +13,9 @@ #endif #include "../../imgui.h" +#ifdef IMGUI_ENABLE_DWRITE +#include "../../misc/dwrite/imgui_dwrite.h" +#endif #ifdef IMGUI_ENABLE_FREETYPE #include "../../misc/freetype/imgui_freetype.h" #endif @@ -23,6 +26,9 @@ #include "../../imgui_draw.cpp" #include "../../imgui_tables.cpp" #include "../../imgui_widgets.cpp" +#ifdef IMGUI_ENABLE_DWRITE +#include "../../misc/dwrite/imgui_dwrite.cpp" +#endif #ifdef IMGUI_ENABLE_FREETYPE #include "../../misc/freetype/imgui_freetype.cpp" #endif From 00d89d33ddf3b6ce82c420224b9c2771391622e6 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 10 Sep 2026 14:30:04 +0200 Subject: [PATCH 2/2] imgui_dwrite: some stylistic improvements --- misc/dwrite/imgui_dwrite.cpp | 41 ++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/misc/dwrite/imgui_dwrite.cpp b/misc/dwrite/imgui_dwrite.cpp index 6decb9c68..abb25b14a 100644 --- a/misc/dwrite/imgui_dwrite.cpp +++ b/misc/dwrite/imgui_dwrite.cpp @@ -180,16 +180,19 @@ static bool font_src_init(ImFontAtlas*, ImFontConfig* src) { unique_im data(IM_NEW(DWriteFontSource)); + // Create "global" objects. System font collection, font fallback handler, etc. + com_ptr font_fallback; RETURN_FALSE_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory5), (IUnknown**)data->factory.addressof())); RETURN_FALSE_IF_FAILED(data->factory->CreateRenderingParams(data->rendering_params.addressof())); RETURN_FALSE_IF_FAILED(data->factory->GetSystemFontCollection(data->font_collection.addressof(), FALSE)); - - com_ptr font_fallback; RETURN_FALSE_IF_FAILED(data->factory->GetSystemFontFallback(font_fallback.addressof())); RETURN_FALSE_IF_FAILED(font_fallback->QueryInterface(data->font_fallback.addressof())); + // Create a `IDWriteFontFace` (a single stylistic variant) out of `src`. if (src->FontData || src->FontDataSize) { + // In-memory font? `IDWriteInMemoryFontFileLoader`! + IM_ASSERT_USER_ERROR(src->FontData != nullptr && src->FontDataSize > 0, "Invalid font data"); // DWriteFontSource assumes that memory_loader != null means it needs to be unregistered. @@ -208,8 +211,7 @@ static bool font_src_init(ImFontAtlas*, ImFontConfig* src) else { // If no font data is present, we assume the Name field contains the family name of the font. - // With DirectWrite, we generally deal with `IDWriteFontFace`s which represent a single stylistic variant. - // As such: Name -> Name, but wchar -> Find font family -> Get any variant out of it + // Name -> Name, but wchar -> Find font family -> Get any variant out of it. const int name_utf8_len = (int)strnlen(src->Name, IM_COUNTOF(src->Name)); const int name_utf16_len = MultiByteToWideChar(CP_UTF8, 0, src->Name, name_utf8_len, &data->family_name[0], IM_COUNTOF(data->family_name) - 1); @@ -233,6 +235,11 @@ static bool font_src_init(ImFontAtlas*, ImFontConfig* src) DWRITE_FONT_METRICS metrics; data->font_face->GetMetrics(&metrics); + // Calculate the ascent/descent of the font in "em". + // 1.1em (in total) means that a 20px font has a 22px line height. + // + // Modern typographical metrics feature a line gap, which indicates the spacing between lines. + // Since ImGui has no concept of a line gap, we add 50% onto the ascent/descent respectively. const float ascent = metrics.ascent; const float descent = metrics.descent; const float half_line_gap = (float)metrics.lineGap * 0.5f; @@ -341,9 +348,8 @@ static bool font_baked_load_glyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontB if (glyph_index == 0) return false; - DWRITE_GLYPH_METRICS glyph_metrics; - RETURN_FALSE_IF_FAILED(font_face->GetDesignGlyphMetrics(&glyph_index, 1, &glyph_metrics, FALSE)); - + // ImGui's font size is actually the line height. Here, we reverse-calculate the actual + // font size by simply dividing the height in em. The result is in 96 DPI pixels. const float ref_size = baked->OwnerFont->Sources[0]->SizePixels; float font_size = baked->Size * src->ExtraSizeScale / (data->ascent + data->descent); if (src->MergeMode && src->SizePixels != 0.0f) @@ -351,7 +357,9 @@ static bool font_baked_load_glyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontB // DirectWrite yields fractional advance widths, but simultaneously we don't implement oversampling. // To prevent ugly smearing, we need to ensure to only return integer advances. - const float advance_x = IM_ROUND((float)glyph_metrics.advanceWidth * data->em_per_unit * font_size); + DWRITE_GLYPH_METRICS metrics; + RETURN_FALSE_IF_FAILED(font_face->GetDesignGlyphMetrics(&glyph_index, 1, &metrics, FALSE)); + const float advance_x = IM_ROUND((float)metrics.advanceWidth * data->em_per_unit * font_size); if (out_advance_x) { @@ -362,7 +370,16 @@ static bool font_baked_load_glyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontB const DWRITE_GLYPH_OFFSET zero_offset = {}; const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity; - const DWRITE_GLYPH_RUN glyph_run = { font_face.get(), font_size * rasterizer_density, 1, &glyph_index, nullptr, &zero_offset, FALSE, 0 }; + const DWRITE_GLYPH_RUN glyph_run = { + /* fontFace */ font_face.get(), + /* fontEmSize */ font_size * rasterizer_density, + /* glyphCount */ 1, + /* glyphIndices */ &glyph_index, + /* glyphAdvances */ nullptr, + /* glyphOffsets */ &zero_offset, + /* isSideways */ FALSE, + /* bidiLevel */ 0, + }; // If you read SDK headers you may notice the very enticing IDWriteBitmapRenderTarget3::DrawGlyphRunWithColorSupport // function, but (and these words are written in anger) that one is exclusive to the Windows App SDK. @@ -374,6 +391,8 @@ static bool font_baked_load_glyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontB // To properly support GASP, a call to GetRecommendedRenderingMode is required. DWRITE_RENDERING_MODE1 rendering_mode; DWRITE_GRID_FIT_MODE grid_fit_mode; + com_ptr analysis; + RECT bounds; RETURN_FALSE_IF_FAILED(font_face->GetRecommendedRenderingMode( /* fontEmSize */ glyph_run.fontEmSize, /* dpiX */ 96.0f, // fontEmSize is already in display pixels @@ -386,8 +405,6 @@ static bool font_baked_load_glyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontB /* renderingMode */ &rendering_mode, /* gridFitMode */ &grid_fit_mode )); - - com_ptr analysis; RETURN_FALSE_IF_FAILED(data->factory->CreateGlyphRunAnalysis( /* glyphRun */ &glyph_run, /* transform */ nullptr, @@ -399,10 +416,8 @@ static bool font_baked_load_glyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontB /* baselineOriginY */ 0, /* glyphRunAnalysis */ analysis.addressof() )); - // Even if it says "aliased", it actually produces an antialiased 8-bit grayscale texture. // I don't get it either. I don't think anyone at Microsoft does. - RECT bounds; RETURN_FALSE_IF_FAILED(analysis->GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1, &bounds)); out_glyph->Codepoint = codepoint;