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..6666e6e8 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(options, Decorated(true))
+ }
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