~eliasnaur/gio-patches

gio: widget/material: add the Decorations widget app: use material.Decorations as default v1 PROPOSED

~pierrec: 1
 widget/material: add the Decorations widget app: use material.Decorations as default

 11 files changed, 621 insertions(+), 10 deletions(-)
#677047 apple.yml success
#677048 freebsd.yml success
#677049 linux.yml success
#677050 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/28584/mbox | git am -3
Learn more about email & git

[PATCH gio] widget/material: add the Decorations widget app: use material.Decorations as default 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
decorations provided by the app.Decorate(app.Decorations) option
are applied. Custom decorations can be provided but by default
it uses the material.Decorations ones.

Decorations are automatically applied on Wayland if the Wayland
server does not provide window decorations.
Signed-off-by: Pierre Curto <pierre.curto@gmail.com>
---
 app/os.go                      |  15 +-
 app/os_android.go              |   2 +
 app/os_ios.go                  |   2 +
 app/os_js.go                   |   2 +
 app/os_macos.go                |   2 +
 app/os_wayland.go              |  90 +++++++++-
 app/os_windows.go              |   2 +
 app/os_x11.go                  |   2 +
 app/window.go                  |  99 ++++++++++-
 io/system/decoration.go        | 122 ++++++++++++++
 widget/material/decorations.go | 293 +++++++++++++++++++++++++++++++++
 11 files changed, 621 insertions(+), 10 deletions(-)
 create mode 100644 io/system/decoration.go
 create mode 100644 widget/material/decorations.go

diff --git a/app/os.go b/app/os.go
index 329f86d0..894ea5fa 100644
--- a/app/os.go
+++ b/app/os.go
@@ -7,11 +7,11 @@ import (
	"image"
	"image/color"

	"gioui.org/io/key"

	"gioui.org/gpu"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/unit"
)

@@ -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,15 @@ type driver interface {

	// Wakeup wakes up the event loop and sends a WakeupEvent.
	Wakeup()

	// Perform actions on the window.
	Perform(system.Action)
}

type decorations interface {
	Decorate(gtx layout.Context, title string) layout.Inset
	Perform(system.Action)
	Actions() system.Action
}

type windowRendezvous struct {
diff --git a/app/os_android.go b/app/os_android.go
index 5540a6c3..d4360eae 100644
--- a/app/os_android.go
+++ b/app/os_android.go
@@ -1190,6 +1190,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 a8a95872..95d2a5ee 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
@@ -552,6 +559,10 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
		conf.windows = append(conf.windows, w)
	}
	w.updateOutputs()
	if w.config.Mode == Minimized {
		// Minimized window got brought back up: it is no longer so.
		w.config.Mode = Windowed
	}
}

//export gio_onSurfaceLeave
@@ -767,15 +778,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
@@ -813,6 +831,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
@@ -973,6 +993,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})
	}
@@ -987,6 +1010,63 @@ func (w *window) setTitle(prev, cnf Config) {
	}
}

func (w *window) Perform(actions system.Action) {
	actions.Walk(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 b6da2af6..e9da517d 100644
--- a/app/os_windows.go
+++ b/app/os_windows.go
@@ -576,6 +576,8 @@ func (w *window) Configure(options []Option) {
	}
}

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

func (w *window) WriteClipboard(s string) {
	w.writeClipboard(s)
}
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..e68ca123 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
		decorations
	}

	callbacks callbacks

@@ -574,9 +583,25 @@ 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 cancel 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 +627,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 +688,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
@@ -670,6 +748,13 @@ func (w *Window) Raise() {
	})
}

// Perform the actions on the window.
func (w *Window) Perform(actions system.Action) {
	w.driverDefer(func(d driver) {
		d.Perform(actions)
	})
}

func (q *queue) Events(k event.Tag) []event.Event {
	return q.q.Events(k)
}
@@ -760,3 +845,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/io/system/decoration.go b/io/system/decoration.go
new file mode 100644
index 00000000..22b4d67c
--- /dev/null
+++ b/io/system/decoration.go
@@ -0,0 +1,122 @@
package system

import (
	"strings"

	"gioui.org/io/pointer"
)

// Action is a set of window decoration actions.
type Action uint

