major: initial release

* initial development

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* pokemon

* pokemon

* pokemon

* pokemon

* pokemon

* pokemon

* improvements

* improvements

* ci/cd

* ci/cd

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

---------

Co-authored-by: sdine <sdine@sdine.com>
This commit is contained in:
Sebastian Dine
2026-05-09 11:05:47 +02:00
committed by GitHub
parent 13262fa015
commit 55ace147bc
149 changed files with 12611 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
#include "ccm/ui/IconListCtrl.hpp"
#include <wx/image.h>
#ifdef __WXMSW__
#include <windows.h>
#include <commctrl.h>
#endif
namespace ccm::ui {
#ifdef __WXMSW__
namespace {
// Convert a `wxBitmap` to a fresh 32 bpp BGRA DIB section with PREMULTIPLIED
// alpha, suitable for use as the source bitmap in a `AlphaBlend` call with
// `AC_SRC_OVER | AC_SRC_ALPHA`.
//
// Going through `wxImage` gives us a known-good straight-RGBA payload
// regardless of how the source `wxBitmap` was originally constructed
// (notably bitmaps from `wxBitmapBundle::FromSVG`). We then premultiply
// once, rounded.
//
// Math notes:
// - The rounded form `(c * a + 127) / 255` is required. Plain `c * a` (no
// divide) overflows the byte and pushes every channel toward 0xFF — the
// "white icons" regression an earlier dev ran into when they tried to
// premultiply manually. `(c * a) / 255` is also wrong for `c=a=0xFF`
// (rounds to 254 instead of 255 and creates 1-bit dimming on opaque
// pixels). The +127 form is the standard premultiply rounding.
HBITMAP makePremultipliedDib(const wxBitmap& bmp) {
if (!bmp.IsOk()) return NULL;
wxImage img = bmp.ConvertToImage();
if (!img.IsOk()) return NULL;
if (!img.HasAlpha()) img.InitAlpha();
const int w = img.GetWidth();
const int h = img.GetHeight();
if (w <= 0 || h <= 0) return NULL;
BITMAPINFO bi{};
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bi.bmiHeader.biWidth = w;
bi.bmiHeader.biHeight = -h; // top-down
bi.bmiHeader.biPlanes = 1;
bi.bmiHeader.biBitCount = 32;
bi.bmiHeader.biCompression = BI_RGB;
HDC screenDc = ::GetDC(NULL);
void* dibPixels = nullptr;
HBITMAP dib = ::CreateDIBSection(screenDc, &bi, DIB_RGB_COLORS,
&dibPixels, NULL, 0);
::ReleaseDC(NULL, screenDc);
if (dib == NULL || dibPixels == nullptr) {
if (dib != NULL) ::DeleteObject(dib);
return NULL;
}
const unsigned char* rgb = img.GetData();
const unsigned char* alpha = img.GetAlpha();
auto* dest = static_cast<unsigned char*>(dibPixels);
const int pixels = w * h;
const auto premul = [](unsigned c, unsigned a) -> unsigned char {
return static_cast<unsigned char>((c * a + 127) / 255);
};
for (int p = 0; p < pixels; ++p) {
const unsigned char r = rgb[p * 3 + 0];
const unsigned char g = rgb[p * 3 + 1];
const unsigned char b = rgb[p * 3 + 2];
const unsigned char a = (alpha != nullptr) ? alpha[p] : 255;
dest[p * 4 + 0] = premul(b, a);
dest[p * 4 + 1] = premul(g, a);
dest[p * 4 + 2] = premul(r, a);
dest[p * 4 + 3] = a;
}
return dib;
}
} // namespace
IconListCtrl::~IconListCtrl() {
destroyDibCache();
}
void IconListCtrl::setIconBitmaps(std::vector<wxBitmap> normal,
std::vector<wxBitmap> selected) {
normalBmps_ = std::move(normal);
selectedBmps_ = std::move(selected);
rebuildDibCache();
}
void IconListCtrl::destroyDibCache() {
for (void* p : dibBitmaps_) {
if (p != nullptr) ::DeleteObject(static_cast<HBITMAP>(p));
}
dibBitmaps_.clear();
dibWidth_ = 0;
dibHeight_ = 0;
}
void IconListCtrl::rebuildDibCache() {
destroyDibCache();
if (normalBmps_.empty() || normalBmps_.size() != selectedBmps_.size()) {
return;
}
dibWidth_ = normalBmps_.front().IsOk() ? normalBmps_.front().GetWidth() : 0;
dibHeight_ = normalBmps_.front().IsOk() ? normalBmps_.front().GetHeight() : 0;
if (dibWidth_ <= 0 || dibHeight_ <= 0) return;
dibBitmaps_.reserve(normalBmps_.size() + selectedBmps_.size());
for (const auto& b : normalBmps_) {
dibBitmaps_.push_back(static_cast<void*>(makePremultipliedDib(b)));
}
for (const auto& b : selectedBmps_) {
dibBitmaps_.push_back(static_cast<void*>(makePremultipliedDib(b)));
}
}
bool IconListCtrl::MSWOnNotify(int idCtrl, WXLPARAM lParam, WXLPARAM* result) {
auto* hdr = reinterpret_cast<NMHDR*>(lParam);
if (hdr != nullptr && hdr->code == NM_CUSTOMDRAW) {
auto* cd = reinterpret_cast<NMLVCUSTOMDRAW*>(lParam);
switch (cd->nmcd.dwDrawStage) {
case CDDS_PREPAINT:
*result = CDRF_NOTIFYITEMDRAW;
return true;
case CDDS_ITEMPREPAINT:
*result = CDRF_NOTIFYSUBITEMDRAW;
return true;
case CDDS_SUBITEM | CDDS_ITEMPREPAINT: {
const int col = cd->iSubItem;
if (col >= firstIconCol_ && col < firstIconCol_ + iconColCount_) {
// Let the default first paint background/selection, then we
// overlay the centered icon in POSTPAINT.
*result = CDRF_NOTIFYPOSTPAINT;
return true;
}
*result = CDRF_DODEFAULT;
return true;
}
case CDDS_SUBITEM | CDDS_ITEMPOSTPAINT: {
const int col = cd->iSubItem;
if (col < firstIconCol_ || col >= firstIconCol_ + iconColCount_) {
*result = CDRF_DODEFAULT;
return true;
}
if (!predicate_ || dibBitmaps_.empty()) {
*result = CDRF_DODEFAULT;
return true;
}
const long row = static_cast<long>(cd->nmcd.dwItemSpec);
const int iconIdx = col - firstIconCol_;
if (iconIdx < 0 || iconIdx >= iconColCount_) {
*result = CDRF_DODEFAULT;
return true;
}
if (!predicate_(row, iconIdx)) {
*result = CDRF_DODEFAULT;
return true;
}
const HWND lcHwnd = reinterpret_cast<HWND>(GetHandle());
// `cd->nmcd.uItemState & CDIS_SELECTED` is unreliable in the
// CDDS_SUBITEM | CDDS_ITEMPOSTPAINT stage on Windows — comctl32
// does not always propagate the item's CDIS_* flags down into
// sub-item draw stages, so we'd silently fall back to the normal
// (dark) variant on selected rows. Query LVIS_SELECTED directly
// off the listview, which is always accurate.
const UINT lvState = ListView_GetItemState(lcHwnd, row, LVIS_SELECTED);
const bool selected = (lvState & LVIS_SELECTED) != 0;
// Sub-item bounds in client coords. Initialize the request as
// documented for LVM_GETSUBITEMRECT: rc.top = sub-item index,
// rc.left = which rect (LVIR_BOUNDS).
RECT rc;
rc.top = col;
rc.left = LVIR_BOUNDS;
::SendMessageW(lcHwnd,
LVM_GETSUBITEMRECT,
static_cast<WPARAM>(row),
reinterpret_cast<LPARAM>(&rc));
const int cellCx = (rc.left + rc.right) / 2;
const int cellCy = (rc.top + rc.bottom) / 2;
const int x = cellCx - dibWidth_ / 2;
const int y = cellCy - dibHeight_ / 2;
const std::size_t imgIdx =
static_cast<std::size_t>(iconIdx) +
(selected ? static_cast<std::size_t>(iconColCount_) : 0);
if (imgIdx >= dibBitmaps_.size() || dibBitmaps_[imgIdx] == nullptr) {
*result = CDRF_DODEFAULT;
return true;
}
HBITMAP src = static_cast<HBITMAP>(dibBitmaps_[imgIdx]);
HDC dstDc = cd->nmcd.hdc;
HDC memDc = ::CreateCompatibleDC(dstDc);
HGDIOBJ oldBmp = ::SelectObject(memDc, src);
BLENDFUNCTION bf{};
bf.BlendOp = AC_SRC_OVER;
bf.BlendFlags = 0;
bf.SourceConstantAlpha = 0xFF;
bf.AlphaFormat = AC_SRC_ALPHA;
::AlphaBlend(dstDc, x, y, dibWidth_, dibHeight_,
memDc, 0, 0, dibWidth_, dibHeight_, bf);
::SelectObject(memDc, oldBmp);
::DeleteDC(memDc);
*result = CDRF_DODEFAULT;
return true;
}
default:
break;
}
}
return wxListCtrl::MSWOnNotify(idCtrl, lParam, result);
}
#else // !__WXMSW__
IconListCtrl::~IconListCtrl() = default;
void IconListCtrl::setIconBitmaps(std::vector<wxBitmap> normal,
std::vector<wxBitmap> selected) {
normalBmps_ = std::move(normal);
selectedBmps_ = std::move(selected);
}
void IconListCtrl::destroyDibCache() {}
void IconListCtrl::rebuildDibCache() {}
#endif // __WXMSW__
} // namespace ccm::ui