From 238dd1aa863ef9fc5a693a1a40ae7fc1f091d907 Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 23 Mar 2021 15:39:25 +0100 Subject: [PATCH] app: added support for fullscreen mode The option field WindowMode allows changing the window mode of an application in either Windowed or Fullscreen. Only macOS, Windows and X11 platforms are currently supported. Updates gio#89. Signed-off-by: pierre --- app/internal/windows/windows.go | 69 +++++++++++++++++++++++++++++++++ app/internal/wm/os_macos.go | 13 +++++++ app/internal/wm/os_macos.m | 5 +++ app/internal/wm/os_windows.go | 36 +++++++++++++++++ app/internal/wm/os_x11.go | 25 +++++++++++- app/internal/wm/window.go | 8 ++++ app/window.go | 16 ++++++++ 7 files changed, 171 insertions(+), 1 deletion(-) diff --git a/app/internal/windows/windows.go b/app/internal/windows/windows.go index 17ce6bb1..1601172e 100644 --- a/app/internal/windows/windows.go +++ b/app/internal/windows/windows.go @@ -54,6 +54,23 @@ type MinMaxInfo struct { PtMaxTrackSize Point } +type WindowPlacement struct { + length uint32 + flags uint32 + showCmd uint32 + ptMinPosition Point + ptMaxPosition Point + rcNormalPosition Rect + rcDevice Rect +} + +type MonitorInfo struct { + cbSize uint32 + Monitor Rect + WorkArea Rect + Flags uint32 +} + const ( TRUE = 1 @@ -63,6 +80,9 @@ const ( CW_USEDEFAULT = -2147483648 + GWL_STYLE = ^(uint32(16) - 1) // -16 + HWND_TOPMOST = ^(uint32(1) - 1) // -1 + HTCLIENT = 1 IDC_ARROW = 32512 @@ -87,6 +107,12 @@ const ( SW_SHOWDEFAULT = 10 + SWP_FRAMECHANGED = 0x0020 + SWP_NOMOVE = 0x0002 + SWP_NOOWNERZORDER = 0x0200 + SWP_NOSIZE = 0x0001 + SWP_NOZORDER = 0x0004 + USER_TIMER_MINIMUM = 0x0000000A VK_CONTROL = 0x11 @@ -237,10 +263,14 @@ var ( _GetKeyState = user32.NewProc("GetKeyState") _GetMessage = user32.NewProc("GetMessageW") _GetMessageTime = user32.NewProc("GetMessageTime") + _GetMonitorInfo = user32.NewProc("GetMonitorInfoW") + _GetWindowLong = user32.NewProc("GetWindowLongPtrW") + _GetWindowPlacement = user32.NewProc("GetWindowPlacement") _KillTimer = user32.NewProc("KillTimer") _LoadCursor = user32.NewProc("LoadCursorW") _LoadImage = user32.NewProc("LoadImageW") _MonitorFromPoint = user32.NewProc("MonitorFromPoint") + _MonitorFromWindow = user32.NewProc("MonitorFromWindow") _MsgWaitForMultipleObjectsEx = user32.NewProc("MsgWaitForMultipleObjectsEx") _OpenClipboard = user32.NewProc("OpenClipboard") _PeekMessage = user32.NewProc("PeekMessageW") @@ -258,6 +288,9 @@ var ( _SetFocus = user32.NewProc("SetFocus") _SetProcessDPIAware = user32.NewProc("SetProcessDPIAware") _SetTimer = user32.NewProc("SetTimer") + _SetWindowLong = user32.NewProc("SetWindowLongPtrW") + _SetWindowPos = user32.NewProc("SetWindowPos") + _SetWindowPlacement = user32.NewProc("SetWindowPlacement") _TranslateMessage = user32.NewProc("TranslateMessage") _UnregisterClass = user32.NewProc("UnregisterClassW") _UpdateWindow = user32.NewProc("UpdateWindow") @@ -417,6 +450,42 @@ func GetWindowDPI(hwnd syscall.Handle) int { } } +func GetWindowPlacement(hwnd syscall.Handle) *WindowPlacement { + var wp WindowPlacement + wp.length = uint32(unsafe.Sizeof(wp)) + _GetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&wp))) + return &wp +} + +func GetMonitorInfo(hwnd syscall.Handle) MonitorInfo { + var mi MonitorInfo + mi.cbSize = uint32(unsafe.Sizeof(mi)) + v, _, _ := _MonitorFromWindow.Call(uintptr(hwnd), MONITOR_DEFAULTTOPRIMARY) + _GetMonitorInfo.Call(v, uintptr(unsafe.Pointer(&mi))) + return mi +} + +func GetWindowLong(hwnd syscall.Handle) (style uintptr) { + style, _, _ = _GetWindowLong.Call(uintptr(hwnd), uintptr(GWL_STYLE)) + return +} + +func SetWindowLong(hwnd syscall.Handle, idx uint32, style uintptr) { + _SetWindowLong.Call(uintptr(hwnd), uintptr(idx), style) +} + +func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) { + _SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp))) +} + +func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) { + _SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter), + uintptr(x), uintptr(y), + uintptr(dx), uintptr(dy), + style, + ) +} + func GlobalAlloc(size int) (syscall.Handle, error) { r, _, err := _GlobalAlloc.Call(GHND, uintptr(size)) if r == 0 { diff --git a/app/internal/wm/os_macos.go b/app/internal/wm/os_macos.go index 09d9d297..68ead7b0 100644 --- a/app/internal/wm/os_macos.go +++ b/app/internal/wm/os_macos.go @@ -41,6 +41,7 @@ __attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void); __attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length); __attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_toggleFullScreen(CFTypeRef windowRef); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); __attribute__ ((visibility ("hidden"))) void gio_makeKeyAndOrderFront(CFTypeRef windowRef); __attribute__ ((visibility ("hidden"))) NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft); @@ -62,6 +63,7 @@ type window struct { cursor pointer.CursorName scale float32 + mode WindowMode } // viewMap is the mapping from Cocoa NSViews to Go windows. @@ -124,6 +126,16 @@ func (w *window) WriteClipboard(s string) { }) } +func (w *window) SetWindowMode(mode WindowMode) { + switch mode { + case w.mode: + return + case Fullscreen: + C.gio_toggleFullScreen(w.window) + } + w.mode = mode +} + func (w *window) SetCursor(name pointer.CursorName) { w.cursor = windowSetCursor(w.cursor, name) } @@ -353,6 +365,7 @@ func NewWindow(win Callbacks, opts *Options) error { } nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft) C.gio_makeKeyAndOrderFront(w.window) + w.SetWindowMode(opts.WindowMode) }) return <-errch } diff --git a/app/internal/wm/os_macos.m b/app/internal/wm/os_macos.m index fa5a98e0..24c906cb 100644 --- a/app/internal/wm/os_macos.m +++ b/app/internal/wm/os_macos.m @@ -167,6 +167,11 @@ void gio_makeKeyAndOrderFront(CFTypeRef windowRef) { [window makeKeyAndOrderFront:nil]; } +void gio_toggleFullScreen(CFTypeRef windowRef) { + NSWindow *window = (__bridge NSWindow *)windowRef; + [window toggleFullScreen:nil]; +} + CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) { @autoreleasepool { NSRect rect = NSMakeRect(0, 0, width, height); diff --git a/app/internal/wm/os_windows.go b/app/internal/wm/os_windows.go index a59fdd8e..f50fa43b 100644 --- a/app/internal/wm/os_windows.go +++ b/app/internal/wm/os_windows.go @@ -52,6 +52,9 @@ type window struct { cursorIn bool cursor syscall.Handle + // placement saves the previous window position when in full screen mode. + placement *windows.WindowPlacement + mu sync.Mutex animating bool @@ -119,6 +122,7 @@ func NewWindow(window Callbacks, opts *Options) error { // Since the window class for the cursor is null, // set it here to show the cursor. w.SetCursor(pointer.CursorDefault) + w.SetWindowMode(opts.WindowMode) if err := w.loop(); err != nil { panic(err) } @@ -524,6 +528,38 @@ func (w *window) readClipboard() error { return nil } +func (w *window) SetWindowMode(mode WindowMode) { + // https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353 + switch mode { + case Windowed: + if w.placement == nil { + return + } + windows.SetWindowPlacement(w.hwnd, w.placement) + w.placement = nil + style := windows.GetWindowLong(w.hwnd) + 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 + } + w.placement = windows.GetWindowPlacement(w.hwnd) + style := windows.GetWindowLong(w.hwnd) + windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style&^windows.WS_OVERLAPPEDWINDOW) + 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, + ) + } +} + func (w *window) WriteClipboard(s string) { w.writeClipboard(s) } diff --git a/app/internal/wm/os_x11.go b/app/internal/wm/os_x11.go index edb568e0..a5a25c66 100644 --- a/app/internal/wm/os_x11.go +++ b/app/internal/wm/os_x11.go @@ -71,6 +71,12 @@ type x11Window struct { atom C.Atom // "GTK_TEXT_BUFFER_CONTENTS" gtk_text_buffer_contents C.Atom + // "_NET_WM_NAME" + wmName C.Atom + // "_NET_WM_STATE" + wmState C.Atom + // _NET_WM_STATE_FULLSCREEN" + wmStateFullscreen C.Atom } stage system.Stage cfg unit.Metric @@ -141,6 +147,18 @@ func (w *x11Window) SetCursor(name pointer.CursorName) { C.XDefineCursor(w.x, w.xw, c) } +func (w *x11Window) SetWindowMode(mode WindowMode) { + switch mode { + case Windowed: + C.XDeleteProperty(w.x, w.xw, w.atoms.wmStateFullscreen) + case Fullscreen: + C.XChangeProperty(w.x, w.xw, w.atoms.wmState, C.XA_ATOM, + 32, C.PropModeReplace, + (*C.uchar)(unsafe.Pointer(&w.atoms.wmStateFullscreen)), 1, + ) + } +} + func (w *x11Window) ShowTextInput(show bool) {} // Close the window. @@ -612,6 +630,9 @@ func newX11Window(gioWin Callbacks, opts *Options) error { w.atoms.clipboardContent = w.atom("CLIPBOARD_CONTENT", false) w.atoms.atom = w.atom("ATOM", false) w.atoms.targets = w.atom("TARGETS", false) + w.atoms.wmName = w.atom("_NET_WM_NAME", false) + w.atoms.wmState = w.atom("_NET_WM_STATE", false) + w.atoms.wmStateFullscreen = w.atom("_NET_WM_STATE_FULLSCREEN", false) // set the name ctitle := C.CString(opts.Title) @@ -625,11 +646,13 @@ func newX11Window(gioWin Callbacks, opts *Options) error { format: 8, nitems: C.ulong(len(opts.Title)), }, - w.atom("_NET_WM_NAME", false)) + w.atoms.wmName) // extensions C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1) + w.SetWindowMode(opts.WindowMode) + // make the window visible on the screen C.XMapWindow(dpy, win) diff --git a/app/internal/wm/window.go b/app/internal/wm/window.go index 42d1df75..b33a1b66 100644 --- a/app/internal/wm/window.go +++ b/app/internal/wm/window.go @@ -19,8 +19,16 @@ type Options struct { MinWidth, MinHeight unit.Value MaxWidth, MaxHeight unit.Value Title string + WindowMode WindowMode } +type WindowMode uint8 + +const ( + Windowed WindowMode = iota + Fullscreen +) + type FrameEvent struct { system.FrameEvent diff --git a/app/window.go b/app/window.go index df8a0179..2de6c1ef 100644 --- a/app/window.go +++ b/app/window.go @@ -444,6 +444,22 @@ func (q *queue) Events(k event.Tag) []event.Event { return q.q.Events(k) } +const ( + // Windowed is the normal window mode with OS specific window decorations. + Windowed = wm.Windowed + // Fullscreen is the full screen window mode. + Fullscreen = wm.Fullscreen +) + +// WindowMode sets the window mode. +// +// Supported platforms are macOS, X11 and Windows. +func WindowMode(mode wm.WindowMode) Option { + return func(opts *wm.Options) { + opts.WindowMode = mode + } +} + // Title sets the title of the wm. func Title(t string) Option { return func(opts *wm.Options) {