444 lines
16 KiB
C
444 lines
16 KiB
C
// 24 march 2014
|
|
|
|
#include "winapi_windows.h"
|
|
#include "_cgo_export.h"
|
|
|
|
static void getScrollPos(HWND hwnd, int *xpos, int *ypos)
|
|
{
|
|
SCROLLINFO si;
|
|
|
|
ZeroMemory(&si, sizeof (SCROLLINFO));
|
|
si.cbSize = sizeof (SCROLLINFO);
|
|
si.fMask = SIF_POS | SIF_TRACKPOS;
|
|
if (GetScrollInfo(hwnd, SB_HORZ, &si) == 0)
|
|
xpanic("error getting horizontal scroll position for Area", GetLastError());
|
|
*xpos = si.nPos;
|
|
// MSDN example code reinitializes this each time, so we'll do it too just to be safe
|
|
ZeroMemory(&si, sizeof (SCROLLINFO));
|
|
si.cbSize = sizeof (SCROLLINFO);
|
|
si.fMask = SIF_POS | SIF_TRACKPOS;
|
|
if (GetScrollInfo(hwnd, SB_VERT, &si) == 0)
|
|
xpanic("error getting vertical scroll position for Area", GetLastError());
|
|
*ypos = si.nPos;
|
|
}
|
|
|
|
#define areaBackgroundBrush ((HBRUSH) (COLOR_BTNFACE + 1))
|
|
|
|
static void paintArea(HWND hwnd, void *data)
|
|
{
|
|
RECT xrect;
|
|
PAINTSTRUCT ps;
|
|
HDC hdc;
|
|
HDC rdc;
|
|
HBITMAP rbitmap, prevrbitmap;
|
|
RECT rrect;
|
|
BITMAPINFO bi;
|
|
VOID *ppvBits;
|
|
HBITMAP ibitmap;
|
|
HDC idc;
|
|
HBITMAP previbitmap;
|
|
BLENDFUNCTION blendfunc;
|
|
void *i;
|
|
intptr_t dx, dy;
|
|
int hscroll, vscroll;
|
|
|
|
// TRUE here indicates a
|
|
if (GetUpdateRect(hwnd, &xrect, TRUE) == 0)
|
|
return; // no update rect; do nothing
|
|
|
|
getScrollPos(hwnd, &hscroll, &vscroll);
|
|
|
|
i = doPaint(&xrect, hscroll, vscroll, data, &dx, &dy);
|
|
if (i == NULL) // cliprect empty
|
|
return;
|
|
// don't convert to BRGA just yet; see below
|
|
|
|
// TODO don't do the above, but always draw the background color?
|
|
|
|
hdc = BeginPaint(hwnd, &ps);
|
|
if (hdc == NULL)
|
|
xpanic("error beginning Area repaint", GetLastError());
|
|
|
|
// very big thanks to Ninjifox for suggesting this technique and helping me go through it
|
|
|
|
// first let's create the destination image, which we fill with the windows background color
|
|
// this is how we fake drawing the background; see also http://msdn.microsoft.com/en-us/library/ms969905.aspx
|
|
rdc = CreateCompatibleDC(hdc);
|
|
if (rdc == NULL)
|
|
xpanic("error creating off-screen rendering DC", GetLastError());
|
|
// the bitmap has to be compatible with the window
|
|
// if we create a bitmap compatible with the DC we just created, it'll be monochrome
|
|
// thanks to David Heffernan in http://stackoverflow.com/questions/23033636/winapi-gdi-fillrectcolor-btnface-fills-with-strange-grid-like-brush-on-window
|
|
rbitmap = CreateCompatibleBitmap(hdc, xrect.right - xrect.left, xrect.bottom - xrect.top);
|
|
if (rbitmap == NULL)
|
|
xpanic("error creating off-screen rendering bitmap", GetLastError());
|
|
prevrbitmap = (HBITMAP) SelectObject(rdc, rbitmap);
|
|
if (prevrbitmap == NULL)
|
|
xpanic("error connecting off-screen rendering bitmap to off-screen rendering DC", GetLastError());
|
|
rrect.left = 0;
|
|
rrect.right = xrect.right - xrect.left;
|
|
rrect.top = 0;
|
|
rrect.bottom = xrect.bottom - xrect.top;
|
|
if (FillRect(rdc, &rrect, areaBackgroundBrush) == 0)
|
|
xpanic("error filling off-screen rendering bitmap with the system background color", GetLastError());
|
|
|
|
// now we need to shove realbits into a bitmap
|
|
// technically bitmaps don't know about alpha; they just ignore the alpha byte
|
|
// AlphaBlend(), however, sees it - see http://msdn.microsoft.com/en-us/library/windows/desktop/dd183352%28v=vs.85%29.aspx
|
|
ZeroMemory(&bi, sizeof (BITMAPINFO));
|
|
bi.bmiHeader.biSize = sizeof (BITMAPINFOHEADER);
|
|
bi.bmiHeader.biWidth = (LONG) dx;
|
|
bi.bmiHeader.biHeight = -((LONG) dy); // negative height to force top-down drawing;
|
|
bi.bmiHeader.biPlanes = 1;
|
|
bi.bmiHeader.biBitCount = 32;
|
|
bi.bmiHeader.biCompression = BI_RGB;
|
|
bi.bmiHeader.biSizeImage = (DWORD) (dx * dy * 4);
|
|
// this is all we need, but because this confused me at first, I will say the two pixels-per-meter fields are unused (see http://blogs.msdn.com/b/oldnewthing/archive/2013/05/15/10418646.aspx and page 581 of Charles Petzold's Programming Windows, Fifth Edition)
|
|
// now for the trouble: CreateDIBSection() allocates the memory for us...
|
|
ibitmap = CreateDIBSection(NULL, // Ninjifox does this, so do some wine tests (http://source.winehq.org/source/dlls/gdi32/tests/bitmap.c#L725, thanks vpovirk in irc.freenode.net/#winehackers) and even Raymond Chen (http://blogs.msdn.com/b/oldnewthing/archive/2006/11/16/1086835.aspx), so.
|
|
&bi, DIB_RGB_COLORS, &ppvBits, 0, 0);
|
|
if (ibitmap == NULL)
|
|
xpanic("error creating HBITMAP for image returned by AreaHandler.Paint()", GetLastError());
|
|
|
|
// now we have to do TWO MORE things before we can finally do alpha blending
|
|
// first, we need to load the bitmap memory, because Windows makes it for us
|
|
// the pixels are arranged in RGBA order, but GDI requires BGRA
|
|
// this turns out to be just ARGB in little endian; let's convert into this memory
|
|
dotoARGB(i, (void *) ppvBits);
|
|
|
|
// the second thing is... make a device context for the bitmap :|
|
|
// Ninjifox just makes another compatible DC; we'll do the same
|
|
idc = CreateCompatibleDC(hdc);
|
|
if (idc == NULL)
|
|
xpanic("error creating HDC for image returned by AreaHandler.Paint()", GetLastError());
|
|
previbitmap = (HBITMAP) SelectObject(idc, ibitmap);
|
|
if (previbitmap == NULL)
|
|
xpanic("error connecting HBITMAP for image returned by AreaHandler.Paint() to its HDC", GetLastError());
|
|
|
|
// AND FINALLY WE CAN DO THE ALPHA BLENDING!!!!!!111
|
|
blendfunc.BlendOp = AC_SRC_OVER;
|
|
blendfunc.BlendFlags = 0;
|
|
blendfunc.SourceConstantAlpha = 255; // only use per-pixel alphas
|
|
blendfunc.AlphaFormat = AC_SRC_ALPHA; // premultiplied
|
|
if (AlphaBlend(rdc, 0, 0, (int) dx, (int) dy, // destination
|
|
idc, 0, 0, (int) dx, (int)dy, // source
|
|
blendfunc) == FALSE)
|
|
xpanic("error alpha-blending image returned by AreaHandler.Paint() onto background", GetLastError());
|
|
|
|
// and finally we can just blit that into the window
|
|
if (BitBlt(hdc, xrect.left, xrect.top, xrect.right - xrect.left, xrect.bottom - xrect.top,
|
|
rdc, 0, 0, // from the rdc's origin
|
|
SRCCOPY) == 0)
|
|
xpanic("error blitting Area image to Area", GetLastError());
|
|
|
|
// now to clean up
|
|
if (SelectObject(idc, previbitmap) != ibitmap)
|
|
xpanic("error reverting HDC for image returned by AreaHandler.Paint() to original HBITMAP", GetLastError());
|
|
if (SelectObject(rdc, prevrbitmap) != rbitmap)
|
|
xpanic("error reverting HDC for off-screen rendering to original HBITMAP", GetLastError());
|
|
if (DeleteObject(ibitmap) == 0)
|
|
xpanic("error deleting HBITMAP for image returned by AreaHandler.Paint()", GetLastError());
|
|
if (DeleteObject(rbitmap) == 0)
|
|
xpanic("error deleting HBITMAP for off-screen rendering", GetLastError());
|
|
if (DeleteDC(idc) == 0)
|
|
xpanic("error deleting HDC for image returned by AreaHandler.Paint()", GetLastError());
|
|
if (DeleteDC(rdc) == 0)
|
|
xpanic("error deleting HDC for off-screen rendering", GetLastError());
|
|
|
|
EndPaint(hwnd, &ps);
|
|
}
|
|
|
|
static SIZE getAreaControlSize(HWND hwnd)
|
|
{
|
|
RECT rect;
|
|
SIZE size;
|
|
|
|
if (GetClientRect(hwnd, &rect) == 0)
|
|
xpanic("error getting size of actual Area control", GetLastError());
|
|
size.cx = (LONG) (rect.right - rect.left);
|
|
size.cy = (LONG) (rect.bottom - rect.top);
|
|
return size;
|
|
}
|
|
|
|
static void scrollArea(HWND hwnd, void *data, WPARAM wParam, int which)
|
|
{
|
|
SCROLLINFO si;
|
|
SIZE size;
|
|
LONG cwid, cht;
|
|
LONG pagesize, maxsize;
|
|
LONG newpos;
|
|
LONG delta;
|
|
LONG dx, dy;
|
|
|
|
size = getAreaControlSize(hwnd);
|
|
cwid = size.cx;
|
|
cht = size.cy;
|
|
if (which == SB_HORZ) {
|
|
pagesize = cwid;
|
|
maxsize = areaWidthLONG(data);
|
|
} else if (which == SB_VERT) {
|
|
pagesize = cht;
|
|
maxsize = areaHeightLONG(data);
|
|
} else
|
|
xpanic("invalid which sent to scrollArea()", 0);
|
|
|
|
ZeroMemory(&si, sizeof (SCROLLINFO));
|
|
si.cbSize = sizeof (SCROLLINFO);
|
|
si.fMask = SIF_POS | SIF_TRACKPOS;
|
|
if (GetScrollInfo(hwnd, which, &si) == 0)
|
|
xpanic("error getting current scroll position for scrolling", GetLastError());
|
|
|
|
newpos = (LONG) si.nPos;
|
|
switch (LOWORD(wParam)) {
|
|
case SB_LEFT: // also SB_TOP; C won't let me have both (C89 §6.6.4.2; C99 §6.8.4.2)
|
|
newpos = 0;
|
|
break;
|
|
case SB_RIGHT: // also SB_BOTTOM
|
|
// see comment in adjustAreaScrollbars() below
|
|
newpos = maxsize - pagesize;
|
|
break;
|
|
case SB_LINELEFT: // also SB_LINEUP
|
|
newpos--;
|
|
break;
|
|
case SB_LINERIGHT: // also SB_LINEDOWN
|
|
newpos++;
|
|
break;
|
|
case SB_PAGELEFT: // also SB_PAGEUP
|
|
newpos -= pagesize;
|
|
break;
|
|
case SB_PAGERIGHT: // also SB_PAGEDOWN
|
|
newpos += pagesize;
|
|
break;
|
|
case SB_THUMBPOSITION:
|
|
// raymond chen says to just set the newpos to the SCROLLINFO nPos for this message; see http://blogs.msdn.com/b/oldnewthing/archive/2003/07/31/54601.aspx and http://blogs.msdn.com/b/oldnewthing/archive/2003/08/05/54602.aspx
|
|
// do nothing here; newpos already has nPos
|
|
break;
|
|
case SB_THUMBTRACK:
|
|
newpos = (LONG) si.nTrackPos;
|
|
}
|
|
// otherwise just keep the current position (that's what MSDN example code says, anyway)
|
|
|
|
// make sure we're not out of range
|
|
if (newpos < 0)
|
|
newpos = 0;
|
|
if (newpos > (maxsize - pagesize))
|
|
newpos = maxsize - pagesize;
|
|
|
|
// this would be where we would put a check to not scroll if the scroll position changed, but see the note about SB_THUMBPOSITION above: Raymond Chen's code always does the scrolling anyway in this case
|
|
|
|
delta = -(newpos - si.nPos); // negative because ScrollWindowEx() scrolls in the opposite direction
|
|
dx = delta;
|
|
dy = 0;
|
|
if (which == SB_VERT) {
|
|
dx = 0;
|
|
dy = delta;
|
|
}
|
|
if (ScrollWindowEx(hwnd,
|
|
(int) dx, (int) dy,
|
|
// these four change what is scrolled and record info about the scroll; we're scrolling the whole client area and don't care about the returned information here
|
|
NULL, NULL, NULL, NULL,
|
|
// mark the remaining rect as needing redraw and erase...
|
|
SW_INVALIDATE | SW_ERASE) == ERROR)
|
|
xpanic("error scrolling Area", GetLastError());
|
|
// ...but don't redraw the window yet; we need to apply our scroll changes
|
|
|
|
// we actually have to commit the change back to the scrollbar; otherwise the scroll position will merely reset itself
|
|
ZeroMemory(&si, sizeof (SCROLLINFO));
|
|
si.cbSize = sizeof (SCROLLINFO);
|
|
si.fMask = SIF_POS;
|
|
si.nPos = (int) newpos;
|
|
// this is not expressly documented as returning an error so IDK what the error code is so assume there is none
|
|
SetScrollInfo(hwnd, which, &si, TRUE); // redraw scrollbar
|
|
|
|
// NOW redraw it
|
|
if (UpdateWindow(hwnd) == 0)
|
|
xpanic("error updating Area after scrolling", GetLastError());
|
|
}
|
|
|
|
static void adjustAreaScrollbars(HWND hwnd, void *data)
|
|
{
|
|
SCROLLINFO si;
|
|
SIZE size;
|
|
LONG cwid, cht;
|
|
|
|
size = getAreaControlSize(hwnd);
|
|
cwid = size.cx;
|
|
cht = size.cy;
|
|
|
|
// the trick is we want a page to be the width/height of the visible area
|
|
// so the scroll range would go from [0..image_dimension - control_dimension]
|
|
// but judging from the sample code on MSDN, we don't need to do this; the scrollbar will do it for us
|
|
// we DO need to handle it when scrolling, though, since the thumb can only go up to this upper limit
|
|
|
|
// have to do horizontal and vertical separately
|
|
ZeroMemory(&si, sizeof (SCROLLINFO));
|
|
si.cbSize = sizeof (SCROLLINFO);
|
|
si.fMask = SIF_RANGE | SIF_PAGE;
|
|
si.nMin = 0;
|
|
si.nMax = (int) (areaWidthLONG(data) - 1); // the max point is inclusive, so we have to pass in the last valid value, not the first invalid one (see http://blogs.msdn.com/b/oldnewthing/archive/2003/07/31/54601.aspx); if we don't, we get weird things like the scrollbar sometimes showing one extra scroll position at the end that you can never scroll to
|
|
si.nPage = (UINT) cwid;
|
|
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE); // redraw the scroll bar
|
|
|
|
// MSDN sample code does this a second time; let's do it too to be safe
|
|
ZeroMemory(&si, sizeof (SCROLLINFO));
|
|
si.cbSize = sizeof (SCROLLINFO);
|
|
si.fMask = SIF_RANGE | SIF_PAGE;
|
|
si.nMin = 0;
|
|
si.nMax = (int) (areaHeightLONG(data) - 1);
|
|
si.nPage = (UINT) cht;
|
|
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
|
|
}
|
|
|
|
void repaintArea(HWND hwnd)
|
|
{
|
|
// NULL - the whole area; TRUE - have windows erase if possible
|
|
if (InvalidateRect(hwnd, NULL, TRUE) == 0)
|
|
xpanic("error flagging Area as needing repainting after event", GetLastError());
|
|
if (UpdateWindow(hwnd) == 0)
|
|
xpanic("error repainting Area after event", GetLastError());
|
|
}
|
|
|
|
void areaMouseEvent(HWND hwnd, void *data, DWORD button, BOOL up, uintptr_t heldButtons, LPARAM lParam)
|
|
{
|
|
int xpos, ypos;
|
|
|
|
// mouse coordinates are relative to control; make them relative to Area
|
|
getScrollPos(hwnd, &xpos, &ypos);
|
|
xpos += GET_X_LPARAM(lParam);
|
|
ypos += GET_Y_LPARAM(lParam);
|
|
finishAreaMouseEvent(data, button, up, heldButtons, xpos, ypos);
|
|
}
|
|
|
|
static LRESULT CALLBACK areaWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
void *data;
|
|
DWORD which;
|
|
uintptr_t heldButtons = (uintptr_t) wParam;
|
|
LRESULT lResult;
|
|
|
|
data = getWindowData(hwnd, uMsg, wParam, lParam, &lResult, storeAreaHWND);
|
|
if (data == NULL)
|
|
return lResult;
|
|
switch (uMsg) {
|
|
case WM_PAINT:
|
|
paintArea(hwnd, data);
|
|
return 0;
|
|
case WM_ERASEBKGND:
|
|
// don't draw a background; we'll do so when painting
|
|
// this is to make things flicker-free; see http://msdn.microsoft.com/en-us/library/ms969905.aspx
|
|
return 1;
|
|
case WM_HSCROLL:
|
|
scrollArea(hwnd, data, wParam, SB_HORZ);
|
|
return 0;
|
|
case WM_VSCROLL:
|
|
scrollArea(hwnd, data, wParam, SB_VERT);
|
|
return 0;
|
|
case WM_SIZE:
|
|
adjustAreaScrollbars(hwnd, data);
|
|
return 0;
|
|
case WM_ACTIVATE:
|
|
// don't keep the double-click timer running if the user switched programs in between clicks
|
|
areaResetClickCounter(data);
|
|
return 0;
|
|
case WM_MOUSEACTIVATE:
|
|
// this happens on every mouse click (apparently), so DON'T reset the click counter, otherwise it will always be reset (not an issue, as MSDN says WM_ACTIVATE is sent alongside WM_MOUSEACTIVATE when necessary)
|
|
// transfer keyboard focus to our Area on an activating click
|
|
// (see http://www.catch22.net/tuts/custom-controls)
|
|
// don't bother checking SetFocus()'s error; see http://stackoverflow.com/questions/24073695/winapi-can-setfocus-return-null-without-an-error-because-thats-what-im-see/24074912#24074912
|
|
SetFocus(hwnd);
|
|
// and don't eat the click, as we want to handle clicks that switch into Windows with Areas from other windows
|
|
return MA_ACTIVATE;
|
|
case WM_MOUSEMOVE:
|
|
areaMouseEvent(hwnd, data, 0, FALSE, heldButtons, lParam);
|
|
return 0;
|
|
case WM_LBUTTONDOWN:
|
|
areaMouseEvent(hwnd, data, 1, FALSE, heldButtons, lParam);
|
|
return 0;
|
|
case WM_LBUTTONUP:
|
|
areaMouseEvent(hwnd, data, 1, TRUE, heldButtons, lParam);
|
|
return 0;
|
|
case WM_MBUTTONDOWN:
|
|
areaMouseEvent(hwnd, data, 2, FALSE, heldButtons, lParam);
|
|
return 0;
|
|
case WM_MBUTTONUP:
|
|
areaMouseEvent(hwnd, data, 2, TRUE, heldButtons, lParam);
|
|
return 0;
|
|
case WM_RBUTTONDOWN:
|
|
areaMouseEvent(hwnd, data, 3, FALSE, heldButtons, lParam);
|
|
return 0;
|
|
case WM_RBUTTONUP:
|
|
areaMouseEvent(hwnd, data, 3, TRUE, heldButtons, lParam);
|
|
return 0;
|
|
case WM_XBUTTONDOWN:
|
|
// values start at 1; we want them to start at 4
|
|
which = (DWORD) GET_XBUTTON_WPARAM(wParam) + 3;
|
|
heldButtons = (uintptr_t) GET_KEYSTATE_WPARAM(wParam);
|
|
areaMouseEvent(hwnd, data, which, FALSE, heldButtons, lParam);
|
|
return TRUE; // XBUTTON messages are different!
|
|
case WM_XBUTTONUP:
|
|
which = (DWORD) GET_XBUTTON_WPARAM(wParam) + 3;
|
|
heldButtons = (uintptr_t) GET_KEYSTATE_WPARAM(wParam);
|
|
areaMouseEvent(hwnd, data, which, TRUE, heldButtons, lParam);
|
|
return TRUE;
|
|
case WM_KEYDOWN:
|
|
areaKeyEvent(data, FALSE, wParam, lParam);
|
|
return 0;
|
|
case WM_KEYUP:
|
|
areaKeyEvent(data, TRUE, wParam, lParam);
|
|
return 0;
|
|
// Alt+[anything] and F10 send these instead and require us to return to DefWindowProc() so global keystrokes such as Alt+Tab can be processed
|
|
case WM_SYSKEYDOWN:
|
|
areaKeyEvent(data, FALSE, wParam, lParam);
|
|
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
|
|
case WM_SYSKEYUP:
|
|
areaKeyEvent(data, TRUE, wParam, lParam);
|
|
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
|
|
case msgAreaSizeChanged:
|
|
adjustAreaScrollbars(hwnd, data);
|
|
repaintArea(hwnd); // this calls for an update
|
|
return 0;
|
|
case msgAreaRepaintAll:
|
|
repaintArea(hwnd);
|
|
return 0;
|
|
default:
|
|
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
|
|
}
|
|
xmissedmsg("Area", "areaWndProc()", uMsg);
|
|
return 0; // unreached
|
|
}
|
|
|
|
DWORD makeAreaWindowClass(char **errmsg)
|
|
{
|
|
WNDCLASSW wc;
|
|
|
|
ZeroMemory(&wc, sizeof (WNDCLASSW));
|
|
wc.style = CS_HREDRAW | CS_VREDRAW; // no CS_DBLCLKS because do that manually
|
|
wc.lpszClassName = areaWindowClass;
|
|
wc.lpfnWndProc = areaWndProc;
|
|
wc.hInstance = hInstance;
|
|
wc.hIcon = hDefaultIcon;
|
|
wc.hCursor = hArrowCursor,
|
|
wc.hbrBackground = NULL; // no brush; we handle WM_ERASEBKGND
|
|
if (RegisterClassW(&wc) == 0) {
|
|
*errmsg = "error registering Area window class";
|
|
return GetLastError();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
HWND newArea(void *data)
|
|
{
|
|
HWND hwnd;
|
|
|
|
hwnd = CreateWindowExW(
|
|
0,
|
|
areaWindowClass, L"",
|
|
WS_HSCROLL | WS_VSCROLL | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
100, 100,
|
|
msgwin, NULL, hInstance, data);
|
|
if (hwnd == NULL)
|
|
xpanic("container creation failed", GetLastError());
|
|
return hwnd;
|
|
}
|