diff --git a/.gitignore b/.gitignore
index ea89168d2d..810938c168 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,5 @@ out
.venv/
debug.log
+Phobos.log
+gamemd.*
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..4d54f91a34
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1 @@
+.github/copilot-instructions.md
diff --git a/Phobos.props b/Phobos.props
index bee83b6da5..e24e1ac0b2 100644
--- a/Phobos.props
+++ b/Phobos.props
@@ -30,7 +30,7 @@
$(Configuration)\
$(Configuration)\IntDir\
- dbghelp.lib;onecore.lib
+ dbghelp.lib;onecore.lib;imm32.lib
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 5d2d15eca0..3b7b63e02b 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -264,7 +264,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -387,6 +411,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/YRpp b/YRpp
index 613922aa10..171250122a 160000
--- a/YRpp
+++ b/YRpp
@@ -1 +1 @@
-Subproject commit 613922aa10f47d547baa00ab4b67e04b6b5de4b7
+Subproject commit 171250122ad6bc5b461b2b896f96eb6ec2d0ba16
diff --git a/src/Misc/RetryDialog.cpp b/src/Misc/RetryDialog.cpp
index e8f6945f10..8e4383c274 100644
--- a/src/Misc/RetryDialog.cpp
+++ b/src/Misc/RetryDialog.cpp
@@ -7,11 +7,6 @@
#include
#include
-namespace RetryDialogFlag
-{
- bool IsCalledFromRetryDialog = false;
-}
-
DEFINE_HOOK(0x686092, DoLose_RetryDialogForCampaigns, 0x7)
{
enum { OK = 0x6860F6, Cancel = 0x6860EE, LoadGame = 0x686231 };
@@ -39,9 +34,7 @@ DEFINE_HOOK(0x686092, DoLose_RetryDialogForCampaigns, 0x7)
case WWMessageBox::Result::Button1:
auto pDialog = GameCreate();
- RetryDialogFlag::IsCalledFromRetryDialog = true;
const bool bIsAboutToLoad = pDialog->LoadDialog();
- RetryDialogFlag::IsCalledFromRetryDialog = false;
GameDelete(pDialog);
if (!bIsAboutToLoad)
@@ -63,26 +56,3 @@ DEFINE_HOOK(0x686092, DoLose_RetryDialogForCampaigns, 0x7)
return LoadGame;
}
-
-DEFINE_HOOK(0x558F4E, LoadOptionClass_Dialog_CenterListBox, 0x5)
-{
- if (RetryDialogFlag::IsCalledFromRetryDialog)
- {
- GET(HWND, hListBox, EAX);
- GET(HWND, hDialog, EDI);
-
- HWND hLoadButton = GetDlgItem(hDialog, 1039);
-
- RECT buttonRect;
- GetWindowRect(hLoadButton, &buttonRect);
-
- float scaleX = static_cast(buttonRect.right - buttonRect.left) / 108;
- float scaleY = static_cast(buttonRect.bottom - buttonRect.top) / 22;
- int X = buttonRect.left - static_cast(346 * scaleX);
- int Y = buttonRect.top - static_cast(44 * scaleY);
-
- SetWindowPos(hListBox, NULL, X, Y, NULL, NULL, SWP_NOSIZE | SWP_NOZORDER);
- }
-
- return 0;
-}
diff --git a/src/OwnerDraw/Button.cpp b/src/OwnerDraw/Button.cpp
new file mode 100644
index 0000000000..af33095f66
--- /dev/null
+++ b/src/OwnerDraw/Button.cpp
@@ -0,0 +1,827 @@
+#include "OwnerDraw.Internal.h"
+
+constexpr UINT OwnerDrawButtonTimerId = 0;
+constexpr UINT OwnerDrawButtonTimerInterval = 1000;
+constexpr int OwnerDrawButtonTextStyle = 5;
+constexpr int OwnerDrawButtonTextAlign = 12;
+constexpr BYTE OwnerDrawButtonDisabledOverlayAlpha = 0x80;
+
+static COLORREF MakeOwnerDrawButtonSideTextColor(BYTE red, WORD greenBlue)
+{
+ const BYTE green = static_cast(greenBlue & 0xFF);
+ const BYTE blue = static_cast((greenBlue >> 8) & 0xFF);
+ const int surfaceColor = Drawing::RGB_To_Int(red, green, blue);
+
+ BYTE outputRed = 0;
+ BYTE outputGreen = 0;
+ BYTE outputBlue = 0;
+ Drawing::Int_To_RGB(surfaceColor, outputRed, outputGreen, outputBlue);
+
+ return static_cast(
+ outputRed
+ | (static_cast(outputGreen) << 8)
+ | ((static_cast(outputBlue) | 0x200) << 16));
+}
+
+static COLORREF GetDisabledOwnerDrawButtonTextColor()
+{
+ if (!SessionClass::Instance.CurrentlyInGame || !ScenarioClass::Instance)
+ return Phobos::UI::ColorDisabledButton;
+
+ switch (ScenarioClass::Instance->PlayerSideIndex)
+ {
+ case 0:
+ return MakeOwnerDrawButtonSideTextColor(
+ OwnerDraw::ButtonDisabledSide0Red,
+ OwnerDraw::ButtonDisabledSide0GreenBlue);
+
+ case 1:
+ return MakeOwnerDrawButtonSideTextColor(
+ OwnerDraw::ButtonDisabledSide1Red,
+ OwnerDraw::ButtonDisabledSide1GreenBlue);
+
+ default:
+ return MakeOwnerDrawButtonSideTextColor(
+ OwnerDraw::ButtonDisabledSideOtherRed,
+ OwnerDraw::ButtonDisabledSideOtherGreenBlue);
+ }
+}
+
+static void EnsureOwnerDrawButtonCache(OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect)
+{
+ if (data.CacheSurface || !DSurface::Alternate)
+ return;
+
+ const int width = clientRect.right + 1;
+ const int height = clientRect.bottom + 1;
+ if (width <= 0 || height <= 0)
+ return;
+
+ data.CacheSurface = GameCreate(width, height);
+ if (!data.CacheSurface)
+ return;
+
+ ++OwnerDraw::CachedSurfaceCount;
+
+ RectangleStruct destRect { 0, 0, width, height };
+ RectangleStruct sourceRect { ownerRect.left, ownerRect.top, width, height };
+ CopySurfacePart(data.CacheSurface, destRect, DSurface::Alternate, sourceRect);
+}
+
+static void RestoreOwnerDrawButtonCache(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect)
+{
+ if (!data.CacheSurface || !DSurface::Alternate)
+ return;
+
+ const int width = clientRect.right + 1;
+ const int height = clientRect.bottom + 1;
+ if (width <= 0 || height <= 0)
+ return;
+
+ RectangleStruct destRect { ownerRect.left, ownerRect.top, width, height };
+ RectangleStruct sourceRect { 0, 0, width, height };
+ CopySurfacePart(DSurface::Alternate, destRect, data.CacheSurface, sourceRect);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+}
+
+static bool DrawOwnerDrawButtonShape(
+ OwnerDrawDialogElement& data,
+ const RectangleStruct& controlRect,
+ int drawItemState,
+ LONG windowStyle,
+ COLORREF& textColor)
+{
+ ConvertClass* pConvert = nullptr;
+ SHPStruct* pShape = nullptr;
+ int frame = 0;
+
+ switch (data.LayoutBand)
+ {
+ case 1:
+ pConvert = OwnerDraw::GetSmallButtonAnimConvert();
+ pShape = OwnerDraw::SmallButtonAnimShape;
+ frame = 2;
+ if (drawItemState & 1)
+ frame = 4;
+ else if (data.AsButton().AlternateFrame())
+ frame = 3;
+ break;
+
+ case 2:
+ pConvert = OwnerDraw::GetSideButtonConvert();
+ pShape = OwnerDraw::SideButtonShape;
+ if (drawItemState & 1)
+ frame = 1;
+ else if (data.AsButton().AlternateFrame())
+ frame = 2;
+ break;
+
+ case 3:
+ pConvert = OwnerDraw::GetCloseButtonConvert();
+ pShape = OwnerDraw::CloseButtonShape;
+ if (drawItemState & 1)
+ frame = 1;
+ else if (data.AsButton().AlternateFrame())
+ frame = 2;
+ break;
+
+ default:
+ break;
+ }
+
+ if (windowStyle & WS_DISABLED)
+ textColor = GetDisabledOwnerDrawButtonTextColor();
+
+ if (!pConvert || !pShape || !DSurface::Alternate)
+ return false;
+
+ Point2D position { controlRect.X, controlRect.Y };
+ RectangleStruct bounds = DSurface::Alternate->GetRect();
+ CC_Draw_Shape(
+ DSurface::Alternate,
+ pConvert,
+ pShape,
+ frame,
+ &position,
+ &bounds,
+ BlitterFlags::bf_400,
+ 0,
+ 0,
+ ZGradient::Ground,
+ 1000,
+ 0,
+ nullptr,
+ 0,
+ 0,
+ 0);
+
+ return true;
+}
+
+static void DrawOwnerDrawButtonImage(
+ OwnerDrawDialogElement& data,
+ const RectangleStruct& controlRect,
+ int drawItemState)
+{
+ auto pImage = data.ControlImage;
+ if (!pImage)
+ return;
+
+ if ((drawItemState & 1) && data.StateImageSurface)
+ pImage = data.StateImageSurface;
+
+ RectangleStruct sourceRect { 0, 0, controlRect.Width, controlRect.Height };
+ CopySurfacePart(DSurface::Alternate, controlRect, pImage, sourceRect);
+}
+
+static int SelectOwnerDrawButtonSliceIndex(int height)
+{
+ return height >= 30 ? 1 : 0;
+}
+
+static void DrawOwnerDrawButtonSlices(
+ HWND hWnd,
+ OwnerDrawDialogElement& data,
+ const RECT& clientRect,
+ const RECT& ownerRect,
+ RectangleStruct& drawRect,
+ int drawItemState,
+ LONG windowStyle)
+{
+ const bool pressed = (drawItemState & 1) != 0;
+ char variant = pressed ? 'd' : 'u';
+
+ if (windowStyle & WS_DISABLED)
+ {
+ variant = 'u';
+ }
+ else if (variant == 'd' && OwnerDraw::ButtonSliceVariant == 'u')
+ {
+ VocClass::PlayGlobal(RulesClass::Instance->GenericClick, 0x2000, 1.0f);
+ }
+
+ OwnerDraw::ButtonSliceVariant = variant;
+
+ const int sliceHeights[2] { 24, 30 };
+ const int leftSliceWidths[2] { 7, 10 };
+ const int rightSliceWidths[2] { 7, 10 };
+ const int sliceIndex = SelectOwnerDrawButtonSliceIndex(drawRect.Height);
+ const int sliceHeight = sliceHeights[sliceIndex];
+ const int leftSliceWidth = leftSliceWidths[sliceIndex];
+ const int rightSliceWidth = rightSliceWidths[sliceIndex];
+
+ RestoreOwnerDrawButtonCache(hWnd, data, clientRect, ownerRect);
+
+ drawRect.Y += (drawRect.Height - sliceHeight) / 2;
+ if (pressed)
+ drawRect.Y += 2;
+
+ char filename[32] {};
+
+ std::snprintf(filename, sizeof(filename), "b%c%c_li%d.pcx", variant, 'e', sliceHeight);
+ if (auto pLeft = GetPCXSurface(filename))
+ {
+ drawRect.Height = pLeft->GetHeight();
+ RectangleStruct destRect { drawRect.X, drawRect.Y, leftSliceWidth, sliceHeight };
+ RectangleStruct sourceRect { 0, 0, leftSliceWidth, sliceHeight };
+ CopySurfacePart(DSurface::Alternate, destRect, pLeft, sourceRect);
+ }
+ else
+ {
+ drawRect.Height = sliceHeight;
+ }
+
+ std::snprintf(filename, sizeof(filename), "b%c%c_mi%d.pcx", variant, 'e', sliceHeight);
+ if (auto pMiddle = GetPCXSurface(filename))
+ {
+ RectangleStruct middleRect
+ {
+ drawRect.X + leftSliceWidth,
+ drawRect.Y,
+ drawRect.Width - leftSliceWidth - rightSliceWidth,
+ pMiddle->GetHeight()
+ };
+
+ if (middleRect.Width > 0 && middleRect.Height > 0)
+ BlitTiledPCX(middleRect, DSurface::Alternate, pMiddle, 0, 0);
+ }
+
+ std::snprintf(filename, sizeof(filename), "b%c%c_ri%d.pcx", variant, 'e', sliceHeight);
+ if (auto pRight = GetPCXSurface(filename))
+ {
+ const int rightHeight = pRight->GetHeight();
+ RectangleStruct destRect
+ {
+ drawRect.X + drawRect.Width - rightSliceWidth,
+ drawRect.Y,
+ rightSliceWidth,
+ rightHeight
+ };
+ RectangleStruct sourceRect { 0, 0, rightSliceWidth, rightHeight };
+ CopySurfacePart(DSurface::Alternate, destRect, pRight, sourceRect);
+ }
+}
+
+static void DrawOwnerDrawButtonText(
+ OwnerDrawDialogElement& data,
+ const RectangleStruct& drawRect,
+ int drawItemState,
+ COLORREF textColor)
+{
+ if (data.ControlImage || !data.TextBuffer)
+ return;
+
+ RECT textRect
+ {
+ drawRect.X,
+ drawRect.Y + 1,
+ drawRect.X + drawRect.Width - 2,
+ drawRect.Y + drawRect.Height
+ };
+
+ if (drawItemState & 1)
+ {
+ textRect.left = drawRect.X + 2;
+ textRect.top += 4;
+ }
+
+ OwnerDraw::DrawWideText(
+ DSurface::Alternate,
+ data.TextBuffer,
+ &textRect,
+ data.AsButton().Font(),
+ textColor,
+ OwnerDrawButtonTextStyle,
+ OwnerDrawButtonTextAlign,
+ 0,
+ 0,
+ 0);
+}
+
+static LRESULT PaintOwnerDrawButton(HWND hWnd, OwnerDrawDialogElement& data, LONG windowStyle)
+{
+ if (data.SkipDraw)
+ {
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+ }
+
+ RECT ownerRect {};
+ RECT clientRect {};
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect))
+ ::GetClientRect(hWnd, &clientRect);
+
+ const int width = ownerRect.right - ownerRect.left;
+ const int height = ownerRect.bottom - ownerRect.top;
+ RectangleStruct controlRect { ownerRect.left, ownerRect.top, width, height };
+ RectangleStruct drawRect = controlRect;
+
+ if (DSurface::Alternate)
+ {
+ EnsureOwnerDrawButtonCache(data, clientRect, ownerRect);
+
+ COLORREF textColor = Phobos::UI::ColorTextButton;
+ const int drawItemState = data.AsButton().DrawItemState();
+ if (data.LayoutBand)
+ {
+ DrawOwnerDrawButtonShape(data, controlRect, drawItemState, windowStyle, textColor);
+ }
+ else if (data.ControlImage)
+ {
+ DrawOwnerDrawButtonImage(data, controlRect, drawItemState);
+ }
+ else
+ {
+ DrawOwnerDrawButtonSlices(hWnd, data, clientRect, ownerRect, drawRect, drawItemState, windowStyle);
+ }
+
+ DrawOwnerDrawButtonText(data, drawRect, drawItemState, textColor);
+
+ if (!data.LayoutBand && (windowStyle & WS_DISABLED))
+ BlendFillRect(controlRect, DSurface::Alternate, 0, OwnerDrawButtonDisabledOverlayAlpha);
+ }
+
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+}
+
+constexpr int CheckboxArtSize = 18;
+constexpr int CheckboxTextOffset = 26;
+constexpr int CheckboxTextStyle = 4;
+constexpr int CheckboxTextAlign = 12;
+
+static const char* SelectCheckboxArtName(OwnerDrawDialogElement& data)
+{
+ const bool checked = data.AsCheckbox().CheckState() == BST_CHECKED;
+
+ if (data.AsCheckbox().UseExtendedArt())
+ {
+ if (checked)
+ return data.AsCheckbox().ArtVariant() ? "cce_i.pcx" : "cce_il.pcx";
+
+ return data.AsCheckbox().ArtVariant() ? "cce_ir.pcx" : "cue_i.pcx";
+ }
+
+ return checked ? "cce_i.pcx" : "cue_i.pcx";
+}
+
+static LRESULT PaintCheckboxCtrl(HWND hWnd, OwnerDrawDialogElement& data, LONG windowStyle)
+{
+ if (!DSurface::Alternate)
+ {
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+ }
+
+ RECT clientRect {};
+ RECT textRect {};
+ RECT ownerRect {};
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect))
+ ::GetClientRect(hWnd, &clientRect);
+ OwnerDraw::GetRectangle(hWnd, &textRect);
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ const RectangleStruct artDest
+ {
+ ownerRect.left,
+ ownerRect.top,
+ CheckboxArtSize,
+ CheckboxArtSize
+ };
+
+ if (auto pArt = GetPCXSurface(SelectCheckboxArtName(data)))
+ {
+ RectangleStruct sourceRect { 0, 0, pArt->GetWidth(), pArt->GetHeight() };
+ CopySurfacePart(DSurface::Alternate, artDest, pArt, sourceRect);
+ }
+
+ if (windowStyle & WS_DISABLED)
+ BlendFillRect(artDest, DSurface::Alternate, 0, OwnerDraw::DisabledOverlayAlpha);
+
+ if (data.TextBuffer)
+ {
+ textRect.left += CheckboxTextOffset;
+ const COLORREF textColor = (windowStyle & WS_DISABLED)
+ ? Phobos::UI::ColorDisabledCheckbox
+ : Phobos::UI::ColorTextCheckbox;
+
+ OwnerDraw::DrawWideText(
+ DSurface::Alternate,
+ data.TextBuffer,
+ &textRect,
+ data.AsCheckbox().Font(),
+ textColor,
+ CheckboxTextStyle,
+ CheckboxTextAlign,
+ 0,
+ 0,
+ 0);
+ }
+
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+}
+
+static bool IsInsideCheckboxArt(HWND hWnd, LPARAM lParam)
+{
+ const POINT point = RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam);
+ return point.x >= 0 && point.y >= 0 && point.x < CheckboxArtSize && point.y < CheckboxArtSize;
+}
+
+static void NotifyCheckboxClicked(HWND hWnd, int checkState)
+{
+ if (RulesClass::Instance)
+ VocClass::PlayGlobal(RulesClass::Instance->GUICheckboxSound, 0x2000, 1.0f);
+
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ {
+ const WPARAM command = static_cast(
+ (::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF)
+ | ((checkState & 0xFFFF) << 16));
+
+ ::SendMessageA(parentHwnd, WM_COMMAND, command, reinterpret_cast(hWnd));
+ }
+}
+
+constexpr int RadioTextStyle = 5;
+constexpr int RadioTextAlign = 12;
+constexpr BYTE RadioDisabledOverlayAlpha = 0x80;
+
+static void EnsureRadioCache(OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect)
+{
+ if (data.CacheSurface || !DSurface::Alternate)
+ return;
+
+ const int width = clientRect.right + 1;
+ const int height = clientRect.bottom + 1;
+ if (width <= 0 || height <= 0)
+ return;
+
+ data.CacheSurface = GameCreate(width, height);
+ if (!data.CacheSurface)
+ return;
+
+ ++OwnerDraw::CachedSurfaceCount;
+
+ RectangleStruct destRect { 0, 0, width, height };
+ RectangleStruct sourceRect { ownerRect.left, ownerRect.top, width, height };
+ CopySurfacePart(data.CacheSurface, destRect, DSurface::Alternate, sourceRect);
+}
+
+static void RestoreRadioCache(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect)
+{
+ if (!data.CacheSurface || !DSurface::Alternate)
+ return;
+
+ const int width = clientRect.right + 1;
+ const int height = clientRect.bottom + 1;
+ if (width <= 0 || height <= 0)
+ return;
+
+ RectangleStruct destRect { ownerRect.left, ownerRect.top, width, height };
+ RectangleStruct sourceRect { 0, 0, width, height };
+ CopySurfacePart(DSurface::Alternate, destRect, data.CacheSurface, sourceRect);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+}
+
+static int SelectRadioSliceHeight(int controlHeight)
+{
+ return controlHeight >= 30 ? 30 : 24;
+}
+
+static void DrawRadioImage(OwnerDrawDialogElement& data, const RectangleStruct& controlRect, int selected)
+{
+ auto pImage = data.ControlImage;
+ if (selected && data.StateImageSurface)
+ pImage = data.StateImageSurface;
+
+ if (!pImage)
+ return;
+
+ RectangleStruct sourceRect { 0, 0, controlRect.Width, controlRect.Height };
+ CopySurfacePart(DSurface::Alternate, controlRect, pImage, sourceRect);
+}
+
+static void DrawRadioSlices(
+ HWND hWnd,
+ OwnerDrawDialogElement& data,
+ const RECT& clientRect,
+ const RECT& ownerRect,
+ const RectangleStruct& controlRect,
+ LONG windowStyle,
+ int selected)
+{
+ char variant = selected ? 'd' : 'u';
+ if (windowStyle & WS_DISABLED)
+ variant = 'u';
+
+ const int sliceHeight = SelectRadioSliceHeight(controlRect.Height);
+ const int leftSliceWidth = 7;
+ const int rightSliceWidth = 10;
+
+ RestoreRadioCache(hWnd, data, clientRect, ownerRect);
+
+ const int sliceY = controlRect.Y + (controlRect.Height - sliceHeight) / 2 + (selected ? 2 : 0);
+ char filename[32] {};
+
+ std::snprintf(filename, sizeof(filename), "b%c%c_li%d.pcx", variant, 'e', sliceHeight);
+ if (auto pLeft = GetPCXSurface(filename))
+ {
+ RectangleStruct destRect { controlRect.X, sliceY, leftSliceWidth, sliceHeight };
+ RectangleStruct sourceRect { 0, 0, leftSliceWidth, sliceHeight };
+ CopySurfacePart(DSurface::Alternate, destRect, pLeft, sourceRect);
+ }
+
+ std::snprintf(filename, sizeof(filename), "b%c%c_mi%d.pcx", variant, 'e', sliceHeight);
+ if (auto pMiddle = GetPCXSurface(filename))
+ {
+ RectangleStruct middleRect
+ {
+ controlRect.X + leftSliceWidth,
+ sliceY,
+ controlRect.Width - rightSliceWidth,
+ pMiddle->GetHeight()
+ };
+
+ BlitTiledPCX(middleRect, DSurface::Alternate, pMiddle, 0, 0);
+ }
+
+ std::snprintf(filename, sizeof(filename), "b%c%c_ri%d.pcx", variant, 'e', sliceHeight);
+ if (auto pRight = GetPCXSurface(filename))
+ {
+ const int rightHeight = pRight->GetHeight();
+ RectangleStruct destRect
+ {
+ controlRect.X + controlRect.Width - rightSliceWidth,
+ sliceY,
+ rightSliceWidth,
+ rightHeight
+ };
+ RectangleStruct sourceRect { 0, 0, rightSliceWidth, rightHeight };
+ CopySurfacePart(DSurface::Alternate, destRect, pRight, sourceRect);
+ }
+
+ if (data.TextBuffer)
+ {
+ RECT textRect
+ {
+ controlRect.X,
+ sliceY + 1,
+ controlRect.X + controlRect.Width - 2,
+ sliceY + controlRect.Height - 2
+ };
+
+ if (selected)
+ {
+ textRect.left += 2;
+ textRect.top += 4;
+ }
+
+ OwnerDraw::DrawWideText(
+ DSurface::Alternate,
+ data.TextBuffer,
+ &textRect,
+ data.AsRadio().Font(),
+ Phobos::UI::ColorTextRadio,
+ RadioTextStyle,
+ RadioTextAlign,
+ 0,
+ 0,
+ 0);
+ }
+}
+
+static LRESULT PaintRadioCtrl(HWND hWnd, OwnerDrawDialogElement& data, LONG windowStyle)
+{
+ if (!DSurface::Alternate)
+ {
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+ }
+
+ RECT ownerRect {};
+ RECT clientRect {};
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect))
+ ::GetClientRect(hWnd, &clientRect);
+
+ const int width = ownerRect.right - ownerRect.left;
+ const int height = ownerRect.bottom - ownerRect.top;
+ RectangleStruct controlRect { ownerRect.left, ownerRect.top, width, height };
+
+ EnsureRadioCache(data, clientRect, ownerRect);
+
+ const int selected = data.AsRadio().CheckState() & 1;
+ if (data.ControlImage)
+ {
+ DrawRadioImage(data, controlRect, selected);
+ }
+ else
+ {
+ DrawRadioSlices(hWnd, data, clientRect, ownerRect, controlRect, windowStyle, selected);
+ }
+
+ if (windowStyle & WS_DISABLED)
+ BlendFillRect(controlRect, DSurface::Alternate, 0, RadioDisabledOverlayAlpha);
+
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+}
+
+LRESULT CALLBACK WWUI::OwnerDrawCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ auto pData = FindOwnerDrawData(hWnd);
+ if (!pData)
+ return forwardOriginal();
+
+ auto& data = *pData;
+
+ switch (message)
+ {
+ case WM_ACTIVATE:
+ case WM_KILLFOCUS:
+ case WM_MOUSEACTIVATE:
+ return 0;
+
+ case WM_PAINT:
+ return PaintOwnerDrawButton(hWnd, data, ::GetWindowLongA(hWnd, GWL_STYLE));
+
+ case WM_TIMER:
+ data.AsButton().AlternateFrame() = !data.AsButton().AlternateFrame();
+ ::InvalidateRect(hWnd, nullptr, TRUE);
+ return forwardOriginal();
+
+ case WM_LBUTTONDOWN:
+ case WM_LBUTTONDBLCLK:
+ if (data.SkipDraw)
+ return 0;
+
+ VocClass::PlayGlobal(RulesClass::Instance->GUIMainButtonSound, 0x2000, 1.0f);
+ return forwardOriginal();
+
+ case WW_BUTTON_SETANIMATED:
+ if (lParam == 1)
+ {
+ if (!data.AsButton().TimerActive())
+ {
+ data.AsButton().TimerActive() = true;
+ ::SetTimer(hWnd, OwnerDrawButtonTimerId, OwnerDrawButtonTimerInterval, nullptr);
+ }
+ }
+ else if (data.AsButton().TimerActive())
+ {
+ data.AsButton().TimerActive() = false;
+ data.AsButton().AlternateFrame() = false;
+ ::KillTimer(hWnd, OwnerDrawButtonTimerId);
+ ::InvalidateRect(hWnd, nullptr, TRUE);
+ }
+
+ return forwardOriginal();
+
+ default:
+ return forwardOriginal();
+ }
+}
+
+LRESULT CALLBACK WWUI::CheckboxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ auto pData = FindOwnerDrawData(hWnd);
+ if (!pData)
+ return 0;
+
+ auto& data = *pData;
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ switch (message)
+ {
+ case BM_GETCHECK:
+ return data.AsCheckbox().CheckState();
+
+ case BM_SETCHECK:
+ data.AsCheckbox().CheckState() = static_cast(wParam);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return 0;
+
+ case WM_SETFOCUS:
+ case WM_KILLFOCUS:
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return forwardOriginal();
+
+ case WM_PAINT:
+ if (!data.AsCheckbox().UseNativePaint())
+ return PaintCheckboxCtrl(hWnd, data, ::GetWindowLongA(hWnd, GWL_STYLE));
+
+ return forwardOriginal();
+
+ case WM_LBUTTONDOWN:
+ case WM_LBUTTONDBLCLK:
+ if (!IsInsideCheckboxArt(hWnd, lParam))
+ return 0;
+
+ data.AsCheckbox().CheckState() = data.AsCheckbox().CheckState() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED;
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ NotifyCheckboxClicked(hWnd, data.AsCheckbox().CheckState());
+ return 0;
+
+ case WW_INITDIALOG:
+ data.AsCheckbox().CheckState() = static_cast(
+ CallSelectedHandler(pOriginalWndProc, hWnd, BM_GETCHECK, 0, 0));
+ return forwardOriginal();
+
+ case WW_CHECKBOX_ENABLEEXTENDEDART:
+ {
+ const bool enabled = lParam != 0;
+ const bool oldArtVariant = data.AsCheckbox().ArtVariant();
+ data.AsCheckbox().UseExtendedArt() = enabled;
+ if (oldArtVariant != enabled)
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+
+ return forwardOriginal();
+ }
+
+ case WW_CHECKBOX_SETARTVARIANT:
+ {
+ const bool variant = lParam != 0;
+ const bool oldArtVariant = data.AsCheckbox().ArtVariant();
+ data.AsCheckbox().ArtVariant() = variant;
+ if (oldArtVariant != variant)
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+
+ return forwardOriginal();
+ }
+
+ case WW_CHECKBOX_GETARTVARIANT:
+ return data.AsCheckbox().ArtVariant();
+
+ default:
+ return forwardOriginal();
+ }
+}
+
+LRESULT CALLBACK WWUI::RadioCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ auto pData = FindOwnerDrawData(hWnd);
+ if (!pData)
+ return 0;
+
+ auto& data = *pData;
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ switch (message)
+ {
+ case BM_GETCHECK:
+ return data.AsRadio().CheckState();
+
+ case BM_SETCHECK:
+ data.AsRadio().CheckState() = static_cast(wParam);
+ ::InvalidateRect(hWnd, nullptr, TRUE);
+ return forwardOriginal();
+
+ case WM_PAINT:
+ return PaintRadioCtrl(hWnd, data, ::GetWindowLongA(hWnd, GWL_STYLE));
+
+ case WM_LBUTTONDOWN:
+ case WM_LBUTTONDBLCLK:
+ if (data.AsRadio().CheckState())
+ return 0;
+
+ data.AsRadio().CheckState() = BST_CHECKED;
+ ::InvalidateRect(hWnd, nullptr, TRUE);
+
+ if (RulesClass::Instance)
+ VocClass::PlayGlobal(RulesClass::Instance->GenericClick, 0x2000, 1.0f);
+
+ return forwardOriginal();
+
+ case WM_LBUTTONUP:
+ ::LockWindowUpdate(::GetParent(hWnd));
+ {
+ const LRESULT result = forwardOriginal();
+ ::LockWindowUpdate(nullptr);
+ return result;
+ }
+
+ case WW_INITDIALOG:
+ data.AsRadio().CheckState() = static_cast(
+ CallSelectedHandler(pOriginalWndProc, hWnd, BM_GETCHECK, 0, 0));
+ return forwardOriginal();
+
+ default:
+ return forwardOriginal();
+ }
+}
diff --git a/src/OwnerDraw/ComboBox.cpp b/src/OwnerDraw/ComboBox.cpp
new file mode 100644
index 0000000000..ebbf9fc478
--- /dev/null
+++ b/src/OwnerDraw/ComboBox.cpp
@@ -0,0 +1,852 @@
+#include "OwnerDraw.Internal.h"
+
+static COLORREF ComboBoxTextColor(bool disabled, bool alternatePalette)
+{
+ if (alternatePalette)
+ return disabled ? OwnerDraw::AltDisabledTextColor : OwnerDraw::AltComboTextColor;
+
+ return disabled ? Phobos::UI::ColorDisabledCombobox : Phobos::UI::ColorTextCombobox;
+}
+
+static void SyncComboDropSelectionColor()
+{
+ OwnerDraw::ListSelectionFillColor = Phobos::UI::ColorSelectionCombobox;
+}
+
+constexpr int ComboBoxArrowWidth = 20;
+constexpr int ComboBoxArrowLeftOffset = 19;
+constexpr int ComboBoxVisibleHeight = 24;
+constexpr int ComboBoxDefaultMaxVisibleDropItems = 9;
+constexpr int ComboBoxMaxColorItems = 50;
+constexpr int ComboBoxTextEntryInlineBytes = 0;
+constexpr int ComboBoxEditListNotificationCode = 0x300;
+constexpr int ComboBoxParentEditChangeNotificationCode = 5;
+
+static bool IsComboBoxDropDownList(HWND hWnd)
+{
+ return (::GetWindowLongA(hWnd, GWL_STYLE) & 3) == CBS_DROPDOWNLIST;
+}
+
+static bool IsComboBoxDropDown(HWND hWnd)
+{
+ return (::GetWindowLongA(hWnd, GWL_STYLE) & 3) == CBS_DROPDOWN;
+}
+
+int BitFontHeight(BitFont* pFont)
+{
+ if (!pFont)
+ pFont = BitFont::Instance;
+
+ if (!pFont)
+ return 10;
+
+ return pFont->field_1C;
+}
+
+static void TrimComboTextToWidth(wchar_t* pText, size_t capacity, BitFont* pFont, int maxWidth)
+{
+ if (!pText || !capacity || maxWidth <= 0)
+ return;
+
+ pText[capacity - 1] = L'\0';
+ size_t length = std::wcslen(pText);
+ if (!length)
+ return;
+
+ int textWidth = 0;
+ int textHeight = 0;
+ if (!pFont)
+ pFont = BitFont::Instance;
+
+ while (length > 0 && pFont)
+ {
+ pFont->GetTextDimension(pText, &textWidth, &textHeight, 0);
+ if (textWidth <= maxWidth)
+ break;
+
+ --length;
+ pText[length] = L'\0';
+ if (length + 3 < capacity)
+ std::wcscat(pText, L"...");
+ }
+}
+
+static WWUIComboBoxItem* AllocateComboBoxItem(OwnerDrawDialogElement& data, const wchar_t* pText, bool isWide)
+{
+ if (!pText)
+ pText = L"";
+
+ const size_t length = std::wcslen(pText);
+ const size_t bytes = sizeof(WWUIComboBoxItem) + (length + 1) * sizeof(wchar_t) + ComboBoxTextEntryInlineBytes;
+ auto pEntry = static_cast(YRMemory::Allocate(bytes));
+ if (!pEntry)
+ return nullptr;
+
+ pEntry->Next = data.AsComboBox().TextEntries();
+ pEntry->ItemData = 0;
+ pEntry->Text = reinterpret_cast(reinterpret_cast(pEntry) + sizeof(WWUIComboBoxItem));
+ pEntry->IsWideText = isWide ? 1 : 0;
+ std::wcscpy(pEntry->Text, pText);
+ data.AsComboBox().TextEntries() = pEntry;
+ return pEntry;
+}
+
+static void RemoveComboBoxItem(OwnerDrawDialogElement& data, WWUIComboBoxItem* pEntry)
+{
+ if (!pEntry)
+ return;
+
+ WWUIComboBoxItem* pPrevious = nullptr;
+ for (auto pCurrent = data.AsComboBox().TextEntries(); pCurrent; pCurrent = pCurrent->Next)
+ {
+ if (pCurrent != pEntry)
+ {
+ pPrevious = pCurrent;
+ continue;
+ }
+
+ if (pPrevious)
+ pPrevious->Next = pCurrent->Next;
+ else
+ data.AsComboBox().TextEntries() = pCurrent->Next;
+
+ YRMemory::Deallocate(pCurrent);
+ return;
+ }
+}
+
+static WWUIComboBoxItem* GetComboBoxItem(WNDPROC pOriginalWndProc, HWND hWnd, int index)
+{
+ const auto result = CallSelectedHandler(pOriginalWndProc, hWnd, CB_GETITEMDATA, index, 0);
+ if (result == CB_ERR || !result)
+ return nullptr;
+
+ return reinterpret_cast(result);
+}
+
+static LRESULT ForwardComboTextMessageToEditList(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ if (IsComboBoxDropDownList(hWnd))
+ return CallSelectedHandler(FindWindowProc(OwnerDraw::DialogProcs, hWnd), hWnd, message, wParam, lParam);
+
+ LRESULT result = 0;
+ for (HWND child = ::GetWindow(hWnd, GW_CHILD); child; child = ::GetWindow(child, GW_HWNDNEXT))
+ {
+ char className[16] {};
+ ::GetClassNameA(child, className, sizeof(className));
+ if (_strcmpi(className, "listbox"))
+ continue;
+
+ if (message == WM_SETFOCUS)
+ {
+ ::SetFocus(child);
+ result = 0;
+ }
+ else
+ {
+ const UINT forwardedMessage = message == CB_LIMITTEXT ? EM_LIMITTEXT : message;
+ result = ::SendMessageA(child, forwardedMessage, wParam, lParam);
+ }
+ }
+
+ return result;
+}
+
+static int ResolveComboBorderColor(COLORREF color, bool disabledColor)
+{
+ if (color == static_cast(-1))
+ return disabledColor ? 0 : -1;
+
+ return ConvertRGBToSurfaceColor(color);
+}
+
+static void DrawComboDropButton(const RectangleStruct& rect, bool dropped, bool alternatePalette)
+{
+ DrawScrollArrow(DSurface::Alternate, rect, dropped, dropped, alternatePalette);
+}
+
+static bool GetComboBoxRenderLocalRect(HWND hWnd, const RECT& ownerRect, RECT& localRect)
+{
+ localRect = ownerRect;
+
+ const HWND parentHwnd = ::GetParent(hWnd);
+ if (!parentHwnd || parentHwnd == Game::hWnd)
+ return true;
+
+ RECT parentRect {};
+ if (!OwnerDraw::GetRectangle(parentHwnd, &parentRect))
+ return false;
+
+ localRect.left -= parentRect.left;
+ localRect.right -= parentRect.left;
+ localRect.top -= parentRect.top;
+ localRect.bottom -= parentRect.top;
+ return true;
+}
+
+static void EnsureComboBoxWindowHeight(HWND hWnd, OwnerDrawDialogElement& data, RECT& clientRect, RECT& ownerRect)
+{
+ const int width = ownerRect.right - ownerRect.left;
+ if (width <= 0 || ownerRect.bottom - ownerRect.top == ComboBoxVisibleHeight)
+ return;
+
+ RECT localRect {};
+ if (!GetComboBoxRenderLocalRect(hWnd, ownerRect, localRect))
+ return;
+
+ const BOOL moved = RenderDX::IsOwnerDrawUsingRawWindowCoordinates()
+ ? ::MoveWindow(hWnd, localRect.left, localRect.top, width, ComboBoxVisibleHeight, FALSE)
+ : RenderDX::MoveWindowInRender(hWnd, localRect.left, localRect.top, width, ComboBoxVisibleHeight, FALSE);
+
+ if (!moved)
+ return;
+
+ ResetOwnerDrawCachedSurface(data);
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect))
+ ::GetClientRect(hWnd, &clientRect);
+}
+
+static void PaintComboBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& ownerRect, WNDPROC pOriginalWndProc)
+{
+ if (!DSurface::Alternate)
+ return;
+
+ auto pFont = data.AsComboBox().Font() ? data.AsComboBox().Font() : BitFont::Instance;
+ const bool dropped = ::SendMessageA(hWnd, CB_GETDROPPEDSTATE, 0, 0) != 0;
+ const int width = ownerRect.right - ownerRect.left;
+ const int height = ownerRect.bottom - ownerRect.top;
+
+ RectangleStruct comboRect
+ {
+ ownerRect.left,
+ ownerRect.top,
+ width,
+ ComboBoxVisibleHeight
+ };
+
+ RectangleStruct localRect { 0, 0, width, height };
+ RectangleStruct parentSourceRect = localRect;
+ OwnerDrawDialogElement* pParentData = nullptr;
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ {
+ pParentData = FindOwnerDrawData(parentHwnd);
+ if (pParentData)
+ {
+ RECT parentRect {};
+ OwnerDraw::GetRectangle(parentHwnd, &parentRect);
+
+ if (pParentData->CacheSurface)
+ {
+ parentSourceRect.X = ownerRect.left - parentRect.left;
+ parentSourceRect.Y = ownerRect.top - parentRect.top;
+ }
+ }
+ }
+ EnsureScrollBarCache(data, pParentData, localRect, parentSourceRect);
+
+ OwnerDraw::CopyDimmedBackground(&comboRect, hWnd, static_cast(data.Alpha));
+ BlendFillRect(comboRect, DSurface::Alternate, 0, static_cast(data.Alpha));
+
+ const LONG style = ::GetWindowLongA(hWnd, GWL_STYLE);
+ const bool disabled = (style & WS_DISABLED) != 0;
+ const bool alternatePalette = data.AsComboBox().UseAlternatePalette();
+ const COLORREF borderColor = alternatePalette
+ ? (disabled ? OwnerDraw::AltDisabledBorderColor : OwnerDraw::AltBorderColor)
+ : (disabled ? OwnerDraw::DisabledBorderColor : OwnerDraw::DefaultBorderColor);
+
+ DrawBeveledBorder(DSurface::Alternate, comboRect, 2, ResolveComboBorderColor(borderColor, disabled));
+
+ RectangleStruct textAreaRect { comboRect.X, comboRect.Y, comboRect.Width - ComboBoxArrowWidth, comboRect.Height };
+ RectangleStruct buttonRect { ownerRect.right - ComboBoxArrowLeftOffset, comboRect.Y + 1, comboRect.Width, comboRect.Height };
+ DrawComboDropButton(buttonRect, dropped, alternatePalette);
+
+ if (disabled)
+ BlendFillRect(comboRect, DSurface::Alternate, 0, static_cast(data.Alpha));
+
+ if ((style & 3) != CBS_DROPDOWNLIST)
+ {
+ ::ValidateRect(hWnd, nullptr);
+ return;
+ }
+
+ const int selectedIndex = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, CB_GETCURSEL, 0, 0));
+ wchar_t textBuffer[0x100] {};
+ if (auto pItem = GetComboBoxItem(pOriginalWndProc, hWnd, selectedIndex))
+ {
+ std::wcsncpy(textBuffer, pItem->Text ? pItem->Text : L"", std::size(textBuffer) - 1);
+ }
+
+ COLORREF textColor = ComboBoxTextColor(disabled, alternatePalette);
+ if (data.AsComboBox().UseItemColorOverrides()
+ && selectedIndex >= 0
+ && selectedIndex < ComboBoxMaxColorItems
+ && data.AsComboBox().ItemColorOverrides()[selectedIndex] >= 0)
+ {
+ textColor = static_cast(data.AsComboBox().ItemColorOverrides()[selectedIndex]);
+ auto fillRect = textAreaRect;
+ InsetSurfaceRect(fillRect, 2, 2);
+ DSurface::Alternate->FillRect(&fillRect, ConvertRGBToSurfaceColor(textColor));
+ }
+
+ const int textMaxWidth = std::max(textAreaRect.Width - 4, 0);
+ TrimComboTextToWidth(textBuffer, std::size(textBuffer), pFont, textMaxWidth);
+
+ RECT textRect
+ {
+ textAreaRect.X + 2,
+ textAreaRect.Y,
+ textAreaRect.X + textAreaRect.Width,
+ textAreaRect.Y + textAreaRect.Height
+ };
+
+ OwnerDraw::DrawWideText(DSurface::Alternate, textBuffer, &textRect, pFont, textColor, 4, 12, 0, 0, 0);
+ ::ValidateRect(hWnd, nullptr);
+}
+
+static LRESULT AddOrInsertComboString(
+ OwnerDrawDialogElement& data,
+ WNDPROC pOriginalWndProc,
+ HWND hWnd,
+ UINT message,
+ WPARAM wParam,
+ LPARAM lParam,
+ bool wideText)
+{
+ char narrowText[2048] {};
+ wchar_t wideBuffer[2048] {};
+
+ const LPARAM nativeTextParam = [&]() -> LPARAM
+ {
+ if (wideText)
+ {
+ const auto pWideText = reinterpret_cast(lParam);
+ std::wcsncpy(wideBuffer, pWideText ? pWideText : L"", std::size(wideBuffer) - 1);
+ WideToCharString(narrowText, std::size(narrowText), wideBuffer);
+ return reinterpret_cast(narrowText);
+ }
+
+ const auto pText = reinterpret_cast(lParam);
+ std::strncpy(narrowText, pText ? pText : "", std::size(narrowText) - 1);
+ CharToWideString(wideBuffer, std::size(wideBuffer), narrowText);
+ return reinterpret_cast(narrowText);
+ }();
+
+ const bool add = message == WW_CB_ADDSTRINGA || message == WW_CB_ADDSTRINGW;
+ const UINT nativeMessage = add ? CB_ADDSTRING : CB_INSERTSTRING;
+ const WPARAM nativeIndex = add ? 0 : wParam;
+ const auto nativeResult = CallSelectedHandler(pOriginalWndProc, hWnd, nativeMessage, nativeIndex, nativeTextParam);
+ if (nativeResult == CB_ERR || nativeResult == CB_ERRSPACE)
+ return nativeResult;
+
+ const int itemIndex = static_cast(nativeResult);
+ auto pEntry = AllocateComboBoxItem(data, wideBuffer, wideText);
+ if (!pEntry)
+ {
+ CallSelectedHandler(pOriginalWndProc, hWnd, CB_DELETESTRING, itemIndex, 0);
+ return CB_ERRSPACE;
+ }
+
+ const auto setDataResult = CallSelectedHandler(
+ pOriginalWndProc,
+ hWnd,
+ CB_SETITEMDATA,
+ itemIndex,
+ reinterpret_cast(pEntry));
+
+ if (setDataResult == CB_ERR || setDataResult == CB_ERRSPACE)
+ {
+ CallSelectedHandler(pOriginalWndProc, hWnd, CB_DELETESTRING, itemIndex, 0);
+ RemoveComboBoxItem(data, pEntry);
+ return setDataResult;
+ }
+
+ return itemIndex;
+}
+
+static LRESULT FindComboString(OwnerDrawDialogElement& data, WNDPROC pOriginalWndProc, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool wideText, bool exact, bool select)
+{
+ (void)data;
+ (void)message;
+
+ wchar_t needle[2048] {};
+ if (wideText)
+ {
+ const auto pText = reinterpret_cast(lParam);
+ std::wcsncpy(needle, pText ? pText : L"", std::size(needle) - 1);
+ }
+ else
+ {
+ CharToWideString(needle, std::size(needle), reinterpret_cast(lParam));
+ }
+
+ const int count = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, CB_GETCOUNT, 0, 0));
+ if (count == CB_ERR)
+ return 0;
+
+ int index = static_cast(wParam);
+ if (index < 0)
+ index = 0;
+
+ if (index >= count)
+ return CB_ERR;
+
+ const size_t needleLength = std::wcslen(needle);
+ for (; index < count; ++index)
+ {
+ const auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, index);
+ const wchar_t* pText = pEntry && pEntry->Text ? pEntry->Text : L"";
+ const bool match = exact
+ ? _wcsicmp(needle, pText) == 0
+ : _wcsnicmp(needle, pText, needleLength) == 0;
+
+ if (!match)
+ continue;
+
+ if (select)
+ return ::SendMessageA(hWnd, CB_SETCURSEL, index, 0);
+
+ return index;
+ }
+
+ return CB_ERR;
+}
+
+static LRESULT GetComboText(WNDPROC pOriginalWndProc, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool wideOutput)
+{
+ if (lParam && message != WW_CB_GETITEMTEXTFORMAT && message != CB_GETLBTEXTLEN)
+ {
+ if (wideOutput)
+ *reinterpret_cast(lParam) = L'\0';
+ else
+ *reinterpret_cast(lParam) = '\0';
+ }
+
+ if (static_cast(wParam) == -1)
+ return 0;
+
+ auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, static_cast(wParam));
+ if (!pEntry)
+ return 0;
+
+ if (message == WW_CB_GETITEMTEXTFORMAT)
+ return pEntry->IsWideText;
+
+ const wchar_t* pText = pEntry->Text ? pEntry->Text : L"";
+ const auto length = static_cast(std::wcslen(pText));
+
+ if (message == WW_CB_GETLBTEXTA || message == WW_CB_GETLBTEXTW)
+ {
+ if (lParam)
+ {
+ if (wideOutput)
+ std::wcscpy(reinterpret_cast(lParam), pText);
+ else
+ WideToCharString(reinterpret_cast(lParam), static_cast(length + 1), pText);
+ }
+ }
+
+ return length < 0 ? 0 : length;
+}
+
+static LRESULT SetComboSelection(OwnerDrawDialogElement& data, WNDPROC pOriginalWndProc, HWND hWnd, WPARAM wParam)
+{
+ const int selection = static_cast(wParam);
+ data.AsComboBox().CurrentSelection() = selection;
+
+ if (selection == -1)
+ {
+ ::SendMessageA(hWnd, WW_SETTEXTA, 0, reinterpret_cast(""));
+ }
+ else if (auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, selection))
+ {
+ if (pEntry->IsWideText)
+ {
+ ::SendMessageA(hWnd, WW_SETTEXTW, 0, reinterpret_cast(pEntry->Text ? pEntry->Text : L""));
+ }
+ else
+ {
+ char buffer[2048] {};
+ WideToCharString(buffer, std::size(buffer), pEntry->Text ? pEntry->Text : L"");
+ ::SendMessageA(hWnd, WW_SETTEXTA, 0, reinterpret_cast(buffer));
+ }
+ }
+
+ if (IsComboBoxDropDown(hWnd))
+ return 0;
+
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return CallSelectedHandler(pOriginalWndProc, hWnd, CB_SETCURSEL, wParam, 0);
+}
+
+static void CloseComboDropDown(OwnerDrawDialogElement& data, HWND hWnd)
+{
+ const HWND dropHwnd = data.AsComboBox().DropDownHwnd();
+ if (!dropHwnd)
+ return;
+
+ ::ReleaseCapture();
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ ::SendMessageA(parentHwnd, WW_BRINGTOTOP, reinterpret_cast(dropHwnd), 0);
+
+ ::DestroyWindow(dropHwnd);
+ CleanupDestroyedWindow(dropHwnd);
+ data.AsComboBox().DropDownHwnd() = nullptr;
+}
+
+static bool GetDroppedControlRectInRender(HWND hWnd, RECT& rect)
+{
+ ::SendMessageA(hWnd, CB_GETDROPPEDCONTROLRECT, 0, reinterpret_cast(&rect));
+
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates())
+ return true;
+
+ POINT topLeft { rect.left, rect.top };
+ POINT bottomRight { rect.right, rect.bottom };
+ if (!RenderDX::ScreenToRenderPoint(&topLeft, false) || !RenderDX::ScreenToRenderPoint(&bottomRight, false))
+ return false;
+
+ rect.left = topLeft.x;
+ rect.top = topLeft.y;
+ rect.right = bottomRight.x;
+ rect.bottom = bottomRight.y;
+ return true;
+}
+
+static LRESULT OpenComboDropDown(OwnerDrawDialogElement& data, HWND hWnd, const RECT& clientRect, const RECT& ownerRect)
+{
+ if (data.AsComboBox().DropDownHwnd())
+ return 1;
+
+ SyncComboDropSelectionColor();
+
+ const HWND parentHwnd = ::GetParent(hWnd);
+ if (!parentHwnd)
+ return 1;
+
+ RECT parentRect {};
+ OwnerDraw::GetRectangle(parentHwnd, &parentRect);
+
+ int itemHeight = static_cast(::SendMessageA(hWnd, CB_GETITEMHEIGHT, 0, 0));
+ if (itemHeight <= 0)
+ itemHeight = 1;
+
+ int itemCount = static_cast(::SendMessageA(hWnd, CB_GETCOUNT, 0, 0));
+ if (itemCount < 1)
+ itemCount = 1;
+
+ const int visibleComboHeight = ComboBoxVisibleHeight;
+ const int maxVisibleItems = data.AsComboBox().MaxVisibleDropItems();
+ int visibleItems = 0;
+ if (maxVisibleItems > 0)
+ {
+ visibleItems = maxVisibleItems;
+ }
+ else
+ {
+ RECT dropRect {};
+ if (GetDroppedControlRectInRender(hWnd, dropRect))
+ {
+ int nativeListHeight = dropRect.bottom - dropRect.top;
+ if (dropRect.top <= ownerRect.top + visibleComboHeight / 2)
+ nativeListHeight -= visibleComboHeight;
+
+ if (nativeListHeight > 0)
+ visibleItems = nativeListHeight / itemHeight;
+ }
+
+ if (visibleItems <= 1 && itemCount > 1)
+ visibleItems = ComboBoxDefaultMaxVisibleDropItems;
+ }
+
+ if (visibleItems < 1)
+ visibleItems = 1;
+
+ visibleItems = std::min(visibleItems, itemCount);
+
+ const int dropTop = ownerRect.top + visibleComboHeight + 1;
+ int maxVisibleByParent = (parentRect.bottom - dropTop) / itemHeight;
+ if (maxVisibleByParent < 1)
+ maxVisibleByParent = 1;
+
+ visibleItems = std::min(visibleItems, maxVisibleByParent);
+
+ int dropHeight = visibleItems * itemHeight;
+ if (dropHeight <= 0)
+ dropHeight = itemHeight;
+
+ const int width = clientRect.right - clientRect.left;
+ const RECT dropRenderRect
+ {
+ ownerRect.left,
+ dropTop,
+ ownerRect.left + width,
+ dropTop + dropHeight
+ };
+
+ RECT dropClientRect {};
+ if (!RenderDX::RenderRectToClient(parentHwnd, dropRenderRect, &dropClientRect))
+ return 1;
+
+ const HWND dropHwnd = ::CreateWindowExA(
+ 0,
+ "ComboDropWin",
+ nullptr,
+ WS_CHILD,
+ dropClientRect.left,
+ dropClientRect.top,
+ dropClientRect.right - dropClientRect.left,
+ dropClientRect.bottom - dropClientRect.top,
+ parentHwnd,
+ nullptr,
+ Game::hInstance,
+ hWnd);
+
+ if (!dropHwnd)
+ return 1;
+
+ if (!FindOwnerDrawData(dropHwnd))
+ {
+ OwnerDrawDialogElement dropData;
+ dropData.AsComboBox().Font() = BitFont::Instance;
+ dropData.ControlType = WWControlType::Default;
+
+ OwnerDraw::Dialogs[dropHwnd] = dropData;
+ }
+
+ SessionIpb::RegisterHwnd(dropHwnd);
+ SessionIpb::RegisterHwnd(dropHwnd);
+
+ ::SendMessageA(dropHwnd, WW_DROPDOWN_INITIALIZE, 0, 0);
+ ::SendMessageA(parentHwnd, WW_BRINGTOTOP, reinterpret_cast(dropHwnd), 1);
+ ::SetCapture(dropHwnd);
+ ::ShowWindow(dropHwnd, SW_SHOWNORMAL);
+ data.AsComboBox().DropDownHwnd() = dropHwnd;
+ return 1;
+}
+
+LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ auto pData = FindOwnerDrawData(hWnd);
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+ if (!pData)
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+
+ auto& data = *pData;
+
+ RECT clientRect {};
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect))
+ ::GetClientRect(hWnd, &clientRect);
+
+ RECT ownerRect {};
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ auto handleItemData = [&]() -> LRESULT
+ {
+ auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, static_cast(wParam));
+ if (!pEntry)
+ return CB_ERR;
+
+ if (message == CB_GETITEMDATA || message == LB_GETITEMDATA)
+ return pEntry->ItemData;
+
+ pEntry->ItemData = static_cast(lParam);
+ return reinterpret_cast(pEntry);
+ };
+
+ switch (message)
+ {
+ case WM_DESTROY:
+ ::SendMessageA(hWnd, CB_SHOWDROPDOWN, 0, 0);
+ return forwardOriginal();
+
+ case WM_PAINT:
+ EnsureComboBoxWindowHeight(hWnd, data, clientRect, ownerRect);
+ PaintComboBox(hWnd, data, ownerRect, pOriginalWndProc);
+ return 0;
+
+ case WM_ERASEBKGND:
+ return 0;
+
+ case WM_LBUTTONDOWN:
+ case WM_LBUTTONDBLCLK:
+ if (RulesClass::Instance)
+ VocClass::PlayGlobal(RulesClass::Instance->GUIComboOpenSound, 0x2000, 1.0f);
+
+ if (RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam).x > clientRect.right - ComboBoxArrowWidth)
+ {
+ const bool dropped = ::SendMessageA(hWnd, CB_GETDROPPEDSTATE, 0, 0) == 1;
+ ::PostMessageA(hWnd, CB_SHOWDROPDOWN, dropped ? 0 : 1, 0);
+ }
+ return 0;
+
+ case WM_SETFOCUS:
+ case WM_SETTEXT:
+ case WM_GETTEXT:
+ case WM_GETTEXTLENGTH:
+ case CB_LIMITTEXT:
+ return ForwardComboTextMessageToEditList(hWnd, message, wParam, lParam);
+
+ case WM_DELETEITEM:
+ if (const auto pDeleteItem = reinterpret_cast(lParam))
+ {
+ RemoveComboBoxItem(data, reinterpret_cast(pDeleteItem->itemData));
+ if (data.AsComboBox().CurrentSelection() == static_cast(pDeleteItem->itemID))
+ data.AsComboBox().CurrentSelection() = -1;
+ }
+ return forwardOriginal();
+
+ case WM_COMMAND:
+ if (HIWORD(wParam) == ComboBoxEditListNotificationCode && !IsComboBoxDropDownList(hWnd))
+ {
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ {
+ const WPARAM command = static_cast(
+ (::GetDlgCtrlID(hWnd) & 0xFFFF)
+ | (ComboBoxParentEditChangeNotificationCode << 16));
+ ::SendMessageA(parentHwnd, WM_COMMAND, command, reinterpret_cast(hWnd));
+ }
+ return 0;
+ }
+ break;
+
+ case CB_GETCURSEL:
+ return data.AsComboBox().CurrentSelection();
+
+ case CB_GETLBTEXTLEN:
+ return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, true);
+
+ case CB_SETCURSEL:
+ return SetComboSelection(data, pOriginalWndProc, hWnd, wParam);
+
+ case CB_SHOWDROPDOWN:
+ EnsureComboBoxWindowHeight(hWnd, data, clientRect, ownerRect);
+ if (wParam)
+ return OpenComboDropDown(data, hWnd, clientRect, ownerRect);
+
+ CloseComboDropDown(data, hWnd);
+ return 1;
+
+ case CB_GETITEMDATA:
+ case CB_SETITEMDATA:
+ case LB_GETITEMDATA:
+ case LB_SETITEMDATA:
+ return handleItemData();
+
+ case WW_INITDIALOG:
+ {
+ const int fontHeight = BitFontHeight(data.AsComboBox().Font());
+ const int selectionHeight = static_cast(::SendMessageA(hWnd, CB_GETITEMHEIGHT, static_cast(-1), 0));
+ if (!data.AsComboBox().HeightInitialized()
+ || selectionHeight != ComboBoxVisibleHeight
+ || ::SendMessageA(hWnd, CB_GETITEMHEIGHT, 0, 0) != fontHeight + 6)
+ {
+ ::SendMessageA(hWnd, CB_SETITEMHEIGHT, static_cast(-1), ComboBoxVisibleHeight);
+ ::SendMessageA(hWnd, CB_SETITEMHEIGHT, 0, fontHeight + 6);
+ data.AsComboBox().HeightInitialized() = 1;
+ }
+
+ EnsureComboBoxWindowHeight(hWnd, data, clientRect, ownerRect);
+ data.AsComboBox().CurrentSelection() = -1;
+ std::memset(data.AsComboBox().ItemColorOverrides(), 0xFF, sizeof(int) * ComboBoxMaxColorItems);
+ return 0;
+ }
+
+ case WW_SETCOLOR:
+ if (wParam <= ComboBoxMaxColorItems)
+ data.AsComboBox().ItemColorOverrides()[wParam] = static_cast(lParam);
+ return forwardOriginal();
+
+ case WW_SETTEXTW:
+ case WW_GETTEXTW:
+ case WW_SETTEXTA:
+ case WW_GETTEXTA:
+ return ForwardComboTextMessageToEditList(hWnd, message, wParam, lParam);
+
+ case WW_CB_FINDSTRINGA:
+ return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false, false, false);
+
+ case WW_CB_FINDSTRINGEXACTA:
+ return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false, true, false);
+
+ case WW_CB_SELECTSTRINGA:
+ return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false, false, true);
+
+ case WW_CB_FINDSTRINGW:
+ return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true, false, false);
+
+ case WW_CB_FINDSTRINGEXACTW:
+ return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true, true, false);
+
+ case WW_CB_SELECTSTRINGW:
+ return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true, false, true);
+
+ case WW_CB_INSERTSTRINGA:
+ case WW_CB_ADDSTRINGA:
+ return AddOrInsertComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false);
+
+ case WW_CB_INSERTSTRINGW:
+ case WW_CB_ADDSTRINGW:
+ return AddOrInsertComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true);
+
+ case WW_CB_GETLBTEXTA:
+ return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, false);
+
+ case WW_CB_GETLBTEXTW:
+ return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, true);
+
+ case WW_CB_GETITEMTEXTFORMAT:
+ return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, true);
+
+ case WW_EDIT_ENTERPRESSED:
+ case WW_EDIT_TABNAV:
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ ::SendMessageA(parentHwnd, message, wParam, lParam);
+ return forwardOriginal();
+
+ case WW_CB_ENABLEITEMCOLORS:
+ data.AsComboBox().UseItemColorOverrides() = lParam == 1;
+ return forwardOriginal();
+
+ case WW_CB_SETMAXVISIBLEDROPITEMS:
+ data.AsComboBox().MaxVisibleDropItems() = static_cast(lParam);
+ return forwardOriginal();
+
+ case WW_QUERYTOOLTIPHIT:
+ if (const HWND dropHwnd = data.AsComboBox().DropDownHwnd())
+ {
+ POINT point { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+ ::ClientToScreen(hWnd, &point);
+ if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates())
+ {
+ point = RenderDX::ScreenToRenderLocalPoint(dropHwnd, point);
+ }
+ else
+ {
+ ::ScreenToClient(dropHwnd, &point);
+ }
+
+ return ::SendMessageA(
+ dropHwnd,
+ WW_QUERYTOOLTIPHIT,
+ 0,
+ MAKELPARAM(static_cast(point.x), static_cast(point.y)));
+ }
+ return -1;
+
+ case WW_CB_SETALTERNATEPALETTE:
+ data.AsComboBox().UseAlternatePalette() = lParam == 1;
+ return forwardOriginal();
+
+ default:
+ break;
+ }
+
+ return forwardOriginal();
+}
diff --git a/src/OwnerDraw/Edit.cpp b/src/OwnerDraw/Edit.cpp
new file mode 100644
index 0000000000..751f432763
--- /dev/null
+++ b/src/OwnerDraw/Edit.cpp
@@ -0,0 +1,1000 @@
+#include "OwnerDraw.Internal.h"
+
+void CharToWideString(wchar_t* pBuffer, int capacity, const char* pText)
+{
+ if (!pBuffer || capacity <= 0)
+ return;
+
+ pBuffer[0] = L'\0';
+ if (!pText)
+ return;
+
+ ::MultiByteToWideChar(CP_ACP, 0, pText, -1, pBuffer, capacity);
+ pBuffer[capacity - 1] = L'\0';
+}
+
+void WideToCharString(char* pBuffer, int capacity, const wchar_t* pText)
+{
+ if (!pBuffer || capacity <= 0)
+ return;
+
+ pBuffer[0] = '\0';
+ if (!pText)
+ return;
+
+ ::WideCharToMultiByte(CP_ACP, 0, pText, -1, pBuffer, capacity, nullptr, nullptr);
+ pBuffer[capacity - 1] = '\0';
+}
+
+static UINT GetCurrentKeyboardCodePage()
+{
+ char buffer[7] {};
+ const WORD language = LOWORD(::GetKeyboardLayout(0));
+ const LCID locale = MAKELCID(language, SORT_DEFAULT);
+
+ if (!::GetLocaleInfoA(locale, LOCALE_IDEFAULTANSICODEPAGE, buffer, static_cast(std::size(buffer))))
+ return CP_ACP;
+
+ const int codePage = std::atoi(buffer);
+ return codePage > 0 ? static_cast(codePage) : CP_ACP;
+}
+
+static wchar_t LocalizeCharacter(char character)
+{
+ wchar_t result {};
+ ::MultiByteToWideChar(GetCurrentKeyboardCodePage(), MB_USEGLYPHCHARS, &character, 1, &result, 1);
+ return result;
+}
+
+static WideWstring* EnsureNewEditText(OwnerDrawDialogElement& data)
+{
+ if (!data.AsNewEdit().Text())
+ {
+ auto pMemory = YRMemory::Allocate(sizeof(WideWstring));
+ if (!pMemory)
+ return nullptr;
+
+ data.AsNewEdit().Text() = new (pMemory) WideWstring();
+ }
+
+ return data.AsNewEdit().Text();
+}
+
+static const wchar_t* NewEditTextBuffer(OwnerDrawDialogElement& data)
+{
+ const auto pText = EnsureNewEditText(data);
+ return pText && pText->Buffer ? pText->Buffer : L"";
+}
+
+static int NewEditTextLength(OwnerDrawDialogElement& data)
+{
+ const auto pText = EnsureNewEditText(data);
+ return pText ? static_cast(pText->GetLength()) : 0;
+}
+
+static void SetNewEditText(OwnerDrawDialogElement& data, const wchar_t* pText)
+{
+ if (auto pTarget = EnsureNewEditText(data))
+ *pTarget = pText ? pText : L"";
+}
+
+static void TrimNewEditTextToLimit(OwnerDrawDialogElement& data)
+{
+ const int limit = data.AsNewEdit().TextLimit();
+ if (limit <= 0)
+ return;
+
+ if (NewEditTextLength(data) <= limit)
+ return;
+
+ std::wstring value(NewEditTextBuffer(data), limit);
+ SetNewEditText(data, value.c_str());
+
+ if (data.AsNewEdit().CaretIndex() > limit)
+ data.AsNewEdit().CaretIndex() = limit;
+}
+
+static bool RemoveNewEditTextRange(OwnerDrawDialogElement& data, int index, int length)
+{
+ std::wstring value(NewEditTextBuffer(data));
+ if (index < 0 || length <= 0 || index >= static_cast(value.size()))
+ return false;
+
+ length = std::min(length, static_cast(value.size()) - index);
+ value.erase(static_cast(index), static_cast(length));
+ SetNewEditText(data, value.c_str());
+ data.AsNewEdit().CaretIndex() = std::clamp(data.AsNewEdit().CaretIndex(), 0, static_cast(value.size()));
+ return true;
+}
+
+static bool InsertNewEditCharacter(OwnerDrawDialogElement& data, wchar_t character)
+{
+ if (!character || character <= 0x1F)
+ return false;
+
+ if (data.AsNewEdit().AsciiOnly() && character >= 0x100)
+ return false;
+
+ if (data.AsNewEdit().RejectChars() && std::wcschr(data.AsNewEdit().RejectChars(), character))
+ return false;
+
+ std::wstring value(NewEditTextBuffer(data));
+ if (data.AsNewEdit().TextLimit() > 0 && static_cast(value.size()) >= data.AsNewEdit().TextLimit())
+ return false;
+
+ int caretIndex = std::clamp(data.AsNewEdit().CaretIndex(), 0, static_cast(value.size()));
+ value.insert(value.begin() + caretIndex, character);
+ SetNewEditText(data, value.c_str());
+ data.AsNewEdit().CaretIndex() = caretIndex + 1;
+ return true;
+}
+
+static void NotifyNewEditTextChanged(HWND hWnd, HWND parentHwnd)
+{
+ if (!parentHwnd)
+ return;
+
+ const int controlId = ::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF;
+ ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x03000000, reinterpret_cast(hWnd));
+ ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x04000000, reinterpret_cast(hWnd));
+}
+
+static void NotifyNewEditEnterPressed(HWND hWnd, HWND parentHwnd)
+{
+ if (parentHwnd)
+ ::SendMessageA(parentHwnd, WW_EDIT_ENTERPRESSED, 0, reinterpret_cast(hWnd));
+}
+
+static void NotifyNewEditMultilineEnter(HWND hWnd, HWND parentHwnd)
+{
+ if (!parentHwnd)
+ return;
+
+ const int controlId = ::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF;
+ ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x05010000, reinterpret_cast(hWnd));
+}
+
+static bool IsComboBoxParent(HWND parentHwnd)
+{
+ if (!parentHwnd)
+ return false;
+
+ char className[32] {};
+ ::GetClassNameA(parentHwnd, className, static_cast(std::size(className)));
+ return std::strcmp(className, "ComboBox") == 0;
+}
+
+static void InvalidateNewEdit(HWND hWnd, HWND parentHwnd)
+{
+ if (IsComboBoxParent(parentHwnd))
+ ::InvalidateRect(parentHwnd, nullptr, FALSE);
+
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+}
+
+static int NewEditTextWidth(BitFont* pFont, const wchar_t* pText)
+{
+ if (!pText || !pText[0])
+ return 0;
+
+ if (!pFont)
+ pFont = BitFont::Instance;
+
+ if (!pFont)
+ return static_cast(std::wcslen(pText)) * 8;
+
+ int textWidth = 0;
+ int textHeight = 0;
+ pFont->GetTextDimension(pText, &textWidth, &textHeight, 0);
+ return textWidth;
+}
+
+static int NewEditFitCharacterCount(BitFont* pFont, const wchar_t* pText, int maxWidth)
+{
+ if (!pText || maxWidth <= 0)
+ return 0;
+
+ const int length = static_cast(std::wcslen(pText));
+ int fitCount = 0;
+
+ for (int count = 1; count <= length; ++count)
+ {
+ std::wstring candidate(pText, pText + count);
+ if (NewEditTextWidth(pFont, candidate.c_str()) > maxWidth)
+ break;
+
+ fitCount = count;
+ }
+
+ return fitCount;
+}
+
+static int PrintNewEditTextSegment(
+ DSurface* pSurface,
+ RectangleStruct& rect,
+ BitFont* pFont,
+ const std::wstring& text,
+ int start,
+ int end,
+ COLORREF color,
+ int animationPos)
+{
+ if (end <= start || rect.Width <= 0)
+ return 0;
+
+ const std::wstring segment(text.begin() + start, text.begin() + end);
+ const int width = NewEditTextWidth(pFont, segment.c_str());
+
+ OwnerDraw::PrintTextFixedLength(
+ color,
+ pFont,
+ &rect,
+ segment.c_str(),
+ static_cast(segment.size()),
+ 0,
+ 0,
+ pSurface,
+ animationPos);
+
+ rect.X += width;
+ rect.Width = std::max(rect.Width - width, 0);
+ return width;
+}
+
+static void AnimatedNewEditTextPrint(
+ DSurface* pSurface,
+ RectangleStruct textRect,
+ const wchar_t* pText,
+ int caretIndex,
+ BitFont* pFont,
+ COLORREF textColor,
+ int& scrollStart,
+ bool hasFocus,
+ bool maskText,
+ bool fillBackground,
+ int animationPos,
+ int caretBlinkState)
+{
+ if (!pSurface || textRect.Width <= 0 || textRect.Height <= 0)
+ return;
+
+ if (!pFont)
+ pFont = BitFont::Instance;
+
+ const wchar_t* pSource = pText ? pText : L"";
+ const int sourceLength = static_cast(std::wcslen(pSource));
+ caretIndex = std::clamp(caretIndex, 0, sourceLength);
+
+ int compositionLength = 0;
+ int compositionCursor = 0;
+ bool composing = false;
+ if (hasFocus)
+ {
+ OwnerDraw::UpdateIMECompositionString();
+ compositionLength = std::clamp(OwnerDraw::IMECompositionStringLength, 0, 0x100);
+ compositionCursor = std::clamp(OwnerDraw::IMECompositionCursorPos, 0, compositionLength);
+ composing = OwnerDraw::IMEComposing != 0;
+ }
+
+ constexpr size_t DisplayBufferCapacity = 0x800;
+ std::wstring displayText(pSource);
+ if (displayText.size() >= DisplayBufferCapacity)
+ displayText.resize(DisplayBufferCapacity - 1);
+
+ const int compositionStart = std::min(caretIndex, static_cast(displayText.size()));
+ if (compositionLength > 0)
+ {
+ displayText.insert(
+ displayText.begin() + compositionStart,
+ OwnerDraw::IMECompositionString,
+ OwnerDraw::IMECompositionString + compositionLength);
+
+ if (displayText.size() >= DisplayBufferCapacity)
+ displayText.resize(DisplayBufferCapacity - 1);
+ }
+
+ if (maskText)
+ std::fill(displayText.begin(), displayText.end(), L'*');
+
+ const int displayLength = static_cast(displayText.size());
+ const int compositionEnd = std::min(compositionStart + compositionLength, displayLength);
+ int displayCaret = composing || compositionLength
+ ? compositionStart + compositionCursor
+ : std::min(caretIndex, displayLength);
+ displayCaret = std::clamp(displayCaret, 0, displayLength);
+
+ scrollStart = std::clamp(scrollStart, 0, displayLength);
+ if (displayCaret < scrollStart + 5)
+ scrollStart = std::max(displayCaret - 5, 0);
+
+ while (scrollStart < displayLength)
+ {
+ const int visibleCount = NewEditFitCharacterCount(pFont, displayText.c_str() + scrollStart, textRect.Width - 5);
+ if (visibleCount >= displayCaret - scrollStart)
+ break;
+
+ ++scrollStart;
+ }
+
+ if (fillBackground)
+ {
+ RectangleStruct fillRect
+ {
+ textRect.X - 1,
+ textRect.Y - 1,
+ NewEditTextWidth(pFont, displayText.c_str() + scrollStart) + 5,
+ textRect.Height + 2
+ };
+ pSurface->FillRect(&fillRect, 0);
+ }
+
+ RectangleStruct drawRect = textRect;
+ int caretX = -1;
+
+ auto drawRange = [&](int rangeStart, int rangeEnd, COLORREF color)
+ {
+ int visibleStart = std::max(rangeStart, scrollStart);
+ int visibleEnd = std::min(rangeEnd, displayLength);
+ if (visibleEnd <= visibleStart)
+ return;
+
+ if (caretX < 0 && displayCaret >= visibleStart && displayCaret <= visibleEnd)
+ {
+ PrintNewEditTextSegment(pSurface, drawRect, pFont, displayText, visibleStart, displayCaret, color, animationPos);
+ caretX = drawRect.X;
+ PrintNewEditTextSegment(pSurface, drawRect, pFont, displayText, displayCaret, visibleEnd, color, animationPos);
+ }
+ else
+ {
+ PrintNewEditTextSegment(pSurface, drawRect, pFont, displayText, visibleStart, visibleEnd, color, animationPos);
+ }
+ };
+
+ drawRange(0, compositionStart, textColor);
+ drawRange(compositionStart, compositionEnd, OwnerDraw::ImeCompositionTextColor);
+ drawRange(compositionEnd, displayLength, textColor);
+
+ if (caretX < 0)
+ {
+ if (displayCaret <= scrollStart)
+ caretX = textRect.X;
+ else if (displayCaret >= displayLength)
+ caretX = drawRect.X;
+ }
+
+ if (hasFocus && caretX >= 0 && !caretBlinkState)
+ {
+ const WORD caretColor = static_cast(ConvertRGBToSurfaceColor(Phobos::UI::ColorCaret));
+ Point2D start { caretX, textRect.Y };
+ Point2D end { caretX, textRect.Y + textRect.Height - 2 };
+ DrawAlphaLine(pSurface, start, end, caretColor, 0xFF);
+
+ ++start.X;
+ ++end.X;
+ DrawAlphaLine(pSurface, start, end, caretColor, 0xFF);
+ }
+}
+
+static void PaintNewEdit(HWND hWnd, OwnerDrawDialogElement& data, HWND parentHwnd)
+{
+ if (!DSurface::Alternate)
+ return;
+
+ RECT ownerRect {};
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ const int width = ownerRect.right - ownerRect.left + 1;
+ const int height = ownerRect.bottom - ownerRect.top + 1;
+ if (width <= 0 || height <= 0)
+ return;
+
+ RectangleStruct drawRect { ownerRect.left, ownerRect.top, width, height };
+ OwnerDraw::CopyDimmedBackground(&drawRect, hWnd, static_cast(data.Alpha));
+
+ if (!IsComboBoxParent(parentHwnd))
+ DrawBeveledBorder(DSurface::Alternate, drawRect, 2, -1);
+
+ RectangleStruct textRect
+ {
+ ownerRect.left + 2,
+ ownerRect.top,
+ ownerRect.right - ownerRect.left + 1,
+ ownerRect.bottom - ownerRect.top + 1
+ };
+
+ AnimatedNewEditTextPrint(
+ DSurface::Alternate,
+ textRect,
+ NewEditTextBuffer(data),
+ data.AsNewEdit().CaretIndex(),
+ data.AsNewEdit().Font(),
+ Phobos::UI::ColorTextEdit,
+ data.AsNewEdit().ScrollStart(),
+ data.HasFocus != 0,
+ ((data.AsNewEdit().StyleFlags() >> 5) & 1) != 0,
+ false,
+ 0,
+ data.AsNewEdit().CaretBlinkState());
+
+ ::ValidateRect(hWnd, nullptr);
+}
+
+static size_t GetEditWideText(HWND hWnd, wchar_t* pWideText, int capacity, int* pSelectionEndChars)
+{
+ if (pSelectionEndChars)
+ *pSelectionEndChars = 0;
+
+ if (!pWideText || capacity <= 0)
+ return 0;
+
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+
+ char ansiText[0x400] {};
+ CallSelectedHandler(
+ pOriginalWndProc,
+ hWnd,
+ WM_GETTEXT,
+ static_cast(std::size(ansiText)),
+ reinterpret_cast(ansiText));
+
+ if (pSelectionEndChars)
+ {
+ DWORD selectionStart = 0;
+ DWORD selectionEnd = 0;
+ const auto selection = static_cast(CallSelectedHandler(
+ pOriginalWndProc,
+ hWnd,
+ EM_GETSEL,
+ reinterpret_cast(&selectionStart),
+ reinterpret_cast(&selectionEnd)));
+ const auto selectionEndBytes = static_cast(HIWORD(selection));
+ *pSelectionEndChars = static_cast(_mbsnccnt(
+ reinterpret_cast(ansiText),
+ selectionEndBytes));
+ }
+
+ pWideText[0] = L'\0';
+ ::MultiByteToWideChar(CP_ACP, 0, ansiText, -1, pWideText, capacity);
+ pWideText[capacity - 1] = L'\0';
+
+ const size_t wideLength = std::wcslen(pWideText);
+ std::wstring normalized;
+ normalized.reserve(wideLength + 2);
+
+ bool removedNewLine = false;
+ for (size_t i = 0; i < wideLength; ++i)
+ {
+ if (pWideText[i] == L'\r' || pWideText[i] == L'\n')
+ {
+ removedNewLine = true;
+ continue;
+ }
+
+ normalized.push_back(pWideText[i]);
+ }
+
+ if (removedNewLine)
+ normalized += L"\r\n";
+
+ std::wcsncpy(pWideText, normalized.c_str(), static_cast(capacity - 1));
+ pWideText[capacity - 1] = L'\0';
+ return std::wcslen(pWideText);
+}
+
+static LRESULT ForwardEditSetText(OwnerDrawDialogElement& data, HWND hWnd, WNDPROC pOriginalWndProc)
+{
+ char ansiText[0x800] {};
+ if (data.TextBuffer)
+ WideToCharString(ansiText, static_cast(std::size(ansiText)), data.TextBuffer);
+
+ return CallSelectedHandler(
+ pOriginalWndProc,
+ hWnd,
+ WM_SETTEXT,
+ 0,
+ reinterpret_cast(ansiText));
+}
+
+static LRESULT CopyEditTextW(HWND hWnd, WPARAM capacityParam, LPARAM lParam)
+{
+ auto pBuffer = reinterpret_cast(lParam);
+ const int capacity = static_cast(capacityParam);
+ if (!pBuffer || capacity <= 0)
+ return 0;
+
+ std::vector text(0x800);
+ GetEditWideText(hWnd, text.data(), static_cast(text.size()), nullptr);
+
+ std::wcsncpy(pBuffer, text.data(), static_cast(capacity - 1));
+ pBuffer[capacity - 1] = L'\0';
+ return static_cast(std::wcslen(pBuffer));
+}
+
+static LRESULT CopyEditTextA(HWND hWnd, WPARAM capacityParam, LPARAM lParam)
+{
+ auto pBuffer = reinterpret_cast(lParam);
+ const int capacity = static_cast(capacityParam);
+ if (!pBuffer || capacity <= 0)
+ return 0;
+
+ std::vector text(0x800);
+ GetEditWideText(hWnd, text.data(), static_cast(text.size()), nullptr);
+ WideToCharString(pBuffer, capacity, text.data());
+ return static_cast(std::strlen(pBuffer));
+}
+
+static LRESULT AppendEditNewLine(HWND hWnd, WNDPROC pOriginalWndProc)
+{
+ const int textLength = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, WM_GETTEXTLENGTH, 0, 0));
+ std::vector text(static_cast(std::max(textLength + 3, 3)), '\0');
+
+ CallSelectedHandler(
+ pOriginalWndProc,
+ hWnd,
+ WM_GETTEXT,
+ static_cast(text.size()),
+ reinterpret_cast(text.data()));
+
+ const size_t copiedLength = std::strlen(text.data());
+ if (copiedLength + 2 < text.size())
+ std::memcpy(text.data() + copiedLength, "\r\n", 3);
+
+ CallSelectedHandler(pOriginalWndProc, hWnd, WM_SETTEXT, 0, reinterpret_cast(text.data()));
+
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ {
+ const int controlId = ::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF;
+ ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x05010000, reinterpret_cast(hWnd));
+ }
+
+ return 0;
+}
+
+static void PaintEdit(HWND hWnd, OwnerDrawDialogElement& data, HWND parentHwnd, UINT message)
+{
+ if (!DSurface::Alternate)
+ return;
+
+ RECT ownerRect {};
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ if (message == WM_PAINT)
+ {
+ RECT updateRect {};
+ if (::GetUpdateRect(hWnd, &updateRect, FALSE))
+ {
+ updateRect.left += ownerRect.left;
+ updateRect.top += ownerRect.top;
+ updateRect.right += ownerRect.left;
+ updateRect.bottom += ownerRect.top;
+ }
+ }
+
+ const int width = ownerRect.right - ownerRect.left + 1;
+ const int height = ownerRect.bottom - ownerRect.top + 1;
+ if (width <= 0 || height <= 0)
+ return;
+
+ RectangleStruct drawRect { ownerRect.left, ownerRect.top, width, height };
+ OwnerDraw::CopyDimmedBackground(&drawRect, hWnd, static_cast(data.Alpha));
+
+ if (!IsComboBoxParent(parentHwnd))
+ DrawBeveledBorder(DSurface::Alternate, drawRect, 2, -1);
+
+ std::vector text(0x1400);
+ int caretIndex = 0;
+ GetEditWideText(hWnd, text.data(), static_cast(text.size()), &caretIndex);
+
+ RectangleStruct textRect { ownerRect.left, ownerRect.top, width, height };
+ const bool maskText = ((::GetWindowLongA(hWnd, GWL_STYLE) >> 5) & 1) != 0;
+
+ AnimatedNewEditTextPrint(
+ DSurface::Alternate,
+ textRect,
+ text.data(),
+ caretIndex,
+ data.AsEdit().TextFont(),
+ Phobos::UI::ColorText,
+ data.AsEdit().TextScrollStart(),
+ data.HasFocus != 0,
+ maskText,
+ false,
+ 0,
+ 0);
+
+ ::ValidateRect(hWnd, nullptr);
+}
+
+LRESULT CALLBACK WWUI::EditCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ auto pData = FindOwnerDrawData(hWnd);
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ if (!pData)
+ return forwardOriginal();
+
+ auto& data = *pData;
+ if (::GetFocus() == hWnd && !data.AsEdit().FocusRestoreReadyFlag())
+ {
+ data.AsEdit().FocusRestorePendingFlag() = 1;
+ ::SetFocus(Game::hWnd);
+ }
+
+ const LONG windowStyle = ::GetWindowLongA(hWnd, GWL_STYLE);
+ const HWND parentHwnd = ::GetParent(hWnd);
+
+ if ((message == WM_KEYDOWN || message == WM_KEYUP) && wParam == VK_TAB)
+ return 0;
+
+ switch (message)
+ {
+ case WW_INITDIALOG:
+ {
+ if (!parentHwnd)
+ return 0;
+
+ RECT windowRect {};
+ RECT clientRect {};
+ RECT parentRect {};
+ ::GetWindowRect(hWnd, &windowRect);
+ ::GetClientRect(hWnd, &clientRect);
+ ::GetWindowRect(parentHwnd, &parentRect);
+
+ ::MoveWindow(
+ hWnd,
+ windowRect.left - parentRect.left + 1,
+ windowRect.top - parentRect.top + 1,
+ clientRect.right - 2,
+ clientRect.bottom - 2,
+ FALSE);
+
+ if (::GetFocus() == hWnd)
+ {
+ data.AsEdit().FocusRestorePendingFlag() = 1;
+ ::SetFocus(Game::hWnd);
+ }
+
+ if (windowStyle & WS_TABSTOP)
+ {
+ data.AsEdit().RestoreTabStopFlag() = 1;
+ ::SetWindowLongA(hWnd, GWL_STYLE, windowStyle & ~static_cast(WS_TABSTOP));
+ }
+
+ return 0;
+ }
+
+ case WM_SETFOCUS:
+ ::SendMessageA(hWnd, EM_SETSEL, static_cast(-1), static_cast(-1));
+ if (!data.AsEdit().FocusRestoreReadyFlag())
+ ::PostMessageA(hWnd, WW_EDIT_DEFERFOCUSRESTORE, 0, 0);
+
+ InvalidateNewEdit(hWnd, parentHwnd);
+ return forwardOriginal();
+
+ case WW_GETTEXTW:
+ return CopyEditTextW(hWnd, wParam, lParam);
+
+ case WW_GETTEXTA:
+ return CopyEditTextA(hWnd, wParam, lParam);
+
+ case WM_GETTEXTLENGTH:
+ {
+ std::vector text(0x800);
+ return static_cast(GetEditWideText(hWnd, text.data(), static_cast(text.size()), nullptr));
+ }
+
+ case WW_SETTEXTW:
+ case WW_SETTEXTA:
+ return ForwardEditSetText(data, hWnd, pOriginalWndProc);
+
+ case WM_CHAR:
+ if (wParam == VK_RETURN)
+ {
+ if (windowStyle & ES_MULTILINE)
+ return AppendEditNewLine(hWnd, pOriginalWndProc);
+
+ return forwardOriginal();
+ }
+
+ if (wParam == VK_TAB)
+ {
+ if (const HWND nextHwnd = ::GetNextDlgTabItem(parentHwnd, hWnd, FALSE))
+ ::SetFocus(nextHwnd);
+ else
+ ::SetFocus(hWnd);
+
+ return 0;
+ }
+
+ return forwardOriginal();
+
+ case WW_EDIT_RESTOREFOCUS:
+ data.AsEdit().FocusRestoreReadyFlag() = 1;
+ if (data.AsEdit().FocusRestorePendingFlag())
+ {
+ ::SetFocus(hWnd);
+ data.AsEdit().FocusRestorePendingFlag() = 0;
+ }
+
+ if (data.AsEdit().RestoreTabStopFlag())
+ ::SetWindowLongA(hWnd, GWL_STYLE, windowStyle | WS_TABSTOP);
+
+ return 0;
+
+ case WM_PAINT:
+ case WM_ERASEBKGND:
+ PaintEdit(hWnd, data, parentHwnd, message);
+ break;
+
+ case WM_CONTEXTMENU:
+ return 1;
+
+ case WM_MOUSEMOVE:
+ return 1;
+
+ default:
+ break;
+ }
+
+ switch (message)
+ {
+ case WM_KEYDOWN:
+ case WM_KEYUP:
+ case WM_SYSKEYDOWN:
+ case WM_SYSKEYUP:
+ case WM_SYSCHAR:
+ case WM_SYSDEADCHAR:
+ case WM_KILLFOCUS:
+ case WM_LBUTTONDOWN:
+ InvalidateNewEdit(hWnd, parentHwnd);
+ break;
+
+ default:
+ break;
+ }
+
+ return forwardOriginal();
+}
+
+LRESULT CALLBACK WWUI::NewEditCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ if (message == WM_GETDLGCODE)
+ return DLGC_WANTALLKEYS;
+
+ auto pData = FindOwnerDrawData(hWnd);
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ if (!pData)
+ return forwardOriginal();
+
+ auto& data = *pData;
+ EnsureNewEditText(data);
+
+ const HWND parentHwnd = ::GetParent(hWnd);
+
+ if ((message == WM_KEYDOWN || message == WM_KEYUP) && wParam == VK_TAB)
+ {
+ if (message == WM_KEYDOWN && parentHwnd)
+ {
+ const WPARAM shiftPressed = (::GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 1 : 0;
+ ::SendMessageA(parentHwnd, WW_EDIT_TABNAV, shiftPressed, reinterpret_cast(hWnd));
+ }
+
+ return 0;
+ }
+
+ auto copyWideText = [&]() -> LRESULT
+ {
+ const int capacity = static_cast(wParam);
+ auto pBuffer = reinterpret_cast(lParam);
+ if (!pBuffer || capacity <= 0)
+ return 0;
+
+ const auto pText = NewEditTextBuffer(data);
+ std::wcsncpy(pBuffer, pText, capacity - 1);
+ pBuffer[capacity - 1] = L'\0';
+ return static_cast(std::wcslen(pBuffer));
+ };
+
+ auto copyAnsiText = [&]() -> LRESULT
+ {
+ const int capacity = static_cast(wParam);
+ auto pBuffer = reinterpret_cast(lParam);
+ if (!pBuffer || capacity <= 0)
+ return 0;
+
+ WideToCharString(pBuffer, capacity, NewEditTextBuffer(data));
+ return static_cast(std::strlen(pBuffer));
+ };
+
+ auto handleInputCharacter = [&](wchar_t character, bool consumedInput) -> LRESULT
+ {
+ if (!character)
+ return consumedInput ? 0 : forwardOriginal();
+
+ if (InsertNewEditCharacter(data, character))
+ NotifyNewEditTextChanged(hWnd, parentHwnd);
+
+ return 0;
+ };
+
+ switch (message)
+ {
+ case WW_INITDIALOG:
+ {
+ if (!parentHwnd)
+ return 0;
+
+ RECT windowRect {};
+ RECT clientRect {};
+ RECT parentRect {};
+ ::GetWindowRect(hWnd, &windowRect);
+ ::GetClientRect(hWnd, &clientRect);
+ ::GetWindowRect(parentHwnd, &parentRect);
+
+ ::SetWindowPos(
+ hWnd,
+ nullptr,
+ windowRect.left - parentRect.left + 1,
+ windowRect.top - parentRect.top + 1,
+ clientRect.right - 2,
+ clientRect.bottom - 2,
+ SWP_SHOWWINDOW);
+ return 0;
+ }
+
+ case EM_LIMITTEXT:
+ data.AsNewEdit().TextLimit() = static_cast(wParam);
+ TrimNewEditTextToLimit(data);
+ return forwardOriginal();
+
+ case WW_GETTEXTW:
+ return copyWideText();
+
+ case WW_GETTEXTA:
+ return copyAnsiText();
+
+ case WM_GETTEXTLENGTH:
+ return NewEditTextLength(data);
+
+ case WW_SETTEXTW:
+ case WW_SETTEXTA:
+ SetNewEditText(data, data.TextBuffer ? data.TextBuffer : L"");
+ data.AsNewEdit().CaretIndex() = 0;
+ data.AsNewEdit().ScrollStart() = 0;
+ TrimNewEditTextToLimit(data);
+ data.AsNewEdit().CaretIndex() = NewEditTextLength(data);
+ break;
+
+ case WM_KEYDOWN:
+ if (wParam == VK_RETURN)
+ {
+ NotifyNewEditEnterPressed(hWnd, parentHwnd);
+ if (data.AsNewEdit().StyleFlags() & 4)
+ {
+ if (auto pText = EnsureNewEditText(data))
+ *pText += L"\r\n";
+
+ NotifyNewEditMultilineEnter(hWnd, parentHwnd);
+ }
+ return 0;
+ }
+ break;
+
+ case WM_SETFOCUS:
+ data.AsNewEdit().CaretBlinkState() = 0;
+ ::SetTimer(hWnd, 0, 1000, nullptr);
+ InvalidateNewEdit(hWnd, parentHwnd);
+ return forwardOriginal();
+
+ case WM_KILLFOCUS:
+ ::KillTimer(hWnd, 0);
+ InvalidateNewEdit(hWnd, parentHwnd);
+ return forwardOriginal();
+
+ case WM_TIMER:
+ data.AsNewEdit().CaretBlinkState() ^= 1;
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return forwardOriginal();
+
+ case WM_PAINT:
+ case WM_ERASEBKGND:
+ PaintNewEdit(hWnd, data, parentHwnd);
+ break;
+
+ case WM_CONTEXTMENU:
+ return 1;
+
+ case WM_MOUSEMOVE:
+ return 1;
+
+ default:
+ break;
+ }
+
+ switch (message)
+ {
+ case WM_KEYDOWN:
+ case WM_KEYUP:
+ case WM_SYSKEYDOWN:
+ case WM_SYSKEYUP:
+ case WM_SYSCHAR:
+ case WM_SYSDEADCHAR:
+ case WM_LBUTTONDOWN:
+ InvalidateNewEdit(hWnd, parentHwnd);
+ break;
+
+ default:
+ break;
+ }
+
+ if (message == WM_CHAR)
+ {
+ if (wParam <= 0x1F)
+ return forwardOriginal();
+
+ return handleInputCharacter(LocalizeCharacter(static_cast(wParam)), true);
+ }
+
+ if (message == WM_KEYDOWN)
+ {
+ bool textChanged = false;
+ switch (wParam)
+ {
+ case VK_BACK:
+ if (data.AsNewEdit().CaretIndex() > 0)
+ {
+ --data.AsNewEdit().CaretIndex();
+ textChanged = RemoveNewEditTextRange(data, data.AsNewEdit().CaretIndex(), 1);
+ }
+ break;
+
+ case VK_DELETE:
+ if (data.AsNewEdit().CaretIndex() < NewEditTextLength(data))
+ textChanged = RemoveNewEditTextRange(data, data.AsNewEdit().CaretIndex(), 1);
+ break;
+
+ case VK_END:
+ data.AsNewEdit().CaretIndex() = NewEditTextLength(data);
+ return 0;
+
+ case VK_HOME:
+ data.AsNewEdit().CaretIndex() = 0;
+ return 0;
+
+ case VK_LEFT:
+ if (data.AsNewEdit().CaretIndex() > 0)
+ --data.AsNewEdit().CaretIndex();
+ return 0;
+
+ case VK_RIGHT:
+ if (data.AsNewEdit().CaretIndex() < NewEditTextLength(data))
+ ++data.AsNewEdit().CaretIndex();
+ return 0;
+
+ default:
+ return forwardOriginal();
+ }
+
+ if (textChanged)
+ NotifyNewEditTextChanged(hWnd, parentHwnd);
+
+ return 0;
+ }
+
+ if (message == WM_IME_CHAR)
+ return handleInputCharacter(OwnerDraw::ConvertIMECharToWide(static_cast(wParam), lParam), true);
+
+ if (message == WW_EDIT_INPUTCHARW)
+ return handleInputCharacter(static_cast(wParam), true);
+
+ return forwardOriginal();
+}
diff --git a/src/OwnerDraw/GroupBox.cpp b/src/OwnerDraw/GroupBox.cpp
new file mode 100644
index 0000000000..c2b9d45ae4
--- /dev/null
+++ b/src/OwnerDraw/GroupBox.cpp
@@ -0,0 +1,128 @@
+#include "OwnerDraw.Internal.h"
+
+static void DrawGroupBoxLine(int startX, int startY, int endX, int endY, int color)
+{
+ Point2D start { startX, startY };
+ Point2D end { endX, endY };
+ DSurface::Alternate->DrawLine(&start, &end, color);
+}
+
+static void DrawGroupBoxPixel(int x, int y, int color)
+{
+ Point2D point { x, y };
+ DSurface::Alternate->SetPixel(&point, color);
+}
+
+static int MeasureGroupBoxCaption(BitFont* pFont, const wchar_t* pCaption, int maxWidth)
+{
+ if (!pFont)
+ pFont = BitFont::Instance;
+
+ int width = 0;
+ int height = 0;
+ if (pFont)
+ pFont->GetTextDimension(pCaption, &width, &height, maxWidth);
+
+ return width;
+}
+
+static void DrawGroupBoxCaption(const RECT& groupRect, const wchar_t* pCaption)
+{
+ RectangleStruct captionRect
+ {
+ groupRect.left + 10,
+ groupRect.top,
+ groupRect.right - groupRect.left,
+ groupRect.bottom - groupRect.top
+ };
+
+ OwnerDraw::PrintTextFixedLength(
+ Phobos::UI::ColorTextGroupbox,
+ nullptr,
+ &captionRect,
+ pCaption,
+ static_cast(std::wcslen(pCaption)),
+ 0,
+ 0,
+ DSurface::Alternate,
+ 0);
+}
+
+static void DrawGroupBoxFramePass(
+ const RECT& groupRect,
+ int topY,
+ int inset,
+ bool hasCaption,
+ int captionWidth,
+ int nearColor,
+ int farColor,
+ int cornerColor)
+{
+ const int left = groupRect.left + inset;
+ const int right = groupRect.right - inset - 1;
+ const int y = topY + inset;
+ const int bottom = groupRect.bottom - inset - 1;
+
+ if (hasCaption)
+ {
+ DrawGroupBoxLine(left, y, groupRect.left + 8, y, nearColor);
+ DrawGroupBoxLine(groupRect.left + captionWidth + 12 + inset, y, right, y, nearColor);
+ }
+ else
+ {
+ DrawGroupBoxLine(left, y, right, y, nearColor);
+ }
+
+ DrawGroupBoxLine(left, y + 1, left, bottom, nearColor);
+ DrawGroupBoxLine(left, bottom, right, bottom, farColor);
+ DrawGroupBoxLine(right, y, right, bottom - 1, farColor);
+ DrawGroupBoxPixel(right, y, cornerColor);
+ DrawGroupBoxPixel(left, bottom, cornerColor);
+}
+
+static LRESULT PaintGroupBoxCtrl(HWND hWnd, OwnerDrawDialogElement& data)
+{
+ const wchar_t* const pCaption = data.TextBuffer ? data.TextBuffer : L"";
+
+ RECT groupRect {};
+ OwnerDraw::GetRectangle(hWnd, &groupRect);
+
+ const int groupWidth = groupRect.right - groupRect.left;
+ const auto pFont = data.AsGroupBox().Font() ? data.AsGroupBox().Font() : BitFont::Instance;
+ const int topY = groupRect.top + BitFontHeight(pFont) / 2;
+ const int captionWidth = MeasureGroupBoxCaption(pFont, pCaption, groupWidth);
+
+ DrawGroupBoxCaption(groupRect, pCaption);
+
+ const int lightColor = ConvertRGBToSurfaceColor(Phobos::UI::ColorBorder1);
+ const int shadowColor = ConvertRGBToSurfaceColor(Phobos::UI::ColorBorder2);
+ const int averageColor = ConvertRGBToSurfaceColor(AverageColor(Phobos::UI::ColorBorder1, Phobos::UI::ColorBorder2));
+ const bool hasCaption = std::wcslen(pCaption) != 0;
+
+ DrawGroupBoxFramePass(groupRect, topY, 0, hasCaption, captionWidth, lightColor, shadowColor, averageColor);
+ DrawGroupBoxFramePass(groupRect, topY, 1, hasCaption, captionWidth, shadowColor, lightColor, averageColor);
+
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+}
+
+LRESULT CALLBACK WWUI::GroupBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ switch (message)
+ {
+ case WM_PAINT:
+ if (auto pData = FindOwnerDrawData(hWnd))
+ return PaintGroupBoxCtrl(hWnd, *pData);
+
+ return 0;
+
+ case WM_ERASEBKGND:
+ return 1;
+
+ case WM_NCPAINT:
+ return 0;
+
+ default:
+ return CallSelectedHandler(FindWindowProc(OwnerDraw::DialogProcs, hWnd), hWnd, message, wParam, lParam);
+ }
+}
diff --git a/src/OwnerDraw/Input.cpp b/src/OwnerDraw/Input.cpp
new file mode 100644
index 0000000000..338188e096
--- /dev/null
+++ b/src/OwnerDraw/Input.cpp
@@ -0,0 +1,194 @@
+#include "OwnerDraw.Internal.h"
+
+constexpr WORD InputHotkeyShift = HOTKEYF_SHIFT << 8;
+constexpr WORD InputHotkeyControl = HOTKEYF_CONTROL << 8;
+constexpr WORD InputHotkeyAlt = HOTKEYF_ALT << 8;
+constexpr WORD InputHotkeyNoText = HOTKEYF_EXT << 8;
+
+static void AppendCString(char* pBuffer, size_t bufferSize, const char* pText)
+{
+ if (!pBuffer || !bufferSize || !pText)
+ return;
+
+ const size_t used = std::strlen(pBuffer);
+ if (used >= bufferSize - 1)
+ return;
+
+ std::strncat(pBuffer, pText, bufferSize - used - 1);
+}
+
+static void AppendKeyName(char* pBuffer, size_t bufferSize, UINT virtualKey, bool appendSeparator)
+{
+ char keyName[32] {};
+ const UINT scanCode = ::MapVirtualKeyA(virtualKey, MAPVK_VK_TO_VSC);
+ ::GetKeyNameTextA(static_cast((scanCode << 16) | 0x02000001), keyName, static_cast(std::size(keyName)));
+
+ AppendCString(pBuffer, bufferSize, keyName);
+ if (appendSeparator)
+ AppendCString(pBuffer, bufferSize, "+");
+}
+
+static void BuildKeyboardKeyString(WORD keyCode, wchar_t* pOutText, size_t outTextSize)
+{
+ if (!pOutText || !outTextSize)
+ return;
+
+ pOutText[0] = L'\0';
+ if (keyCode & InputHotkeyNoText)
+ return;
+
+ char keyText[500] {};
+ if (keyCode & InputHotkeyAlt)
+ AppendKeyName(keyText, std::size(keyText), VK_MENU, true);
+
+ if (keyCode & InputHotkeyControl)
+ AppendKeyName(keyText, std::size(keyText), VK_CONTROL, true);
+
+ if (keyCode & InputHotkeyShift)
+ AppendKeyName(keyText, std::size(keyText), VK_SHIFT, true);
+
+ AppendKeyName(keyText, std::size(keyText), LOBYTE(keyCode), false);
+ std::swprintf(pOutText, outTextSize, L"%hs", keyText);
+ pOutText[outTextSize - 1] = L'\0';
+}
+
+static int DrawWideTextBasic(Surface* pSurface, const wchar_t* pText, const RECT& textRect, BitFont* pFont, COLORREF color)
+{
+ if (!pSurface || !pText)
+ return 0;
+
+ auto pDrawFont = pFont ? pFont : BitFont::Instance;
+ if (!pDrawFont || !BitText::Instance)
+ return 0;
+
+ LTRBStruct bounds { textRect.left, textRect.top, textRect.right, textRect.bottom };
+ pDrawFont->SetClipMode(true);
+ pDrawFont->SetRectangle(&bounds);
+ pDrawFont->SetColor(static_cast(ConvertRGBToSurfaceColor(color)));
+
+ BitText::Instance->DrawText(
+ pDrawFont,
+ pSurface,
+ pText,
+ textRect.left,
+ textRect.top,
+ textRect.right - textRect.left,
+ textRect.bottom - textRect.top,
+ 0,
+ 0,
+ 0);
+
+ return 0;
+}
+
+static void EnsureInputCache(OwnerDrawDialogElement& data, const RectangleStruct& screenRect)
+{
+ if (data.CacheSurface || !DSurface::Alternate || screenRect.Width <= 0 || screenRect.Height <= 0)
+ return;
+
+ data.CacheSurface = GameCreate(screenRect.Width, screenRect.Height);
+ if (!data.CacheSurface)
+ return;
+
+ ++OwnerDraw::CachedSurfaceCount;
+
+ RectangleStruct cacheRect { 0, 0, screenRect.Width, screenRect.Height };
+ CopySurfacePart(data.CacheSurface, cacheRect, DSurface::Alternate, screenRect);
+}
+
+static LRESULT PaintInputCtrl(HWND hWnd, OwnerDrawDialogElement& data)
+{
+ if (!DSurface::Alternate)
+ {
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+ }
+
+ RECT ownerRect {};
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ RectangleStruct screenRect
+ {
+ ownerRect.left,
+ ownerRect.top,
+ ownerRect.right - ownerRect.left + 1,
+ ownerRect.bottom - ownerRect.top + 1
+ };
+
+ EnsureInputCache(data, screenRect);
+
+ wchar_t keyText[256] {};
+ BuildKeyboardKeyString(static_cast(::SendMessageA(hWnd, WW_INPUT_GETKEY, 0, 0)), keyText, std::size(keyText));
+
+ if (data.CacheSurface)
+ {
+ RectangleStruct cacheRect { 0, 0, screenRect.Width, screenRect.Height };
+ CopySurfacePart(DSurface::Alternate, screenRect, data.CacheSurface, cacheRect);
+ }
+
+ DrawBeveledBorder(DSurface::Alternate, screenRect, 2, -1);
+
+ if (std::wcslen(keyText))
+ {
+ RECT textRect
+ {
+ ownerRect.left + 4,
+ ownerRect.top + 4,
+ ownerRect.right - 4,
+ ownerRect.bottom - 4
+ };
+
+ DrawWideTextBasic(DSurface::Alternate, keyText, textRect, data.AsInput().Font(), OwnerDraw::PrimaryTextColor);
+ }
+
+ ::ValidateRect(hWnd, nullptr);
+ return 0;
+}
+
+LRESULT CALLBACK WWUI::InputCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ switch (message)
+ {
+ case WM_PAINT:
+ if (auto pData = FindOwnerDrawData(hWnd))
+ return PaintInputCtrl(hWnd, *pData);
+
+ return 0;
+
+ case WM_ERASEBKGND:
+ return 1;
+
+ case WM_NCPAINT:
+ return 0;
+
+ default:
+ return forwardOriginal();
+ }
+}
+
+LRESULT CALLBACK WWUI::SysListViewCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+
+ RECT ownerRect {};
+ (void)OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ RECT clientRect {};
+ (void)::GetClientRect(hWnd, &clientRect);
+
+ if (message == WM_CTLCOLOREDIT)
+ {
+ const auto hdc = reinterpret_cast(wParam);
+ ::SetTextColor(hdc, OwnerDraw::PrimaryTextColor);
+ ::SetBkMode(hdc, TRANSPARENT);
+ return reinterpret_cast(::GetStockObject(NULL_BRUSH));
+ }
+
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+}
diff --git a/src/OwnerDraw/ListBox.cpp b/src/OwnerDraw/ListBox.cpp
new file mode 100644
index 0000000000..8e88303291
--- /dev/null
+++ b/src/OwnerDraw/ListBox.cpp
@@ -0,0 +1,1602 @@
+#include "OwnerDraw.Internal.h"
+
+static COLORREF ListBoxTextColor()
+{
+ return Phobos::UI::ColorTextList;
+}
+
+static COLORREF ListBoxSelectionFillColor()
+{
+ return Phobos::UI::ColorSelectionList;
+}
+
+static COLORREF ListBoxDisabledTextColor()
+{
+ return Phobos::UI::ColorDisabledList;
+}
+
+constexpr int ListBoxScrollBarExtraWidth = 18;
+constexpr int ListBoxTextEntryInlineBytes = 2;
+
+static int SignedLowWord(LPARAM value)
+{
+ return static_cast(LOWORD(value));
+}
+
+static int SignedHighWord(LPARAM value)
+{
+ return static_cast(HIWORD(value));
+}
+
+static WPARAM ListBoxCommand(HWND hWnd, int notificationCode)
+{
+ return static_cast(
+ (::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF)
+ | ((notificationCode & 0xFFFF) << 16));
+}
+
+static void NotifyListBoxSelectionChanged(HWND hWnd)
+{
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ ::SendMessageA(parentHwnd, WM_COMMAND, ListBoxCommand(hWnd, LBN_SELCHANGE), reinterpret_cast(hWnd));
+}
+
+static void PostListBoxDoubleClick(HWND hWnd)
+{
+ if (const HWND parentHwnd = ::GetParent(hWnd))
+ ::PostMessageA(parentHwnd, WM_COMMAND, ListBoxCommand(hWnd, LBN_DBLCLK), reinterpret_cast(hWnd));
+}
+
+static WWUIIntArray* CreateIntArray()
+{
+ auto pArray = static_cast(YRMemory::Allocate(sizeof(WWUIIntArray)));
+ if (pArray)
+ *pArray = {};
+
+ return pArray;
+}
+
+static void DeleteIntArray(WWUIIntArray*& pArray)
+{
+ if (!pArray)
+ return;
+
+ YRMemory::Deallocate(pArray->Items);
+ YRMemory::Deallocate(pArray);
+ pArray = nullptr;
+}
+
+static void ResizeIntArrayStorage(WWUIIntArray& array, int capacity)
+{
+ if (capacity < 10)
+ capacity = 10;
+
+ auto pItems = static_cast(YRMemory::Allocate(sizeof(int) * capacity));
+ std::memset(pItems, 0, sizeof(int) * capacity);
+
+ if (array.Items && array.Count > 0)
+ std::memcpy(pItems, array.Items, sizeof(int) * array.Count);
+
+ YRMemory::Deallocate(array.Items);
+ array.Items = pItems;
+ array.Capacity = capacity;
+}
+
+static void EnsureIntArraySize(WWUIIntArray& array, int count, int fillValue)
+{
+ if (count <= array.Count)
+ return;
+
+ if (count > array.Capacity)
+ {
+ int capacity = array.Capacity;
+ do
+ {
+ capacity = std::max(capacity * 2, 10);
+ }
+ while (capacity < count);
+
+ ResizeIntArrayStorage(array, capacity);
+ }
+
+ for (int i = array.Count; i < count; ++i)
+ array.Items[i] = fillValue;
+
+ array.Count = count;
+}
+
+static void MaybeShrinkIntArray(WWUIIntArray& array)
+{
+ if (array.Capacity <= 10 || array.Count * 3 > array.Capacity)
+ return;
+
+ ResizeIntArrayStorage(array, std::max(array.Capacity / 2, 10));
+}
+
+static void RemoveIntArrayItem(WWUIIntArray* pArray, int index)
+{
+ if (!pArray || index < 0 || index >= pArray->Count)
+ return;
+
+ if (index < pArray->Count - 1)
+ {
+ std::memmove(
+ &pArray->Items[index],
+ &pArray->Items[index + 1],
+ sizeof(int) * (pArray->Count - index - 1));
+ }
+
+ --pArray->Count;
+ MaybeShrinkIntArray(*pArray);
+}
+
+static void InsertIntArrayItem(WWUIIntArray* pArray, int index, int value)
+{
+ if (!pArray)
+ return;
+
+ index = std::clamp(index, 0, pArray->Count);
+ EnsureIntArraySize(*pArray, pArray->Count + 1, value);
+
+ if (index < pArray->Count - 1)
+ {
+ std::memmove(
+ &pArray->Items[index + 1],
+ &pArray->Items[index],
+ sizeof(int) * (pArray->Count - index - 1));
+ }
+
+ pArray->Items[index] = value;
+}
+
+static void SetIntArrayValue(WWUIIntArray*& pArray, int index, int value, int fillValue)
+{
+ if (index < 0)
+ return;
+
+ if (!pArray)
+ pArray = CreateIntArray();
+
+ if (!pArray)
+ return;
+
+ EnsureIntArraySize(*pArray, index + 1, fillValue);
+ pArray->Items[index] = value;
+}
+
+static int GetIntArrayValue(const WWUIIntArray* pArray, int index, int defaultValue)
+{
+ if (!pArray || index < 0 || index >= pArray->Count)
+ return defaultValue;
+
+ return pArray->Items[index];
+}
+
+static void ConstructListBoxCell(WWUIListBoxCell& cell)
+{
+ new (&cell) WWUIListBoxCell();
+ cell.Format = WWUIListBoxCellFormat::Empty;
+ cell.TextColor = static_cast(-1);
+ cell.Image = nullptr;
+ cell.Value = -1;
+}
+
+static void ResetListBoxCell(WWUIListBoxCell& cell)
+{
+ cell.~WWUIListBoxCell();
+ ConstructListBoxCell(cell);
+}
+
+static WWUIListBoxCell* AllocateListBoxCells(int capacity)
+{
+ auto pItems = static_cast(YRMemory::Allocate(sizeof(WWUIListBoxCell) * capacity));
+ for (int i = 0; i < capacity; ++i)
+ ConstructListBoxCell(pItems[i]);
+
+ return pItems;
+}
+
+static void DeleteListBoxCells(WWUIListBoxCell*& pItems, int capacity)
+{
+ if (!pItems)
+ return;
+
+ for (int i = 0; i < capacity; ++i)
+ pItems[i].~WWUIListBoxCell();
+
+ YRMemory::Deallocate(pItems);
+ pItems = nullptr;
+}
+
+static void ResizeListBoxCellStorage(WWUIListBoxColumn& column, int capacity)
+{
+ if (capacity < 10)
+ capacity = 10;
+
+ auto pItems = AllocateListBoxCells(capacity);
+ for (int i = 0; i < column.CellCount; ++i)
+ pItems[i] = column.Cells[i];
+
+ DeleteListBoxCells(column.Cells, column.CellCapacity);
+ column.Cells = pItems;
+ column.CellCapacity = capacity;
+}
+
+static void EnsureListBoxCellCount(WWUIListBoxColumn& column, int count, WWUIListBoxCellFormat defaultFormat)
+{
+ if (count <= column.CellCount)
+ return;
+
+ if (count > column.CellCapacity)
+ {
+ int capacity = column.CellCapacity;
+ do
+ {
+ capacity = std::max(capacity * 2, 10);
+ }
+ while (capacity < count);
+
+ ResizeListBoxCellStorage(column, capacity);
+ }
+
+ for (int i = column.CellCount; i < count; ++i)
+ {
+ auto& cell = column.Cells[i];
+ ResetListBoxCell(cell);
+ cell.Format = defaultFormat;
+ }
+
+ column.CellCount = count;
+}
+
+static void ClearListBoxColumnCells(WWUIListBoxColumn& column, bool releaseStorage)
+{
+ if (!column.Cells)
+ {
+ column.CellCount = 0;
+ column.CellCapacity = 0;
+ return;
+ }
+
+ for (int i = 0; i < column.CellCapacity; ++i)
+ ResetListBoxCell(column.Cells[i]);
+
+ column.CellCount = 0;
+
+ if (releaseStorage)
+ {
+ DeleteListBoxCells(column.Cells, column.CellCapacity);
+ column.CellCapacity = 0;
+ }
+}
+
+static void DeleteListBoxColumns(WWUIListBoxColumnArray*& pColumns)
+{
+ if (!pColumns)
+ return;
+
+ for (int i = 0; i < pColumns->Count; ++i)
+ ClearListBoxColumnCells(pColumns->Items[i], true);
+
+ YRMemory::Deallocate(pColumns->Items);
+ YRMemory::Deallocate(pColumns);
+ pColumns = nullptr;
+}
+
+static WWUIListBoxColumnArray* CreateListBoxColumnArray()
+{
+ auto pArray = static_cast(YRMemory::Allocate(sizeof(WWUIListBoxColumnArray)));
+ if (pArray)
+ *pArray = {};
+
+ return pArray;
+}
+
+static void ResizeListBoxColumnStorage(WWUIListBoxColumnArray& columns, int capacity)
+{
+ if (capacity < 10)
+ capacity = 10;
+
+ auto pItems = static_cast(YRMemory::Allocate(sizeof(WWUIListBoxColumn) * capacity));
+ std::memset(pItems, 0, sizeof(WWUIListBoxColumn) * capacity);
+
+ if (columns.Items && columns.Count > 0)
+ std::memcpy(pItems, columns.Items, sizeof(WWUIListBoxColumn) * columns.Count);
+
+ YRMemory::Deallocate(columns.Items);
+ columns.Items = pItems;
+ columns.Capacity = capacity;
+}
+
+static WWUIListBoxColumn* FindListBoxColumn(WWUIListBoxColumnArray* pColumns, int x)
+{
+ if (!pColumns)
+ return nullptr;
+
+ for (int i = 0; i < pColumns->Count; ++i)
+ {
+ if (pColumns->Items[i].X == x)
+ return &pColumns->Items[i];
+ }
+
+ return nullptr;
+}
+
+static WWUIListBoxColumn* FindListBoxColumnAtX(WWUIListBoxColumnArray* pColumns, int x)
+{
+ if (!pColumns)
+ return nullptr;
+
+ WWUIListBoxColumn* pBest = nullptr;
+ for (int i = 0; i < pColumns->Count; ++i)
+ {
+ auto& column = pColumns->Items[i];
+ if (column.X <= x && (!pBest || column.X > pBest->X))
+ pBest = &column;
+ }
+
+ return pBest;
+}
+
+static WWUIListBoxTextEntry* AllocateListBoxTextEntry(OwnerDrawDialogElement& data, const wchar_t* pText, bool isWide)
+{
+ if (!pText)
+ pText = L"";
+
+ const size_t length = std::wcslen(pText);
+ const size_t bytes = sizeof(WWUIListBoxTextEntry) + (length + 1) * sizeof(wchar_t) + ListBoxTextEntryInlineBytes;
+ auto pEntry = static_cast(YRMemory::Allocate(bytes));
+ if (!pEntry)
+ return nullptr;
+
+ pEntry->Next = data.AsListBox().TextEntries();
+ pEntry->ItemData = 0;
+ pEntry->Text = reinterpret_cast(reinterpret_cast(pEntry) + sizeof(WWUIListBoxTextEntry));
+ pEntry->IsWide = isWide ? 1 : 0;
+ std::wcscpy(pEntry->Text, pText);
+ data.AsListBox().TextEntries() = pEntry;
+ return pEntry;
+}
+
+static void RemoveListBoxTextEntry(OwnerDrawDialogElement& data, WWUIListBoxTextEntry* pEntry)
+{
+ if (!pEntry)
+ return;
+
+ WWUIListBoxTextEntry* pPrevious = nullptr;
+ for (auto pCurrent = data.AsListBox().TextEntries(); pCurrent; pCurrent = pCurrent->Next)
+ {
+ if (pCurrent != pEntry)
+ {
+ pPrevious = pCurrent;
+ continue;
+ }
+
+ if (pPrevious)
+ pPrevious->Next = pCurrent->Next;
+ else
+ data.AsListBox().TextEntries() = pCurrent->Next;
+
+ YRMemory::Deallocate(pCurrent);
+ return;
+ }
+}
+
+static void ClearListBoxTextEntries(OwnerDrawDialogElement& data)
+{
+ auto pEntry = data.AsListBox().TextEntries();
+ while (pEntry)
+ {
+ auto pNext = pEntry->Next;
+ YRMemory::Deallocate(pEntry);
+ pEntry = pNext;
+ }
+
+ data.AsListBox().TextEntries() = nullptr;
+}
+
+static WWUIListBoxTextEntry* GetListBoxTextEntry(WNDPROC pOriginalWndProc, HWND hWnd, int index)
+{
+ const auto result = CallSelectedHandler(pOriginalWndProc, hWnd, LB_GETITEMDATA, index, 0);
+ if (result == LB_ERR || !result)
+ return nullptr;
+
+ return reinterpret_cast(result);
+}
+
+static void RemoveListBoxRow(OwnerDrawDialogElement& data, int index)
+{
+ RemoveIntArrayItem(data.AsListBox().ItemData(), index);
+ RemoveIntArrayItem(data.AsListBox().SelectionStates(), index);
+
+ if (auto pColumns = data.AsListBox().Columns())
+ {
+ for (int i = 0; i < pColumns->Count; ++i)
+ {
+ auto& column = pColumns->Items[i];
+ if (index < 0 || index >= column.CellCount)
+ continue;
+
+ for (int cellIndex = index; cellIndex < column.CellCount - 1; ++cellIndex)
+ column.Cells[cellIndex] = column.Cells[cellIndex + 1];
+
+ ResetListBoxCell(column.Cells[column.CellCount - 1]);
+ --column.CellCount;
+
+ if (column.CellCapacity > 10 && column.CellCount * 3 <= column.CellCapacity)
+ ResizeListBoxCellStorage(column, std::max(column.CellCapacity / 2, 10));
+ }
+ }
+}
+
+static void InsertListBoxRow(OwnerDrawDialogElement& data, int index)
+{
+ InsertIntArrayItem(data.AsListBox().ItemData(), index, -1);
+ InsertIntArrayItem(data.AsListBox().SelectionStates(), index, 0);
+
+ if (auto pColumns = data.AsListBox().Columns())
+ {
+ for (int i = 0; i < pColumns->Count; ++i)
+ {
+ auto& column = pColumns->Items[i];
+ const int insertIndex = std::clamp(index, 0, column.CellCount);
+ EnsureListBoxCellCount(
+ column,
+ column.CellCount + 1,
+ i == 0 ? WWUIListBoxCellFormat::ItemText : WWUIListBoxCellFormat::Empty);
+
+ for (int cellIndex = column.CellCount - 1; cellIndex > insertIndex; --cellIndex)
+ column.Cells[cellIndex] = column.Cells[cellIndex - 1];
+
+ ResetListBoxCell(column.Cells[insertIndex]);
+ column.Cells[insertIndex].Format = i == 0 ? WWUIListBoxCellFormat::ItemText : WWUIListBoxCellFormat::Empty;
+ }
+ }
+}
+
+static void ClearListBoxRows(OwnerDrawDialogElement& data, bool destroyColumns)
+{
+ DeleteIntArray(data.AsListBox().ItemData());
+ DeleteIntArray(data.AsListBox().SelectionStates());
+ data.AsListBox().TopIndex() = 0;
+ data.AsListBox().CurrentSelection() = -1;
+
+ if (destroyColumns)
+ {
+ DeleteListBoxColumns(data.AsListBox().Columns());
+ }
+ else if (auto pColumns = data.AsListBox().Columns())
+ {
+ for (int i = 0; i < pColumns->Count; ++i)
+ ClearListBoxColumnCells(pColumns->Items[i], false);
+ }
+}
+
+static void PaintListBoxCellText(
+ HWND hWnd,
+ BitFont* pFont,
+ RectangleStruct rect,
+ const wchar_t* pText,
+ COLORREF textColor,
+ int maxWidth)
+{
+ if (!pText)
+ pText = L"";
+
+ wchar_t buffer[512] {};
+ std::wcsncpy(buffer, pText, std::size(buffer) - 1);
+
+ int textWidth = 0;
+ int textHeight = 0;
+ auto pMeasureFont = pFont ? pFont : BitFont::Instance;
+
+ if (pMeasureFont)
+ {
+ size_t length = std::wcslen(buffer);
+ while (length > 0)
+ {
+ pMeasureFont->GetTextDimension(buffer, &textWidth, &textHeight, maxWidth);
+ if (textWidth <= maxWidth)
+ break;
+
+ --length;
+ buffer[length] = L'\0';
+ if (length > 3)
+ std::wcscat(buffer, L"...");
+ }
+ }
+
+ OwnerDraw::PrintTextFixedLength(
+ textColor,
+ pFont,
+ &rect,
+ buffer,
+ static_cast(std::wcslen(buffer)),
+ 0,
+ 0,
+ nullptr,
+ 0);
+}
+
+static void DrawListBoxProgressCell(RectangleStruct rect, int value)
+{
+ if (!DSurface::Alternate)
+ return;
+
+ WORD color = static_cast(ConvertRGBToSurfaceColor(RGB(0, 0, 192)));
+ if (value < 0)
+ {
+ color = static_cast(ConvertRGBToSurfaceColor(RGB(0, 0, 192)));
+ value = 1000;
+ }
+ else if (value < 300)
+ {
+ color = static_cast(ConvertRGBToSurfaceColor(RGB(0, 192, 0)));
+ }
+ else if (value < 500)
+ {
+ color = static_cast(ConvertRGBToSurfaceColor(RGB(192, 192, 0)));
+ }
+ else
+ {
+ color = static_cast(ConvertRGBToSurfaceColor(RGB(192, 0, 0)));
+ }
+
+ BlendGradientRect(rect, DSurface::Alternate, color, (value << 16) / 1000);
+}
+
+static void PaintListBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect, WNDPROC pOriginalWndProc)
+{
+ if (data.SkipDraw)
+ {
+ ::ValidateRect(hWnd, nullptr);
+ return;
+ }
+
+ if (!DSurface::Alternate)
+ return;
+
+ RectangleStruct drawRect
+ {
+ ownerRect.left,
+ ownerRect.top,
+ clientRect.right - clientRect.left,
+ clientRect.bottom - clientRect.top
+ };
+
+ OwnerDraw::CopyDimmedBackground(&drawRect, hWnd, static_cast(data.Alpha));
+ DrawBeveledBorder(DSurface::Alternate, drawRect, 2, -1);
+
+ RECT updateRect {};
+ if (!::GetUpdateRect(hWnd, &updateRect, FALSE))
+ return;
+
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ int itemIndex = static_cast(::SendMessageA(hWnd, LB_GETTOPINDEX, 0, 0));
+ const LONG style = ::GetWindowLongA(hWnd, GWL_STYLE);
+ const int selectionColor = ConvertRGBToSurfaceColor(ListBoxSelectionFillColor());
+ auto pFont = data.AsListBox().Font();
+
+ while (itemIndex >= 0 && itemIndex < itemCount)
+ {
+ RECT itemRect {};
+ if (::SendMessageA(hWnd, LB_GETITEMRECT, itemIndex, reinterpret_cast(&itemRect)) == LB_ERR)
+ break;
+
+ if (clientRect.top + itemRect.bottom > clientRect.bottom)
+ break;
+
+ wchar_t itemText[512] {};
+ if (auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, itemIndex))
+ std::wcsncpy(itemText, pEntry->Text ? pEntry->Text : L"", std::size(itemText) - 1);
+
+ const bool selected = ::SendMessageA(hWnd, LB_GETSEL, itemIndex, 0) > 0;
+ RectangleStruct rowRect
+ {
+ ownerRect.left + itemRect.left,
+ ownerRect.top + itemRect.top,
+ itemRect.right - itemRect.left,
+ itemRect.bottom - itemRect.top
+ };
+
+ if (selected)
+ DSurface::Alternate->FillRect(&rowRect, selectionColor);
+
+ if (auto pColumns = data.AsListBox().Columns())
+ {
+ for (int columnIndex = 0; columnIndex < pColumns->Count; ++columnIndex)
+ {
+ auto& column = pColumns->Items[columnIndex];
+ if (itemIndex < 0 || itemIndex >= column.CellCount || !column.Cells)
+ continue;
+
+ auto& cell = column.Cells[itemIndex];
+ if (cell.Format == WWUIListBoxCellFormat::Empty)
+ continue;
+
+ RectangleStruct cellRect
+ {
+ ownerRect.left + itemRect.left + column.X,
+ ownerRect.top + itemRect.top,
+ column.Width ? column.Width : rowRect.Width,
+ rowRect.Height
+ };
+
+ if (cellRect.Width <= 0)
+ cellRect.Width = 0xFFFF;
+
+ const int availableWidth = std::min(
+ cellRect.Width,
+ static_cast(ownerRect.right - cellRect.X));
+
+ switch (cell.Format)
+ {
+ case WWUIListBoxCellFormat::Text:
+ case WWUIListBoxCellFormat::ItemText:
+ {
+ COLORREF textColor = cell.TextColor == static_cast(-1)
+ ? ListBoxTextColor()
+ : cell.TextColor;
+
+ if (style & WS_DISABLED)
+ textColor = ListBoxDisabledTextColor();
+
+ const wchar_t* pText = cell.Format == WWUIListBoxCellFormat::Text
+ ? GetWideTextBuffer(cell.PrimaryText)
+ : itemText;
+
+ PaintListBoxCellText(hWnd, pFont, cellRect, pText, textColor, availableWidth);
+ break;
+ }
+
+ case WWUIListBoxCellFormat::Image:
+ if (cell.Image)
+ {
+ RectangleStruct imageRect
+ {
+ cellRect.X,
+ cellRect.Y + (cellRect.Height - cell.Image->GetHeight()) / 2,
+ cell.Image->GetWidth(),
+ cell.Image->GetHeight()
+ };
+ PCX::Instance.BlitToSurface(&imageRect, DSurface::Alternate, static_cast(cell.Image));
+ }
+ break;
+
+ case WWUIListBoxCellFormat::Progress:
+ {
+ RectangleStruct progressRect { cellRect.X, cellRect.Y, 32, 12 };
+ DrawListBoxProgressCell(progressRect, cell.Value);
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+ }
+ else
+ {
+ COLORREF textColor = GetIntArrayValue(data.AsListBox().ItemData(), itemIndex, ListBoxTextColor());
+ if (textColor == static_cast(-1))
+ textColor = ListBoxTextColor();
+
+ if (style & WS_DISABLED)
+ textColor = ListBoxDisabledTextColor();
+
+ RectangleStruct textRect
+ {
+ rowRect.X + 2,
+ rowRect.Y,
+ rowRect.Width,
+ rowRect.Height
+ };
+
+ OwnerDraw::PrintTextFixedLength(
+ textColor,
+ pFont,
+ &textRect,
+ itemText,
+ static_cast(std::wcslen(itemText)),
+ 0,
+ 0,
+ nullptr,
+ 0);
+ }
+
+ ++itemIndex;
+ }
+
+ ::ValidateRect(hWnd, &updateRect);
+ if (data.AsListBox().ScrollBarHwnd())
+ ::InvalidateRect(data.AsListBox().ScrollBarHwnd(), nullptr, FALSE);
+}
+
+static void SyncListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, int itemCount, int itemHeight)
+{
+ if (itemHeight <= 0)
+ itemHeight = 1;
+
+ const int visibleItems = itemHeight ? (clientRect.bottom - clientRect.top) / itemHeight : 0;
+ int maxTopIndex = itemCount - visibleItems;
+ if (maxTopIndex < 0)
+ maxTopIndex = 0;
+
+ if (data.AsListBox().TopIndex() > maxTopIndex)
+ data.AsListBox().TopIndex() = maxTopIndex;
+
+ if (data.AsListBox().ScrollBarHwnd() && reinterpret_cast(data.AsListBox().ScrollBarHwnd()) > 1)
+ {
+ SCROLLINFO scrollInfo {};
+ scrollInfo.cbSize = sizeof(scrollInfo);
+ scrollInfo.fMask = SIF_RANGE | SIF_POS;
+ scrollInfo.nMin = 0;
+ scrollInfo.nMax = maxTopIndex;
+ scrollInfo.nPos = data.AsListBox().TopIndex();
+ ::SendMessageA(data.AsListBox().ScrollBarHwnd(), SBM_SETSCROLLINFO, 0, reinterpret_cast(&scrollInfo));
+ }
+}
+
+static bool HasListBoxScrollBar(OwnerDrawDialogElement& data)
+{
+ const HWND scrollBarHwnd = data.AsListBox().ScrollBarHwnd();
+ return scrollBarHwnd && reinterpret_cast(scrollBarHwnd) > 1 && ::IsWindow(scrollBarHwnd);
+}
+
+static void PositionListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, BOOL repaint)
+{
+ if (!HasListBoxScrollBar(data))
+ return;
+
+ const HWND parentHwnd = ::GetParent(hWnd);
+ if (!parentHwnd)
+ return;
+
+ RECT parentRect {};
+ RECT listRect {};
+ if (!OwnerDraw::GetRectangle(parentHwnd, &parentRect) || !OwnerDraw::GetRectangle(hWnd, &listRect))
+ return;
+
+ const int inset = OwnerDraw::ControlInsetPx;
+ const int scrollBarWidth = 2 * inset + ListBoxScrollBarExtraWidth;
+ const int x = listRect.right - parentRect.left - 2 * inset + 1;
+ const int y = listRect.top - parentRect.top;
+ const int height = listRect.bottom - listRect.top;
+
+ if (height <= 0)
+ return;
+
+ const HWND scrollBarHwnd = data.AsListBox().ScrollBarHwnd();
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates())
+ {
+ ::MoveWindow(scrollBarHwnd, x, y, scrollBarWidth, height, repaint);
+ }
+ else
+ {
+ RenderDX::MoveWindowInRender(scrollBarHwnd, x, y, scrollBarWidth, height, repaint);
+ }
+
+ if (auto pScrollData = FindOwnerDrawData(scrollBarHwnd))
+ ResetOwnerDrawCachedSurface(*pScrollData);
+
+ ::InvalidateRect(scrollBarHwnd, nullptr, FALSE);
+}
+
+void WWUI::SyncListBoxScrollBarPositions(HWND rootHwnd)
+{
+ if (!rootHwnd)
+ {
+ for (auto it = OwnerDraw::Dialogs.begin(); it != OwnerDraw::Dialogs.end(); ++it)
+ {
+ const HWND hWnd = it->Key;
+ auto& data = it->Value;
+ if (::IsWindow(hWnd) && data.ControlType == WWControlType::ListBox)
+ PositionListBoxScrollBar(hWnd, data, FALSE);
+ }
+
+ return;
+ }
+
+ if (auto pData = FindOwnerDrawData(rootHwnd))
+ {
+ if (pData->ControlType == WWControlType::ListBox)
+ PositionListBoxScrollBar(rootHwnd, *pData, FALSE);
+ }
+
+ for (HWND child = ::GetWindow(rootHwnd, GW_CHILD); child; child = ::GetWindow(child, GW_HWNDNEXT))
+ SyncListBoxScrollBarPositions(child);
+}
+
+static void UpdateListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect)
+{
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1);
+ const bool needsScrollbar = itemCount * itemHeight > clientRect.bottom - clientRect.top;
+ const int scrollBarWidth = 2 * OwnerDraw::ControlInsetPx + ListBoxScrollBarExtraWidth;
+
+ SyncListBoxScrollBar(hWnd, data, clientRect, itemCount, itemHeight);
+
+ if (needsScrollbar)
+ {
+ if (!data.AsListBox().ScrollBarHwnd())
+ {
+ data.AsListBox().ScrollBarHwnd() = reinterpret_cast(1);
+
+ const HWND parentHwnd = ::GetParent(hWnd);
+ RECT parentRect {};
+ RECT listRect {};
+ OwnerDraw::GetRectangle(parentHwnd, &parentRect);
+ OwnerDraw::GetRectangle(hWnd, &listRect);
+
+ const int height = listRect.bottom - listRect.top;
+ RECT scrollClientRect {};
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates())
+ {
+ scrollClientRect.left = listRect.left - parentRect.left - scrollBarWidth + clientRect.right + 1;
+ scrollClientRect.top = listRect.top - parentRect.top + clientRect.top;
+ scrollClientRect.right = scrollClientRect.left + scrollBarWidth;
+ scrollClientRect.bottom = scrollClientRect.top + height;
+ }
+ else
+ {
+ const RECT scrollRenderRect
+ {
+ listRect.left - scrollBarWidth + clientRect.right + 1,
+ listRect.top + clientRect.top,
+ listRect.left + clientRect.right + 1,
+ listRect.top + clientRect.top + height
+ };
+
+ if (!RenderDX::RenderRectToClient(parentHwnd, scrollRenderRect, &scrollClientRect))
+ {
+ data.AsListBox().ScrollBarHwnd() = nullptr;
+ return;
+ }
+ }
+
+ data.AsListBox().ScrollBarHwnd() = ::CreateWindowExA(
+ 0,
+ "Scrollbar",
+ nullptr,
+ WS_CHILD | WS_VISIBLE | SBS_VERT | WS_TABSTOP,
+ scrollClientRect.left,
+ scrollClientRect.top,
+ scrollClientRect.right - scrollClientRect.left,
+ scrollClientRect.bottom - scrollClientRect.top,
+ parentHwnd,
+ nullptr,
+ reinterpret_cast(Phobos::hInstance),
+ nullptr);
+
+ data.AsListBox().ScrollBarWidth() = scrollBarWidth;
+ OwnerDraw::RegisterChildControlProc(data.AsListBox().ScrollBarHwnd(), 0);
+
+ if (auto pScrollData = FindOwnerDrawData(data.AsListBox().ScrollBarHwnd()))
+ {
+ pScrollData->AsScrollBar().NotifyHwnd() = hWnd;
+ pScrollData->AsScrollBar().Disabled() = false;
+ }
+
+ SyncListBoxScrollBar(hWnd, data, clientRect, itemCount, itemHeight);
+
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates())
+ {
+ ::SetWindowPos(
+ hWnd,
+ nullptr,
+ 0,
+ 0,
+ listRect.right - listRect.left - scrollBarWidth,
+ listRect.bottom - listRect.top,
+ SWP_NOMOVE | SWP_NOZORDER);
+ }
+ else
+ {
+ RenderDX::SetWindowPosInRender(
+ hWnd,
+ nullptr,
+ 0,
+ 0,
+ listRect.right - listRect.left - scrollBarWidth,
+ listRect.bottom - listRect.top,
+ SWP_NOMOVE | SWP_NOZORDER);
+ }
+
+ ::ShowWindow(data.AsListBox().ScrollBarHwnd(), SW_SHOW);
+ ::BringWindowToTop(data.AsListBox().ScrollBarHwnd());
+ PositionListBoxScrollBar(hWnd, data, FALSE);
+ ::InvalidateRect(data.AsListBox().ScrollBarHwnd(), nullptr, FALSE);
+ ::UpdateWindow(data.AsListBox().ScrollBarHwnd());
+ }
+
+ return;
+ }
+
+ if (!data.AsListBox().ScrollBarHwnd() || data.NeedsControlImage)
+ return;
+
+ const HWND scrollBarHwnd = data.AsListBox().ScrollBarHwnd();
+ ::DestroyWindow(scrollBarHwnd);
+ CleanupDestroyedWindow(scrollBarHwnd);
+ data.AsListBox().ScrollBarHwnd() = nullptr;
+
+ RECT listRect {};
+ OwnerDraw::GetRectangle(hWnd, &listRect);
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates())
+ {
+ ::SetWindowPos(
+ hWnd,
+ nullptr,
+ 0,
+ 0,
+ listRect.right - listRect.left + scrollBarWidth,
+ listRect.bottom - listRect.top,
+ SWP_NOMOVE | SWP_NOZORDER);
+ }
+ else
+ {
+ RenderDX::SetWindowPosInRender(
+ hWnd,
+ nullptr,
+ 0,
+ 0,
+ listRect.right - listRect.left + scrollBarWidth,
+ listRect.bottom - listRect.top,
+ SWP_NOMOVE | SWP_NOZORDER);
+ }
+
+ data.AsListBox().ScrollBarWidth() = 0;
+}
+
+LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ auto pData = FindOwnerDrawData(hWnd);
+ if (!pData)
+ return 0;
+
+ auto& data = *pData;
+ const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd);
+
+ RECT clientRect {};
+ if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect))
+ ::GetClientRect(hWnd, &clientRect);
+ const RECT fullClientRect = clientRect;
+
+ RECT ownerRect {};
+ OwnerDraw::GetRectangle(hWnd, &ownerRect);
+
+ const int inset = OwnerDraw::ControlInsetPx;
+ clientRect.right -= 2 * inset;
+ clientRect.bottom -= 2 * inset;
+ ownerRect.left += inset;
+ ownerRect.top += inset;
+ ownerRect.right -= inset;
+ ownerRect.bottom -= inset;
+
+ const bool updateScrollBar = message != LB_GETCOUNT
+ && message != LB_GETITEMHEIGHT
+ && message != WM_VSCROLL;
+
+ if (updateScrollBar)
+ {
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1);
+ SyncListBoxScrollBar(hWnd, data, clientRect, itemCount, itemHeight);
+ }
+
+ auto finish = [&](LRESULT result) -> LRESULT
+ {
+ if (updateScrollBar)
+ UpdateListBoxScrollBar(hWnd, data, clientRect);
+
+ return result;
+ };
+
+ auto forwardOriginal = [&]() -> LRESULT
+ {
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+ };
+
+ auto setSelection = [&](int index, int selected)
+ {
+ SetIntArrayValue(data.AsListBox().SelectionStates(), index, selected ? 1 : 0, 0);
+ };
+
+ auto playClick = []()
+ {
+ if (RulesClass::Instance)
+ VocClass::PlayGlobal(RulesClass::Instance->GenericClick, 0x2000, 1.0f);
+ };
+
+ auto addOrInsertString = [&](bool wideText, bool insert) -> LRESULT
+ {
+ char narrowText[2048] {};
+ wchar_t wideBuffer[2048] {};
+
+ const LPARAM nativeTextParam = [&]() -> LPARAM
+ {
+ if (wideText)
+ {
+ const auto pWideText = reinterpret_cast(lParam);
+ std::wcsncpy(wideBuffer, pWideText ? pWideText : L"", std::size(wideBuffer) - 1);
+ WideToCharString(narrowText, std::size(narrowText), wideBuffer);
+ return reinterpret_cast(narrowText);
+ }
+
+ const auto pText = reinterpret_cast(lParam);
+ std::strncpy(narrowText, pText ? pText : "", std::size(narrowText) - 1);
+ CharToWideString(wideBuffer, std::size(wideBuffer), narrowText);
+ return reinterpret_cast(narrowText);
+ }();
+
+ const UINT nativeMessage = insert ? LB_INSERTSTRING : LB_ADDSTRING;
+ const WPARAM nativeIndex = insert ? wParam : 0;
+ const auto nativeResult = CallSelectedHandler(pOriginalWndProc, hWnd, nativeMessage, nativeIndex, nativeTextParam);
+ if (nativeResult == LB_ERR || nativeResult == LB_ERRSPACE)
+ return nativeResult;
+
+ const int itemIndex = static_cast(nativeResult);
+ auto pEntry = AllocateListBoxTextEntry(data, wideBuffer, wideText);
+ if (!pEntry)
+ {
+ CallSelectedHandler(pOriginalWndProc, hWnd, LB_DELETESTRING, itemIndex, 0);
+ return LB_ERRSPACE;
+ }
+
+ const auto setDataResult = CallSelectedHandler(
+ pOriginalWndProc,
+ hWnd,
+ LB_SETITEMDATA,
+ itemIndex,
+ reinterpret_cast(pEntry));
+
+ if (setDataResult == LB_ERR)
+ {
+ RemoveListBoxTextEntry(data, pEntry);
+ CallSelectedHandler(pOriginalWndProc, hWnd, LB_DELETESTRING, itemIndex, 0);
+ return LB_ERR;
+ }
+
+ InsertListBoxRow(data, itemIndex);
+ return itemIndex;
+ };
+
+ auto findString = [&](bool wideText, bool exact, bool select) -> LRESULT
+ {
+ wchar_t needle[2048] {};
+ if (wideText)
+ {
+ const auto pText = reinterpret_cast(lParam);
+ std::wcsncpy(needle, pText ? pText : L"", std::size(needle) - 1);
+ }
+ else
+ {
+ CharToWideString(needle, std::size(needle), reinterpret_cast(lParam));
+ }
+
+ const int count = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ int index = static_cast(wParam);
+ if (index < 0)
+ index = 0;
+
+ if (index >= count)
+ return LB_ERR;
+
+ const size_t needleLength = std::wcslen(needle);
+ for (; index < count; ++index)
+ {
+ const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, index);
+ const wchar_t* pText = pEntry && pEntry->Text ? pEntry->Text : L"";
+ const bool match = exact
+ ? _wcsicmp(needle, pText) == 0
+ : _wcsnicmp(needle, pText, needleLength) == 0;
+
+ if (!match)
+ continue;
+
+ if (select)
+ ::SendMessageA(hWnd, LB_SETCURSEL, index, 0);
+
+ return index;
+ }
+
+ return LB_ERR;
+ };
+
+ switch (message)
+ {
+ case WM_SIZE:
+ PositionListBoxScrollBar(hWnd, data, TRUE);
+
+ if (data.CacheSurface
+ && (fullClientRect.right != data.CacheSurface->GetWidth() || fullClientRect.bottom != data.CacheSurface->GetHeight()))
+ {
+ ResetOwnerDrawCachedSurface(data);
+ }
+
+ return finish(forwardOriginal());
+
+ case WM_PAINT:
+ PaintListBox(hWnd, data, clientRect, ownerRect, pOriginalWndProc);
+ return finish(0);
+
+ case WM_ERASEBKGND:
+ return 0;
+
+ case WM_DELETEITEM:
+ if (lParam)
+ {
+ const auto pDeleteItem = reinterpret_cast(lParam);
+ RemoveListBoxTextEntry(data, reinterpret_cast(pDeleteItem->itemData));
+ }
+ return 1;
+
+ case WM_SETFONT:
+ {
+ TEXTMETRICA metrics {};
+ if (const HDC hdc = ::GetDC(hWnd))
+ {
+ ::GetTextMetricsA(hdc, &metrics);
+ ::ReleaseDC(hWnd, hdc);
+ }
+
+ ::SendMessageA(hWnd, LB_SETITEMHEIGHT, static_cast(-1), LOWORD(metrics.tmHeight + 2));
+ data.AsListBox().SavedFont() = static_cast(wParam);
+ return 0;
+ }
+
+ case WM_VSCROLL:
+ if (data.AsListBox().ScrollBarHwnd())
+ {
+ const auto position = ::SendMessageA(data.AsListBox().ScrollBarHwnd(), SBM_GETPOS, 0, 0);
+ if (position != ::SendMessageA(hWnd, LB_GETTOPINDEX, 0, 0))
+ ::SendMessageA(hWnd, LB_SETTOPINDEX, position, 0);
+ }
+ return 0;
+
+ case WM_RBUTTONDOWN:
+ if (::GetWindowLongA(hWnd, GWL_STYLE) & LBS_MULTIPLESEL)
+ {
+ if (auto pSelections = data.AsListBox().SelectionStates())
+ {
+ for (int i = 0; i < pSelections->Count; ++i)
+ pSelections->Items[i] = 0;
+ }
+
+ data.AsListBox().CurrentSelection() = -1;
+ NotifyListBoxSelectionChanged(hWnd);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return 0;
+ }
+ break;
+
+ case WM_LBUTTONDBLCLK:
+ PostListBoxDoubleClick(hWnd);
+ return 0;
+
+ case WM_LBUTTONDOWN:
+ {
+ const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1);
+ const POINT point = RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam);
+ if (point.y < 0)
+ return 0;
+
+ const int itemIndex = data.AsListBox().TopIndex() + point.y / itemHeight;
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ if (itemIndex < 0 || itemIndex >= itemCount)
+ return 0;
+
+ const LONG style = ::GetWindowLongA(hWnd, GWL_STYLE);
+ ::SetFocus(hWnd);
+ const auto previousImageState = ::SendMessageA(hWnd, WW_SETHASIMAGE, 0, 1);
+
+ if (style & LBS_MULTIPLESEL)
+ {
+ const bool selected = ::SendMessageA(hWnd, LB_GETSEL, itemIndex, 0) == 0;
+ playClick();
+ ::SendMessageA(hWnd, LB_SETSEL, selected, itemIndex);
+ }
+ else if (!(style & LBS_NOSEL))
+ {
+ playClick();
+ ::SendMessageA(hWnd, LB_SETCURSEL, itemIndex, 0);
+ }
+
+ ::SendMessageA(hWnd, WW_SETHASIMAGE, 0, previousImageState);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ NotifyListBoxSelectionChanged(hWnd);
+ return 0;
+ }
+
+ case LB_SETSEL:
+ {
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ int index = static_cast(lParam);
+ if (index < -1)
+ return LB_ERR;
+
+ if (index >= itemCount)
+ index = itemCount - 1;
+
+ if (!data.AsListBox().SelectionStates())
+ data.AsListBox().SelectionStates() = CreateIntArray();
+
+ if (index == -1)
+ {
+ if (data.AsListBox().SelectionStates())
+ {
+ EnsureIntArraySize(*data.AsListBox().SelectionStates(), itemCount, 0);
+ for (int i = 0; i < data.AsListBox().SelectionStates()->Count; ++i)
+ data.AsListBox().SelectionStates()->Items[i] = wParam ? 1 : 0;
+ }
+ }
+ else
+ {
+ setSelection(index, wParam ? 1 : 0);
+ }
+
+ NotifyListBoxSelectionChanged(hWnd);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return finish(0);
+ }
+
+ case LB_GETSEL:
+ return GetIntArrayValue(data.AsListBox().SelectionStates(), static_cast(wParam), 0);
+
+ case LB_GETSELCOUNT:
+ {
+ int count = 0;
+ if (auto pSelections = data.AsListBox().SelectionStates())
+ {
+ for (int i = 0; i < pSelections->Count; ++i)
+ {
+ if (pSelections->Items[i])
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ case LB_GETSELITEMS:
+ {
+ int written = 0;
+ auto pOut = reinterpret_cast(lParam);
+ if (pOut)
+ {
+ if (auto pSelections = data.AsListBox().SelectionStates())
+ {
+ for (int i = 0; i < pSelections->Count && written < static_cast(wParam); ++i)
+ {
+ if (pSelections->Items[i])
+ pOut[written++] = i;
+ }
+ }
+ }
+ return written;
+ }
+
+ case LB_SELITEMRANGE:
+ {
+ int first = SignedLowWord(lParam);
+ int last = SignedHighWord(lParam);
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ if (first < 0 || last < first)
+ return LB_ERR;
+
+ if (last >= itemCount)
+ last = itemCount - 1;
+
+ if (!data.AsListBox().SelectionStates())
+ data.AsListBox().SelectionStates() = CreateIntArray();
+
+ if (data.AsListBox().SelectionStates())
+ {
+ EnsureIntArraySize(*data.AsListBox().SelectionStates(), last + 1, 0);
+ for (int i = first; i <= last; ++i)
+ data.AsListBox().SelectionStates()->Items[i] = wParam ? 1 : 0;
+ }
+
+ NotifyListBoxSelectionChanged(hWnd);
+ return finish(0);
+ }
+
+ case LB_SETCURSEL:
+ {
+ int selection = static_cast(wParam);
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ if (selection >= -1 && selection < itemCount)
+ {
+ if (data.AsListBox().CurrentSelection() != -1)
+ setSelection(data.AsListBox().CurrentSelection(), 0);
+
+ data.AsListBox().CurrentSelection() = selection;
+ if (selection != -1)
+ setSelection(selection, 1);
+ }
+
+ NotifyListBoxSelectionChanged(hWnd);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return finish(0);
+ }
+
+ case LB_GETCURSEL:
+ return data.AsListBox().CurrentSelection();
+
+ case LB_GETTOPINDEX:
+ return data.AsListBox().TopIndex();
+
+ case LB_SETTOPINDEX:
+ {
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1);
+ const int visibleItems = (clientRect.bottom - clientRect.top) / itemHeight;
+ int topIndex = static_cast(wParam);
+ if (topIndex < 0)
+ topIndex = 0;
+
+ if (itemCount - visibleItems <= 0)
+ topIndex = 0;
+ else if (topIndex > itemCount - visibleItems)
+ topIndex = itemCount - visibleItems;
+
+ if (topIndex != data.AsListBox().TopIndex())
+ {
+ data.AsListBox().TopIndex() = topIndex;
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ }
+
+ return finish(0);
+ }
+
+ case LB_GETITEMRECT:
+ {
+ const int index = static_cast(wParam);
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1);
+ const int visibleIndex = index - data.AsListBox().TopIndex();
+
+ if (index < data.AsListBox().TopIndex() || index >= itemCount || visibleIndex > (clientRect.bottom - clientRect.top) / itemHeight)
+ return LB_ERR;
+
+ if (auto pRect = reinterpret_cast(lParam))
+ {
+ pRect->left = clientRect.left;
+ pRect->top = visibleIndex * itemHeight;
+ pRect->right = clientRect.right - clientRect.left;
+ pRect->bottom = pRect->top + itemHeight;
+ }
+ return 0;
+ }
+
+ case LB_GETITEMDATA:
+ case LB_SETITEMDATA:
+ case CB_GETITEMDATA:
+ case CB_SETITEMDATA:
+ {
+ const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, static_cast(wParam));
+ if (!pEntry)
+ return LB_ERR;
+
+ if (message == LB_GETITEMDATA || message == CB_GETITEMDATA)
+ return pEntry->ItemData;
+
+ pEntry->ItemData = static_cast(lParam);
+ return reinterpret_cast(pEntry);
+ }
+
+ case LB_DELETESTRING:
+ {
+ const int index = static_cast(wParam);
+ const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, index);
+ RemoveListBoxRow(data, index);
+ const auto result = CallSelectedHandler(pOriginalWndProc, hWnd, LB_DELETESTRING, wParam, lParam);
+ RemoveListBoxTextEntry(data, pEntry);
+ return finish(result);
+ }
+
+ case LB_RESETCONTENT:
+ ClearListBoxRows(data, false);
+ ClearListBoxTextEntries(data);
+ NotifyListBoxSelectionChanged(hWnd);
+ return finish(CallSelectedHandler(pOriginalWndProc, hWnd, LB_RESETCONTENT, wParam, lParam));
+
+ case WM_NCDESTROY:
+ ClearListBoxRows(data, true);
+ ClearListBoxTextEntries(data);
+ return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam);
+
+ case LB_GETTEXTLEN:
+ case WW_GETTEXTW:
+ case WW_GETTEXTA:
+ case WW_LB_GETTEXTW:
+ case WW_LB_GETTEXTA:
+ case WW_LB_GETITEMTEXTFORMAT:
+ {
+ const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, static_cast(wParam));
+ if (!pEntry)
+ return LB_ERR;
+
+ if (message == WW_LB_GETITEMTEXTFORMAT)
+ return pEntry->IsWide;
+
+ const wchar_t* pText = pEntry->Text ? pEntry->Text : L"";
+ const auto length = static_cast(std::wcslen(pText));
+ if (message == LB_GETTEXTLEN)
+ return length;
+
+ if (lParam)
+ {
+ if (message == WW_GETTEXTA || message == WW_LB_GETTEXTA)
+ WideToCharString(reinterpret_cast(lParam), static_cast(length + 1), pText);
+ else
+ std::wcscpy(reinterpret_cast(lParam), pText);
+ }
+ return length;
+ }
+
+ case WW_LB_FINDSTRINGA:
+ return findString(false, false, false);
+
+ case WW_LB_FINDSTRINGEXACTA:
+ return findString(false, true, false);
+
+ case WW_LB_SELECTSTRINGA:
+ return findString(false, false, true);
+
+ case WW_LB_FINDSTRINGW:
+ return findString(true, false, false);
+
+ case WW_LB_FINDSTRINGEXACTW:
+ return findString(true, true, false);
+
+ case WW_LB_SELECTSTRINGW:
+ return findString(true, false, true);
+
+ case WW_LB_INSERTSTRINGA:
+ return finish(addOrInsertString(false, true));
+
+ case WW_LB_ADDSTRINGA:
+ return finish(addOrInsertString(false, false));
+
+ case WW_LB_INSERTSTRINGW:
+ return finish(addOrInsertString(true, true));
+
+ case WW_LB_ADDSTRINGW:
+ return finish(addOrInsertString(true, false));
+
+ case WW_QUERYTOOLTIPHIT:
+ {
+ const POINT point = RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam);
+ const int x = point.x;
+ const int y = point.y;
+ if (x >= 0 && y >= 0 && x < clientRect.right && y < clientRect.bottom)
+ {
+ const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1);
+ const int itemIndex = data.AsListBox().TopIndex() + y / itemHeight;
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ if (itemIndex >= 0 && itemIndex < itemCount)
+ return itemIndex;
+ }
+ return -1;
+ }
+
+ case WW_LB_GETSCROLLBARHWND:
+ return reinterpret_cast(data.AsListBox().ScrollBarHwnd());
+
+ case WW_LB_ADDCOLUMN:
+ {
+ if (!data.AsListBox().Columns())
+ data.AsListBox().Columns() = CreateListBoxColumnArray();
+
+ auto pColumns = data.AsListBox().Columns();
+ if (!pColumns)
+ return -1;
+
+ const int x = static_cast(lParam);
+ if (FindListBoxColumn(pColumns, x))
+ return x;
+
+ if (pColumns->Count >= pColumns->Capacity)
+ ResizeListBoxColumnStorage(*pColumns, std::max(pColumns->Capacity * 2, 10));
+
+ auto& column = pColumns->Items[pColumns->Count++];
+ column = {};
+ column.X = x;
+ column.Width = static_cast(wParam);
+ return x;
+ }
+
+ case WW_LB_REMOVECOLUMN:
+ {
+ auto pColumns = data.AsListBox().Columns();
+ if (!pColumns)
+ return -1;
+
+ const int x = static_cast(lParam);
+ for (int i = 0; i < pColumns->Count; ++i)
+ {
+ if (pColumns->Items[i].X != x)
+ continue;
+
+ ClearListBoxColumnCells(pColumns->Items[i], true);
+ if (i < pColumns->Count - 1)
+ {
+ std::memmove(
+ &pColumns->Items[i],
+ &pColumns->Items[i + 1],
+ sizeof(WWUIListBoxColumn) * (pColumns->Count - i - 1));
+ }
+ --pColumns->Count;
+ std::memset(&pColumns->Items[pColumns->Count], 0, sizeof(WWUIListBoxColumn));
+ return x;
+ }
+ return -1;
+ }
+
+ case WW_LB_SETCELLTEXT:
+ {
+ auto pColumns = data.AsListBox().Columns();
+ const int columnX = LOWORD(wParam);
+ const int rowIndex = HIWORD(wParam);
+ auto pColumn = FindListBoxColumn(pColumns, columnX);
+
+ if (!pColumn || rowIndex >= ::SendMessageA(hWnd, LB_GETCOUNT, 0, 0))
+ return -1;
+
+ const auto defaultFormat = pColumn == &pColumns->Items[0]
+ ? WWUIListBoxCellFormat::ItemText
+ : WWUIListBoxCellFormat::Empty;
+
+ EnsureListBoxCellCount(*pColumn, rowIndex + 1, defaultFormat);
+ auto& target = pColumn->Cells[rowIndex];
+ ResetListBoxCell(target);
+
+ if (const auto pSource = reinterpret_cast(lParam))
+ target = *pSource;
+
+ return columnX;
+ }
+
+ case WW_LB_GETCELLTEXT:
+ {
+ const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0));
+ const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1);
+ const int rowIndex = data.AsListBox().TopIndex() + SignedHighWord(wParam) / itemHeight;
+ const int x = SignedLowWord(wParam);
+ auto pColumn = FindListBoxColumnAtX(data.AsListBox().Columns(), x);
+ if (!pColumn || rowIndex < 0 || rowIndex >= itemCount || rowIndex >= pColumn->CellCount)
+ return 0;
+
+ const auto& text = pColumn->Cells[rowIndex].SecondaryText;
+ if (lParam)
+ std::wcscpy(reinterpret_cast(lParam), GetWideTextBuffer(text));
+
+ return IsEmpty(text) ? 1 : 0;
+ }
+
+ case WW_INITDIALOG:
+ {
+ data.AsListBox().CurrentSelection() = -1;
+
+ int fontHeight = 10;
+ if (const auto pFont = data.AsListBox().Font() ? data.AsListBox().Font() : BitFont::Instance)
+ {
+ if (pFont->InternalPTR)
+ fontHeight = pFont->InternalPTR->FontHeight;
+ }
+
+ ::SendMessageA(hWnd, LB_SETITEMHEIGHT, static_cast(-1), LOWORD(fontHeight + 2));
+ return finish(0);
+ }
+
+ case WW_SETCOLOR:
+ SetIntArrayValue(data.AsListBox().ItemData(), static_cast(wParam), static_cast(lParam), -1);
+ ::InvalidateRect(hWnd, nullptr, FALSE);
+ return finish(0);
+
+ default:
+ break;
+ }
+
+ return finish(forwardOriginal());
+}
diff --git a/src/OwnerDraw/OwnerDraw.Hooks.cpp b/src/OwnerDraw/OwnerDraw.Hooks.cpp
new file mode 100644
index 0000000000..b0239cbf3f
--- /dev/null
+++ b/src/OwnerDraw/OwnerDraw.Hooks.cpp
@@ -0,0 +1,23 @@
+#include
+
+#include "OwnerDraw.h"
+
+DEFINE_PATCH_TYPED(void*, 0x60FF06, WWUI::OwnerDrawWindowProc);
+DEFINE_FUNCTION_JUMP(LJMP, 0x610CA0, WWUI::OwnerDrawWindowProc);
+DEFINE_FUNCTION_JUMP(LJMP, 0x612B70, WWUI::OwnerDrawCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x6163A0, WWUI::CheckboxCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x616980, WWUI::RadioCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x6137D0, WWUI::TabCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x614190, WWUI::EditCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x614B30, WWUI::NewEditCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x6153E0, WWUI::StaticCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x612A60, WWUI::SysListViewCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x617250, WWUI::ComboBoxCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x618D40, WWUI::ListBoxCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x61D950, WWUI::SliderCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x61E700, WWUI::GroupBoxCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x61ECA0, WWUI::InputCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x61D6D0, WWUI::ProgressCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x61C690, WWUI::ScrollBarCtrl);
+DEFINE_FUNCTION_JUMP(LJMP, 0x622820, WWUI::RegisterOwnerDrawWindow);
+DEFINE_FUNCTION_JUMP(LJMP, 0x622B50, WWUI::OwnerDrawStandardWndProc);
diff --git a/src/OwnerDraw/OwnerDraw.Internal.h b/src/OwnerDraw/OwnerDraw.Internal.h
new file mode 100644
index 0000000000..1f5326f5e0
--- /dev/null
+++ b/src/OwnerDraw/OwnerDraw.Internal.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include "OwnerDraw.h"
+
+#include "../Render/Functions.h"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include