| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build windows |
| |
| // Package win32 implements a partial shiny screen driver using the Win32 API. |
| // It provides window, lifecycle, key, and mouse management, but no drawing. |
| // That is left to windriver (using GDI) or gldriver (using DirectX via ANGLE). |
| package win32 // import "golang.org/x/exp/shiny/driver/internal/win32" |
| |
| import ( |
| "fmt" |
| "runtime" |
| "sync" |
| "syscall" |
| "unsafe" |
| |
| "golang.org/x/exp/shiny/screen" |
| "golang.org/x/mobile/event/key" |
| "golang.org/x/mobile/event/lifecycle" |
| "golang.org/x/mobile/event/mouse" |
| "golang.org/x/mobile/event/paint" |
| "golang.org/x/mobile/event/size" |
| "golang.org/x/mobile/geom" |
| ) |
| |
| // screenHWND is the handle to the "Screen window". |
| // The Screen window encapsulates all screen.Screen operations |
| // in an actual Windows window so they all run on the main thread. |
| // Since any messages sent to a window will be executed on the |
| // main thread, we can safely use the messages below. |
| var screenHWND syscall.Handle |
| |
| const ( |
| msgCreateWindow = _WM_USER + iota |
| msgMainCallback |
| msgShow |
| msgQuit |
| msgLast |
| ) |
| |
| // userWM is used to generate private (WM_USER and above) window message IDs |
| // for use by screenWindowWndProc and windowWndProc. |
| type userWM struct { |
| sync.Mutex |
| id uint32 |
| } |
| |
| func (m *userWM) next() uint32 { |
| m.Lock() |
| if m.id == 0 { |
| m.id = msgLast |
| } |
| r := m.id |
| m.id++ |
| m.Unlock() |
| return r |
| } |
| |
| var currentUserWM userWM |
| |
| func newWindow(opts *screen.NewWindowOptions) (syscall.Handle, error) { |
| // TODO(brainman): convert windowClass to *uint16 once (in initWindowClass) |
| wcname, err := syscall.UTF16PtrFromString(windowClass) |
| if err != nil { |
| return 0, err |
| } |
| title, err := syscall.UTF16PtrFromString(opts.GetTitle()) |
| if err != nil { |
| return 0, err |
| } |
| hwnd, err := _CreateWindowEx(0, |
| wcname, title, |
| _WS_OVERLAPPEDWINDOW, |
| _CW_USEDEFAULT, _CW_USEDEFAULT, |
| _CW_USEDEFAULT, _CW_USEDEFAULT, |
| 0, 0, hThisInstance, 0) |
| if err != nil { |
| return 0, err |
| } |
| // TODO(andlabs): use proper nCmdShow |
| // TODO(andlabs): call UpdateWindow() |
| |
| return hwnd, nil |
| } |
| |
| // ResizeClientRect makes hwnd client rectangle opts.Width by opts.Height in size. |
| func ResizeClientRect(hwnd syscall.Handle, opts *screen.NewWindowOptions) error { |
| if opts == nil || opts.Width <= 0 || opts.Height <= 0 { |
| return nil |
| } |
| var cr, wr _RECT |
| err := _GetClientRect(hwnd, &cr) |
| if err != nil { |
| return err |
| } |
| err = _GetWindowRect(hwnd, &wr) |
| if err != nil { |
| return err |
| } |
| w := (wr.Right - wr.Left) - (cr.Right - int32(opts.Width)) |
| h := (wr.Bottom - wr.Top) - (cr.Bottom - int32(opts.Height)) |
| return _MoveWindow(hwnd, wr.Left, wr.Top, w, h, false) |
| } |
| |
| // Show shows a newly created window. |
| // It sends the appropriate lifecycle events, makes the window appear |
| // on the screen, and sends an initial size event. |
| // |
| // This is a separate step from NewWindow to give the driver a chance |
| // to setup its internal state for a window before events start being |
| // delivered. |
| func Show(hwnd syscall.Handle) { |
| SendMessage(hwnd, msgShow, 0, 0) |
| } |
| |
| func Release(hwnd syscall.Handle) { |
| SendMessage(hwnd, _WM_CLOSE, 0, 0) |
| } |
| |
| func sendFocus(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr) { |
| switch uMsg { |
| case _WM_SETFOCUS: |
| LifecycleEvent(hwnd, lifecycle.StageFocused) |
| case _WM_KILLFOCUS: |
| LifecycleEvent(hwnd, lifecycle.StageVisible) |
| default: |
| panic(fmt.Sprintf("unexpected focus message: %d", uMsg)) |
| } |
| return _DefWindowProc(hwnd, uMsg, wParam, lParam) |
| } |
| |
| func sendShow(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr) { |
| LifecycleEvent(hwnd, lifecycle.StageVisible) |
| _ShowWindow(hwnd, _SW_SHOWDEFAULT) |
| sendSize(hwnd) |
| return 0 |
| } |
| |
| func sendSizeEvent(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr) { |
| wp := (*_WINDOWPOS)(unsafe.Pointer(lParam)) |
| if wp.Flags&_SWP_NOSIZE != 0 { |
| return 0 |
| } |
| sendSize(hwnd) |
| return 0 |
| } |
| |
| func sendSize(hwnd syscall.Handle) { |
| var r _RECT |
| if err := _GetClientRect(hwnd, &r); err != nil { |
| panic(err) // TODO(andlabs) |
| } |
| |
| width := int(r.Right - r.Left) |
| height := int(r.Bottom - r.Top) |
| |
| // TODO(andlabs): don't assume that PixelsPerPt == 1 |
| SizeEvent(hwnd, size.Event{ |
| WidthPx: width, |
| HeightPx: height, |
| WidthPt: geom.Pt(width), |
| HeightPt: geom.Pt(height), |
| PixelsPerPt: 1, |
| }) |
| } |
| |
| func sendClose(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr) { |
| // TODO(ktye): DefWindowProc calls DestroyWindow by default. |
| // To intercept destruction of the window, return 0 and call |
| // DestroyWindow when appropriate. |
| LifecycleEvent(hwnd, lifecycle.StageDead) |
| return _DefWindowProc(hwnd, uMsg, wParam, lParam) |
| } |
| |
| func sendMouseEvent(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr) { |
| e := mouse.Event{ |
| X: float32(_GET_X_LPARAM(lParam)), |
| Y: float32(_GET_Y_LPARAM(lParam)), |
| Modifiers: keyModifiers(), |
| } |
| |
| switch uMsg { |
| case _WM_MOUSEMOVE: |
| e.Direction = mouse.DirNone |
| case _WM_LBUTTONDOWN, _WM_MBUTTONDOWN, _WM_RBUTTONDOWN: |
| e.Direction = mouse.DirPress |
| case _WM_LBUTTONUP, _WM_MBUTTONUP, _WM_RBUTTONUP: |
| e.Direction = mouse.DirRelease |
| case _WM_MOUSEWHEEL: |
| // TODO: On a trackpad, a scroll can be a drawn-out affair with a |
| // distinct beginning and end. Should the intermediate events be |
| // DirNone? |
| e.Direction = mouse.DirStep |
| |
| // Convert from screen to window coordinates. |
| p := _POINT{ |
| int32(e.X), |
| int32(e.Y), |
| } |
| _ScreenToClient(hwnd, &p) |
| e.X = float32(p.X) |
| e.Y = float32(p.Y) |
| default: |
| panic("sendMouseEvent() called on non-mouse message") |
| } |
| |
| switch uMsg { |
| case _WM_MOUSEMOVE: |
| // No-op. |
| case _WM_LBUTTONDOWN, _WM_LBUTTONUP: |
| e.Button = mouse.ButtonLeft |
| case _WM_MBUTTONDOWN, _WM_MBUTTONUP: |
| e.Button = mouse.ButtonMiddle |
| case _WM_RBUTTONDOWN, _WM_RBUTTONUP: |
| e.Button = mouse.ButtonRight |
| case _WM_MOUSEWHEEL: |
| // TODO: handle horizontal scrolling |
| delta := _GET_WHEEL_DELTA_WPARAM(wParam) / _WHEEL_DELTA |
| switch { |
| case delta > 0: |
| e.Button = mouse.ButtonWheelUp |
| case delta < 0: |
| e.Button = mouse.ButtonWheelDown |
| delta = -delta |
| default: |
| return |
| } |
| for delta > 0 { |
| MouseEvent(hwnd, e) |
| delta-- |
| } |
| return |
| } |
| |
| MouseEvent(hwnd, e) |
| |
| return 0 |
| } |
| |
| // Precondition: this is called in immediate response to the message that triggered the event (so not after w.Send). |
| func keyModifiers() (m key.Modifiers) { |
| down := func(x int32) bool { |
| // GetKeyState gets the key state at the time of the message, so this is what we want. |
| return _GetKeyState(x)&0x80 != 0 |
| } |
| |
| if down(_VK_CONTROL) { |
| m |= key.ModControl |
| } |
| if down(_VK_MENU) { |
| m |= key.ModAlt |
| } |
| if down(_VK_SHIFT) { |
| m |= key.ModShift |
| } |
| if down(_VK_LWIN) || down(_VK_RWIN) { |
| m |= key.ModMeta |
| } |
| return m |
| } |
| |
| var ( |
| MouseEvent func(hwnd syscall.Handle, e mouse.Event) |
| PaintEvent func(hwnd syscall.Handle, e paint.Event) |
| SizeEvent func(hwnd syscall.Handle, e size.Event) |
| KeyEvent func(hwnd syscall.Handle, e key.Event) |
| LifecycleEvent func(hwnd syscall.Handle, e lifecycle.Stage) |
| |
| // TODO: use the golang.org/x/exp/shiny/driver/internal/lifecycler package |
| // instead of or together with the LifecycleEvent callback? |
| ) |
| |
| func sendPaint(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr) { |
| PaintEvent(hwnd, paint.Event{}) |
| return _DefWindowProc(hwnd, uMsg, wParam, lParam) |
| } |
| |
| var screenMsgs = map[uint32]func(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr){} |
| |
| func AddScreenMsg(fn func(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr)) uint32 { |
| uMsg := currentUserWM.next() |
| screenMsgs[uMsg] = func(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) uintptr { |
| fn(hwnd, uMsg, wParam, lParam) |
| return 0 |
| } |
| return uMsg |
| } |
| |
| func screenWindowWndProc(hwnd syscall.Handle, uMsg uint32, wParam uintptr, lParam uintptr) (lResult uintptr) { |
| switch uMsg { |
| case msgCreateWindow: |
| p := (*newWindowParams)(unsafe.Pointer(lParam)) |
| p.w, p.err = newWindow(p.opts) |
| case msgMainCallback: |
| go func() { |
| mainCallback() |
| SendScreenMessage(msgQuit, 0, 0) |
| }() |
| case msgQuit: |
| _PostQuitMessage(0) |
| } |
| fn := screenMsgs[uMsg] |
| if fn != nil { |
| return fn(hwnd, uMsg, wParam, lParam) |
| } |
| return _DefWindowProc(hwnd, uMsg, wParam, lParam) |
| } |
| |
| //go:uintptrescapes |
| |
| func SendScreenMessage(uMsg uint32, wParam uintptr, lParam uintptr) (lResult uintptr) { |
| return SendMessage(screenHWND, uMsg, wParam, lParam) |
| } |
| |
| var windowMsgs = map[uint32]func(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) (lResult uintptr){ |
| _WM_SETFOCUS: sendFocus, |
| _WM_KILLFOCUS: sendFocus, |
| _WM_PAINT: sendPaint, |
| msgShow: sendShow, |
| _WM_WINDOWPOSCHANGED: sendSizeEvent, |
| _WM_CLOSE: sendClose, |
| |
| _WM_LBUTTONDOWN: sendMouseEvent, |
| _WM_LBUTTONUP: sendMouseEvent, |
| _WM_MBUTTONDOWN: sendMouseEvent, |
| _WM_MBUTTONUP: sendMouseEvent, |
| _WM_RBUTTONDOWN: sendMouseEvent, |
| _WM_RBUTTONUP: sendMouseEvent, |
| _WM_MOUSEMOVE: sendMouseEvent, |
| _WM_MOUSEWHEEL: sendMouseEvent, |
| |
| _WM_KEYDOWN: sendKeyEvent, |
| _WM_KEYUP: sendKeyEvent, |
| // TODO case _WM_SYSKEYDOWN, _WM_SYSKEYUP: |
| } |
| |
| func AddWindowMsg(fn func(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr)) uint32 { |
| uMsg := currentUserWM.next() |
| windowMsgs[uMsg] = func(hwnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) uintptr { |
| fn(hwnd, uMsg, wParam, lParam) |
| return 0 |
| } |
| return uMsg |
| } |
| |
| func windowWndProc(hwnd syscall.Handle, uMsg uint32, wParam uintptr, lParam uintptr) (lResult uintptr) { |
| fn := windowMsgs[uMsg] |
| if fn != nil { |
| return fn(hwnd, uMsg, wParam, lParam) |
| } |
| return _DefWindowProc(hwnd, uMsg, wParam, lParam) |
| } |
| |
| type newWindowParams struct { |
| opts *screen.NewWindowOptions |
| w syscall.Handle |
| err error |
| } |
| |
| func NewWindow(opts *screen.NewWindowOptions) (syscall.Handle, error) { |
| var p newWindowParams |
| p.opts = opts |
| SendScreenMessage(msgCreateWindow, 0, uintptr(unsafe.Pointer(&p))) |
| return p.w, p.err |
| } |
| |
| const windowClass = "shiny_Window" |
| const screenWindowClass = "shiny_ScreenWindow" |
| |
| func initWindowClass() (err error) { |
| wcname, err := syscall.UTF16PtrFromString(windowClass) |
| if err != nil { |
| return err |
| } |
| _, err = _RegisterClass(&_WNDCLASS{ |
| LpszClassName: wcname, |
| LpfnWndProc: syscall.NewCallback(windowWndProc), |
| HIcon: hDefaultIcon, |
| HCursor: hDefaultCursor, |
| HInstance: hThisInstance, |
| // TODO(andlabs): change this to something else? NULL? the hollow brush? |
| HbrBackground: syscall.Handle(_COLOR_BTNFACE + 1), |
| }) |
| return err |
| } |
| |
| func closeWindowClass() (err error) { |
| wcname, err := syscall.UTF16PtrFromString(windowClass) |
| if err != nil { |
| return err |
| } |
| _UnregisterClass(wcname, hThisInstance) |
| |
| return nil |
| } |
| |
| func initScreenWindow() (err error) { |
| swc, err := syscall.UTF16PtrFromString(screenWindowClass) |
| if err != nil { |
| return err |
| } |
| emptyString, err := syscall.UTF16PtrFromString("") |
| if err != nil { |
| return err |
| } |
| wc := _WNDCLASS{ |
| LpszClassName: swc, |
| LpfnWndProc: syscall.NewCallback(screenWindowWndProc), |
| HIcon: hDefaultIcon, |
| HCursor: hDefaultCursor, |
| HInstance: hThisInstance, |
| HbrBackground: syscall.Handle(_COLOR_BTNFACE + 1), |
| } |
| _, err = _RegisterClass(&wc) |
| if err != nil { |
| return err |
| } |
| screenHWND, err = _CreateWindowEx(0, |
| swc, emptyString, |
| _WS_OVERLAPPEDWINDOW, |
| _CW_USEDEFAULT, _CW_USEDEFAULT, |
| _CW_USEDEFAULT, _CW_USEDEFAULT, |
| _HWND_MESSAGE, 0, hThisInstance, 0) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func closeScreenWindow() (err error) { |
| // first destroy window |
| _DestroyWindow(screenHWND) |
| |
| // then unregister class |
| swc, err := syscall.UTF16PtrFromString(screenWindowClass) |
| if err != nil { |
| return err |
| } |
| _UnregisterClass(swc, hThisInstance) |
| |
| return nil |
| } |
| |
| var ( |
| hDefaultIcon syscall.Handle |
| hDefaultCursor syscall.Handle |
| hThisInstance syscall.Handle |
| ) |
| |
| func initCommon() (err error) { |
| hDefaultIcon, err = _LoadIcon(0, _IDI_APPLICATION) |
| if err != nil { |
| return err |
| } |
| hDefaultCursor, err = _LoadCursor(0, _IDC_ARROW) |
| if err != nil { |
| return err |
| } |
| // TODO(andlabs) hThisInstance |
| return nil |
| } |
| |
| //go:uintptrescapes |
| |
| func SendMessage(hwnd syscall.Handle, uMsg uint32, wParam uintptr, lParam uintptr) (lResult uintptr) { |
| return sendMessage(hwnd, uMsg, wParam, lParam) |
| } |
| |
| var mainCallback func() |
| |
| func Main(f func()) (retErr error) { |
| // It does not matter which OS thread we are on. |
| // All that matters is that we confine all UI operations |
| // to the thread that created the respective window. |
| runtime.LockOSThread() |
| |
| if err := initCommon(); err != nil { |
| return err |
| } |
| |
| if err := initScreenWindow(); err != nil { |
| return err |
| } |
| defer func() { |
| // TODO(andlabs): log an error if this fails? |
| closeScreenWindow() |
| }() |
| |
| if err := initWindowClass(); err != nil { |
| return err |
| } |
| defer func() { |
| // TODO(andlabs): log an error if this fails? |
| closeWindowClass() |
| }() |
| |
| // Prime the pump. |
| mainCallback = f |
| _PostMessage(screenHWND, msgMainCallback, 0, 0) |
| |
| // Main message pump. |
| var m _MSG |
| for { |
| done, err := _GetMessage(&m, 0, 0, 0) |
| if err != nil { |
| return fmt.Errorf("win32 GetMessage failed: %v", err) |
| } |
| if done == 0 { // WM_QUIT |
| break |
| } |
| _TranslateMessage(&m) |
| _DispatchMessage(&m) |
| } |
| |
| return nil |
| } |