~pierrec: 3 app: use material.Decorations on undecorated platforms [Wayland] set app.Config.Decorated according to the Wayland server decorations requests app: swapped the Wayland and X11 drivers 12 files changed, 248 insertions(+), 12 deletions(-)
gio/patches: FAILED in 20m47s [app: use material.Decorations on undecorated platforms][0] from [~pierrec][1] [0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/28709 [1]: mailto:pierre.curto@gmail.com ✗ #680603 FAILED gio/patches/apple.yml https://builds.sr.ht/~eliasnaur/job/680603 ✓ #680606 SUCCESS gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/680606 ✓ #680605 SUCCESS gio/patches/linux.yml https://builds.sr.ht/~eliasnaur/job/680605 ✓ #680604 SUCCESS gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/680604
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/28709/mbox | git am -3Learn more about email & git
From: Pierre Curto <pierre.curto@gmail.com> 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. References: https://todo.sr.ht/~eliasnaur/gio/318 Signed-off-by: Pierre Curto <pierre.curto@gmail.com> --- app/os.go | 14 ++++++++ app/os_android.go | 8 +++++ app/os_ios.go | 14 +++++++- app/os_js.go | 8 +++++ app/os_macos.go | 7 ++++ app/os_wayland.go | 82 ++++++++++++++++++++++++++++++++++++++++--- app/os_windows.go | 4 +++ app/os_x11.go | 7 ++++ app/window.go | 89 +++++++++++++++++++++++++++++++++++++++++++++-- 9 files changed, 224 insertions(+), 9 deletions(-) diff --git a/app/os.go b/app/os.go index 329f86d0..e44b0d51 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 f835cbdb..f3529422 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 3e8c3ba1..d7f3a6e3 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -273,7 +273,19 @@ 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 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() {} diff --git a/app/os_js.go b/app/os_js.go index 23e49a99..728ca14a 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 10feab26..67b17b1e 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_wayland.go b/app/os_wayland.go index ed103694..d958b55e 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -149,6 +149,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 +189,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 +214,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 @@ -499,6 +501,7 @@ 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_onOutputMode @@ -772,15 +775,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 +828,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 +990,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 +1007,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 9a9028c7..f453c7d4 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 97525bec..a65f637c 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -160,6 +160,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: @@ -241,6 +243,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}) } @@ -264,6 +269,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 8a4259d3..dc1576ee 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 @@ -574,9 +583,19 @@ 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() + decoRec := op.Record(wrapper) + e2.FrameEvent.Insets = w.decorate(d, e2.FrameEvent, wrapper) + op.Defer(wrapper, decoRec.Stop()) + 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, e2.Size, e2.Sync, wrapper) if gotFrame { // We're done with frame, let the client continue. w.frameAck <- struct{}{} @@ -602,6 +621,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{}) @@ -660,6 +682,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) system.Insets { + if w.decorations.Config.Decorated || w.decorations.Config.Mode == Fullscreen { + return e.Insets + } + 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.Sprintf("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), + } + insets := deco.Decorate(gtx, w.decorations.Config.Title) + return system.Insets{ + Top: unit.Add(e.Metric, e.Insets.Top, insets.Top), + Bottom: unit.Add(e.Metric, e.Insets.Bottom, insets.Bottom), + Left: unit.Add(e.Metric, e.Insets.Left, insets.Left), + Right: unit.Add(e.Metric, e.Insets.Right, insets.Right), + } +} + // 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 @@ -760,3 +835,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 + } +} -- 2.32.0
From: Pierre Curto <pierre.curto@gmail.com> Server side decorations are no longer requested. The Decorated flag is set according to the server's requests. Signed-off-by: Pierre Curto <pierre.curto@gmail.com> --- app/os_wayland.c | 5 +++++ app/os_wayland.go | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/os_wayland.c b/app/os_wayland.c index b137e1f9..a9752aaa 100644 --- a/app/os_wayland.c +++ b/app/os_wayland.c @@ -6,6 +6,7 @@ #include <wayland-client.h> #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 d958b55e..6390d47a 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; @@ -371,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 @@ -504,6 +504,22 @@ func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, 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 +} + //export gio_onOutputMode func gio_onOutputMode(data unsafe.Pointer, output *C.struct_wl_output, flags C.uint32_t, width, height, refresh C.int32_t) { if flags&C.WL_OUTPUT_MODE_CURRENT == 0 { -- 2.32.0
From: Pierre Curto <pierre.curto@gmail.com> Wayland is now the default driver for UNIX platforms. Signed-off-by: Pierre Curto <pierre.curto@gmail.com> --- app/os_unix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/os_unix.go b/app/os_unix.go index ee831b30..c834b6e1 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 } -- 2.32.0
builds.sr.ht <builds@sr.ht>gio/patches: FAILED in 20m47s [app: use material.Decorations on undecorated platforms][0] from [~pierrec][1] [0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/28709 [1]: mailto:pierre.curto@gmail.com ✗ #680603 FAILED gio/patches/apple.yml https://builds.sr.ht/~eliasnaur/job/680603 ✓ #680606 SUCCESS gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/680606 ✓ #680605 SUCCESS gio/patches/linux.yml https://builds.sr.ht/~eliasnaur/job/680605 ✓ #680604 SUCCESS gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/680604