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);
+ }
}
}