// 24 march 2014 package ui import ( "fmt" "syscall" "unsafe" "sync" "image" ) const ( areastyle = _WS_HSCROLL | _WS_VSCROLL | controlstyle areaxstyle = 0 | controlxstyle ) const ( areaWndClassFormat = "gouiarea%X" ) var ( areaWndClassNum uintptr areaWndClassNumLock sync.Mutex ) func getScrollPos(hwnd _HWND) (xpos int32, ypos int32) { var si _SCROLLINFO si.cbSize = uint32(unsafe.Sizeof(si)) si.fMask = _SIF_POS | _SIF_TRACKPOS r1, _, err := _getScrollInfo.Call( uintptr(hwnd), uintptr(_SB_HORZ), uintptr(unsafe.Pointer(&si))) if r1 == 0 { // failure panic(fmt.Errorf("error getting horizontal scroll position for Area: %v", err)) } xpos = si.nPos si.cbSize = uint32(unsafe.Sizeof(si)) // MSDN example code reinitializes this each time, so we'll do it too just to be safe si.fMask = _SIF_POS | _SIF_TRACKPOS r1, _, err = _getScrollInfo.Call( uintptr(hwnd), uintptr(_SB_VERT), uintptr(unsafe.Pointer(&si))) if r1 == 0 { // failure panic(fmt.Errorf("error getting vertical scroll position for Area: %v", err)) } ypos = si.nPos return xpos, ypos } var ( _getUpdateRect = user32.NewProc("GetUpdateRect") _beginPaint = user32.NewProc("BeginPaint") _endPaint = user32.NewProc("EndPaint") _gdipCreateBitmapFromScan0 = gdiplus.NewProc("GdipCreateBitmapFromScan0") _gdipCreateFromHDC = gdiplus.NewProc("GdipCreateFromHDC") _gdipDrawImageI = gdiplus.NewProc("GdipDrawImageI") _gdipDeleteGraphics = gdiplus.NewProc("GdipDeleteGraphics") _gdipDisposeImage = gdiplus.NewProc("GdipDisposeImage") ) const ( // from winuser.h _WM_PAINT = 0x000F ) func paintArea(s *sysData) { const ( // from gdipluspixelformats.h _PixelFormatGDI = 0x00020000 _PixelFormatAlpha = 0x00040000 _PixelFormatCanonical = 0x00200000 _PixelFormat32bppARGB = (10 | (32 << 8) | _PixelFormatAlpha | _PixelFormatGDI | _PixelFormatCanonical) ) var xrect _RECT var ps _PAINTSTRUCT r1, _, _ := _getUpdateRect.Call( uintptr(s.hwnd), uintptr(unsafe.Pointer(&xrect)), uintptr(_TRUE)) // erase the update rect with the background color if r1 == 0 { // no update rect; do nothing return } hscroll, vscroll := getScrollPos(s.hwnd) cliprect := image.Rect(int(xrect.Left), int(xrect.Top), int(xrect.Right), int(xrect.Bottom)) cliprect = cliprect.Add(image.Pt(int(hscroll), int(vscroll))) // adjust by scroll position // make sure the cliprect doesn't fall outside the size of the Area cliprect = cliprect.Intersect(image.Rect(0, 0, s.areawidth, s.areaheight)) if cliprect.Empty() { // still no update rect return } r1, _, err := _beginPaint.Call( uintptr(s.hwnd), uintptr(unsafe.Pointer(&ps))) if r1 == 0 { // failure panic(fmt.Errorf("error beginning Area repaint: %v", err)) } hdc := _HANDLE(r1) i := s.handler.Paint(cliprect) // the pixels are arranged in RGBA order, but GDI+ requires BGRA // we don't have a choice but to convert it ourselves // TODO make realbits a part of sysData to conserve memory realbits := make([]byte, 4 * i.Rect.Dx() * i.Rect.Dy()) p := 0 q := 0 for y := i.Rect.Min.Y; y < i.Rect.Max.Y; y++ { nextp := p + i.Stride for x := i.Rect.Min.X; x < i.Rect.Max.X; x++ { realbits[q + 0] = byte(i.Pix[p + 2]) // B realbits[q + 1] = byte(i.Pix[p + 1]) // G realbits[q + 2] = byte(i.Pix[p + 0]) // R realbits[q + 3] = byte(i.Pix[p + 3]) // A p += 4 q += 4 } p = nextp } var bitmap, graphics uintptr r1, _, err = _gdipCreateBitmapFromScan0.Call( uintptr(i.Rect.Dx()), uintptr(i.Rect.Dy()), uintptr(i.Rect.Dx() * 4), // got rid of extra stride uintptr(_PixelFormat32bppARGB), uintptr(unsafe.Pointer(&realbits[0])), uintptr(unsafe.Pointer(&bitmap))) if r1 != 0 { // failure panic(fmt.Errorf("error creating GDI+ bitmap to blit (GDI+ error code %d; Windows last error %v)", r1, err)) } r1, _, err = _gdipCreateFromHDC.Call( uintptr(hdc), uintptr(unsafe.Pointer(&graphics))) if r1 != 0 { // failure panic(fmt.Errorf("error creating GDI+ graphics context to blit to (GDI+ error code %d; Windows last error %v)", r1, err)) } r1, _, err = _gdipDrawImageI.Call( graphics, bitmap, uintptr(xrect.Left), // cliprect is adjusted; use original uintptr(xrect.Top)) if r1 != 0 { // failure panic(fmt.Errorf("error blitting GDI+ bitmap (GDI+ error code %d; Windows last error %v)", r1, err)) } r1, _, err = _gdipDeleteGraphics.Call(graphics) if r1 != 0 { // failure panic(fmt.Errorf("error freeing GDI+ graphics context to blit to (GDI+ error code %d; Windows last error %v)", r1, err)) } // TODO this is the destructor of Image (Bitmap's base class); I don't see a specific destructor for Bitmap itself so r1, _, err = _gdipDisposeImage.Call(bitmap) if r1 != 0 { // failure panic(fmt.Errorf("error freeing GDI+ bitmap to blit (GDI+ error code %d; Windows last error %v)", r1, err)) } // return value always nonzero according to MSDN _endPaint.Call( uintptr(s.hwnd), uintptr(unsafe.Pointer(&ps))) } func getAreaControlSize(hwnd _HWND) (width int, height int) { var rect _RECT r1, _, err := _getClientRect.Call( uintptr(hwnd), uintptr(unsafe.Pointer(&rect))) if r1 == 0 { // failure panic(fmt.Errorf("error getting size of actual Area control: %v", err)) } return int(rect.Right - rect.Left), int(rect.Bottom - rect.Top) } func scrollArea(s *sysData, wparam _WPARAM, which uintptr) { var si _SCROLLINFO cwid, cht := getAreaControlSize(s.hwnd) pagesize := int32(cwid) maxsize := int32(s.areawidth) if which == uintptr(_SB_VERT) { pagesize = int32(cht) maxsize = int32(s.areaheight) } si.cbSize = uint32(unsafe.Sizeof(si)) si.fMask = _SIF_POS | _SIF_TRACKPOS r1, _, err := _getScrollInfo.Call( uintptr(s.hwnd), which, uintptr(unsafe.Pointer(&si))) if r1 == 0 { // failure panic(fmt.Errorf("error getting current scroll position for scrolling: %v", err)) } newpos := si.nPos switch wparam & 0xFFFF { case _SB_LEFT: // also _SB_TOP but Go won't let me newpos = 0 case _SB_RIGHT: // also _SB_BOTTOM // see comment in adjustAreaScrollbars() below newpos = maxsize - pagesize case _SB_LINELEFT: // also _SB_LINEUP newpos-- case _SB_LINERIGHT: // also _SB_LINEDOWN newpos++ case _SB_PAGELEFT: // also _SB_PAGEUP newpos -= pagesize case _SB_PAGERIGHT: // also _SB_PAGEDOWN newpos += pagesize case _SB_THUMBPOSITION: // TODO is this the same as SB_THUMBTRACK instead? MSDN says use of thumb pos is only for that one // do nothing; newpos already has the thumb's position case _SB_THUMBTRACK: newpos = 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 } // TODO is this the right thing to do for SB_THUMBTRACK? or will it conflict? if newpos == si.nPos { // no change; no scrolling return } delta := -(newpos - si.nPos) // negative because ScrollWindowEx() scrolls in the opposite direction dx := delta dy := int32(0) if which == uintptr(_SB_VERT) { dx = int32(0) dy = delta } r1, _, err = _scrollWindowEx.Call( uintptr(s.hwnd), uintptr(dx), uintptr(dy), uintptr(0), // 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 uintptr(0), uintptr(0), uintptr(0), // TODO also SW_ERASE? or will the GetUpdateRect() call handle it? uintptr(_SW_INVALIDATE)) // mark the remaining rect as needing redraw... if r1 == _ERROR { // failure panic(fmt.Errorf("error scrolling Area: %v", err)) } r1, _, err = _updateWindow.Call(uintptr(s.hwnd)) // ...and redraw it if r1 == 0 { // failure panic(fmt.Errorf("error updating Area after scrolling: %v", err)) } // we actually have to commit the change back to the scrollbar; otherwise the scroll position will merely reset itself si.cbSize = uint32(unsafe.Sizeof(si)) si.fMask = _SIF_POS si.nPos = newpos _setScrollInfo.Call( uintptr(s.hwnd), which, uintptr(unsafe.Pointer(&si))) // TODO in some cases wine will show a thumb one pixel away from the advance arrow button if going to the end; the values are correct though... weirdness in wine or something I never noticed about Windows? } func adjustAreaScrollbars(s *sysData) { var si _SCROLLINFO cwid, cht := getAreaControlSize(s.hwnd) // 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 si.cbSize = uint32(unsafe.Sizeof(si)) si.fMask = _SIF_RANGE | _SIF_PAGE si.nMin = 0 si.nMax = int32(s.areawidth) si.nPage = uint32(cwid) _setScrollInfo.Call( uintptr(s.hwnd), uintptr(_SB_HORZ), uintptr(unsafe.Pointer(&si)), uintptr(_TRUE)) // redraw the scroll bar si.cbSize = uint32(unsafe.Sizeof(si)) // MSDN sample code does this a second time; let's do it too to be safe si.fMask = _SIF_RANGE | _SIF_PAGE si.nMin = 0 si.nMax = int32(s.areaheight) si.nPage = uint32(cht) _setScrollInfo.Call( uintptr(s.hwnd), uintptr(_SB_VERT), uintptr(unsafe.Pointer(&si)), uintptr(_TRUE)) // redraw the scroll bar } var ( _invalidateRect = user32.NewProc("InvalidateRect") ) func repaintArea(s *sysData) { r1, _, err := _invalidateRect.Call( uintptr(s.hwnd), uintptr(0), // the whole area uintptr(_FALSE)) // TODO use _TRUE to mark that we should erase? or will the GetUpdateRect() call handle it? if r1 == 0 { // failure panic(fmt.Errorf("error flagging Area as needing repainting after event (last error: %v)", err)) } r1, _, err = _updateWindow.Call(uintptr(s.hwnd)) if r1 == 0 { // failure panic(fmt.Errorf("error repainting Area after event: %v", err)) } } var ( _getKeyState = user32.NewProc("GetKeyState") ) func getModifiers() (m Modifiers) { down := func(x uintptr) bool { r1, _, _ := _getKeyState.Call(x) return (r1 & 0x80) != 0 } if down(_VK_CONTROL) { m |= Ctrl } if down(_VK_MENU) { m |= Alt } if down(_VK_SHIFT) { m |= Shift } // TODO windows key (super) return m } func areaMouseEvent(s *sysData, button uint, up bool, count uint, wparam _WPARAM, lparam _LPARAM) { var me MouseEvent xpos, ypos := getScrollPos(s.hwnd) // mouse coordinates are relative to control; make them relative to Area xpos += lparam._X() ypos += lparam._Y() me.Pos = image.Pt(int(xpos), int(ypos)) if up { me.Up = button } else { me.Down = button me.Count = count } // though wparam will contain control and shift state, let's use just one function to get modifiers for both keyboard and mouse events; it'll work the same anyway since we have to do this for alt and windows key (super) me.Modifiers = getModifiers() if button != 1 && (wparam & _MK_LBUTTON) != 0 { me.Held = append(me.Held, 1) } if button != 2 && (wparam & _MK_MBUTTON) != 0 { me.Held = append(me.Held, 2) } if button != 3 && (wparam & _MK_RBUTTON) != 0 { me.Held = append(me.Held, 3) } // TODO XBUTTONs? repaint := s.handler.Mouse(me) if repaint { repaintArea(s) } } func areaKeyEvent(s *sysData, up bool, wparam _WPARAM, lparam _LPARAM) bool { var ke KeyEvent scancode := byte((lparam >> 16) & 0xFF) ke.Modifiers = getModifiers() if wparam == _VK_RETURN && (lparam & 0x01000000) != 0 { // the above is special handling for numpad enter // bit 24 of LPARAM (0x01000000) indicates right-hand keys ke.ExtKey = NEnter } else if extkey, ok := extkeys[wparam]; ok { ke.ExtKey = extkey } else if xke, ok := fromScancode(uintptr(scancode)); ok { // one of these will be nonzero ke.Key = xke.Key ke.ExtKey = xke.ExtKey } else if ke.Modifiers == 0 { // no key, extkey, or modifiers; do nothing but mark not handled return false } ke.Up = up handled, repaint := s.handler.Key(ke) if repaint { repaintArea(s) } return handled } var extkeys = map[_WPARAM]ExtKey{ _VK_ESCAPE: Escape, _VK_INSERT: Insert, _VK_DELETE: Delete, _VK_HOME: Home, _VK_END: End, _VK_PRIOR: PageUp, _VK_NEXT: PageDown, _VK_UP: Up, _VK_DOWN: Down, _VK_LEFT: Left, _VK_RIGHT: Right, _VK_F1: F1, _VK_F2: F2, _VK_F3: F3, _VK_F4: F4, _VK_F5: F5, _VK_F6: F6, _VK_F7: F7, _VK_F8: F8, _VK_F9: F9, _VK_F10: F10, _VK_F11: F11, _VK_F12: F12, // numpad numeric keys and . are handled in events_notdarwin.go // numpad enter is handled in code above _VK_ADD: NAdd, _VK_SUBTRACT: NSubtract, _VK_MULTIPLY: NMultiply, _VK_DIVIDE: NDivide, } // sanity check func init() { included := make([]bool, _nextkeys) for _, v := range extkeys { included[v] = true } for i := 1; i < int(_nextkeys); i++ { if i >= int(N0) && i <= int(N9) { // skip numpad numbers, ., and enter continue } if i == int(NDot) || i == int(NEnter) { continue } if !included[i] { panic(fmt.Errorf("error: not all ExtKeys defined on Windows (missing %d)", i)) } } } var ( _setFocus = user32.NewProc("SetFocus") ) func areaWndProc(s *sysData) func(hwnd _HWND, uMsg uint32, wParam _WPARAM, lParam _LPARAM) _LRESULT { return func(hwnd _HWND, uMsg uint32, wParam _WPARAM, lParam _LPARAM) _LRESULT { const ( _MA_ACTIVATE = 1 ) switch uMsg { case _WM_PAINT: paintArea(s) return 0 case _WM_HSCROLL: // TODO make this unnecessary if s != nil && s.hwnd != 0 { // this message can be sent before s is assigned properly scrollArea(s, wParam, _SB_HORZ) } return 0 case _WM_VSCROLL: // TODO make this unnecessary if s != nil && s.hwnd != 0 { // this message can be sent before s is assigned properly scrollArea(s, wParam, _SB_VERT) } return 0 case _WM_SIZE: // TODO make this unnecessary if s != nil && s.hwnd != 0 { // this message can be sent before s is assigned properly adjustAreaScrollbars(s) } return 0 case _WM_MOUSEACTIVATE: // register our window for keyboard input // (see http://www.catch22.net/tuts/custom-controls) r1, _, err := _setFocus.Call(uintptr(s.hwnd)) if r1 == 0 { // failure panic(fmt.Errorf("error giving Area keyboard focus: %v", err)) } return _MA_ACTIVATE // TODO eat the click? case _WM_MOUSEMOVE: areaMouseEvent(s, 0, false, 0, wParam, lParam) return 0 case _WM_LBUTTONDOWN: areaMouseEvent(s, 1, false, 1, wParam, lParam) return 0 case _WM_LBUTTONDBLCLK: areaMouseEvent(s, 1, false, 2, wParam, lParam) return 0 case _WM_LBUTTONUP: areaMouseEvent(s, 1, true, 0, wParam, lParam) return 0 case _WM_MBUTTONDOWN: areaMouseEvent(s, 2, false, 1, wParam, lParam) return 0 case _WM_MBUTTONDBLCLK: areaMouseEvent(s, 2, false, 2, wParam, lParam) return 0 case _WM_MBUTTONUP: areaMouseEvent(s, 2, true, 0, wParam, lParam) return 0 case _WM_RBUTTONDOWN: areaMouseEvent(s, 3, false, 1, wParam, lParam) return 0 case _WM_RBUTTONDBLCLK: areaMouseEvent(s, 3, false, 2, wParam, lParam) return 0 case _WM_RBUTTONUP: areaMouseEvent(s, 3, true, 0, wParam, lParam) return 0 // TODO XBUTTONs? case _WM_KEYDOWN: areaKeyEvent(s, false, wParam, lParam) return 0 case _WM_KEYUP: areaKeyEvent(s, true, wParam, lParam) return 0 case msgSetAreaSize: s.areawidth = int(wParam) // see setAreaSize() in sysdata_windows.go s.areaheight = int(lParam) adjustAreaScrollbars(s) repaintArea(s) // this calls for an update return 0 default: r1, _, _ := defWindowProc.Call( uintptr(hwnd), uintptr(uMsg), uintptr(wParam), uintptr(lParam)) return _LRESULT(r1) } panic(fmt.Sprintf("areaWndProc message %d did not return: internal bug in ui library", uMsg)) } } func registerAreaWndClass(s *sysData) (newClassName string, err error) { const ( // from winuser.h _CS_DBLCLKS = 0x0008 ) areaWndClassNumLock.Lock() newClassName = fmt.Sprintf(areaWndClassFormat, areaWndClassNum) areaWndClassNum++ areaWndClassNumLock.Unlock() wc := &_WNDCLASS{ style: _CS_DBLCLKS, // needed to be able to register double-clicks lpszClassName: uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(newClassName))), lpfnWndProc: syscall.NewCallback(areaWndProc(s)), hInstance: hInstance, hIcon: icon, hCursor: cursor, hbrBackground: _HBRUSH(_COLOR_BTNFACE + 1), } ret := make(chan uiret) defer close(ret) uitask <- &uimsg{ call: _registerClass, p: []uintptr{uintptr(unsafe.Pointer(wc))}, ret: ret, } r := <-ret if r.ret == 0 { // failure return "", r.err } return newClassName, nil } type _PAINTSTRUCT struct { hdc _HANDLE fErase int32 // originally BOOL rcPaint _RECT fRestore int32 // originally BOOL fIncUpdate int32 // originally BOOL rgbReserved [32]byte }