button.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package material
   4  
   5  import (
   6  	"image"
   7  	"image/color"
   8  	"math"
   9  
  10  	"github.com/p9c/p9/pkg/gel/gio/f32"
  11  	"github.com/p9c/p9/pkg/gel/gio/internal/f32color"
  12  	"github.com/p9c/p9/pkg/gel/gio/io/pointer"
  13  	"github.com/p9c/p9/pkg/gel/gio/layout"
  14  	"github.com/p9c/p9/pkg/gel/gio/op"
  15  	"github.com/p9c/p9/pkg/gel/gio/op/clip"
  16  	"github.com/p9c/p9/pkg/gel/gio/op/paint"
  17  	"github.com/p9c/p9/pkg/gel/gio/text"
  18  	"github.com/p9c/p9/pkg/gel/gio/unit"
  19  	"github.com/p9c/p9/pkg/gel/gio/widget"
  20  )
  21  
  22  type ButtonStyle struct {
  23  	Text string
  24  	// Color is the text color.
  25  	Color        color.NRGBA
  26  	Font         text.Font
  27  	TextSize     unit.Value
  28  	Background   color.NRGBA
  29  	CornerRadius unit.Value
  30  	Inset        layout.Inset
  31  	Button       *widget.Clickable
  32  	shaper       text.Shaper
  33  }
  34  
  35  type ButtonLayoutStyle struct {
  36  	Background   color.NRGBA
  37  	CornerRadius unit.Value
  38  	Button       *widget.Clickable
  39  }
  40  
  41  type IconButtonStyle struct {
  42  	Background color.NRGBA
  43  	// Color is the icon color.
  44  	Color color.NRGBA
  45  	Icon  *widget.Icon
  46  	// Size is the icon size.
  47  	Size   unit.Value
  48  	Inset  layout.Inset
  49  	Button *widget.Clickable
  50  }
  51  
  52  func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
  53  	return ButtonStyle{
  54  		Text:         txt,
  55  		Color:        th.Palette.ContrastFg,
  56  		CornerRadius: unit.Dp(4),
  57  		Background:   th.Palette.ContrastBg,
  58  		TextSize:     th.TextSize.Scale(14.0 / 16.0),
  59  		Inset: layout.Inset{
  60  			Top: unit.Dp(10), Bottom: unit.Dp(10),
  61  			Left: unit.Dp(12), Right: unit.Dp(12),
  62  		},
  63  		Button: button,
  64  		shaper: th.Shaper,
  65  	}
  66  }
  67  
  68  func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle {
  69  	return ButtonLayoutStyle{
  70  		Button:       button,
  71  		Background:   th.Palette.ContrastBg,
  72  		CornerRadius: unit.Dp(4),
  73  	}
  74  }
  75  
  76  func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon) IconButtonStyle {
  77  	return IconButtonStyle{
  78  		Background: th.Palette.ContrastBg,
  79  		Color:      th.Palette.ContrastFg,
  80  		Icon:       icon,
  81  		Size:       unit.Dp(24),
  82  		Inset:      layout.UniformInset(unit.Dp(12)),
  83  		Button:     button,
  84  	}
  85  }
  86  
  87  // Clickable lays out a rectangular clickable widget without further
  88  // decoration.
  89  func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions {
  90  	return layout.Stack{}.Layout(gtx,
  91  		layout.Expanded(button.Layout),
  92  		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
  93  			clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops)
  94  			for _, c := range button.History() {
  95  				drawInk(gtx, c)
  96  			}
  97  			return layout.Dimensions{Size: gtx.Constraints.Min}
  98  		}),
  99  		layout.Stacked(w),
 100  	)
 101  }
 102  
 103  func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
 104  	return ButtonLayoutStyle{
 105  		Background:   b.Background,
 106  		CornerRadius: b.CornerRadius,
 107  		Button:       b.Button,
 108  	}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
 109  		return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
 110  			paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
 111  			return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text)
 112  		})
 113  	})
 114  }
 115  
 116  func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
 117  	min := gtx.Constraints.Min
 118  	return layout.Stack{Alignment: layout.Center}.Layout(gtx,
 119  		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
 120  			rr := float32(gtx.Px(b.CornerRadius))
 121  			clip.UniformRRect(f32.Rectangle{Max: f32.Point{
 122  				X: float32(gtx.Constraints.Min.X),
 123  				Y: float32(gtx.Constraints.Min.Y),
 124  			}}, rr).Add(gtx.Ops)
 125  			background := b.Background
 126  			switch {
 127  			case gtx.Queue == nil:
 128  				background = f32color.Disabled(b.Background)
 129  			case b.Button.Hovered():
 130  				background = f32color.Hovered(b.Background)
 131  			}
 132  			paint.Fill(gtx.Ops, background)
 133  			for _, c := range b.Button.History() {
 134  				drawInk(gtx, c)
 135  			}
 136  			return layout.Dimensions{Size: gtx.Constraints.Min}
 137  		}),
 138  		layout.Stacked(func(gtx layout.Context) layout.Dimensions {
 139  			gtx.Constraints.Min = min
 140  			return layout.Center.Layout(gtx, w)
 141  		}),
 142  		layout.Expanded(b.Button.Layout),
 143  	)
 144  }
 145  
 146  func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
 147  	return layout.Stack{Alignment: layout.Center}.Layout(gtx,
 148  		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
 149  			sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y
 150  			sizexf, sizeyf := float32(sizex), float32(sizey)
 151  			rr := (sizexf + sizeyf) * .25
 152  			clip.UniformRRect(f32.Rectangle{
 153  				Max: f32.Point{X: sizexf, Y: sizeyf},
 154  			}, rr).Add(gtx.Ops)
 155  			background := b.Background
 156  			switch {
 157  			case gtx.Queue == nil:
 158  				background = f32color.Disabled(b.Background)
 159  			case b.Button.Hovered():
 160  				background = f32color.Hovered(b.Background)
 161  			}
 162  			paint.Fill(gtx.Ops, background)
 163  			for _, c := range b.Button.History() {
 164  				drawInk(gtx, c)
 165  			}
 166  			return layout.Dimensions{Size: gtx.Constraints.Min}
 167  		}),
 168  		layout.Stacked(func(gtx layout.Context) layout.Dimensions {
 169  			return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
 170  				size := gtx.Px(b.Size)
 171  				if b.Icon != nil {
 172  					b.Icon.Color = b.Color
 173  					b.Icon.Layout(gtx, unit.Px(float32(size)))
 174  				}
 175  				return layout.Dimensions{
 176  					Size: image.Point{X: size, Y: size},
 177  				}
 178  			})
 179  		}),
 180  		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
 181  			pointer.Ellipse(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
 182  			return b.Button.Layout(gtx)
 183  		}),
 184  	)
 185  }
 186  
 187  func drawInk(gtx layout.Context, c widget.Press) {
 188  	// duration is the number of seconds for the
 189  	// completed animation: expand while fading in, then
 190  	// out.
 191  	const (
 192  		expandDuration = float32(0.5)
 193  		fadeDuration   = float32(0.9)
 194  	)
 195  
 196  	now := gtx.Now
 197  
 198  	t := float32(now.Sub(c.Start).Seconds())
 199  
 200  	end := c.End
 201  	if end.IsZero() {
 202  		// If the press hasn't ended, don't fade-out.
 203  		end = now
 204  	}
 205  
 206  	endt := float32(end.Sub(c.Start).Seconds())
 207  
 208  	// Compute the fade-in/out position in [0;1].
 209  	var alphat float32
 210  	{
 211  		var haste float32
 212  		if c.Cancelled {
 213  			// If the press was cancelled before the inkwell
 214  			// was fully faded in, fast forward the animation
 215  			// to match the fade-out.
 216  			if h := 0.5 - endt/fadeDuration; h > 0 {
 217  				haste = h
 218  			}
 219  		}
 220  		// Fade in.
 221  		half1 := t/fadeDuration + haste
 222  		if half1 > 0.5 {
 223  			half1 = 0.5
 224  		}
 225  
 226  		// Fade out.
 227  		half2 := float32(now.Sub(end).Seconds())
 228  		half2 /= fadeDuration
 229  		half2 += haste
 230  		if half2 > 0.5 {
 231  			// Too old.
 232  			return
 233  		}
 234  
 235  		alphat = half1 + half2
 236  	}
 237  
 238  	// Compute the expand position in [0;1].
 239  	sizet := t
 240  	if c.Cancelled {
 241  		// Freeze expansion of cancelled presses.
 242  		sizet = endt
 243  	}
 244  	sizet /= expandDuration
 245  
 246  	// Animate only ended presses, and presses that are fading in.
 247  	if !c.End.IsZero() || sizet <= 1.0 {
 248  		op.InvalidateOp{}.Add(gtx.Ops)
 249  	}
 250  
 251  	if sizet > 1.0 {
 252  		sizet = 1.0
 253  	}
 254  
 255  	if alphat > .5 {
 256  		// Start fadeout after half the animation.
 257  		alphat = 1.0 - alphat
 258  	}
 259  	// Twice the speed to attain fully faded in at 0.5.
 260  	t2 := alphat * 2
 261  	// BeziƩr ease-in curve.
 262  	alphaBezier := t2 * t2 * (3.0 - 2.0*t2)
 263  	sizeBezier := sizet * sizet * (3.0 - 2.0*sizet)
 264  	size := float32(gtx.Constraints.Min.X)
 265  	if h := float32(gtx.Constraints.Min.Y); h > size {
 266  		size = h
 267  	}
 268  	// Cover the entire constraints min rectangle.
 269  	size *= 2 * float32(math.Sqrt(2))
 270  	// Apply curve values to size and color.
 271  	size *= sizeBezier
 272  	alpha := 0.7 * alphaBezier
 273  	const col = 0.8
 274  	ba, bc := byte(alpha*0xff), byte(col*0xff)
 275  	defer op.Save(gtx.Ops).Load()
 276  	rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba)
 277  	ink := paint.ColorOp{Color: rgba}
 278  	ink.Add(gtx.Ops)
 279  	rr := size * .5
 280  	op.Offset(c.Position.Add(f32.Point{
 281  		X: -rr,
 282  		Y: -rr,
 283  	})).Add(gtx.Ops)
 284  	clip.UniformRRect(f32.Rectangle{Max: f32.Pt(size, size)}, rr).Add(gtx.Ops)
 285  	paint.PaintOp{}.Add(gtx.Ops)
 286  }
 287