#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