Files
Qian Nian acf5635575 widget/material: add hover state to window decorations
Decorations buttons already use widget.Clickable and draw press ink, but they don't draw a hover/focus background. This makes custom window decorations appear unresponsive when the pointer is over minimize, maximize, or close buttons.

Register each button's system action over its clickable area and draw the same hovered background used by other material buttons. This also lets platforms query the button action from the material decorations hit area.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-14 10:50:56 +02:00

198 lines
5.4 KiB
Go

package material
import (
"image"
"image/color"
"gioui.org/f32"
"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 {
Decorations *widget.Decorations
Actions system.Action
Title LabelStyle
Background color.NRGBA
Foreground color.NRGBA
}
// Decorations returns the style to decorate a window.
func Decorations(th *Theme, deco *widget.Decorations, actions system.Action, title string) DecorationsStyle {
titleStyle := Body1(th, title)
titleStyle.Color = th.Palette.ContrastFg
return DecorationsStyle{
Decorations: deco,
Actions: actions,
Title: titleStyle,
Background: th.Palette.ContrastBg,
Foreground: th.Palette.ContrastFg,
}
}
// Layout a window with its title and action buttons.
func (d DecorationsStyle) Layout(gtx layout.Context) layout.Dimensions {
rec := op.Record(gtx.Ops)
dims := d.layoutDecorations(gtx)
decos := rec.Stop()
r := clip.Rect{Max: dims.Size}
paint.FillShape(gtx.Ops, d.Background, r.Op())
decos.Add(gtx.Ops)
return dims
}
func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.Y = 0
inset := layout.UniformInset(10)
return layout.Flex{
Axis: layout.Horizontal,
Alignment: layout.Middle,
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return d.Decorations.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions {
return inset.Layout(gtx, d.Title.Layout)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
// Remove the unmaximize action as it is taken care of by maximize.
actions := d.Actions &^ system.ActionUnmaximize
var size image.Point
for a := system.Action(1); actions != 0; a <<= 1 {
if a&actions == 0 {
continue
}
actions &^= a
var w layout.Widget
switch a {
case system.ActionMinimize:
w = minimizeWindow
case system.ActionMaximize:
if d.Decorations.Maximized {
w = maximizedWindow
} else {
w = maximizeWindow
}
case system.ActionClose:
w = closeWindow
default:
continue
}
cl := d.Decorations.Clickable(a)
dims := Clickable(gtx, cl, func(gtx layout.Context) layout.Dimensions {
system.ActionInputOp(a).Add(gtx.Ops)
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
return inset.Layout(gtx, w)
})
size.X += dims.Size.X
if size.Y < dims.Size.Y {
size.Y = dims.Size.Y
}
op.Offset(image.Pt(dims.Size.X, 0)).Add(gtx.Ops)
}
return layout.Dimensions{Size: size}
}),
)
}
const (
winIconSize = unit.Dp(20)
winIconMargin = unit.Dp(4)
winIconStroke = unit.Dp(2)
)
// minimizeWindows draws a line icon representing the minimize action.
func minimizeWindow(gtx layout.Context) layout.Dimensions {
size := gtx.Dp(winIconSize)
size32 := float32(size)
margin := float32(gtx.Dp(winIconMargin))
width := float32(gtx.Dp(winIconStroke))
var p clip.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 maximizeWindow(gtx layout.Context) layout.Dimensions {
size := gtx.Dp(winIconSize)
margin := gtx.Dp(winIconMargin)
width := gtx.Dp(winIconStroke)
r := clip.RRect{
Rect: image.Rect(margin, margin, size-margin, size-margin),
}
st := clip.Stroke{
Path: r.Path(gtx.Ops),
Width: float32(width),
}.Op().Push(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
st.Pop()
r.Rect.Max = image.Pt(size-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 maximizedWindow(gtx layout.Context) layout.Dimensions {
size := gtx.Dp(winIconSize)
margin := gtx.Dp(winIconMargin)
width := gtx.Dp(winIconStroke)
r := clip.RRect{
Rect: image.Rect(margin, margin, size-2*margin, size-2*margin),
}
st := clip.Stroke{
Path: r.Path(gtx.Ops),
Width: float32(width),
}.Op().Push(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
st.Pop()
r = clip.RRect{
Rect: image.Rect(2*margin, 2*margin, size-margin, size-margin),
}
st = clip.Stroke{
Path: r.Path(gtx.Ops),
Width: float32(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 closeWindow(gtx layout.Context) layout.Dimensions {
size := gtx.Dp(winIconSize)
size32 := float32(size)
margin := float32(gtx.Dp(winIconMargin))
width := float32(gtx.Dp(winIconStroke))
var p clip.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)}
}