button.go 7.7 KB

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