aboutsummaryrefslogtreecommitdiff
path: root/color-picker.c
diff options
context:
space:
mode:
Diffstat (limited to 'color-picker.c')
-rw-r--r--color-picker.c632
1 files changed, 632 insertions, 0 deletions
diff --git a/color-picker.c b/color-picker.c
new file mode 100644
index 0000000..a15b850
--- /dev/null
+++ b/color-picker.c
@@ -0,0 +1,632 @@
+// Tray Color Picker - single-file Win32 app
+#define UNICODE
+#define _UNICODE
+#include <windows.h>
+#include <shellapi.h>
+#include <stdio.h>
+#include <math.h>
+
+#define WM_TRAYICON (WM_APP + 1)
+#define ID_TRAY 1
+#define IDM_PICK 100
+#define IDM_EXIT 101
+#define IDM_CLEARHIST 102
+#define IDM_HISTORY0 200 // 200..219
+#define HISTORY_CAP 20 // absolute max storage
+// settings menu ids
+#define IDM_ZOOM_BASE 300
+#define IDM_CELL_BASE 320
+#define IDM_INFO_HEX 340
+#define IDM_INFO_RGB 341
+#define IDM_INFO_BOTH 342
+#define IDM_GRID 350
+#define IDM_COPY_HEX 360 // "#RRGGBB"
+#define IDM_COPY_RGB 361 // "rgb(r, g, b)"
+#define IDM_COPY_RAW 362 // "RRGGBB"
+#define IDM_COPY_NONE 363 // don't copy
+#define IDM_HSIZE_BASE 380 // + index into HSIZES
+
+static const int ZOOMS[] = { 4, 8, 12, 16 };
+static const int CELLS[] = { 11, 15, 21, 27 };
+static const int HSIZES[] = { 0, 5, 10, 20 };
+#define NZOOMS 4
+#define NCELLS 4
+#define NHSIZES 4
+
+// ---- settings (persisted) ----
+static int g_zoom = 8;
+static int g_cells = 15;
+static int g_infoMode = 2; // 0=hex, 1=rgb, 2=both
+static BOOL g_grid = FALSE;
+static int g_copyFmt = 0; // 0=#RRGGBB, 1=rgb(), 2=RRGGBB, 3=none
+static int g_histSize = 10; // 0, 5, 10, 20
+
+static HWND g_hwndMain, g_hwndLoupe;
+static HHOOK g_mouseHook, g_kbdHook;
+static BOOL g_picking = FALSE;
+static COLORREF g_history[HISTORY_CAP];
+static int g_historyCount = 0;
+static NOTIFYICONDATA g_nid = {0};
+static HDC g_screenDC;
+static HFONT g_fontBig, g_fontSmall, g_fontMenu;
+
+// ---------------- settings persistence ----------------
+static void RegGetInt(HKEY k, const wchar_t *name, int *out) {
+ DWORD v, sz = sizeof(DWORD);
+ if (RegQueryValueEx(k, name, NULL, NULL, (BYTE*)&v, &sz) == ERROR_SUCCESS) *out = (int)v;
+}
+
+static void LoadSettings(void) {
+ HKEY k;
+ if (RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\TrayColorPicker", 0, KEY_READ, &k) != ERROR_SUCCESS) return;
+ int g = g_grid;
+ RegGetInt(k, L"Zoom", &g_zoom);
+ RegGetInt(k, L"Cells", &g_cells);
+ RegGetInt(k, L"InfoMode", &g_infoMode);
+ RegGetInt(k, L"Grid", &g); g_grid = (BOOL)g;
+ RegGetInt(k, L"CopyFmt", &g_copyFmt);
+ RegGetInt(k, L"HistSize", &g_histSize);
+ if (g_histSize < 0 || g_histSize > HISTORY_CAP) g_histSize = 10;
+ RegCloseKey(k);
+}
+
+static void SaveSettings(void) {
+ HKEY k;
+ if (RegCreateKeyEx(HKEY_CURRENT_USER, L"Software\\TrayColorPicker", 0, NULL, 0,
+ KEY_WRITE, NULL, &k, NULL) != ERROR_SUCCESS) return;
+ DWORD v;
+ v = g_zoom; RegSetValueEx(k, L"Zoom", 0, REG_DWORD, (BYTE*)&v, sizeof(v));
+ v = g_cells; RegSetValueEx(k, L"Cells", 0, REG_DWORD, (BYTE*)&v, sizeof(v));
+ v = g_infoMode; RegSetValueEx(k, L"InfoMode", 0, REG_DWORD, (BYTE*)&v, sizeof(v));
+ v = g_grid; RegSetValueEx(k, L"Grid", 0, REG_DWORD, (BYTE*)&v, sizeof(v));
+ v = g_copyFmt; RegSetValueEx(k, L"CopyFmt", 0, REG_DWORD, (BYTE*)&v, sizeof(v));
+ v = g_histSize; RegSetValueEx(k, L"HistSize", 0, REG_DWORD, (BYTE*)&v, sizeof(v));
+ RegCloseKey(k);
+}
+
+// ---------------- geometry ----------------
+static int LoupePx(void) { return g_zoom * g_cells; }
+static int InfoH(void) { return (g_infoMode == 2) ? 46 : 30; }
+static int LoupeW(void) { int w = LoupePx() + 2; return w < 168 ? 168 : w; }
+static int LoupeH(void) { return LoupePx() + InfoH() + 2; }
+
+// ---------------- rainbow icon (in-memory + .ico export) ----------------
+static void HsvToRgb(double h, double s, double v, BYTE *r, BYTE *g, BYTE *b) {
+ double c = v * s, hp = h / 60.0;
+ double x = c * (1.0 - fabs(fmod(hp, 2.0) - 1.0));
+ double rr = 0, gg = 0, bb = 0;
+ if (hp < 1) { rr = c; gg = x; }
+ else if (hp < 2) { rr = x; gg = c; }
+ else if (hp < 3) { gg = c; bb = x; }
+ else if (hp < 4) { gg = x; bb = c; }
+ else if (hp < 5) { rr = x; bb = c; }
+ else { rr = c; bb = x; }
+ double m = v - c;
+ *r = (BYTE)((rr + m) * 255.0); *g = (BYTE)((gg + m) * 255.0); *b = (BYTE)((bb + m) * 255.0);
+}
+
+// fill a top-down 32bpp ARGB pixel buffer with the rainbow wheel
+static void FillRainbowPixels(DWORD *px, int size) {
+ double cx = (size - 1) / 2.0, cy = (size - 1) / 2.0;
+ double radius = size / 2.0 - 1.0;
+ for (int y = 0; y < size; y++) {
+ for (int x = 0; x < size; x++) {
+ double dx = x - cx, dy = y - cy;
+ double dist = sqrt(dx*dx + dy*dy);
+ DWORD out = 0;
+ if (dist <= radius + 0.5) {
+ double hue = fmod(atan2(dy, dx) * 180.0 / 3.14159265358979 + 360.0, 360.0);
+ double sat = dist / radius; if (sat > 1.0) sat = 1.0;
+ BYTE r, g, b;
+ HsvToRgb(hue, sat, 1.0, &r, &g, &b);
+ double a = (radius + 0.5 - dist); if (a > 1.0) a = 1.0; if (a < 0.0) a = 0.0;
+ BYTE alpha = (BYTE)(a * 255.0);
+ if (alpha < 255) { r = (BYTE)(r * a); g = (BYTE)(g * a); b = (BYTE)(b * a); }
+ out = ((DWORD)alpha << 24) | ((DWORD)r << 16) | ((DWORD)g << 8) | b;
+ }
+ px[y * size + x] = out;
+ }
+ }
+}
+
+static HICON CreateRainbowIcon(int size) {
+ BITMAPV5HEADER bi = {0};
+ bi.bV5Size = sizeof(bi);
+ bi.bV5Width = size; bi.bV5Height = -size;
+ bi.bV5Planes = 1; bi.bV5BitCount = 32;
+ bi.bV5Compression = BI_BITFIELDS;
+ bi.bV5RedMask = 0x00FF0000; bi.bV5GreenMask = 0x0000FF00;
+ bi.bV5BlueMask = 0x000000FF; bi.bV5AlphaMask = 0xFF000000;
+
+ void *bits = NULL;
+ HDC hdc = GetDC(NULL);
+ HBITMAP color = CreateDIBSection(hdc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, &bits, NULL, 0);
+ ReleaseDC(NULL, hdc);
+ if (!color) return NULL;
+ FillRainbowPixels((DWORD*)bits, size);
+
+ HBITMAP mask = CreateBitmap(size, size, 1, 1, NULL);
+ ICONINFO ii = {0};
+ ii.fIcon = TRUE; ii.hbmColor = color; ii.hbmMask = mask;
+ HICON icon = CreateIconIndirect(&ii);
+ DeleteObject(color); DeleteObject(mask);
+ return icon;
+}
+
+// write rainbow.ico with 16/32/48/256 px images (for embedding via windres)
+static BOOL WriteRainbowIco(const wchar_t *path) {
+ static const int sizes[] = { 16, 32, 48, 256 };
+ const int n = 4;
+ FILE *f = _wfopen(path, L"wb");
+ if (!f) return FALSE;
+
+ // ICONDIR
+ WORD zero = 0, one = 1, count = (WORD)n;
+ fwrite(&zero, 2, 1, f); fwrite(&one, 2, 1, f); fwrite(&count, 2, 1, f);
+
+ DWORD offset = 6 + 16 * n;
+ // write directory entries
+ for (int i = 0; i < n; i++) {
+ int s = sizes[i];
+ DWORD imgSize = 40 + s*s*4 + ((s+31)/32)*4 * s; // BITMAPINFOHEADER + XOR + AND mask
+ BYTE w = (s == 256) ? 0 : (BYTE)s, h = w;
+ BYTE colors = 0, res = 0;
+ WORD planes = 1, bpp = 32;
+ fwrite(&w, 1, 1, f); fwrite(&h, 1, 1, f);
+ fwrite(&colors, 1, 1, f); fwrite(&res, 1, 1, f);
+ fwrite(&planes, 2, 1, f); fwrite(&bpp, 2, 1, f);
+ fwrite(&imgSize, 4, 1, f); fwrite(&offset, 4, 1, f);
+ offset += imgSize;
+ }
+ // write images
+ for (int i = 0; i < n; i++) {
+ int s = sizes[i];
+ BITMAPINFOHEADER bih = {0};
+ bih.biSize = 40; bih.biWidth = s; bih.biHeight = s * 2; // *2: XOR+AND convention
+ bih.biPlanes = 1; bih.biBitCount = 32; bih.biCompression = BI_RGB;
+ fwrite(&bih, 40, 1, f);
+
+ DWORD *px = (DWORD*)malloc(s * s * 4);
+ if (!px) { fclose(f); return FALSE; }
+ FillRainbowPixels(px, s);
+ // ICO stores rows bottom-up
+ for (int y = s - 1; y >= 0; y--) fwrite(&px[y * s], 4, s, f);
+ free(px);
+ // AND mask: all zero (alpha channel handles transparency)
+ int maskRow = ((s + 31) / 32) * 4;
+ BYTE *zeros = (BYTE*)calloc(maskRow, 1);
+ for (int y = 0; y < s; y++) fwrite(zeros, 1, maskRow, f);
+ free(zeros);
+ }
+ fclose(f);
+ return TRUE;
+}
+
+// ---------------- clipboard / notifications ----------------
+static void FormatColor(COLORREF c, wchar_t *buf, int n) {
+ if (g_copyFmt == 1) swprintf(buf, n, L"rgb(%d, %d, %d)", GetRValue(c), GetGValue(c), GetBValue(c));
+ else if (g_copyFmt == 2) swprintf(buf, n, L"%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c));
+ else swprintf(buf, n, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c));
+}
+
+static void CopyColorToClipboard(COLORREF c, HWND owner) {
+ if (g_copyFmt == 3) return; // None
+ wchar_t buf[32];
+ FormatColor(c, buf, 32);
+ if (OpenClipboard(owner)) {
+ EmptyClipboard();
+ size_t bytes = (wcslen(buf) + 1) * sizeof(wchar_t);
+ HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, bytes);
+ if (h) {
+ memcpy(GlobalLock(h), buf, bytes);
+ GlobalUnlock(h);
+ SetClipboardData(CF_UNICODETEXT, h);
+ }
+ CloseClipboard();
+ }
+}
+
+static void ShowBalloon(COLORREF c) {
+ wchar_t fmt[32];
+ swprintf(fmt, 32, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c));
+ if (g_copyFmt == 3)
+ swprintf(g_nid.szInfo, 256, L"%s (RGB %d, %d, %d)",
+ fmt, GetRValue(c), GetGValue(c), GetBValue(c));
+ else {
+ wchar_t copied[32]; FormatColor(c, copied, 32);
+ swprintf(g_nid.szInfo, 256, L"%s copied to clipboard (RGB %d, %d, %d)",
+ copied, GetRValue(c), GetGValue(c), GetBValue(c));
+ }
+ wcscpy(g_nid.szInfoTitle, L"Color picked");
+ g_nid.uFlags = NIF_INFO;
+ g_nid.dwInfoFlags = NIIF_INFO | NIIF_NOSOUND;
+ Shell_NotifyIcon(NIM_MODIFY, &g_nid);
+}
+
+static void AddHistory(COLORREF c) {
+ if (g_histSize == 0) return;
+ for (int i = g_histSize - 1; i > 0; i--) g_history[i] = g_history[i-1];
+ g_history[0] = c; // newest at top
+ if (g_historyCount < g_histSize) g_historyCount++;
+}
+
+// ---------------- picking ----------------
+static void PositionLoupe(POINT pt) {
+ int w = LoupeW(), h = LoupeH();
+ int x = pt.x + 24, y = pt.y + 24;
+ RECT wa; SystemParametersInfo(SPI_GETWORKAREA, 0, &wa, 0);
+ if (x + w > wa.right) x = pt.x - w - 24;
+ if (y + h > wa.bottom) y = pt.y - h - 24;
+ SetWindowPos(g_hwndLoupe, HWND_TOPMOST, x, y, w, h, SWP_NOACTIVATE | SWP_SHOWWINDOW);
+ InvalidateRect(g_hwndLoupe, NULL, FALSE);
+}
+
+static void StopPicking(void) {
+ if (!g_picking) return;
+ g_picking = FALSE;
+ if (g_mouseHook) { UnhookWindowsHookEx(g_mouseHook); g_mouseHook = NULL; }
+ if (g_kbdHook) { UnhookWindowsHookEx(g_kbdHook); g_kbdHook = NULL; }
+ ShowWindow(g_hwndLoupe, SW_HIDE);
+}
+
+static void PickColorAt(POINT pt) {
+ COLORREF c = GetPixel(g_screenDC, pt.x, pt.y);
+ StopPicking();
+ AddHistory(c);
+ CopyColorToClipboard(c, g_hwndMain);
+ ShowBalloon(c);
+}
+
+static LRESULT CALLBACK MouseHook(int nCode, WPARAM wParam, LPARAM lParam) {
+ if (nCode == HC_ACTION && g_picking) {
+ MSLLHOOKSTRUCT *m = (MSLLHOOKSTRUCT*)lParam;
+ if (wParam == WM_MOUSEMOVE) {
+ PositionLoupe(m->pt);
+ } else if (wParam == WM_LBUTTONDOWN) {
+ PickColorAt(m->pt);
+ return 1;
+ } else if (wParam == WM_LBUTTONUP) {
+ return 1;
+ } else if (wParam == WM_RBUTTONDOWN || wParam == WM_RBUTTONUP) {
+ StopPicking();
+ return 1;
+ }
+ }
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+}
+
+static LRESULT CALLBACK KbdHook(int nCode, WPARAM wParam, LPARAM lParam) {
+ if (nCode == HC_ACTION && g_picking && wParam == WM_KEYDOWN) {
+ KBDLLHOOKSTRUCT *k = (KBDLLHOOKSTRUCT*)lParam;
+ if (k->vkCode == VK_ESCAPE) { StopPicking(); return 1; }
+ }
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+}
+
+static void StartPicking(void) {
+ if (g_picking) return;
+ g_picking = TRUE;
+ g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHook, GetModuleHandle(NULL), 0);
+ g_kbdHook = SetWindowsHookEx(WH_KEYBOARD_LL, KbdHook, GetModuleHandle(NULL), 0);
+ POINT pt; GetCursorPos(&pt);
+ PositionLoupe(pt);
+}
+
+// ---------------- loupe painting ----------------
+static void PaintLoupe(HWND hwnd) {
+ PAINTSTRUCT ps;
+ HDC hdc = BeginPaint(hwnd, &ps);
+ RECT rc; GetClientRect(hwnd, &rc);
+
+ HDC mem = CreateCompatibleDC(hdc);
+ HBITMAP bmp = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
+ HBITMAP old = SelectObject(mem, bmp);
+
+ HBRUSH dark = CreateSolidBrush(RGB(32,32,32));
+ FillRect(mem, &rc, dark);
+ DeleteObject(dark);
+
+ POINT pt; GetCursorPos(&pt);
+ int half = g_cells / 2;
+ int lp = LoupePx();
+ int ox = (rc.right - lp) / 2;
+
+ SetStretchBltMode(mem, COLORONCOLOR);
+ StretchBlt(mem, ox, 1, lp, lp,
+ g_screenDC, pt.x - half, pt.y - half, g_cells, g_cells, SRCCOPY);
+
+ if (g_grid && g_zoom >= 6) {
+ HPEN gpen = CreatePen(PS_SOLID, 1, RGB(70,70,70));
+ HPEN op = SelectObject(mem, gpen);
+ for (int i = 1; i < g_cells; i++) {
+ MoveToEx(mem, ox + i*g_zoom, 1, NULL); LineTo(mem, ox + i*g_zoom, 1 + lp);
+ MoveToEx(mem, ox, 1 + i*g_zoom, NULL); LineTo(mem, ox + lp, 1 + i*g_zoom);
+ }
+ SelectObject(mem, op); DeleteObject(gpen);
+ }
+
+ COLORREF c = GetPixel(g_screenDC, pt.x, pt.y);
+ HPEN pen = CreatePen(PS_SOLID, 2, RGB(255,60,60));
+ HPEN oldPen = SelectObject(mem, pen);
+ SelectObject(mem, GetStockObject(NULL_BRUSH));
+ Rectangle(mem, ox + half*g_zoom - 1, 1 + half*g_zoom - 1,
+ ox + half*g_zoom + g_zoom + 1, 1 + half*g_zoom + g_zoom + 1);
+ SelectObject(mem, oldPen); DeleteObject(pen);
+
+ int iy = lp + 2;
+ int swSize = InfoH() - 14;
+ HBRUSH sw = CreateSolidBrush(c);
+ RECT swr = { 8, iy + 7, 8 + swSize, iy + 7 + swSize };
+ FillRect(mem, &swr, sw); DeleteObject(sw);
+ FrameRect(mem, &swr, GetStockObject(WHITE_BRUSH));
+
+ wchar_t hex[16], rgb[32];
+ swprintf(hex, 16, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c));
+ swprintf(rgb, 32, L"RGB %d, %d, %d", GetRValue(c), GetGValue(c), GetBValue(c));
+ SetBkMode(mem, TRANSPARENT);
+ int tx = swr.right + 8;
+
+ if (g_infoMode == 2) {
+ SelectObject(mem, g_fontBig);
+ SetTextColor(mem, RGB(255,255,255));
+ TextOut(mem, tx, iy + 5, hex, (int)wcslen(hex));
+ SelectObject(mem, g_fontSmall);
+ SetTextColor(mem, RGB(190,190,190));
+ TextOut(mem, tx, iy + 25, rgb, (int)wcslen(rgb));
+ } else {
+ const wchar_t *s = (g_infoMode == 1) ? rgb : hex;
+ SelectObject(mem, g_fontBig);
+ SetTextColor(mem, RGB(255,255,255));
+ TextOut(mem, tx, iy + (InfoH() - 20) / 2, s, (int)wcslen(s));
+ }
+
+ FrameRect(mem, &rc, GetStockObject(BLACK_BRUSH));
+ BitBlt(hdc, 0, 0, rc.right, rc.bottom, mem, 0, 0, SRCCOPY);
+ SelectObject(mem, old); DeleteObject(bmp); DeleteDC(mem);
+ EndPaint(hwnd, &ps);
+}
+
+// ---------------- menus ----------------
+static void ShowTrayMenu(HWND hwnd) {
+ HMENU settings = CreatePopupMenu();
+
+ HMENU zoomMenu = CreatePopupMenu();
+ for (int i = 0; i < NZOOMS; i++) {
+ wchar_t b[16]; swprintf(b, 16, L"%d\u00D7", ZOOMS[i]);
+ AppendMenu(zoomMenu, MF_STRING | (g_zoom == ZOOMS[i] ? MF_CHECKED : 0), IDM_ZOOM_BASE + i, b);
+ }
+ AppendMenu(settings, MF_POPUP, (UINT_PTR)zoomMenu, L"Magnification");
+
+ HMENU cellMenu = CreatePopupMenu();
+ for (int i = 0; i < NCELLS; i++) {
+ wchar_t b[24]; swprintf(b, 24, L"%d \u00D7 %d pixels", CELLS[i], CELLS[i]);
+ AppendMenu(cellMenu, MF_STRING | (g_cells == CELLS[i] ? MF_CHECKED : 0), IDM_CELL_BASE + i, b);
+ }
+ AppendMenu(settings, MF_POPUP, (UINT_PTR)cellMenu, L"Capture area");
+
+ HMENU infoMenu = CreatePopupMenu();
+ AppendMenu(infoMenu, MF_STRING | (g_infoMode == 0 ? MF_CHECKED : 0), IDM_INFO_HEX, L"Hex only");
+ AppendMenu(infoMenu, MF_STRING | (g_infoMode == 1 ? MF_CHECKED : 0), IDM_INFO_RGB, L"RGB only");
+ AppendMenu(infoMenu, MF_STRING | (g_infoMode == 2 ? MF_CHECKED : 0), IDM_INFO_BOTH, L"Hex + RGB");
+ AppendMenu(settings, MF_POPUP, (UINT_PTR)infoMenu, L"Readout");
+
+ AppendMenu(settings, MF_STRING | (g_grid ? MF_CHECKED : 0), IDM_GRID, L"Pixel grid");
+
+ HMENU copyMenu = CreatePopupMenu();
+ AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 0 ? MF_CHECKED : 0), IDM_COPY_HEX, L"#RRGGBB");
+ AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 2 ? MF_CHECKED : 0), IDM_COPY_RAW, L"RRGGBB");
+ AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 1 ? MF_CHECKED : 0), IDM_COPY_RGB, L"rgb(r, g, b)");
+ AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 3 ? MF_CHECKED : 0), IDM_COPY_NONE, L"None (don't copy)");
+ AppendMenu(settings, MF_POPUP, (UINT_PTR)copyMenu, L"Copy format");
+
+ HMENU histMenu = CreatePopupMenu();
+ for (int i = 0; i < NHSIZES; i++) {
+ wchar_t b[24];
+ if (HSIZES[i] == 0) wcscpy(b, L"Off");
+ else swprintf(b, 24, L"%d colors", HSIZES[i]);
+ AppendMenu(histMenu, MF_STRING | (g_histSize == HSIZES[i] ? MF_CHECKED : 0), IDM_HSIZE_BASE + i, b);
+ }
+ AppendMenu(settings, MF_POPUP, (UINT_PTR)histMenu, L"History size");
+
+ HMENU menu = CreatePopupMenu();
+ AppendMenu(menu, MF_STRING, IDM_PICK, L"Pick Color");
+ AppendMenu(menu, MF_POPUP, (UINT_PTR)settings, L"Settings");
+ if (g_historyCount > 0) {
+ AppendMenu(menu, MF_SEPARATOR, 0, NULL);
+ // non-clickable hint header
+ AppendMenu(menu, MF_STRING | MF_GRAYED | MF_DISABLED, 0, L"Recent colors (newest first)");
+ for (int i = 0; i < g_historyCount; i++) {
+ // owner-drawn: swatch is painted in WM_DRAWITEM; itemData = history index
+ AppendMenu(menu, MF_OWNERDRAW, IDM_HISTORY0 + i, (LPCWSTR)(UINT_PTR)i);
+ }
+ AppendMenu(menu, MF_STRING, IDM_CLEARHIST, L"Clear history");
+ }
+ AppendMenu(menu, MF_SEPARATOR, 0, NULL);
+ AppendMenu(menu, MF_STRING, IDM_EXIT, L"Exit");
+
+ POINT pt; GetCursorPos(&pt);
+ SetForegroundWindow(hwnd);
+ TrackPopupMenu(menu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, NULL);
+ DestroyMenu(menu);
+}
+
+// owner-drawn history rows: [swatch] #RRGGBB (r, g, b)
+#define HITEM_H 24
+#define HITEM_W 190
+
+static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
+ switch (msg) {
+ case WM_TRAYICON:
+ if (lParam == WM_LBUTTONUP) StartPicking();
+ else if (lParam == WM_RBUTTONUP) ShowTrayMenu(hwnd);
+ return 0;
+
+ case WM_MEASUREITEM: {
+ MEASUREITEMSTRUCT *mi = (MEASUREITEMSTRUCT*)lParam;
+ if (mi->CtlType == ODT_MENU) {
+ mi->itemWidth = HITEM_W;
+ mi->itemHeight = HITEM_H;
+ return TRUE;
+ }
+ break;
+ }
+
+ case WM_DRAWITEM: {
+ DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT*)lParam;
+ if (di->CtlType == ODT_MENU) {
+ int idx = (int)(UINT_PTR)di->itemData;
+ if (idx < 0 || idx >= g_historyCount) return TRUE;
+ COLORREF c = g_history[idx];
+ BOOL sel = (di->itemState & ODS_SELECTED) != 0;
+
+ // background
+ HBRUSH bg = CreateSolidBrush(sel ? GetSysColor(COLOR_HIGHLIGHT) : GetSysColor(COLOR_MENU));
+ FillRect(di->hDC, &di->rcItem, bg);
+ DeleteObject(bg);
+
+ // swatch with border
+ RECT swr = { di->rcItem.left + 6, di->rcItem.top + 4,
+ di->rcItem.left + 6 + (HITEM_H - 8), di->rcItem.bottom - 4 };
+ HBRUSH sw = CreateSolidBrush(c);
+ FillRect(di->hDC, &swr, sw);
+ DeleteObject(sw);
+ FrameRect(di->hDC, &swr, (HBRUSH)GetStockObject(GRAY_BRUSH));
+
+ // text
+ wchar_t buf[48];
+ swprintf(buf, 48, L"#%02X%02X%02X (%d, %d, %d)",
+ GetRValue(c), GetGValue(c), GetBValue(c),
+ GetRValue(c), GetGValue(c), GetBValue(c));
+ SetBkMode(di->hDC, TRANSPARENT);
+ SetTextColor(di->hDC, sel ? GetSysColor(COLOR_HIGHLIGHTTEXT) : GetSysColor(COLOR_MENUTEXT));
+ HFONT of = SelectObject(di->hDC, g_fontMenu);
+ RECT tr = di->rcItem;
+ tr.left = swr.right + 8;
+ DrawText(di->hDC, buf, -1, &tr, DT_SINGLELINE | DT_VCENTER | DT_LEFT);
+ SelectObject(di->hDC, of);
+ return TRUE;
+ }
+ break;
+ }
+
+ case WM_COMMAND: {
+ WORD id = LOWORD(wParam);
+ if (id == IDM_PICK) StartPicking();
+ else if (id == IDM_EXIT) DestroyWindow(hwnd);
+ else if (id == IDM_CLEARHIST) g_historyCount = 0;
+ else if (id >= IDM_ZOOM_BASE && id < IDM_ZOOM_BASE + NZOOMS) { g_zoom = ZOOMS[id - IDM_ZOOM_BASE]; SaveSettings(); }
+ else if (id >= IDM_CELL_BASE && id < IDM_CELL_BASE + NCELLS) { g_cells = CELLS[id - IDM_CELL_BASE]; SaveSettings(); }
+ else if (id == IDM_INFO_HEX) { g_infoMode = 0; SaveSettings(); }
+ else if (id == IDM_INFO_RGB) { g_infoMode = 1; SaveSettings(); }
+ else if (id == IDM_INFO_BOTH) { g_infoMode = 2; SaveSettings(); }
+ else if (id == IDM_GRID) { g_grid = !g_grid; SaveSettings(); }
+ else if (id == IDM_COPY_HEX) { g_copyFmt = 0; SaveSettings(); }
+ else if (id == IDM_COPY_RGB) { g_copyFmt = 1; SaveSettings(); }
+ else if (id == IDM_COPY_RAW) { g_copyFmt = 2; SaveSettings(); }
+ else if (id == IDM_COPY_NONE) { g_copyFmt = 3; SaveSettings(); }
+ else if (id >= IDM_HSIZE_BASE && id < IDM_HSIZE_BASE + NHSIZES) {
+ g_histSize = HSIZES[id - IDM_HSIZE_BASE];
+ if (g_historyCount > g_histSize) g_historyCount = g_histSize; // trim
+ SaveSettings();
+ }
+ else if (id >= IDM_HISTORY0 && id < IDM_HISTORY0 + HISTORY_CAP) {
+ COLORREF c = g_history[id - IDM_HISTORY0];
+ if (g_copyFmt == 3) {
+ // Copy format is None; still copy hex on explicit history click
+ wchar_t buf[16];
+ swprintf(buf, 16, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c));
+ if (OpenClipboard(hwnd)) {
+ EmptyClipboard();
+ size_t bytes = (wcslen(buf) + 1) * sizeof(wchar_t);
+ HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, bytes);
+ if (h) { memcpy(GlobalLock(h), buf, bytes); GlobalUnlock(h); SetClipboardData(CF_UNICODETEXT, h); }
+ CloseClipboard();
+ }
+ } else {
+ CopyColorToClipboard(c, hwnd);
+ }
+ ShowBalloon(c);
+ }
+ return 0;
+ }
+
+ case WM_DESTROY:
+ StopPicking();
+ Shell_NotifyIcon(NIM_DELETE, &g_nid);
+ PostQuitMessage(0);
+ return 0;
+ }
+ return DefWindowProc(hwnd, msg, wParam, lParam);
+}
+
+static LRESULT CALLBACK LoupeProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
+ if (msg == WM_PAINT) { PaintLoupe(hwnd); return 0; }
+ return DefWindowProc(hwnd, msg, wParam, lParam);
+}
+
+int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, LPWSTR cmd, int show) {
+ // "--make-ico" mode: write rainbow.ico next to the exe and exit
+ if (cmd && wcsstr(cmd, L"--make-ico")) {
+ if (WriteRainbowIco(L"rainbow.ico"))
+ MessageBox(NULL, L"rainbow.ico written to the current folder.", L"Color Picker", MB_OK | MB_ICONINFORMATION);
+ else
+ MessageBox(NULL, L"Failed to write rainbow.ico.", L"Color Picker", MB_OK | MB_ICONERROR);
+ return 0;
+ }
+
+ SetProcessDPIAware();
+ LoadSettings();
+ g_screenDC = GetDC(NULL);
+
+ g_fontBig = CreateFont(-15, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0, DEFAULT_CHARSET,
+ OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
+ DEFAULT_PITCH, L"Segoe UI");
+ g_fontSmall = CreateFont(-12, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET,
+ OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
+ DEFAULT_PITCH, L"Segoe UI");
+ g_fontMenu = CreateFont(-13, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET,
+ OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
+ DEFAULT_PITCH, L"Segoe UI");
+
+ WNDCLASS wc = {0};
+ wc.lpfnWndProc = WndProc;
+ wc.hInstance = hInst;
+ wc.lpszClassName = L"TrayColorPickerMain";
+ RegisterClass(&wc);
+ g_hwndMain = CreateWindow(wc.lpszClassName, L"", 0, 0,0,0,0, NULL, NULL, hInst, NULL);
+
+ WNDCLASS lc = {0};
+ lc.lpfnWndProc = LoupeProc;
+ lc.hInstance = hInst;
+ lc.lpszClassName = L"TrayColorPickerLoupe";
+ lc.hCursor = LoadCursor(NULL, IDC_CROSS);
+ RegisterClass(&lc);
+ g_hwndLoupe = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
+ lc.lpszClassName, L"", WS_POPUP,
+ 0, 0, LoupeW(), LoupeH(),
+ NULL, NULL, hInst, NULL);
+
+ // Prefer the embedded icon resource (id 1, added via windres); fall back to generated
+ HICON trayIcon = (HICON)LoadImage(hInst, MAKEINTRESOURCE(1), IMAGE_ICON, 16, 16, 0);
+ HICON generated = NULL;
+ if (!trayIcon) { generated = CreateRainbowIcon(32); trayIcon = generated; }
+
+ g_nid.cbSize = sizeof(g_nid);
+ g_nid.hWnd = g_hwndMain;
+ g_nid.uID = ID_TRAY;
+ g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
+ g_nid.uCallbackMessage = WM_TRAYICON;
+ g_nid.hIcon = trayIcon ? trayIcon : LoadIcon(NULL, IDI_APPLICATION);
+ wcscpy(g_nid.szTip, L"Color Picker \u2014 click to pick a color");
+ Shell_NotifyIcon(NIM_ADD, &g_nid);
+
+ MSG m;
+ while (GetMessage(&m, NULL, 0, 0)) {
+ TranslateMessage(&m);
+ DispatchMessage(&m);
+ }
+ if (generated) DestroyIcon(generated);
+ DeleteObject(g_fontBig); DeleteObject(g_fontSmall); DeleteObject(g_fontMenu);
+ ReleaseDC(NULL, g_screenDC);
+ return 0;
+}