Files
gio/widget/material/button.go
T
Chris Waldon cf787a1a8c widget/material: make clickable respect constraints
This change makes material.Clickable propagate the constraints it is
invoked with to the widget being made clickable. Without this, the
internal use of layout.Stack resets the minimum constraints to zero.
This has the confusing effect of breaking a working layout when you
decide to wrap one element in a Clickable, which I think is sufficiently
surprising that we should eliminate the footgun.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-03-10 08:42:43 +01:00

307 lines
8.1 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package material
import (
"image"
"image/color"
"math"
"gioui.org/f32"
"gioui.org/internal/f32color"
"gioui.org/io/semantic"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
)
type ButtonStyle struct {
Text string
// Color is the text color.
Color color.NRGBA
Font text.Font
TextSize unit.Value
Background color.NRGBA
CornerRadius unit.Value
Inset layout.Inset
Button *widget.Clickable
shaper text.Shaper
}
type ButtonLayoutStyle struct {
Background color.NRGBA
CornerRadius unit.Value
Button *widget.Clickable
}
type IconButtonStyle struct {
Background color.NRGBA
// Color is the icon color.
Color color.NRGBA
Icon *widget.Icon
// Size is the icon size.
Size unit.Value
Inset layout.Inset
Button *widget.Clickable
Description string
}
func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
return ButtonStyle{
Text: txt,
Color: th.Palette.ContrastFg,
CornerRadius: unit.Dp(4),
Background: th.Palette.ContrastBg,
TextSize: th.TextSize.Scale(14.0 / 16.0),
Inset: layout.Inset{
Top: unit.Dp(10), Bottom: unit.Dp(10),
Left: unit.Dp(12), Right: unit.Dp(12),
},
Button: button,
shaper: th.Shaper,
}
}
func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle {
return ButtonLayoutStyle{
Button: button,
Background: th.Palette.ContrastBg,
CornerRadius: unit.Dp(4),
}
}
func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon, description string) IconButtonStyle {
return IconButtonStyle{
Background: th.Palette.ContrastBg,
Color: th.Palette.ContrastFg,
Icon: icon,
Size: unit.Dp(24),
Inset: layout.UniformInset(unit.Dp(12)),
Button: button,
Description: description,
}
}
// Clickable lays out a rectangular clickable widget without further
// decoration.
func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions {
return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
constraints := gtx.Constraints
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
if button.Hovered() || button.Focused() {
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
}
for _, c := range button.History() {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints = constraints
return w(gtx)
}),
)
})
}
func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
return ButtonLayoutStyle{
Background: b.Background,
CornerRadius: b.CornerRadius,
Button: b.Button,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text)
})
})
}
func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
min := gtx.Constraints.Min
return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
rr := float32(gtx.Px(b.CornerRadius))
defer clip.UniformRRect(f32.Rectangle{Max: f32.Point{
X: float32(gtx.Constraints.Min.X),
Y: float32(gtx.Constraints.Min.Y),
}}, rr).Push(gtx.Ops).Pop()
background := b.Background
switch {
case gtx.Queue == nil:
background = f32color.Disabled(b.Background)
case b.Button.Hovered() || b.Button.Focused():
background = f32color.Hovered(b.Background)
}
paint.Fill(gtx.Ops, background)
for _, c := range b.Button.History() {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min = min
return layout.Center.Layout(gtx, w)
}),
)
})
}
func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
m := op.Record(gtx.Ops)
dims := b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
if d := b.Description; d != "" {
semantic.DescriptionOp(b.Description).Add(gtx.Ops)
}
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y
sizexf, sizeyf := float32(sizex), float32(sizey)
rr := (sizexf + sizeyf) * .25
defer clip.UniformRRect(f32.Rectangle{
Max: f32.Point{X: sizexf, Y: sizeyf},
}, rr).Push(gtx.Ops).Pop()
background := b.Background
switch {
case gtx.Queue == nil:
background = f32color.Disabled(b.Background)
case b.Button.Hovered() || b.Button.Focused():
background = f32color.Hovered(b.Background)
}
paint.Fill(gtx.Ops, background)
for _, c := range b.Button.History() {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
size := gtx.Px(b.Size)
if b.Icon != nil {
gtx.Constraints.Min = image.Point{X: size}
b.Icon.Layout(gtx, b.Color)
}
return layout.Dimensions{
Size: image.Point{X: size, Y: size},
}
})
}),
)
})
c := m.Stop()
bounds := f32.Rectangle{Max: layout.FPt(dims.Size)}
defer clip.Ellipse(bounds).Push(gtx.Ops).Pop()
c.Add(gtx.Ops)
return dims
}
func drawInk(gtx layout.Context, c widget.Press) {
// duration is the number of seconds for the
// completed animation: expand while fading in, then
// out.
const (
expandDuration = float32(0.5)
fadeDuration = float32(0.9)
)
now := gtx.Now
t := float32(now.Sub(c.Start).Seconds())
end := c.End
if end.IsZero() {
// If the press hasn't ended, don't fade-out.
end = now
}
endt := float32(end.Sub(c.Start).Seconds())
// Compute the fade-in/out position in [0;1].
var alphat float32
{
var haste float32
if c.Cancelled {
// If the press was cancelled before the inkwell
// was fully faded in, fast forward the animation
// to match the fade-out.
if h := 0.5 - endt/fadeDuration; h > 0 {
haste = h
}
}
// Fade in.
half1 := t/fadeDuration + haste
if half1 > 0.5 {
half1 = 0.5
}
// Fade out.
half2 := float32(now.Sub(end).Seconds())
half2 /= fadeDuration
half2 += haste
if half2 > 0.5 {
// Too old.
return
}
alphat = half1 + half2
}
// Compute the expand position in [0;1].
sizet := t
if c.Cancelled {
// Freeze expansion of cancelled presses.
sizet = endt
}
sizet /= expandDuration
// Animate only ended presses, and presses that are fading in.
if !c.End.IsZero() || sizet <= 1.0 {
op.InvalidateOp{}.Add(gtx.Ops)
}
if sizet > 1.0 {
sizet = 1.0
}
if alphat > .5 {
// Start fadeout after half the animation.
alphat = 1.0 - alphat
}
// Twice the speed to attain fully faded in at 0.5.
t2 := alphat * 2
// Beziér ease-in curve.
alphaBezier := t2 * t2 * (3.0 - 2.0*t2)
sizeBezier := sizet * sizet * (3.0 - 2.0*sizet)
size := float32(gtx.Constraints.Min.X)
if h := float32(gtx.Constraints.Min.Y); h > size {
size = h
}
// Cover the entire constraints min rectangle.
size *= 2 * float32(math.Sqrt(2))
// Apply curve values to size and color.
size *= sizeBezier
alpha := 0.7 * alphaBezier
const col = 0.8
ba, bc := byte(alpha*0xff), byte(col*0xff)
rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba)
ink := paint.ColorOp{Color: rgba}
ink.Add(gtx.Ops)
rr := size * .5
defer op.Offset(c.Position.Add(f32.Point{
X: -rr,
Y: -rr,
})).Push(gtx.Ops).Pop()
defer clip.UniformRRect(f32.Rectangle{Max: f32.Pt(size, size)}, rr).Push(gtx.Ops).Pop()
paint.PaintOp{}.Add(gtx.Ops)
}