mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-05 01:15:35 +00:00
io/system,widget/material: add decorations
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>
This commit is contained in:
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -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)}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user