gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/widget/material/button.go (about)

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