diff --git a/app/internal/windows/windows.go b/app/internal/windows/windows.go index 65a01d3f..cf28cb55 100644 --- a/app/internal/windows/windows.go +++ b/app/internal/windows/windows.go @@ -106,9 +106,13 @@ const ( SIZE_MINIMIZED = 1 SIZE_RESTORED = 0 - SW_SHOWDEFAULT = 10 + SW_SHOWDEFAULT = 10 + SW_SHOWMINIMIZED = 2 + SW_SHOWMAXIMIZED = 3 + SW_SHOWNORMAL = 1 + SW_SHOW = 5 + SWP_FRAMECHANGED = 0x0020 - SWP_FRAMECHANGED = 0x0020 SWP_NOMOVE = 0x0002 SWP_NOOWNERZORDER = 0x0200 SWP_NOSIZE = 0x0001 @@ -166,42 +170,44 @@ const ( UNICODE_NOCHAR = 65535 - WM_CANCELMODE = 0x001F - WM_CHAR = 0x0102 - WM_CREATE = 0x0001 - WM_DPICHANGED = 0x02E0 - WM_DESTROY = 0x0002 - WM_ERASEBKGND = 0x0014 - WM_KEYDOWN = 0x0100 - WM_KEYUP = 0x0101 - WM_LBUTTONDOWN = 0x0201 - WM_LBUTTONUP = 0x0202 - WM_MBUTTONDOWN = 0x0207 - WM_MBUTTONUP = 0x0208 - WM_MOUSEMOVE = 0x0200 - WM_MOUSEWHEEL = 0x020A - WM_MOUSEHWHEEL = 0x020E - WM_PAINT = 0x000F - WM_CLOSE = 0x0010 - WM_QUIT = 0x0012 - WM_SETCURSOR = 0x0020 - WM_SETFOCUS = 0x0007 - WM_KILLFOCUS = 0x0008 - WM_SHOWWINDOW = 0x0018 - WM_SIZE = 0x0005 - WM_SYSKEYDOWN = 0x0104 - WM_SYSKEYUP = 0x0105 - WM_RBUTTONDOWN = 0x0204 - WM_RBUTTONUP = 0x0205 - WM_TIMER = 0x0113 - WM_UNICHAR = 0x0109 - WM_USER = 0x0400 - WM_GETMINMAXINFO = 0x0024 - + WM_CANCELMODE = 0x001F + WM_CHAR = 0x0102 + WM_CREATE = 0x0001 + WM_DPICHANGED = 0x02E0 + WM_DESTROY = 0x0002 + WM_ERASEBKGND = 0x0014 + WM_KEYDOWN = 0x0100 + WM_KEYUP = 0x0101 + WM_LBUTTONDOWN = 0x0201 + WM_LBUTTONUP = 0x0202 + WM_MBUTTONDOWN = 0x0207 + WM_MBUTTONUP = 0x0208 + WM_MOUSEMOVE = 0x0200 + WM_MOUSEWHEEL = 0x020A + WM_MOUSEHWHEEL = 0x020E + WM_PAINT = 0x000F + WM_CLOSE = 0x0010 + WM_QUIT = 0x0012 + WM_SETCURSOR = 0x0020 + WM_SETFOCUS = 0x0007 + WM_KILLFOCUS = 0x0008 + WM_SHOWWINDOW = 0x0018 + WM_SIZE = 0x0005 + WM_SYSKEYDOWN = 0x0104 + WM_SYSKEYUP = 0x0105 + WM_RBUTTONDOWN = 0x0204 + WM_RBUTTONUP = 0x0205 + WM_TIMER = 0x0113 + WM_UNICHAR = 0x0109 + WM_USER = 0x0400 + WM_GETMINMAXINFO = 0x0024 + WM_WINDOWPOSCHANGED = 0x0047 WS_CLIPCHILDREN = 0x00010000 WS_CLIPSIBLINGS = 0x04000000 WS_MAXIMIZE = 0x01000000 + WS_ICONIC = 0x20000000 WS_VISIBLE = 0x10000000 + WS_OVERLAPPED = 0x00000000 WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX @@ -259,7 +265,7 @@ var ( _DestroyWindow = user32.NewProc("DestroyWindow") _DispatchMessage = user32.NewProc("DispatchMessageW") _EmptyClipboard = user32.NewProc("EmptyClipboard") - _GetClientRect = user32.NewProc("GetClientRect") + _GetWindowRect = user32.NewProc("GetWindowRect") _GetClipboardData = user32.NewProc("GetClipboardData") _GetDC = user32.NewProc("GetDC") _GetDpiForWindow = user32.NewProc("GetDpiForWindow") @@ -370,8 +376,8 @@ func EmptyClipboard() error { return nil } -func GetClientRect(hwnd syscall.Handle, r *Rect) { - _GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r))) +func GetWindowRect(hwnd syscall.Handle, r *Rect) { + _GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r))) issue34474KeepAlive(r) } @@ -683,6 +689,21 @@ func (p WindowPlacement) Rect() Rect { return p.rcNormalPosition } +func (p WindowPlacement) IsMinimized() bool { + return p.showCmd == SW_SHOWMINIMIZED +} + +func (p WindowPlacement) IsMaximized() bool { + return p.showCmd == SW_SHOWMAXIMIZED +} + +func (p *WindowPlacement) Set(Left, Top, Right, Bottom int) { + p.rcNormalPosition.Left = int32(Left) + p.rcNormalPosition.Top = int32(Top) + p.rcNormalPosition.Right = int32(Right) + p.rcNormalPosition.Bottom = int32(Bottom) +} + // issue34474KeepAlive calls runtime.KeepAlive as a // workaround for golang.org/issue/34474. func issue34474KeepAlive(v interface{}) { diff --git a/app/os.go b/app/os.go index b524a357..329f86d0 100644 --- a/app/os.go +++ b/app/os.go @@ -41,6 +41,8 @@ type Config struct { // CustomRenderer is true when the window content is rendered by the // client. CustomRenderer bool + // center is a flag used to center the window. Set by option. + center bool } // ConfigEvent is sent whenever the configuration of a Window changes. @@ -57,8 +59,8 @@ func (c *Config) apply(m unit.Metric, options []Option) { type wakeupEvent struct{} // WindowMode is the window mode (WindowMode.Option sets it). -// -// Supported platforms are macOS, X11, Windows, Android and JS. +// Note that mode can be changed programatically as well as by the user +// clicking on the minimize/maximize buttons on the window's title bar. type WindowMode uint8 const ( @@ -66,20 +68,30 @@ const ( Windowed WindowMode = iota // Fullscreen is the full screen window mode. Fullscreen + // Minimized is for systems where the window can be minimized to an icon. + Minimized + // Maximized is for systems where the window can be made to fill the available monitor area. + Maximized ) +// Option changes the mode of a Window. func (m WindowMode) Option() Option { return func(_ unit.Metric, cnf *Config) { cnf.Mode = m } } +// String returns the mode name. func (m WindowMode) String() string { switch m { case Windowed: return "windowed" case Fullscreen: return "fullscreen" + case Minimized: + return "minimized" + case Maximized: + return "maximized" } return "" } @@ -165,11 +177,6 @@ type driver interface { // Wakeup wakes up the event loop and sends a WakeupEvent. Wakeup() - - // Maximize will make the window as large as possible, but keep the frame decorations. - Maximize() - // Center will place the window at monitor center. - Center() } type windowRendezvous struct { diff --git a/app/os_android.go b/app/os_android.go index 4f46b24f..5540a6c3 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -1261,12 +1261,6 @@ func setNavigationColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) { // Close the window. Not implemented for Android. func (w *window) Close() {} -// Maximize maximizes the window. Not implemented for Android. -func (w *window) Maximize() {} - -// Center the window. Not implemented for Android. -func (w *window) Center() {} - // runOnMain runs a function on the Java main thread. func runOnMain(f func(env *C.JNIEnv)) { go func() { diff --git a/app/os_ios.go b/app/os_ios.go index f6f6dea6..df45fde9 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -336,12 +336,6 @@ func (w *window) SetInputHint(_ key.InputHint) {} // Close the window. Not implemented for iOS. func (w *window) Close() {} -// Maximize the window. Not implemented for iOS. -func (w *window) Maximize() {} - -// Center the window. Not implemented for iOS. -func (w *window) Center() {} - func newWindow(win *callbacks, options []Option) error { mainWindow.in <- windowAndConfig{win, options} return <-mainWindow.errs diff --git a/app/os_js.go b/app/os_js.go index 5debe9b3..55ec2694 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -566,12 +566,6 @@ func (w *window) SetInputHint(mode key.InputHint) { // Close the window. Not implemented for js. func (w *window) Close() {} -// Maximize the window. Not implemented for js. -func (w *window) Maximize() {} - -// Center the window. Not implemented for js. -func (w *window) Center() {} - func (w *window) resize() { w.scale = float32(w.window.Get("devicePixelRatio").Float()) diff --git a/app/os_wayland.go b/app/os_wayland.go index ac7aa172..c715e854 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -1487,12 +1487,6 @@ func (w *window) Close() { w.dead = true } -// Maximize the window. Not implemented for Wayland. -func (w *window) Maximize() {} - -// Center the window. Not implemented for Wayland. -func (w *window) Center() {} - func (w *window) NewContext() (context, error) { var firstErr error if f := newWaylandVulkanContext; f != nil { diff --git a/app/os_windows.go b/app/os_windows.go index 50460d3f..42594bd2 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -108,7 +108,6 @@ func newWindow(window *callbacks, options []Option) error { w.w.SetDriver(w) w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)}) w.Configure(options) - windows.ShowWindow(w.hwnd, windows.SW_SHOWDEFAULT) windows.SetForegroundWindow(w.hwnd) windows.SetFocus(w.hwnd) // Since the window class for the cursor is null, @@ -185,6 +184,43 @@ func createNativeWindow() (*window, error) { return w, nil } +// update() handles changes done by the user, and updates the configuration. +// It reads the window style and size/position and updates w.config. +// If anything has changed it emits a ConfigEvent to notify the application. +func (w *window) update() { + var triggerEvent bool + var r windows.Rect + windows.GetWindowRect(w.hwnd, &r) + size := image.Point{ + X: int(r.Right - r.Left - w.deltas.width), + Y: int(r.Bottom - r.Top - w.deltas.height), + } + if size != w.config.Size { + w.config.Size = size + triggerEvent = true + } + // Check the window mode. + mode := w.config.Mode + p := windows.GetWindowPlacement(w.hwnd) + style := windows.GetWindowLong(w.hwnd) + if style&windows.WS_OVERLAPPEDWINDOW == 0 { + mode = Fullscreen + } else if p.IsMinimized() { + mode = Minimized + } else if p.IsMaximized() { + mode = Maximized + } else { + mode = Windowed + } + if mode != w.config.Mode { + w.config.Mode = mode + triggerEvent = true + } + if triggerEvent { + w.w.Event(ConfigEvent{Config: w.config}) + } +} + func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr { win, exists := winMap.Load(hwnd) if !exists { @@ -280,29 +316,10 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr case windows.WM_SIZE: switch wParam { case windows.SIZE_MINIMIZED: + w.update() w.setStage(system.StagePaused) case windows.SIZE_MAXIMIZED, windows.SIZE_RESTORED: - var triggerEvent bool - // Check the window size change. - var r windows.Rect - windows.GetClientRect(w.hwnd, &r) - size := image.Point{ - X: int(r.Right - r.Left), - Y: int(r.Bottom - r.Top), - } - if size != w.config.Size { - w.config.Size = size - triggerEvent = true - } - // Check the window mode. - mode := w.config.Mode - w.updateWindowMode() - if mode != w.config.Mode { - triggerEvent = true - } - if triggerEvent { - w.w.Event(ConfigEvent{Config: w.config}) - } + w.update() w.setStage(system.StageRunning) } case windows.WM_GETMINMAXINFO: @@ -503,127 +520,44 @@ func (w *window) readClipboard() error { return nil } -func (w *window) updateWindowMode() { - p := windows.GetWindowPlacement(w.hwnd) - r := p.Rect() - mi := windows.GetMonitorInfo(w.hwnd) - if r == mi.Monitor { - w.config.Mode = Fullscreen - } else { - w.config.Mode = Windowed - } -} - func (w *window) Configure(options []Option) { + oldConfig := w.config dpi := windows.GetSystemDPI() - cfg := configForDPI(dpi) - prev := w.config - w.updateWindowMode() - cnf := w.config - cnf.apply(cfg, options) + metric := configForDPI(dpi) + w.config.apply(metric, options) + switch w.config.Mode { + case Minimized: + windows.ShowWindow(w.hwnd, windows.SW_SHOWMINIMIZED) - if cnf.Mode != Fullscreen && prev.Size != cnf.Size { - width := int32(cnf.Size.X) - height := int32(cnf.Size.Y) - w.config.Size = cnf.Size - - // Include the window decorations. - wr := windows.Rect{ - Right: width, - Bottom: height, - } - dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW) - dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE) - windows.AdjustWindowRectEx(&wr, dwStyle, 0, dwExStyle) - - dw, dh := width, height - width = wr.Right - wr.Left - height = wr.Bottom - wr.Top - w.deltas.width = width - dw - w.deltas.height = height - dh - - windows.MoveWindow(w.hwnd, 0, 0, width, height, true) - } - if prev.MinSize != cnf.MinSize { - w.config.MinSize = cnf.MinSize - } - if prev.MaxSize != cnf.MaxSize { - w.config.MaxSize = cnf.MaxSize - } - if prev.Title != cnf.Title { - w.config.Title = cnf.Title - windows.SetWindowText(w.hwnd, cnf.Title) - } - if prev.Mode != cnf.Mode { - w.SetWindowMode(cnf.Mode) - } - if w.config != prev { - w.w.Event(ConfigEvent{Config: w.config}) - } -} - -// Maximize the window. It will have no effect when in fullscreen mode. -func (w *window) Maximize() { - if w.config.Mode == Fullscreen { - return - } - style := windows.GetWindowLong(w.hwnd) - windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW|windows.WS_MAXIMIZE) - mi := windows.GetMonitorInfo(w.hwnd) - windows.SetWindowPos(w.hwnd, 0, - mi.Monitor.Left, mi.Monitor.Top, - mi.Monitor.Right-mi.Monitor.Left, - mi.Monitor.Bottom-mi.Monitor.Top, - windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, - ) -} - -// Center will place window at monitor center. -func (w *window) Center() { - // Make sure that the window is sizeable - style := windows.GetWindowLong(w.hwnd) & (^uintptr(windows.WS_MAXIMIZE)) - windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW) - - // Find with/height including the window decorations. - wr := windows.Rect{ - Right: int32(w.config.Size.X), - Bottom: int32(w.config.Size.Y), - } - dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW) - dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE) - windows.AdjustWindowRectEx(&wr, dwStyle, 0, dwExStyle) - width := wr.Right - wr.Left - height := wr.Bottom - wr.Top - - // Move to center of current monitor - mi := windows.GetMonitorInfo(w.hwnd).Monitor - x := mi.Left + (mi.Right-mi.Left-width)/2 - y := mi.Top + (mi.Bottom-mi.Top-height)/2 - windows.MoveWindow(w.hwnd, x, y, width, height, true) -} - -func (w *window) SetWindowMode(mode WindowMode) { - // https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353 - switch mode { - case Windowed: - if w.placement == nil { - return - } - w.config.Mode = Windowed - windows.SetWindowPlacement(w.hwnd, w.placement) - w.placement = nil - style := windows.GetWindowLong(w.hwnd) + case Maximized: + windows.SetWindowText(w.hwnd, w.config.Title) + style := windows.GetWindowLong(w.hwnd) & (^uintptr(windows.WS_MAXIMIZE)) windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW) - windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, - 0, 0, 0, 0, - windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, - ) - case Fullscreen: - if w.placement != nil { - return + windows.ShowWindow(w.hwnd, windows.SW_SHOWMAXIMIZED) + + case Windowed: + var r windows.Rect + windows.SetWindowText(w.hwnd, w.config.Title) + windows.GetWindowRect(w.hwnd, &r) + style := windows.GetWindowLong(w.hwnd) & (^uintptr(windows.WS_MAXIMIZE)) + windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW) + width := int32(w.config.Size.X) + height := int32(w.config.Size.Y) + + if w.config.center { + // Calculate center position on current monitor + mi := windows.GetMonitorInfo(w.hwnd).Monitor + r.Left = mi.Left + (mi.Right-mi.Left-width)/2 + r.Top = mi.Top + (mi.Bottom-mi.Top-height)/2 + // Centering is done only once. + w.config.center = false } - w.config.Mode = Fullscreen - w.placement = windows.GetWindowPlacement(w.hwnd) + + windows.SetWindowPos(w.hwnd, 0, r.Left, r.Top, width, height, + windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED) + windows.ShowWindow(w.hwnd, windows.SW_SHOWNORMAL) + + case Fullscreen: style := windows.GetWindowLong(w.hwnd) windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style&^windows.WS_OVERLAPPEDWINDOW) mi := windows.GetMonitorInfo(w.hwnd) @@ -633,6 +567,12 @@ func (w *window) SetWindowMode(mode WindowMode) { mi.Monitor.Bottom-mi.Monitor.Top, windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, ) + windows.ShowWindow(w.hwnd, windows.SW_SHOWNORMAL) + } + + // A config event is sent to the main event loop whenever the configuration is changed + if oldConfig.Mode != w.config.Mode || oldConfig.Size != w.config.Size { + w.w.Event(ConfigEvent{Config: w.config}) } } diff --git a/app/window.go b/app/window.go index 40acc5a5..d0764a0c 100644 --- a/app/window.go +++ b/app/window.go @@ -321,22 +321,6 @@ func (w *Window) Close() { }) } -// Maximize the window. -// Note: only implemented on Windows, macOS and X11. -func (w *Window) Maximize() { - w.driverDefer(func(d driver) { - d.Maximize() - }) -} - -// Center the window. -// Note: only implemented on Windows, macOS and X11. -func (w *Window) Center() { - w.driverDefer(func(d driver) { - d.Center() - }) -} - // Run f in the same thread as the native window event loop, and wait for f to // return or the window to close. Run is guaranteed not to deadlock if it is // invoked during the handling of a ViewEvent, system.FrameEvent, @@ -703,8 +687,7 @@ func Title(t string) Option { } } -// Size sets the size of the window. The option is ignored -// in Fullscreen mode. +// Size sets the size of the window. The mode will be changed to Windowed. func Size(w, h unit.Value) Option { if w.V <= 0 { panic("width must be larger than or equal to 0") @@ -713,6 +696,7 @@ func Size(w, h unit.Value) Option { panic("height must be larger than or equal to 0") } return func(m unit.Metric, cnf *Config) { + cnf.Mode = Windowed cnf.Size = image.Point{ X: m.Px(w), Y: m.Px(h), @@ -720,6 +704,15 @@ func Size(w, h unit.Value) Option { } } +// Center is an option to center the window on the screen. +// The option is ignored in Fullscreen mode. +func Centered() Option { + return func(m unit.Metric, cnf *Config) { + // Set the flag so the driver can later do the actual centering. + cnf.center = true + } +} + // MaxSize sets the maximum size of the window. func MaxSize(w, h unit.Value) Option { if w.V <= 0 {