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