diff options
Diffstat (limited to 'redo/area_windows.c')
| -rw-r--r-- | redo/area_windows.c | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/redo/area_windows.c b/redo/area_windows.c new file mode 100644 index 0000000..4ee2335 --- /dev/null +++ b/redo/area_windows.c @@ -0,0 +1,443 @@ +/* 24 march 2014 */ + +/* TODO either strip the // comments or find out if --std=c99 is safe for cgo */ + +#include "winapi_windows.h" +#include "_cgo_epxort.h" + +LPWSTR areaWindowClass = L"gouiarea"; + +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(s.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 = int32(i.Rect.Dx()) + bi.bmiHeader.biHeight = -int32(i.Rect.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, 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) +{ + SCROLILNFO 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 (TODO will C let me?) + 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, + dx, dy, // TODO correct types + // 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 + // TODO pointers? + 0, 0, 0, 0, + // 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; + // TODO double-check that this doesn't return an error + SetScrollInfo(hwnd, which, &si); + + // NOW redraw it + if (UpdateWindow(hwnd) == 0) + panic("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) +{ + // TODO second argument pointer? + // 0 - the whole area; TRUE - have windows erase if possible + if (InvalidateRect(hwnd, 0, 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, 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, xpos, ypos); +} + +var ( + _setFocus = user32.NewProc("SetFocus") +) + +static LRESULT CALLBACK areaWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + void *data; + DWORD which; + + data = (void *) GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (data == NULL) { + /* the lpParam is available during WM_NCCREATE and WM_CREATE */ + if (uMsg == WM_NCCREATE) { + storelpParam(hwnd, lParam); + data = (void *) GetWindowLongPtrW(hwnd, GWLP_USERDATA); + storeAreaHWND(data, hwnd); + } + /* act as if we're not ready yet, even during WM_NCCREATE (nothing important to the switch statement below happens here anyway) */ + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + + 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, lParam); + return 0; + case WM_LBUTTONDOWN: + areaMouseEvent(hwnd, data, 1, FALSE, lParam); + return 0; + case WM_LBUTTONUP: + areaMouseEvent(hwnd, data, 1, TRUE, lParam) + return 0; + case WM_MBUTTONDOWN: + areaMouseEvent(hwnd, data, 2, FALSE, lParam); + return 0 + case WM_MBUTTONUP: + areaMouseEvent(hwnd, data, 2, TRUE, lParam); + return 0; + case WM_RBUTTONDOWN: + areaMouseEvent(hwnd, data, 3, FALSE, lParam); + return 0; + case WM_RBUTTONUP: + areaMouseEvent(hwnd, data, 3, TRUE, lParam); + return 0; + case WM_XBUTTONDOWN: + // values start at 1; we want them to start at 4 + which = (DWORD) GET_XBUTTON_WPARAM(wParam) + 3; + areaMouseEvent(hwnd, data, which, FALSE, lParam); + return TRUE; // XBUTTON messages are different! + case WM_XBUTTONUP: + which = (DWORD) GET_XBUTTON_WPARAM(wParam) + 3; + areaMouseEvent(hwnd, data, which, TRUE, 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; +} |
