From 5ce1e982827ba0ab4af963b8719701aab8681ac9 Mon Sep 17 00:00:00 2001 From: Pierre Curto Date: Sun, 23 Jan 2022 21:54:14 +0100 Subject: [PATCH] app: use material.Decorations on undecorated platforms This patch implements a mechanism for customizing window decorations. If a window is configured with app.Decorated(true), then the widget/material.Decorations are applied. On Wayland, the option is automatically set when the server does not provide window decorations. Server side decorations are no longer requested. The Decorated flag is set according to the server's requests. Wayland is now the default driver for UNIX platforms. References: https://todo.sr.ht/~eliasnaur/gio/318 Signed-off-by: Pierre Curto --- app/os.go | 14 +++++ app/os_android.go | 8 +++ app/os_ios.go | 12 +++- app/os_js.go | 8 +++ app/os_macos.go | 7 +++ app/os_unix.go | 2 +- app/os_wayland.c | 5 ++ app/os_wayland.go | 103 ++++++++++++++++++++++++++++++--- app/os_windows.go | 4 ++ app/os_x11.go | 7 +++ app/window.go | 86 ++++++++++++++++++++++++++- widget/material/decorations.go | 6 +- 12 files changed, 246 insertions(+), 16 deletions(-) diff --git a/app/os.go b/app/os.go index 329f86d09..e44b0d514 100644 --- a/app/os.go +++ b/app/os.go @@ -43,6 +43,8 @@ type Config struct { CustomRenderer bool // center is a flag used to center the window. Set by option. center bool + // Decorated reports whether window decorations are provided automatically. + Decorated bool } // ConfigEvent is sent whenever the configuration of a Window changes. @@ -177,6 +179,9 @@ type driver interface { // Wakeup wakes up the event loop and sends a WakeupEvent. Wakeup() + + // Perform actions on the window. + Perform(system.Action) } type windowRendezvous struct { @@ -218,3 +223,12 @@ func newWindowRendezvous() *windowRendezvous { func (wakeupEvent) ImplementsEvent() {} func (ConfigEvent) ImplementsEvent() {} + +func walkActions(actions system.Action, do func(system.Action)) { + for a := system.Action(1); actions != 0; a <<= 1 { + if actions&a != 0 { + actions &^= a + do(a) + } + } +} diff --git a/app/os_android.go b/app/os_android.go index f835cbdb6..f35294222 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -1166,6 +1166,9 @@ func (w *window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(unit.Metric{}, options) + // Decorations are never disabled. + cnf.Decorated = true + if prev.Orientation != cnf.Orientation { w.config.Orientation = cnf.Orientation setOrientation(env, w.view, cnf.Orientation) @@ -1188,12 +1191,17 @@ func (w *window) Configure(options []Option) { w.config.Mode = Windowed } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.callbacks.Event(ConfigEvent{Config: w.config}) } }) } +func (w *window) Perform(system.Action) {} + func (w *window) Raise() {} func (w *window) SetCursor(name pointer.CursorName) { diff --git a/app/os_ios.go b/app/os_ios.go index 3e8c3ba17..f6fc61473 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -98,6 +98,7 @@ type window struct { visible bool cursor pointer.CursorName + config Config pointerMap []C.CFTypeRef } @@ -273,7 +274,16 @@ func (w *window) WriteClipboard(s string) { C.writeClipboard(chars, C.NSUInteger(len(u16))) } -func (w *window) Configure([]Option) {} +func (w *window) Configure([]Option) { + prev := w.config + // Decorations are never disabled. + w.config.Decorated = true + if w.config != prev { + w.w.Event(ConfigEvent{Config: w.config}) + } +} + +func (w *window) Perform(system.Action) {} func (w *window) Raise() {} diff --git a/app/os_js.go b/app/os_js.go index 23e49a993..728ca14a2 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -513,6 +513,9 @@ func (w *window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(unit.Metric{}, options) + // Decorations are never disabled. + cnf.Decorated = true + if prev.Title != cnf.Title { w.config.Title = cnf.Title w.document.Set("title", cnf.Title) @@ -528,11 +531,16 @@ func (w *window) Configure(options []Option) { w.config.Orientation = cnf.Orientation w.orientation(cnf.Orientation) } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } } +func (w *window) Perform(system.Action) {} + func (w *window) Raise() {} func (w *window) SetCursor(name pointer.CursorName) { diff --git a/app/os_macos.go b/app/os_macos.go index 10feab26c..67b17b1e9 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -261,6 +261,8 @@ func (w *window) Configure(options []Option) { cnf.Size = cnf.Size.Div(int(screenScale)) cnf.MinSize = cnf.MinSize.Div(int(screenScale)) cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) + // Decorations are never disabled. + cnf.Decorated = true switch cnf.Mode { case Fullscreen: @@ -325,6 +327,9 @@ func (w *window) Configure(options []Option) { C.setScreenFrame(w.window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } @@ -339,6 +344,8 @@ func (w *window) setTitle(prev, cnf Config) { } } +func (w *window) Perform(system.Action) {} + func (w *window) SetCursor(name pointer.CursorName) { w.cursor = windowSetCursor(w.cursor, name) } diff --git a/app/os_unix.go b/app/os_unix.go index ee831b308..c834b6e1f 100644 --- a/app/os_unix.go +++ b/app/os_unix.go @@ -29,7 +29,7 @@ var wlDriver, x11Driver windowDriver func newWindow(window *callbacks, options []Option) error { var errFirst error - for _, d := range []windowDriver{x11Driver, wlDriver} { + for _, d := range []windowDriver{wlDriver, x11Driver} { if d == nil { continue } diff --git a/app/os_wayland.c b/app/os_wayland.c index b137e1f9e..a9752aaa0 100644 --- a/app/os_wayland.c +++ b/app/os_wayland.c @@ -6,6 +6,7 @@ #include #include "wayland_xdg_shell.h" +#include "wayland_xdg_decoration.h" #include "wayland_text_input.h" #include "_cgo_export.h" @@ -29,6 +30,10 @@ const struct xdg_toplevel_listener gio_xdg_toplevel_listener = { .close = gio_onToplevelClose, }; +const struct zxdg_toplevel_decoration_v1_listener gio_zxdg_toplevel_decoration_v1_listener = { + .configure = gio_onToplevelDecorationConfigure, +}; + static void xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) { xdg_wm_base_pong(wm, serial); } diff --git a/app/os_wayland.go b/app/os_wayland.go index ed103694c..cb22d7db0 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -64,6 +64,7 @@ extern const struct wl_registry_listener gio_registry_listener; extern const struct wl_surface_listener gio_surface_listener; extern const struct xdg_surface_listener gio_xdg_surface_listener; extern const struct xdg_toplevel_listener gio_xdg_toplevel_listener; +extern const struct zxdg_toplevel_decoration_v1_listener gio_zxdg_toplevel_decoration_v1_listener; extern const struct xdg_wm_base_listener gio_xdg_wm_base_listener; extern const struct wl_callback_listener gio_callback_listener; extern const struct wl_output_listener gio_output_listener; @@ -149,6 +150,7 @@ type repeatState struct { type window struct { w *callbacks disp *wlDisplay + seat *wlSeat surf *C.struct_wl_surface wmSurf *C.struct_xdg_surface topLvl *C.struct_xdg_toplevel @@ -188,9 +190,10 @@ type window struct { newScale bool scale int // size is the unscaled window size (unlike config.Size which is scaled). - size image.Point - config Config - wsize image.Point // window config size before going fullscreen + size image.Point + config Config + wsize image.Point // window config size before going fullscreen or maximized + inCompositor bool // window is moving or being resized wakeups chan struct{} } @@ -212,7 +215,7 @@ type wlOutput struct { } // callbackMap maps Wayland native handles to corresponding Go -// references. It is necessary because the the Wayland client API +// references. It is necessary because the Wayland client API // forces the use of callbacks and storing pointers to Go values // in C is forbidden. var callbackMap sync.Map @@ -369,9 +372,8 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) { C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, unsafe.Pointer(w.surf)) if d.decor != nil { - // Request server side decorations. w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(d.decor, w.topLvl) - C.zxdg_toplevel_decoration_v1_set_mode(w.decor, C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) + C.zxdg_toplevel_decoration_v1_add_listener(w.decor, &C.gio_zxdg_toplevel_decoration_v1_listener, unsafe.Pointer(w.surf)) } w.updateOpaqueRegion() return w, nil @@ -499,6 +501,24 @@ func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, w.size = image.Pt(int(width), int(height)) w.updateOpaqueRegion() } + w.needAck = true +} + +//export gio_onToplevelDecorationConfigure +func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_toplevel_decoration_v1, mode C.uint32_t) { + w := callbackLoad(data).(*window) + decorated := w.config.Decorated + switch mode { + case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: + w.config.Decorated = false + case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: + w.config.Decorated = true + } + if decorated != w.config.Decorated { + w.w.Event(ConfigEvent{Config: w.config}) + } + w.needAck = true + w.draw(true) } //export gio_onOutputMode @@ -772,15 +792,22 @@ func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, seria s := callbackLoad(data).(*wlSeat) s.serial = serial w := callbackLoad(unsafe.Pointer(surf)).(*window) + w.seat = s s.pointerFocus = w w.setCursor(pointer, serial) w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)} } //export gio_onPointerLeave -func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.uint32_t, surface *C.struct_wl_surface) { +func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.uint32_t, surf *C.struct_wl_surface) { + w := callbackLoad(unsafe.Pointer(surf)).(*window) + w.seat = nil s := callbackLoad(data).(*wlSeat) s.serial = serial + if w.inCompositor { + w.inCompositor = false + w.w.Event(pointer.Event{Type: pointer.Cancel}) + } } //export gio_onPointerMotion @@ -818,6 +845,8 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, case 0: w.pointerBtns &^= btn typ = pointer.Release + // Move or resize gestures no longer applies. + w.inCompositor = false case 1: w.pointerBtns |= btn typ = pointer.Press @@ -978,6 +1007,9 @@ func (w *window) Configure(options []Option) { C.xdg_toplevel_set_max_size(w.topLvl, C.int32_t(cnf.MaxSize.X), C.int32_t(cnf.MaxSize.Y)) } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } @@ -992,6 +1024,63 @@ func (w *window) setTitle(prev, cnf Config) { } } +func (w *window) Perform(actions system.Action) { + walkActions(actions, func(action system.Action) { + switch action { + case system.ActionMinimize: + w.Configure([]Option{Minimized.Option()}) + case system.ActionMaximize: + w.Configure([]Option{Maximized.Option()}) + case system.ActionUnmaximize: + w.Configure([]Option{Windowed.Option()}) + case system.ActionClose: + w.Close() + case system.ActionMove: + w.move() + default: + w.resize(action) + } + }) +} + +func (w *window) move() { + if !w.inCompositor && w.seat != nil { + w.inCompositor = true + s := w.seat + C.xdg_toplevel_move(w.topLvl, s.seat, s.serial) + } +} + +func (w *window) resize(a system.Action) { + if w.inCompositor || w.seat == nil { + return + } + var edge int + switch a { + case system.ActionResizeNorth: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_TOP + case system.ActionResizeSouth: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM + case system.ActionResizeEast: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_LEFT + case system.ActionResizeWest: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_RIGHT + case system.ActionResizeNorthWest: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT + case system.ActionResizeNorthEast: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT + case system.ActionResizeSouthEast: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT + case system.ActionResizeSouthWest: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT + default: + return + } + w.inCompositor = true + s := w.seat + C.xdg_toplevel_resize(w.topLvl, s.seat, s.serial, C.uint32_t(edge)) +} + func (w *window) Raise() { // NB. there is no way for a minimized window to be unminimized. // https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_minimized diff --git a/app/os_windows.go b/app/os_windows.go index 9a9028c77..f453c7d40 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -531,6 +531,8 @@ func (w *window) Configure(options []Option) { metric := configForDPI(dpi) w.config.apply(metric, options) windows.SetWindowText(w.hwnd, w.config.Title) + // Decorations are never disabled. + w.config.Decorated = true switch w.config.Mode { case Minimized: @@ -691,6 +693,8 @@ func (w *window) Close() { windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0) } +func (w *window) Perform(system.Action) {} + func (w *window) Raise() { windows.SetForegroundWindow(w.hwnd) windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, 0, 0, 0, 0, diff --git a/app/os_x11.go b/app/os_x11.go index c95e5d95e..90e1c3943 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -164,6 +164,8 @@ func (w *x11Window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(w.metric, options) + // Decorations are never disabled. + cnf.Decorated = true switch cnf.Mode { case Fullscreen: @@ -245,6 +247,9 @@ func (w *x11Window) Configure(options []Option) { C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y)) } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } @@ -268,6 +273,8 @@ func (w *x11Window) setTitle(prev, cnf Config) { } } +func (w *x11Window) Perform(system.Action) {} + func (w *x11Window) Raise() { var xev C.XEvent ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) diff --git a/app/window.go b/app/window.go index d4f450a6d..ebb3a3fa4 100644 --- a/app/window.go +++ b/app/window.go @@ -11,14 +11,18 @@ import ( "time" "gioui.org/f32" + "gioui.org/font/gofont" "gioui.org/gpu" + "gioui.org/internal/ops" "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/io/profile" "gioui.org/io/router" "gioui.org/io/system" + "gioui.org/layout" "gioui.org/op" "gioui.org/unit" + "gioui.org/widget/material" _ "gioui.org/app/internal/log" ) @@ -59,8 +63,13 @@ type Window struct { nextFrame time.Time delayedDraw *time.Timer - queue queue - cursor pointer.CursorName + queue queue + cursor pointer.CursorName + decorations struct { + op.Ops + Config + *material.Decorations + } callbacks callbacks @@ -578,9 +587,16 @@ func (w *Window) processEvent(d driver, e event.Event) { w.hasNextFrame = false e2.Frame = w.update e2.Queue = &w.queue + + // Prepare the decorations and update the frame insets. + wrapper := &w.decorations.Ops + wrapper.Reset() + size := e2.Size // save the initial window size as the decorations will change it. + e2.FrameEvent.Size = w.decorate(d, e2.FrameEvent, wrapper) w.out <- e2.FrameEvent frame, gotFrame := w.waitFrame() - err := w.validateAndProcess(d, e2.Size, e2.Sync, frame) + ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) + err := w.validateAndProcess(d, size, e2.Sync, wrapper) if gotFrame { // We're done with frame, let the client continue. w.frameAck <- struct{}{} @@ -606,6 +622,9 @@ func (w *Window) processEvent(d driver, e event.Event) { w.out <- e2 w.waitAck() case wakeupEvent: + case ConfigEvent: + w.decorations.Config = e2.Config + w.out <- e case event.Event: if w.queue.q.Queue(e2) { w.setNextFrame(time.Time{}) @@ -664,6 +683,59 @@ func (w *Window) updateCursor(d driver) { } } +// decorate the window if enabled and returns the corresponding Insets. +func (w *Window) decorate(d driver, e system.FrameEvent, o *op.Ops) image.Point { + if w.decorations.Config.Decorated || w.decorations.Config.Mode == Fullscreen { + return e.Size + } + deco := w.decorations.Decorations + if deco == nil { + theme := material.NewTheme(gofont.Collection()) + allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize | + system.ActionClose | system.ActionMove | + system.ActionResizeNorth | system.ActionResizeSouth | + system.ActionResizeWest | system.ActionResizeEast | + system.ActionResizeNorthWest | system.ActionResizeSouthWest | + system.ActionResizeNorthEast | system.ActionResizeSouthEast + deco = &material.Decorations{ + DecorationsStyle: material.Decorate(theme, allActions), + } + w.decorations.Decorations = deco + } + // Update the decorations based on the current window mode. + var actions system.Action + switch m := w.decorations.Config.Mode; m { + case Windowed: + actions |= system.ActionUnmaximize + case Minimized: + actions |= system.ActionMinimize + case Maximized: + actions |= system.ActionMaximize + case Fullscreen: + actions |= system.ActionFullscreen + default: + panic(fmt.Errorf("unknown WindowMode %v", m)) + } + deco.Perform(actions) + // Update the window based on the actions on the decorations. + d.Perform(deco.Actions()) + + gtx := layout.Context{ + Ops: o, + Now: e.Now, + Queue: e.Queue, + Metric: e.Metric, + Constraints: layout.Exact(e.Size), + } + rec := op.Record(o) + dims := deco.Decorate(gtx, w.decorations.Config.Title) + op.Defer(o, rec.Stop()) + // Offset to place the frame content below the decorations. + size := image.Point{Y: dims.Size.Y} + op.Offset(f32.Point{Y: float32(size.Y)}).Add(o) + return e.Size.Sub(size) +} + // Raise requests that the platform bring this window to the top of all open windows. // Some platforms do not allow this except under certain circumstances, such as when // a window from the same application already has focus. If the platform does not @@ -764,3 +836,11 @@ func CustomRenderer(custom bool) Option { cnf.CustomRenderer = custom } } + +// Decorated controls whether automatic window decorations +// are enabled. +func Decorated(enabled bool) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.Decorated = enabled + } +} diff --git a/widget/material/decorations.go b/widget/material/decorations.go index 92f0861aa..edf393b42 100644 --- a/widget/material/decorations.go +++ b/widget/material/decorations.go @@ -57,7 +57,7 @@ type Decorations struct { // Decorate a window with the title and actions defined in DecorationsStyle. // The space used by the decorations is returned as an inset for the window // content. -func (d *Decorations) Decorate(gtx layout.Context, title string) layout.Inset { +func (d *Decorations) Decorate(gtx layout.Context, title string) layout.Dimensions { rec := op.Record(gtx.Ops) dims := d.layoutDecorations(gtx, title) decos := rec.Stop() @@ -65,9 +65,7 @@ func (d *Decorations) Decorate(gtx layout.Context, title string) layout.Inset { paint.FillShape(gtx.Ops, d.DecorationsStyle.Background, r.Op()) decos.Add(gtx.Ops) d.layoutResizing(gtx) - return layout.Inset{ - Top: unit.Px(float32(dims.Size.Y)), - } + return dims } func (d *Decorations) layoutResizing(gtx layout.Context) {