~eliasnaur/gio-patches

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
2 2

[PATCH gio] widget/material: add the Decorations widget app: use material.Decorations as default

Details
Message ID
<164274887578.7802.16297441185238841488-0@git.sr.ht>
DKIM signature
missing
Download raw message
Patch: +621 -10
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] build success

builds.sr.ht <builds@sr.ht>
Details
Message ID
<CHB6AE54AW2Y.2YAW86WQI8H6G@cirno>
In-Reply-To
<164274887578.7802.16297441185238841488-0@git.sr.ht> (view parent)
DKIM signature
missing
Download raw message
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]: 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
Details
Message ID
<CHBFXF7XYLTS.FCGRL0X0K6KM@macbog.local>
In-Reply-To
<164274887578.7802.16297441185238841488-0@git.sr.ht> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
On Fri Jan 21, 2022 at 08:07 CET, ~pierrec wrote:
> 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.
>

References: https://todo.sr.ht/~eliasnaur/gio/318

> 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
> @@ -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 {

Do you need this interface anymore?

> +	Decorate(gtx layout.Context, title string) layout.Inset
> +	Perform(system.Action)
> +	Actions() system.Action
>  }
>  
>  type windowRendezvous struct {
> 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
> @@ -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...)

Is this unused? I don't see decoration handling in os_wayland anymore
(as expected).

> +	}
>  	go func() {
>  		defer d.destroy()
>  		defer w.destroy()
> @@ -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
> +	}

Bug fix? If so, please separate.

>  }
>  
>  //export gio_onSurfaceLeave
> diff --git a/app/window.go b/app/window.go
> index 8a4259d3..e68ca123 100644
> --- a/app/window.go
> +++ b/app/window.go
> @@ -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)

Why does an empty op.Offset cancel anything? Isn't it a no-op?

> +		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{}{}
> @@ -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) {

If you're going to expose this, I think Close should be deleted.
It's rarely used so not worth the shortcut.

app.Centered and app.Window.Raise should probably also be system.Actions in a
follow-up.

> +	w.driverDefer(func(d driver) {
> +		d.Perform(actions)
> +	})
> +}
> +
>  func (q *queue) Events(k event.Tag) []event.Event {
>  	return q.q.Events(k)
>  }
> 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

I think the platform independent changes to io/system and widget/material should
be in a separate patch, at least to help anyone bisecting a future app.Window
issue.

> @@ -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.

Nit: mention that the user controls the movement? Same for
resize actions below. Say,

	// ActionMove moves the window directed by the user.

> +	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)) {

I'm not convinced Walk is worth exporting. Let's have it as an unexported
function in package app for now.

> +	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 {

A description would be nice, in particular because the method name and return type
are unusual.

> +	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.

It's unclear why you want to call Perform. How about

	// Perform updates the decorations as if the specified actions were
	// performed by the user.

> +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.

"on the window" is confusing. Maybe

	// Actions returns the set of actions activated by the user.

> +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

Expand a bit, or delete comment.

	// minimizeWindows draws a line icon representing the minimize action.

> +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

Ditto.

> +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

Ditto.

> +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

Ditto.

> +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
Reply to thread Export thread (mbox)