From: Pierre Curto <pierre.curto@gmail.com>
Add the Decorations material widget and the related system
elements in preparation for the automatic window decoration
patch.
Signed-off-by: Pierre Curto <pierre.curto@gmail.com>
---
io/system/decoration.go | 112 +++++++++++++
widget/material/decorations.go | 297 +++++++++++++++++++++++++++++++++
2 files changed, 409 insertions(+)
create mode 100644 io/system/decoration.go
create mode 100644 widget/material/decorations.go
diff --git a/io/system/decoration.go b/io/system/decoration.go
new file mode 100644
index 00000000..ad008666
--- /dev/null
+++ b/io/system/decoration.go
@@ -0,0 +1,112 @@
+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 directed by the user.
+ ActionMove
+ // ActionResizeNorth resizes the top border of a window (directed by the user).
+ ActionResizeNorth
+ // ActionResizeSouth resizes the bottom border of a window (directed by the user).
+ ActionResizeSouth
+ // ActionResizeWest resizes the right border of a window (directed by the user).
+ ActionResizeWest
+ // ActionResizeEast resizes the left border of a window (directed by the user).
+ ActionResizeEast
+ // ActionResizeNorthWest resizes the top-left corner of a window (directed by the user).
+ ActionResizeNorthWest
+ // ActionResizeSouthWest resizes the bottom-left corner of a window (directed by the user).
+ ActionResizeSouthWest
+ // ActionResizeNorthEast resizes the top-right corner of a window (directed by the user).
+ ActionResizeNorthEast
+ // ActionResizeSouthEast resizes the bottom-right corner of a window (directed by the user).
+ ActionResizeSouthEast
+)
+
+// 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..92f0861a
--- /dev/null
+++ b/widget/material/decorations.go
@@ -0,0 +1,297 @@
+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
+}
+
+// Decorate a window with the title and actions defined in DecorationsStyle.
+// The space used by the decorations is returned as an inset for the window
+// content.
+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 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 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)
+)
+
+// 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)}
+}
+
+// maximizeWindow draws a rectangle representing the maximize action.
+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)}
+}
+
+// maximizedWindow draws interleaved rectangles representing the un-maximize action.
+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)}
+}
+
+// closeWindow draws a cross representing the close action.
+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