const (
	// ActionMinimize minimizes a window.
	ActionMinimize Action = 1 << iota
	// ActionMaximize maximizes a window.
	ActionMaximize
	// ActionUnmaximize restores a maximized window.
	ActionUnmaximize
	// ActionFullscreen makes a window fullscreen.
	ActionFullscreen
	// ActionClose closes a window.
	ActionClose
	// ActionMove moves a window on the screen.
	ActionMove
	// ActionResizeNorth resizes the top border of a window.
	ActionResizeNorth
	// ActionResizeSouth resizes the bottom border of a window.
	ActionResizeSouth
	// ActionResizeWest resizes the right border of a window.
	ActionResizeWest
	// ActionResizeEast resizes the left border of a window.
	ActionResizeEast
	// ActionResizeNorthWest resizes the top-left corner of a window.
	ActionResizeNorthWest
	// ActionResizeSouthWest resizes the bottom-left corner of a window.
	ActionResizeSouthWest
	// ActionResizeNorthEast resizes the top-right corner of a window.
	ActionResizeNorthEast
	// ActionResizeSouthEast resizes the bottom-right corner of a window.
	ActionResizeSouthEast
)

// Walk over all actions and execute do for each one of them.
func (actions Action) Walk(do func(Action)) {
	for a := Action(1); actions != 0; a <<= 1 {
		if actions&a != 0 {
			actions &^= a
			do(a)
		}
	}
}

// CursorName returns the cursor for the action.
// It must be a single action otherwise the default
// cursor is returned.
func (a Action) CursorName() pointer.CursorName {
	switch a {
	case ActionResizeNorthWest:
		return pointer.CursorTopLeftResize
	case ActionResizeSouthEast:
		return pointer.CursorBottomRightResize
	case ActionResizeNorthEast:
		return pointer.CursorTopRightResize
	case ActionResizeSouthWest:
		return pointer.CursorBottomLeftResize
	case ActionResizeWest:
		return pointer.CursorLeftResize
	case ActionResizeEast:
		return pointer.CursorRightResize
	case ActionResizeNorth:
		return pointer.CursorTopResize
	case ActionResizeSouth:
		return pointer.CursorBottomResize
	}
	return pointer.CursorDefault
}

func (a Action) String() string {
	var buf strings.Builder
	for b := Action(1); a != 0; b <<= 1 {
		if a&b != 0 {
			if buf.Len() > 0 {
				buf.WriteByte('|')
			}
			buf.WriteString(b.string())
			a &^= b
		}
	}
	return buf.String()
}

func (a Action) string() string {
	switch a {
	case ActionMinimize:
		return "ActionMinimize"
	case ActionMaximize:
		return "ActionMaximize"
	case ActionUnmaximize:
		return "ActionUnmaximize"
	case ActionClose:
		return "ActionClose"
	case ActionMove:
		return "ActionMove"
	case ActionResizeNorth:
		return "ActionResizeNorth"
	case ActionResizeSouth:
		return "ActionResizeSouth"
	case ActionResizeWest:
		return "ActionResizeWest"
	case ActionResizeEast:
		return "ActionResizeEast"
	case ActionResizeNorthWest:
		return "ActionResizeNorthWest"
	case ActionResizeSouthWest:
		return "ActionResizeSouthWest"
	case ActionResizeNorthEast:
		return "ActionResizeNorthEast"
	case ActionResizeSouthEast:
		return "ActionResizeSouthEast"
	}
	return ""
}
diff --git a/widget/material/decorations.go b/widget/material/decorations.go
new file mode 100644
index 00000000..415c80c8
--- /dev/null
+++ b/widget/material/decorations.go
@@ -0,0 +1,293 @@
package material

import (
	"image"
	"image/color"
	"math/bits"

	"gioui.org/f32"
	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
)

// DecorationsStyle provides the style elements for Decorations.
type DecorationsStyle struct {
	Actions    system.Action
	Title      LabelStyle
	Background color.NRGBA
	Foreground color.NRGBA
}

// Decorate a window.
func Decorate(th *Theme, actions system.Action) DecorationsStyle {
	titleStyle := Body1(th, "")
	titleStyle.Color = th.Palette.ContrastFg
	return DecorationsStyle{
		Actions:    actions,
		Title:      titleStyle,
		Background: th.Palette.ContrastBg,
		Foreground: th.Palette.ContrastFg,
	}
}

// Decorations provides window decorations.
type Decorations struct {
	DecorationsStyle
	actions struct {
		layout.List
		clicks []widget.Clickable
		move   gesture.Drag
		resize [8]struct {
			gesture.Hover
			gesture.Drag
		}
	}
	actioned  system.Action
	path      clip.Path
	maximized bool
}

