~eliasnaur/gio-patches

gio: app: use material.Decorations on undecorated platforms v1 PROPOSED

~pierrec: 1
 app: use material.Decorations on undecorated platforms

 9 files changed, 197 insertions(+), 8 deletions(-)
#677769 apple.yml success
#678889 freebsd.yml failed
#677771 linux.yml success
#677772 openbsd.yml success
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/28615/mbox | git am -3
Learn more about email & git

[PATCH gio] app: use material.Decorations on undecorated platforms Export this patch

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 |  2 +
 app/os_ios.go     |  2 +
 app/os_js.go      |  2 +
 app/os_macos.go   |  2 +
 app/os_wayland.go | 86 ++++++++++++++++++++++++++++++++++++++++---
 app/os_windows.go |  2 +
 app/os_x11.go     |  2 +
 app/window.go     | 93 +++++++++++++++++++++++++++++++++++++++++++++--
 9 files changed, 197 insertions(+), 8 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 ab7f1ada..a639e612 100644
--- a/app/os_android.go
+++ b/app/os_android.go
@@ -1194,6 +1194,8 @@ func (w *window) Configure(options []Option) {
	})
}

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 df45fde9..0e70035e 100644
--- a/app/os_ios.go
+++ b/app/os_ios.go
@@ -275,6 +275,8 @@ func (w *window) WriteClipboard(s string) {

func (w *window) Configure([]Option) {}

func (w *window) Perform(system.Action) {}

func (w *window) Raise() {}

func (w *window) SetAnimating(anim bool) {
diff --git a/app/os_js.go b/app/os_js.go
index 55ec2694..2cb7f210 100644
--- a/app/os_js.go
+++ b/app/os_js.go
@@ -533,6 +533,8 @@ func (w *window) Configure(options []Option) {
	}
}

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..5dbdccbb 100644
--- a/app/os_macos.go
+++ b/app/os_macos.go
@@ -339,6 +339,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..d728cb6a 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
@@ -241,6 +243,10 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
		return err
	}
	w.w = callbacks
	if w.decor == nil {
		// No decorations provided by the compositor, add them.
		options = append([]Option{Decorated(true)}, options...)
	}
	go func() {
		defer d.destroy()
		defer w.destroy()
@@ -499,6 +505,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 +779,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 +832,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 +994,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 +1011,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..ad62b0f0 100644
--- a/app/os_windows.go
+++ b/app/os_windows.go
@@ -691,6 +691,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..a82eee41 100644
--- a/app/os_x11.go
+++ b/app/os_x11.go
@@ -264,6 +264,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..ed45b22a 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,26 @@ 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)
		w.decorate(d, &e2.FrameEvent, wrapper)
		decoCall := decoRec.Stop()

		w.out <- e2.FrameEvent
		frame, gotFrame := w.waitFrame()
		err := w.validateAndProcess(d, e2.Size, e2.Sync, frame)

		// Wrap the frame ops into an Offset to isolate the offset
		// introduced by the insets.
		off := op.Offset(f32.Point{}).Push(wrapper)
		ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
		off.Pop()
		// Now add the decorations on top of the frame ops.
		decoCall.Add(wrapper)

		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 +628,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 +689,56 @@ func (w *Window) updateCursor(d driver) {
	}
}

// decorate the window if enabled. Insets are updated to accommodate for
// the decorations space.
func (w *Window) decorate(d driver, e *system.FrameEvent, o *op.Ops) {
	if !w.decorations.Config.Decorated || w.decorations.Config.Mode == Fullscreen {
		return
	}
	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 w.decorations.Config.Mode {
	case Windowed:
		actions |= system.ActionUnmaximize
	case Minimized:
		actions |= system.ActionMinimize
	case Maximized:
		actions |= system.ActionMaximize
	case Fullscreen:
		actions |= system.ActionFullscreen
	}
	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)
	e.Insets.Top = unit.Add(e.Metric, e.Insets.Top, insets.Top)
	e.Insets.Bottom = unit.Add(e.Metric, e.Insets.Bottom, insets.Bottom)
	e.Insets.Left = unit.Add(e.Metric, e.Insets.Left, insets.Left)
	e.Insets.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 +839,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
gio/patches: FAILED in 20m12s

[app: use material.Decorations on undecorated platforms][0] from [~pierrec][1]

[0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/28615
[1]: mailto:pierre.curto@gmail.com

✓ #677772 SUCCESS gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/677772
✓ #677769 SUCCESS gio/patches/apple.yml   https://builds.sr.ht/~eliasnaur/job/677769
✗ #677770 FAILED  gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/677770
✓ #677771 SUCCESS gio/patches/linux.yml   https://builds.sr.ht/~eliasnaur/job/677771
As I said before, this is great work, and the decorations look really good.

There are a few issues below, and here's a list of issues to be addressed
in follow-ups to make Wayland the default driver:

- Don't ask for server-side decorations. Listen for decoration mode callbacks
from Wayland server and set Decorated accordingly.
- Account for fallback decorations in window sizes.
- Swap order of Wayland and X11 drivers.

On Sat Jan 22, 2022 at 09:32, ~pierrec wrote: