From b8bfc7ffd392084466772385d590555f1f6fcb4a Mon Sep 17 00:00:00 2001 From: pasharm Date: Fri, 2 Oct 2026 15:55:33 +0300 Subject: [PATCH] Draw emoji in colour in the clip list GDI DrawText renders colour fonts (Segoe UI Emoji) black and white. Rows whose text contains emoji-like characters are now drawn through an ID2D1DCRenderTarget + DirectWrite with ENABLE_COLOR_FONT, using a GDI-compatible layout and GDI classic rendering so ordinary letters in the row match the GDI rows next to it. Rows without emoji and any DirectWrite failure keep the old DrawText path. Not covered: rows drawn by the RichEdit control ("Draw RTF text in list"), search highlight (DrawHTML), the description window. Part of #251. --- CP_Main.vcxproj | 2 + CP_Main.vcxproj.filters | 6 ++ src/ColorEmojiText.cpp | 200 ++++++++++++++++++++++++++++++++++++++++ src/ColorEmojiText.h | 13 +++ src/QListCtrl.cpp | 7 +- 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 src/ColorEmojiText.cpp create mode 100644 src/ColorEmojiText.h diff --git a/CP_Main.vcxproj b/CP_Main.vcxproj index cd39b313..b6496a2f 100644 --- a/CP_Main.vcxproj +++ b/CP_Main.vcxproj @@ -548,6 +548,7 @@ + @@ -816,6 +817,7 @@ + diff --git a/CP_Main.vcxproj.filters b/CP_Main.vcxproj.filters index ba9944ba..4ff6d8c9 100644 --- a/CP_Main.vcxproj.filters +++ b/CP_Main.vcxproj.filters @@ -55,6 +55,9 @@ src + + src + src @@ -550,6 +553,9 @@ src + + src + src diff --git a/src/ColorEmojiText.cpp b/src/ColorEmojiText.cpp new file mode 100644 index 00000000..d69e8da8 --- /dev/null +++ b/src/ColorEmojiText.cpp @@ -0,0 +1,200 @@ +#include "stdafx.h" +#include "ColorEmojiText.h" +#include +#include +#include +#include + +#pragma comment(lib, "d2d1.lib") +#pragma comment(lib, "dwrite.lib") + +namespace +{ + CComPtr g_d2dFactory; + CComPtr g_writeFactory; + CComPtr g_gdiInterop; + CComPtr g_target; + bool g_factoriesFailed = false; + + // text format for the last used GDI font, the list draws every row with the same one + CComPtr g_format; + LOGFONT g_formatLogFont = {}; + TEXTMETRIC g_formatMetrics = {}; + + // Rows without these characters keep the plain GDI path, so ordinary text looks exactly as before. + bool HasEmoji(const CString& text) + { + for (int i = 0; i < text.GetLength(); i++) + { + wchar_t c = text[i]; + if (IS_HIGH_SURROGATE(c) || // U+1F000 and up: most emoji + c == 0xFE0F || // emoji presentation selector + c == 0x200D || // zero width joiner in emoji sequences + (c >= 0x231A && c <= 0x231B) || // watch, hourglass + (c >= 0x23E9 && c <= 0x23FA) || // media buttons, alarm clock, pause + (c >= 0x25FD && c <= 0x25FE) || // small squares + (c >= 0x2600 && c <= 0x27BF) || // misc symbols, dingbats + (c >= 0x2B00 && c <= 0x2BFF)) // arrows and stars like U+2B50 + { + return true; + } + } + return false; + } + + bool CreateResources() + { + if (g_target) + return true; + if (g_factoriesFailed) + return false; + + HRESULT hr = S_OK; + if (!g_d2dFactory) + { + hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &g_d2dFactory.p); + if (SUCCEEDED(hr)) + hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(&g_writeFactory.p)); + if (SUCCEEDED(hr)) + hr = g_writeFactory->GetGdiInterop(&g_gdiInterop); + if (FAILED(hr)) + { + g_factoriesFailed = true; + return false; + } + } + + // software target: the rows are small and GDI owns the DC, a GPU round trip per row costs more than it gives. + // 96 dpi makes one DIP equal one pixel, so GDI rects and font sizes are used as is. + D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_SOFTWARE, + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE), + 96.0f, 96.0f); + + hr = g_d2dFactory->CreateDCRenderTarget(&props, &g_target); + if (FAILED(hr)) + { + g_factoriesFailed = true; + return false; + } + + // GDI classic rendering, so letters in an emoji row look like the GDI rows next to it + CComPtr defaults; + CComPtr gdiClassic; + if (SUCCEEDED(g_writeFactory->CreateRenderingParams(&defaults)) && + SUCCEEDED(g_writeFactory->CreateCustomRenderingParams(defaults->GetGamma(), defaults->GetEnhancedContrast(), + defaults->GetClearTypeLevel(), defaults->GetPixelGeometry(), DWRITE_RENDERING_MODE_GDI_CLASSIC, &gdiClassic))) + { + g_target->SetTextRenderingParams(gdiClassic); + } + return true; + } + + IDWriteTextFormat* GetTextFormat(CDC* pDC) + { + LOGFONT lf = {}; + CFont* font = pDC->GetCurrentFont(); + if (font == NULL || font->GetLogFont(&lf) == 0) + return NULL; + + TEXTMETRIC tm = {}; + if (pDC->GetTextMetrics(&tm) == FALSE) + return NULL; + + if (g_format && + memcmp(&lf, &g_formatLogFont, sizeof(lf)) == 0 && + memcmp(&tm, &g_formatMetrics, sizeof(tm)) == 0) + { + return g_format; + } + + g_format.Release(); + + // the GDI face name ("Segoe UI Semibold") is not always a DirectWrite family name, ask GDI interop for the real one + CComPtr writeFont; + if (FAILED(g_gdiInterop->CreateFontFromLOGFONT(&lf, &writeFont))) + return NULL; + + CComPtr family; + CComPtr names; + if (FAILED(writeFont->GetFontFamily(&family)) || + FAILED(family->GetFamilyNames(&names))) + { + return NULL; + } + + UINT32 index = 0; + BOOL exists = FALSE; + if (FAILED(names->FindLocaleName(L"en-us", &index, &exists)) || exists == FALSE) + index = 0; + + UINT32 length = 0; + if (FAILED(names->GetStringLength(index, &length))) + return NULL; + + std::wstring familyName(length + 1, L'\0'); + if (FAILED(names->GetString(index, &familyName[0], length + 1))) + return NULL; + + // em size the way GDI sees it: cell height without internal leading + float emSize = static_cast(tm.tmHeight - tm.tmInternalLeading); + if (emSize <= 0) + return NULL; + + CComPtr format; + if (FAILED(g_writeFactory->CreateTextFormat(familyName.c_str(), NULL, + writeFont->GetWeight(), writeFont->GetStyle(), writeFont->GetStretch(), + emSize, L"", &format))) + { + return NULL; + } + + // same geometry as GDI DrawText: no wrapping, line height = tmHeight, tab = 8 average chars + format->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); + format->SetLineSpacing(DWRITE_LINE_SPACING_METHOD_UNIFORM, static_cast(tm.tmHeight), static_cast(tm.tmAscent)); + format->SetIncrementalTabStop(static_cast(8 * tm.tmAveCharWidth)); + + g_format = format; + g_formatLogFont = lf; + g_formatMetrics = tm; + return g_format; + } +} + +bool ColorEmojiText::DrawColorText(CDC* pDC, const CString& text, const CRect& rc) +{ + if (rc.IsRectEmpty() || HasEmoji(text) == false || CreateResources() == false) + return false; + + IDWriteTextFormat* format = GetTextFormat(pDC); + if (format == NULL) + return false; + + // GDI-compatible layout: whole-pixel advances, same text widths as GDI DrawText + CComPtr layout; + if (FAILED(g_writeFactory->CreateGdiCompatibleTextLayout(text, text.GetLength(), format, + static_cast(rc.Width()), static_cast(rc.Height()), 1.0f, NULL, FALSE, &layout))) + { + return false; + } + + RECT bindRect = rc; + if (FAILED(g_target->BindDC(pDC->GetSafeHdc(), &bindRect))) + return false; + + COLORREF color = pDC->GetTextColor(); + CComPtr brush; + if (FAILED(g_target->CreateSolidColorBrush(D2D1::ColorF(GetRValue(color) / 255.0f, GetGValue(color) / 255.0f, GetBValue(color) / 255.0f), &brush))) + return false; + + g_target->BeginDraw(); + g_target->SetTransform(D2D1::Matrix3x2F::Identity()); + g_target->DrawTextLayout(D2D1::Point2F(0, 0), layout, brush, + D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT | D2D1_DRAW_TEXT_OPTIONS_CLIP); + HRESULT hr = g_target->EndDraw(); + + if (hr == D2DERR_RECREATE_TARGET) + g_target.Release(); + + return SUCCEEDED(hr); +} diff --git a/src/ColorEmojiText.h b/src/ColorEmojiText.h new file mode 100644 index 00000000..da451319 --- /dev/null +++ b/src/ColorEmojiText.h @@ -0,0 +1,13 @@ +#pragma once + +// Colour emoji in the clip list (issue #251). +// GDI DrawText renders colour fonts (Segoe UI Emoji) black and white, so rows +// that contain emoji are drawn through Direct2D/DirectWrite instead. +namespace ColorEmojiText +{ + // Draws text the way CDC::DrawText(text, rc, DT_EXPANDTABS | DT_NOPREFIX) does, + // with the font and text colour currently selected into pDC, but keeps colour glyphs. + // Returns false without drawing anything when the text has no emoji-like characters + // or DirectWrite failed - the caller then draws with GDI as before. + bool DrawColorText(CDC* pDC, const CString& text, const CRect& rc); +} diff --git a/src/QListCtrl.cpp b/src/QListCtrl.cpp index 3c9cf307..7d3d34fc 100644 --- a/src/QListCtrl.cpp +++ b/src/QListCtrl.cpp @@ -10,6 +10,7 @@ #include "DittoCopyBuffer.h" #include #include "DrawHTML.h" +#include "ColorEmojiText.h" #include "..\Shared\TextConvert.h" #include #include @@ -599,7 +600,11 @@ void CQListCtrl::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) } else { - pDC->DrawText(csText, rcText, DT_VCENTER | DT_EXPANDTABS | DT_NOPREFIX); + // rows with emoji go through DirectWrite, GDI draws them black and white + if (ColorEmojiText::DrawColorText(pDC, csText, rcText) == false) + { + pDC->DrawText(csText, rcText, DT_VCENTER | DT_EXPANDTABS | DT_NOPREFIX); + } } }