From dd4aa6b163ccf18b9b0a810e2f5afe5bd6d133d8 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 23 May 2026 21:07:28 -0500 Subject: [PATCH] Fix unicode support for win9x --- .../src/ui/gdi_font.cpp | 154 +++++++++++++++--- 1 file changed, 128 insertions(+), 26 deletions(-) diff --git a/LANCommander.Launcher.Legacy/src/ui/gdi_font.cpp b/LANCommander.Launcher.Legacy/src/ui/gdi_font.cpp index 73256698..d4ce4569 100644 --- a/LANCommander.Launcher.Legacy/src/ui/gdi_font.cpp +++ b/LANCommander.Launcher.Legacy/src/ui/gdi_font.cpp @@ -1,6 +1,15 @@ // gdi_font.cpp — TrueType font rendering via GDI, blitted into Allegro // BITMAPs. This file includes Allegro AND Windows headers; winalleg.h // resolves the BITMAP typedef conflict. +// +// Unicode strategy (MSLU / "unicows" pattern): +// All text input is UTF-8. We always convert to UTF-16 internally. +// At runtime we detect whether the OS supports W (wide) Win32 APIs: +// - NT-based (2000+): call W functions directly → full Unicode. +// - Win9x (95/98/Me): convert UTF-16 → current codepage via +// WideCharToMultiByte, then call A functions → best-effort rendering. +// This is the same approach Microsoft's unicows.dll takes, implemented +// inline for the small set of GDI functions we actually use. #include #ifdef ALLEGRO_WINDOWS @@ -19,8 +28,18 @@ namespace HFONT g_hfont = NULL; HDC g_measure_dc = NULL; // off-screen DC kept for measuring only int g_font_height = 0; + bool g_wide_ok = false; // true on NT-based systems (W APIs work) - // Convert a UTF-8 string to UTF-16 for wide Win32 APIs. + // Detect whether the OS supports W (wide/Unicode) Win32 APIs. + void detect_unicode_support() + { + OSVERSIONINFOA ovi; + ovi.dwOSVersionInfoSize = sizeof(ovi); + GetVersionExA(&ovi); + g_wide_ok = (ovi.dwPlatformId == VER_PLATFORM_WIN32_NT); + } + + // Convert UTF-8 to UTF-16. Works on all Win32 platforms. std::wstring utf8_to_wide(const char *s, int len = -1) { if (!s || (len == 0) || (len == -1 && !*s)) @@ -30,27 +49,59 @@ namespace return std::wstring(); std::wstring w(wlen, L'\0'); MultiByteToWideChar(CP_UTF8, 0, s, len, &w[0], wlen); - // If len was -1, MultiByteToWideChar includes the null terminator in wlen. if (len == -1 && !w.empty() && w.back() == L'\0') w.pop_back(); return w; } + // Convert UTF-16 to the current ANSI codepage (for Win9x A-function fallback). + std::string wide_to_ansi(const std::wstring &w) + { + if (w.empty()) + return std::string(); + int alen = WideCharToMultiByte(CP_ACP, 0, w.c_str(), (int)w.size(), + NULL, 0, NULL, NULL); + if (alen <= 0) + return std::string(); + std::string a(alen, '\0'); + WideCharToMultiByte(CP_ACP, 0, w.c_str(), (int)w.size(), + &a[0], alen, NULL, NULL); + return a; + } + // Try to create the font with the given face name. HFONT try_create(const char *face, int pt) { - std::wstring wface = utf8_to_wide(face); - return CreateFontW( - -pt, // negative = point size (not cell height) - 0, 0, 0, - FW_NORMAL, // weight - FALSE, FALSE, FALSE, // italic, underline, strikeout - DEFAULT_CHARSET, - OUT_TT_PRECIS, // prefer TrueType - CLIP_DEFAULT_PRECIS, - CLEARTYPE_QUALITY, - DEFAULT_PITCH | FF_SWISS, - wface.c_str()); + // Win9x doesn't have ClearType. + DWORD quality = g_wide_ok ? CLEARTYPE_QUALITY : ANTIALIASED_QUALITY; + + if (g_wide_ok) + { + std::wstring wface = utf8_to_wide(face); + return CreateFontW( + -pt, 0, 0, 0, + FW_NORMAL, + FALSE, FALSE, FALSE, + DEFAULT_CHARSET, + OUT_TT_PRECIS, + CLIP_DEFAULT_PRECIS, + quality, + DEFAULT_PITCH | FF_SWISS, + wface.c_str()); + } + else + { + return CreateFontA( + -pt, 0, 0, 0, + FW_NORMAL, + FALSE, FALSE, FALSE, + DEFAULT_CHARSET, + OUT_TT_PRECIS, + CLIP_DEFAULT_PRECIS, + quality, + DEFAULT_PITCH | FF_SWISS, + face); + } } // Verify the returned font actually matches the requested face name @@ -59,17 +110,60 @@ namespace { HDC dc = GetDC(NULL); HFONT old = (HFONT)SelectObject(dc, hf); - wchar_t actual[LF_FACESIZE] = {}; - GetTextFaceW(dc, LF_FACESIZE, actual); + + bool match = false; + if (g_wide_ok) + { + wchar_t actual[LF_FACESIZE] = {}; + GetTextFaceW(dc, LF_FACESIZE, actual); + std::wstring wexpected = utf8_to_wide(expected); + match = (_wcsicmp(actual, wexpected.c_str()) == 0); + } + else + { + char actual[LF_FACESIZE] = {}; + GetTextFaceA(dc, LF_FACESIZE, actual); + match = (_stricmp(actual, expected) == 0); + } + SelectObject(dc, old); ReleaseDC(NULL, dc); - std::wstring wexpected = utf8_to_wide(expected); - return _wcsicmp(actual, wexpected.c_str()) == 0; + return match; + } + + // Measure text width using the appropriate API. + void measure_text(HDC dc, const std::wstring &wtext, SIZE *sz) + { + if (g_wide_ok) + { + GetTextExtentPoint32W(dc, wtext.c_str(), (int)wtext.size(), sz); + } + else + { + std::string ansi = wide_to_ansi(wtext); + GetTextExtentPoint32A(dc, ansi.c_str(), (int)ansi.size(), sz); + } + } + + // Draw text into a DC using the appropriate API. + void draw_to_dc(HDC dc, const std::wstring &wtext, RECT *rc, UINT fmt) + { + if (g_wide_ok) + { + DrawTextW(dc, wtext.c_str(), (int)wtext.size(), rc, fmt); + } + else + { + std::string ansi = wide_to_ansi(wtext); + DrawTextA(dc, ansi.c_str(), (int)ansi.size(), rc, fmt); + } } } void gdi_font_init(int point_size) { + detect_unicode_support(); + // Try Inter first, then Segoe UI, then Arial. static const char *faces[] = {"Inter", "Segoe UI", "Arial"}; for (int i = 0; i < 3; ++i) @@ -92,9 +186,18 @@ void gdi_font_init(int point_size) g_measure_dc = CreateCompatibleDC(NULL); SelectObject(g_measure_dc, g_hfont); - TEXTMETRICW tm; - GetTextMetricsW(g_measure_dc, &tm); - g_font_height = tm.tmHeight; + if (g_wide_ok) + { + TEXTMETRICW tm; + GetTextMetricsW(g_measure_dc, &tm); + g_font_height = tm.tmHeight; + } + else + { + TEXTMETRICA tm; + GetTextMetricsA(g_measure_dc, &tm); + g_font_height = tm.tmHeight; + } } void gdi_font_shutdown() @@ -122,7 +225,7 @@ int gdi_font_text_width(const char *text) return 0; std::wstring w = utf8_to_wide(text); SIZE sz; - GetTextExtentPoint32W(g_measure_dc, w.c_str(), (int)w.size(), &sz); + measure_text(g_measure_dc, w, &sz); return sz.cx; } @@ -132,7 +235,7 @@ int gdi_font_text_width(const char *text) // Strategy: // 1. Create a small DIB section just big enough for the text. // 2. Fill it with black (so GDI anti-aliasing blends against black). -// 3. Use DrawTextA to render white text onto the DIB. +// 3. Render white text onto the DIB. // 4. Read pixels back — the white channel gives us coverage (alpha). // 5. Blend each pixel into the Allegro BITMAP at the requested color. // ----------------------------------------------------------------------- @@ -143,10 +246,9 @@ static void render(BITMAP *dst, int x, int y, int allegro_color, const char *tex return; std::wstring wtext = utf8_to_wide(text); - int wlen = (int)wtext.size(); SIZE sz; - GetTextExtentPoint32W(g_measure_dc, wtext.c_str(), wlen, &sz); + measure_text(g_measure_dc, wtext, &sz); int tw = sz.cx; int th = sz.cy; if (tw <= 0 || th <= 0) @@ -181,7 +283,7 @@ static void render(BITMAP *dst, int x, int y, int allegro_color, const char *tex SetTextColor(dc, (COLORREF)0x00FFFFFF); // white SetBkMode(dc, TRANSPARENT); RECT rc = {0, 0, tw, th}; - DrawTextW(dc, wtext.c_str(), wlen, &rc, DT_LEFT | DT_TOP | DT_NOPREFIX | DT_SINGLELINE); + draw_to_dc(dc, wtext, &rc, DT_LEFT | DT_TOP | DT_NOPREFIX | DT_SINGLELINE); GdiFlush();