func (d *Decorations) Decorate(gtx layout.Context, title string) layout.Inset {
	rec := op.Record(gtx.Ops)
	dims := d.layoutDecorations(gtx, title)
	decos := rec.Stop()
	r := clip.Rect{Max: dims.Size}
	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)),
	}
}

func (d *Decorations) layoutResizing(gtx layout.Context) {
	cs := gtx.Constraints.Min
	wh := gtx.Px(unit.Dp(10))
	s := []struct {
		system.Action
		image.Rectangle
	}{
		{system.ActionResizeNorth, image.Rect(0, 0, cs.X, wh)},
		{system.ActionResizeSouth, image.Rect(0, cs.Y-wh, cs.X, cs.Y)},
		{system.ActionResizeWest, image.Rect(cs.X-wh, 0, cs.X, cs.Y)},
		{system.ActionResizeEast, image.Rect(0, 0, wh, cs.Y)},
		{system.ActionResizeNorthWest, image.Rect(0, 0, wh, wh)},
		{system.ActionResizeSouthWest, image.Rect(cs.X-wh, 0, cs.X, wh)},
		{system.ActionResizeNorthEast, image.Rect(0, cs.Y-wh, wh, cs.Y)},
		{system.ActionResizeSouthEast, image.Rect(cs.X-wh, cs.Y-wh, cs.X, cs.Y)},
	}
	for i, data := range s {
		action := data.Action
		if d.DecorationsStyle.Actions&action == 0 {
			continue
		}
		rsz := &d.actions.resize[i]
		rsz.Events(gtx.Metric, gtx, gesture.Both)
		if rsz.Drag.Dragging() {
			d.actioned |= action
		}
		st := clip.Rect(data.Rectangle).Push(gtx.Ops)
		if rsz.Hover.Hovered(gtx) {
			pointer.CursorNameOp{Name: action.CursorName()}.Add(gtx.Ops)
		}
		rsz.Drag.Add(gtx.Ops)
		pass := pointer.PassOp{}.Push(gtx.Ops)
		rsz.Hover.Add(gtx.Ops)
		pass.Pop()
		st.Pop()
	}
}

func (d *Decorations) layoutDecorations(gtx layout.Context, title string) layout.Dimensions {
	gtx.Constraints.Min.Y = 0
	inset := layout.UniformInset(unit.Dp(10))
	return layout.Flex{
		Axis:      layout.Horizontal,
		Alignment: layout.Middle,
		Spacing:   layout.SpaceBetween,
	}.Layout(gtx,
		layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
			d.DecorationsStyle.Title.Text = title
			dims := inset.Layout(gtx, d.DecorationsStyle.Title.Layout)
			if d.DecorationsStyle.Actions&system.ActionMove != 0 {
				d.actions.move.Events(gtx.Metric, gtx, gesture.Both)

				st := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
				d.actions.move.Add(gtx.Ops)
				if d.actions.move.Pressed() {
					d.actioned |= system.ActionMove
				}
				st.Pop()
			}
			return dims
		}),
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			// Remove the unmaximize action as it is taken care of by maximize.
			actions := d.DecorationsStyle.Actions &^ system.ActionUnmaximize
			an := bits.OnesCount(uint(actions))
			if n := len(d.actions.clicks); n < an {
				d.actions.clicks = append(d.actions.clicks, make([]widget.Clickable, an-n)...)
			}
			return d.actions.Layout(gtx, an, func(gtx layout.Context, idx int) layout.Dimensions {
				action := system.Action(1 << idx)
				var w layout.Widget
				switch actions & action {
				case system.ActionMinimize:
					w = d.minimizeWindow
				case system.ActionMaximize:
					if d.maximized {
						w = d.maximizedWindow
					} else {
						w = d.maximizeWindow
					}
				case system.ActionClose:
					w = d.closeWindow
				default:
					return layout.Dimensions{}
				}
				click := &d.actions.clicks[idx]
				if click.Clicked() {
					if action == system.ActionMaximize {
						if d.maximized {
							d.maximized = false
							d.actioned |= system.ActionUnmaximize
						} else {
							d.maximized = true
							d.actioned |= system.ActionMaximize
						}
					} else {
						d.actioned |= action
					}
				}
				return Clickable(gtx, click, func(gtx layout.Context) layout.Dimensions {
					return inset.Layout(gtx, w)
				})
			})
		}),
	)
}

