From abd6e8f9cdd4905e027750dc9d38a1785a3c22ea Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 6 Apr 2021 18:46:29 +0200 Subject: [PATCH] app: support changing Window options at runtime A Window can now be requested to change its options after it has been started via its Option method. All options are supported on macOS, Windows and X11. On Wayland, only the Size and Title options can be changed at runtime. Signed-off-by: pierre --- app/internal/wm/os_macos.go | 5 ++- app/internal/wm/os_wayland.go | 15 ++++++++- app/internal/wm/os_windows.go | 15 +++++++++ app/internal/wm/os_x11.go | 63 +++++++++++++++++++++++++++++------ app/window.go | 11 ++++++ 5 files changed, 95 insertions(+), 14 deletions(-) diff --git a/app/internal/wm/os_macos.go b/app/internal/wm/os_macos.go index d088a924..b9266d83 100644 --- a/app/internal/wm/os_macos.go +++ b/app/internal/wm/os_macos.go @@ -172,11 +172,10 @@ func (w *window) Option(opts *Options) { func (w *window) SetWindowMode(mode WindowMode) { switch mode { case w.mode: - return - case Fullscreen: + case Windowed, Fullscreen: C.gio_toggleFullScreen(w.window) + w.mode = mode } - w.mode = mode } func (w *window) SetCursor(name pointer.CursorName) { diff --git a/app/internal/wm/os_wayland.go b/app/internal/wm/os_wayland.go index 1e8c59d9..03790959 100644 --- a/app/internal/wm/os_wayland.go +++ b/app/internal/wm/os_wayland.go @@ -181,6 +181,7 @@ type window struct { mu sync.Mutex animating bool + opts *Options needAck bool // The most recent configure serial waiting to be ack'ed. serial C.uint32_t @@ -357,7 +358,7 @@ func (d *wlDisplay) createNativeWindow(opts *Options) (*window, error) { C.xdg_surface_add_listener(w.wmSurf, &C.gio_xdg_surface_listener, unsafe.Pointer(w.surf)) C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, unsafe.Pointer(w.surf)) - w.Option(opts) + w.setOptions(opts) if d.decor != nil { // Request server side decorations. @@ -910,6 +911,13 @@ func (w *window) WriteClipboard(s string) { } func (w *window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + w.disp.wakeup() +} + +func (w *window) setOptions(opts *Options) { _, _, cfg := w.config() if o := opts.Size; o != nil { w.width = cfg.Px(o.Width) @@ -1143,8 +1151,10 @@ func (w *window) process() { w.mu.Lock() readClipboard := w.readClipboard writeClipboard := w.writeClipboard + opts := w.opts w.readClipboard = false w.writeClipboard = nil + w.opts = nil w.mu.Unlock() if readClipboard { r, err := w.disp.readClipboard() @@ -1163,6 +1173,9 @@ func (w *window) process() { if writeClipboard != nil { w.disp.writeClipboard([]byte(*writeClipboard)) } + if opts != nil { + w.setOptions(opts) + } // pass false to skip unnecessary drawing. w.draw(false) } diff --git a/app/internal/wm/os_windows.go b/app/internal/wm/os_windows.go index 611c7cb0..05d440ea 100644 --- a/app/internal/wm/os_windows.go +++ b/app/internal/wm/os_windows.go @@ -66,6 +66,7 @@ type window struct { const ( _WM_REDRAW = windows.WM_USER + iota _WM_CURSOR + _WM_OPTION ) type gpuAPI struct { @@ -317,6 +318,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr windows.SetCursor(w.cursor) return windows.TRUE } + case _WM_OPTION: + w.setOptions() } return windows.DefWindowProc(hwnd, msg, wParam, lParam) @@ -520,6 +523,18 @@ func (w *window) readClipboard() error { } func (w *window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + if err := windows.PostMessage(w.hwnd, _WM_OPTION, 0, 0); err != nil { + panic(err) + } +} + +func (w *window) setOptions() { + w.mu.Lock() + opts := w.opts + w.mu.Unlock() if o := opts.Size; o != nil { dpi := windows.GetSystemDPI() cfg := configForDPI(dpi) diff --git a/app/internal/wm/os_x11.go b/app/internal/wm/os_x11.go index b921d468..75b956e2 100644 --- a/app/internal/wm/os_x11.go +++ b/app/internal/wm/os_x11.go @@ -89,6 +89,7 @@ type x11Window struct { mu sync.Mutex animating bool + opts *Options pointerBtns pointer.Buttons @@ -98,6 +99,7 @@ type x11Window struct { content []byte } cursor pointer.CursorName + mode WindowMode } func (w *x11Window) SetAnimating(anim bool) { @@ -124,22 +126,33 @@ func (w *x11Window) WriteClipboard(s string) { } func (w *x11Window) Option(opts *Options) { - dpy := w.x - win := w.xw - cfg := w.cfg + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + w.wakeup() +} + +func (w *x11Window) setOptions() { + w.mu.Lock() + opts := w.opts + w.opts = nil + w.mu.Unlock() + if opts == nil { + return + } var shints C.XSizeHints if o := opts.MinSize; o != nil { - shints.min_width = C.int(cfg.Px(o.Width)) - shints.min_height = C.int(cfg.Px(o.Height)) + shints.min_width = C.int(w.cfg.Px(o.Width)) + shints.min_height = C.int(w.cfg.Px(o.Height)) shints.flags = C.PMinSize } if o := opts.MaxSize; o != nil { - shints.max_width = C.int(cfg.Px(o.Width)) - shints.max_height = C.int(cfg.Px(o.Height)) + shints.max_width = C.int(w.cfg.Px(o.Width)) + shints.max_height = C.int(w.cfg.Px(o.Height)) shints.flags = shints.flags | C.PMaxSize } if shints.flags != 0 { - C.XSetWMNormalHints(dpy, win, &shints) + C.XSetWMNormalHints(w.x, w.xw, &shints) } var title string @@ -148,9 +161,9 @@ func (w *x11Window) Option(opts *Options) { } ctitle := C.CString(title) defer C.free(unsafe.Pointer(ctitle)) - C.XStoreName(dpy, win, ctitle) + C.XStoreName(w.x, w.xw, ctitle) // set _NET_WM_NAME as well for UTF-8 support in window title. - C.XSetTextProperty(dpy, win, + C.XSetTextProperty(w.x, w.xw, &C.XTextProperty{ value: (*C.uchar)(unsafe.Pointer(ctitle)), encoding: w.atoms.utf8string, @@ -190,6 +203,8 @@ func (w *x11Window) SetCursor(name pointer.CursorName) { func (w *x11Window) SetWindowMode(mode WindowMode) { switch mode { + case w.mode: + return case Windowed: C.XDeleteProperty(w.x, w.xw, w.atoms.wmStateFullscreen) case Fullscreen: @@ -197,7 +212,34 @@ func (w *x11Window) SetWindowMode(mode WindowMode) { 32, C.PropModeReplace, (*C.uchar)(unsafe.Pointer(&w.atoms.wmStateFullscreen)), 1, ) + default: + return } + w.mode = mode + // "A Client wishing to change the state of a window MUST send + // a _NET_WM_STATE client message to the root window (see below)." + var xev C.XEvent + ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) + *ev = C.XClientMessageEvent{ + _type: C.ClientMessage, + display: w.x, + window: w.xw, + message_type: w.atoms.wmState, + format: 32, + } + arr := (*[5]C.long)(unsafe.Pointer(&ev.data)) + arr[0] = 2 // _NET_WM_STATE_TOGGLE + arr[1] = C.long(w.atoms.wmStateFullscreen) + arr[2] = 0 + arr[3] = 1 // application + arr[4] = 0 + C.XSendEvent( + w.x, + C.XDefaultRootWindow(w.x), // MUST be the root window + C.False, + C.SubstructureNotifyMask|C.SubstructureRedirectMask, + &xev, + ) } func (w *x11Window) ShowTextInput(show bool) {} @@ -287,6 +329,7 @@ loop: } } } + w.setOptions() // Clear notifications. for { _, err := syscall.Read(w.notify.read, buf) diff --git a/app/window.go b/app/window.go index e8f691a9..20ee8b07 100644 --- a/app/window.go +++ b/app/window.go @@ -211,6 +211,17 @@ func (w *Window) Invalidate() { } } +// Option applies the options to the window. +func (w *Window) Option(opts ...Option) { + go w.driverDo(func() { + o := new(wm.Options) + for _, opt := range opts { + opt(o) + } + w.driver.Option(o) + }) +} + // ReadClipboard initiates a read of the clipboard in the form // of a clipboard.Event. Multiple reads may be coalesced // to a single event.