~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] app: use material.Decorations on undecorated platforms

Details
Message ID
<164284040356.10820.16579677740060362960-0@git.sr.ht>
DKIM signature
missing
Download raw message
Patch: +197 -8
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] build failed

builds.sr.ht <builds@sr.ht>
Details
Message ID
<CHC2R0B8O6S2.AL2WUG6I166Y@cirno>
In-Reply-To
<164284040356.10820.16579677740060362960-0@git.sr.ht> (view parent)
DKIM signature
missing
Download raw message
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]: 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
Details
Message ID
<CHD32YTFI9TB.2T2QATFPY8PYI@themachine>
In-Reply-To
<164284040356.10820.16579677740060362960-0@git.sr.ht> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
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:
> 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>
> 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.
> 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
> @@ -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...)

I understand now, but I think the inverted meaning of Decorated is
confusing. See my comment for app.Window.decorate.

> +	}
>  	go func() {
>  		defer d.destroy()
>  		defer w.destroy()
> diff --git a/app/window.go b/app/window.go
> index 8a4259d3..ed45b22a 100644
> --- a/app/window.go
> +++ b/app/window.go
> @@ -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()

I understand why the dummy offset works now: off.Pop pops the unbalanced Offset in layout.NewContext.
I suggest either adding an explicit negative offset, or using op.Defer:

	diff --git app/window.go app/window.go
	index ed45b22a..c699492d 100644
	--- app/window.go
	+++ app/window.go
	@@ -589,18 +589,12 @@ func (w *Window) processEvent(d driver, e event.Event) {
	 		wrapper.Reset()
	 		decoRec := op.Record(wrapper)
	 		w.decorate(d, &e2.FrameEvent, wrapper)
	-		decoCall := decoRec.Stop()
	+		op.Defer(wrapper, decoRec.Stop())
	 
	 		w.out <- e2.FrameEvent
	 		frame, gotFrame := w.waitFrame()
	 
	-		// 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 {


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

The FrameEvent pointer argument is a bit ugly. How about just taking a FrameEvent
by value and return the updated insets?

> +	if !w.decorations.Config.Decorated || w.decorations.Config.Mode == Fullscreen {

It seems to me the meaning of Decorated is inverted. The documentation states that
Decorated is true when automatic decorations are active. Yet here you're only drawing
fallback decorations when the driver reports Decorated = true.

I know its a bit more work, but I believe it best to keep Decorated consistent, and set
it to true in all drivers except the Wayland driver in client-side decoration mode.

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

You're insetting here, but not adjusting app.Size etc. It seems to me material.Decoration
must export static insets to use them at window sizing time which comes before rendering
the decorations.

Just a note, ok to leave for later.

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