// Perform the actions on the window.
func (d *Decorations) Perform(actions system.Action) {
	if actions&system.ActionMaximize != 0 {
		d.maximized = true
	}
	if actions&(system.ActionUnmaximize|system.ActionMinimize|system.ActionFullscreen) != 0 {
		d.maximized = false
	}
}

// Actions performed on the window.
func (d *Decorations) Actions() system.Action {
	a := d.actioned
	d.actioned = 0
	return a
}

var (
	winIconSize   = unit.Dp(20)
	winIconMargin = unit.Dp(4)
	winIconStroke = unit.Dp(2)
)

// line
func (d *Decorations) minimizeWindow(gtx layout.Context) layout.Dimensions {
	paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops)
	size := gtx.Px(winIconSize)
	size32 := float32(size)
	margin := float32(gtx.Px(winIconMargin))
	width := float32(gtx.Px(winIconStroke))
	p := &d.path
	p.Begin(gtx.Ops)
	p.MoveTo(f32.Point{X: margin, Y: size32 - margin})
	p.LineTo(f32.Point{X: size32 - 2*margin, Y: size32 - margin})
	st := clip.Stroke{
		Path:  p.End(),
		Width: width,
	}.Op().Push(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	st.Pop()
	return layout.Dimensions{Size: image.Pt(size, size)}
}

// rectangle
func (d *Decorations) maximizeWindow(gtx layout.Context) layout.Dimensions {
	paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops)
	size := gtx.Px(winIconSize)
	size32 := float32(size)
	margin := float32(gtx.Px(winIconMargin))
	width := float32(gtx.Px(winIconStroke))
	r := clip.RRect{
		Rect: f32.Rect(margin, margin, size32-margin, size32-margin),
	}
	st := clip.Stroke{
		Path:  r.Path(gtx.Ops),
		Width: width,
	}.Op().Push(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	st.Pop()
	r.Rect.Max = f32.Pt(size32-margin, 2*margin)
	st = clip.Outline{
		Path: r.Path(gtx.Ops),
	}.Op().Push(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	st.Pop()
	return layout.Dimensions{Size: image.Pt(size, size)}
}

// rectangles
func (d *Decorations) maximizedWindow(gtx layout.Context) layout.Dimensions {
	paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops)
	size := gtx.Px(winIconSize)
	size32 := float32(size)
	margin := float32(gtx.Px(winIconMargin))
	width := float32(gtx.Px(winIconStroke))
	r := clip.RRect{
		Rect: f32.Rect(margin, margin, size32-2*margin, size32-2*margin),
	}
	st := clip.Stroke{
		Path:  r.Path(gtx.Ops),
		Width: width,
	}.Op().Push(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	st.Pop()
	r = clip.RRect{
		Rect: f32.Rect(2*margin, 2*margin, size32-margin, size32-margin),
	}
	st = clip.Stroke{
		Path:  r.Path(gtx.Ops),
		Width: width,
	}.Op().Push(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	st.Pop()
	return layout.Dimensions{Size: image.Pt(size, size)}
}

// cross
func (d *Decorations) closeWindow(gtx layout.Context) layout.Dimensions {
	paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops)
	size := gtx.Px(winIconSize)
	size32 := float32(size)
	margin := float32(gtx.Px(winIconMargin))
	width := float32(gtx.Px(winIconStroke))
	p := &d.path
	p.Begin(gtx.Ops)
	p.MoveTo(f32.Point{X: margin, Y: margin})
	p.LineTo(f32.Point{X: size32 - margin, Y: size32 - margin})
	p.MoveTo(f32.Point{X: size32 - margin, Y: margin})
	p.LineTo(f32.Point{X: margin, Y: size32 - margin})
	st := clip.Stroke{
		Path:  p.End(),
		Width: width,
	}.Op().Push(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	st.Pop()
	return layout.Dimensions{Size: image.Pt(size, size)}
}
-- 
2.32.0
gio/patches: SUCCESS in 19m22s

[widget/material: add the Decorations widget app: use material.Decorations as default][0] from [~pierrec][1]

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

✓ #677047 SUCCESS gio/patches/apple.yml   https://builds.sr.ht/~eliasnaur/job/677047
✓ #677048 SUCCESS gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/677048
✓ #677050 SUCCESS gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/677050
✓ #677049 SUCCESS gio/patches/linux.yml   https://builds.sr.ht/~eliasnaur/job/677049