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 }