fix: make the buttons non-responsive to the spacebar

This commit is contained in:
qm210 2024-11-02 21:14:50 +01:00 committed by GitHub
parent 2a2934b4e4
commit b08f5d4b1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 753 additions and 17 deletions

View File

@ -60,6 +60,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
wrong scaling ([#150][i150])
- Empty patch should not crash the native synth ([#148][i148])
- sointu-play does not default to the native synth yet, choose via `-tags=native`
- Space Bar does not re-trigger the Button with current focus (Enter still does)
### Changed
- The keyboard shortcuts are now again closer to what they were old trackers

View File

@ -3,15 +3,16 @@ package gioui
import (
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/tracker/gioui/patch"
patched "github.com/vsariola/sointu/tracker/gioui/patch/material"
)
type (
TipClickable struct {
Clickable widget.Clickable
Clickable patch.Clickable
TipArea component.TipArea
}
@ -22,12 +23,12 @@ type (
TipIconButtonStyle struct {
TipArea *component.TipArea
IconButtonStyle material.IconButtonStyle
IconButtonStyle patched.IconButtonStyle
Tooltip component.Tooltip
}
BoolClickable struct {
Clickable widget.Clickable
Clickable patch.Clickable
TipArea component.TipArea
Bool tracker.Bool
}
@ -63,7 +64,7 @@ func ActionIcon(gtx C, th *material.Theme, w *ActionClickable, icon []byte, tip
}
func TipIcon(th *material.Theme, w *TipClickable, icon []byte, tip string) TipIconButtonStyle {
iconButtonStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
iconButtonStyle := patched.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
iconButtonStyle.Color = primaryColor
iconButtonStyle.Background = transparent
iconButtonStyle.Inset = layout.UniformInset(unit.Dp(6))
@ -84,7 +85,7 @@ func ToggleIcon(gtx C, th *material.Theme, w *BoolClickable, offIcon, onIcon []b
for w.Clickable.Clicked(gtx) {
w.Bool.Toggle()
}
ibStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
ibStyle := patched.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
ibStyle.Background = transparent
ibStyle.Inset = layout.UniformInset(unit.Dp(6))
ibStyle.Color = primaryColor
@ -102,11 +103,11 @@ func (t *TipIconButtonStyle) Layout(gtx C) D {
return t.TipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
}
func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) material.ButtonStyle {
func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) patched.ButtonStyle {
for w.Clickable.Clicked(gtx) {
w.Action.Do()
}
ret := material.Button(th, &w.Clickable, text)
ret := patched.Button(th, &w.Clickable, text)
ret.Color = th.Palette.Fg
if !w.Action.Allowed() {
ret.Color = disabledTextColor
@ -116,11 +117,11 @@ func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) ma
return ret
}
func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) material.ButtonStyle {
func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) patched.ButtonStyle {
for b.Clickable.Clicked(gtx) {
b.Bool.Toggle()
}
ret := material.Button(th, &b.Clickable, text)
ret := patched.Button(th, &b.Clickable, text)
ret.Background = transparent
ret.Inset = layout.UniformInset(unit.Dp(6))
if b.Bool.Value() {
@ -133,16 +134,16 @@ func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) mate
return ret
}
func LowEmphasisButton(th *material.Theme, w *widget.Clickable, text string) material.ButtonStyle {
ret := material.Button(th, w, text)
func LowEmphasisButton(th *material.Theme, w *patch.Clickable, text string) patched.ButtonStyle {
ret := patched.Button(th, w, text)
ret.Color = th.Palette.Fg
ret.Background = transparent
ret.Inset = layout.UniformInset(unit.Dp(6))
return ret
}
func HighEmphasisButton(th *material.Theme, w *widget.Clickable, text string) material.ButtonStyle {
ret := material.Button(th, w, text)
func HighEmphasisButton(th *material.Theme, w *patch.Clickable, text string) patched.ButtonStyle {
ret := patched.Button(th, w, text)
ret.Color = th.Palette.ContrastFg
ret.Background = th.Palette.Fg
ret.Inset = layout.UniformInset(unit.Dp(6))

View File

@ -8,6 +8,7 @@ import (
"gioui.org/unit"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
patched "github.com/vsariola/sointu/tracker/gioui/patch/material"
)
type Dialog struct {
@ -22,9 +23,9 @@ type DialogStyle struct {
Text string
Inset layout.Inset
TextInset layout.Inset
AltStyle material.ButtonStyle
OkStyle material.ButtonStyle
CancelStyle material.ButtonStyle
AltStyle patched.ButtonStyle
OkStyle patched.ButtonStyle
CancelStyle patched.ButtonStyle
Shaper *text.Shaper
}

View File

@ -0,0 +1,22 @@
2024/10/27 qm210
As suggested by
https://github.com/vsariola/sointu/issues/156
and further by
https://github.com/vsariola/sointu/issues/151
the current Gio (0.7.1) Buttons (Clickables) have intrinsic behaviour
to re-trigger when SPACE is pressed.
This is so not what anyone using a Tracker expects, which is why for now,
this original Button component is copied here with that behaviour changed
https://github.com/gioui/gio/blob/v0.7.1/widget/button.go
https://github.com/gioui/gio/blob/v0.7.1/widget/material/button.go
https://github.com/gioui/gio/blob/v0.7.1/internal/f32color/rgba.go
This is obviously dangerous, because it decouples this Button from future
Gio releases, and our solution is a shady hack for now, but,
that spacebar shenanigans needs to end.
That is allowed, cf. comments in these files:
// SPDX-License-Identifier: Unlicense OR MIT

View File

@ -0,0 +1,196 @@
////////////
// qm210: Had to copy this button because Gioui v0.7.1 makes the Space Bar trigger the last Clicked button again.
// this interferes too much with the mindset of a music tracker.
////////////
// SPDX-License-Identifier: Unlicense OR MIT
package patch
import (
"image"
"time"
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
)
// Clickable represents a clickable area.
type Clickable struct {
click gesture.Click
history []Press
requestClicks int
pressedKey key.Name
}
// Click represents a click.
type Click struct {
Modifiers key.Modifiers
NumClicks int
}
// Press represents a past pointer press.
type Press struct {
// Position of the press.
Position image.Point
// Start is when the press began.
Start time.Time
// End is when the press was ended by a release or cancel.
// A zero End means it hasn't ended yet.
End time.Time
// Cancelled is true for cancelled presses.
Cancelled bool
}
// Click executes a simple programmatic click.
func (b *Clickable) Click() {
b.requestClicks++
}
// Clicked calls Update and reports whether a click was registered.
func (b *Clickable) Clicked(gtx layout.Context) bool {
return b.clicked(b, gtx)
}
func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool {
_, clicked := b.update(t, gtx)
return clicked
}
// Hovered reports whether a pointer is over the element.
func (b *Clickable) Hovered() bool {
return b.click.Hovered()
}
// Pressed reports whether a pointer is pressing the element.
func (b *Clickable) Pressed() bool {
return b.click.Pressed()
}
// History is the past pointer presses useful for drawing markers.
// History is retained for a short duration (about a second).
func (b *Clickable) History() []Press {
return b.history
}
// Layout and update the button state.
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
return b.layout(b, gtx, w)
}
func (b *Clickable) layout(t event.Tag, gtx layout.Context, w layout.Widget) layout.Dimensions {
for {
_, ok := b.update(t, gtx)
if !ok {
break
}
}
m := op.Record(gtx.Ops)
dims := w(gtx)
c := m.Stop()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
b.click.Add(gtx.Ops)
event.Op(gtx.Ops, t)
c.Add(gtx.Ops)
return dims
}
// Update the button state by processing events, and return the next
// click, if any.
func (b *Clickable) Update(gtx layout.Context) (Click, bool) {
return b.update(b, gtx)
}
func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
for len(b.history) > 0 {
c := b.history[0]
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
break
}
n := copy(b.history, b.history[1:])
b.history = b.history[:n]
}
if c := b.requestClicks; c > 0 {
b.requestClicks = 0
return Click{
NumClicks: c,
}, true
}
for {
e, ok := b.click.Update(gtx.Source)
if !ok {
break
}
switch e.Kind {
case gesture.KindClick:
if l := len(b.history); l > 0 {
b.history[l-1].End = gtx.Now
}
return Click{
Modifiers: e.Modifiers,
NumClicks: e.NumClicks,
}, true
case gesture.KindCancel:
for i := range b.history {
b.history[i].Cancelled = true
if b.history[i].End.IsZero() {
b.history[i].End = gtx.Now
}
}
case gesture.KindPress:
if e.Source == pointer.Mouse {
gtx.Execute(key.FocusCmd{Tag: t})
}
b.history = append(b.history, Press{
Position: e.Position,
Start: gtx.Now,
})
}
}
// qm210: removed the additional Filter for key.NameSpace
for {
e, ok := gtx.Event(
key.FocusFilter{Target: t},
key.Filter{Focus: t, Name: key.NameReturn},
)
if !ok {
break
}
switch e := e.(type) {
case key.FocusEvent:
if e.Focus {
b.pressedKey = ""
}
case key.Event:
if !gtx.Focused(t) {
break
}
if e.Name != key.NameReturn {
break
}
switch e.State {
case key.Press:
b.pressedKey = e.Name
case key.Release:
if b.pressedKey != e.Name {
break
}
// only register a key as a click if the key was pressed and released while this button was focused
b.pressedKey = ""
return Click{
Modifiers: e.Modifiers,
NumClicks: 1,
}, true
}
}
}
return Click{}, false
}

View File

@ -0,0 +1,191 @@
// SPDX-License-Identifier: Unlicense OR MIT
package f32color
import (
"image/color"
"math"
)
//go:generate go run ./f32colorgen -out tables.go
// RGBA is a 32 bit floating point linear premultiplied color space.
type RGBA struct {
R, G, B, A float32
}
// Array returns rgba values in a [4]float32 array.
func (rgba RGBA) Array() [4]float32 {
return [4]float32{rgba.R, rgba.G, rgba.B, rgba.A}
}
// Float32 returns r, g, b, a values.
func (col RGBA) Float32() (r, g, b, a float32) {
return col.R, col.G, col.B, col.A
}
// SRGBA converts from linear to sRGB color space.
func (col RGBA) SRGB() color.NRGBA {
if col.A == 0 {
return color.NRGBA{}
}
return color.NRGBA{
R: uint8(linearTosRGB(col.R/col.A)*255 + .5),
G: uint8(linearTosRGB(col.G/col.A)*255 + .5),
B: uint8(linearTosRGB(col.B/col.A)*255 + .5),
A: uint8(col.A*255 + .5),
}
}
// Luminance calculates the relative luminance of a linear RGBA color.
// Normalized to 0 for black and 1 for white.
//
// See https://www.w3.org/TR/WCAG20/#relativeluminancedef for more details
func (col RGBA) Luminance() float32 {
return 0.2126*col.R + 0.7152*col.G + 0.0722*col.B
}
// Opaque returns the color without alpha component.
func (col RGBA) Opaque() RGBA {
col.A = 1.0
return col
}
// LinearFromSRGB converts from col in the sRGB colorspace to RGBA.
func LinearFromSRGB(col color.NRGBA) RGBA {
af := float32(col.A) / 0xFF
return RGBA{
R: srgb8ToLinear[col.R] * af, // sRGBToLinear(float32(col.R)/0xff) * af,
G: srgb8ToLinear[col.G] * af, // sRGBToLinear(float32(col.G)/0xff) * af,
B: srgb8ToLinear[col.B] * af, // sRGBToLinear(float32(col.B)/0xff) * af,
A: af,
}
}
// NRGBAToRGBA converts from non-premultiplied sRGB color to premultiplied sRGB color.
//
// Each component in the result is `sRGBToLinear(c * alpha)`, where `c`
// is the linear color.
func NRGBAToRGBA(col color.NRGBA) color.RGBA {
if col.A == 0xFF {
return color.RGBA(col)
}
c := LinearFromSRGB(col)
return color.RGBA{
R: uint8(linearTosRGB(c.R)*255 + .5),
G: uint8(linearTosRGB(c.G)*255 + .5),
B: uint8(linearTosRGB(c.B)*255 + .5),
A: col.A,
}
}
// NRGBAToLinearRGBA converts from non-premultiplied sRGB color to premultiplied linear RGBA color.
//
// Each component in the result is `c * alpha`, where `c` is the linear color.
func NRGBAToLinearRGBA(col color.NRGBA) color.RGBA {
if col.A == 0xFF {
return color.RGBA(col)
}
c := LinearFromSRGB(col)
return color.RGBA{
R: uint8(c.R*255 + .5),
G: uint8(c.G*255 + .5),
B: uint8(c.B*255 + .5),
A: col.A,
}
}
// RGBAToNRGBA converts from premultiplied sRGB color to non-premultiplied sRGB color.
func RGBAToNRGBA(col color.RGBA) color.NRGBA {
if col.A == 0xFF {
return color.NRGBA(col)
}
linear := RGBA{
R: sRGBToLinear(float32(col.R) / 0xff),
G: sRGBToLinear(float32(col.G) / 0xff),
B: sRGBToLinear(float32(col.B) / 0xff),
A: float32(col.A) / 0xff,
}
return linear.SRGB()
}
// linearTosRGB transforms color value from linear to sRGB.
func linearTosRGB(c float32) float32 {
// Formula from EXT_sRGB.
switch {
case c <= 0:
return 0
case 0 < c && c < 0.0031308:
return 12.92 * c
case 0.0031308 <= c && c < 1:
return 1.055*float32(math.Pow(float64(c), 0.41666)) - 0.055
}
return 1
}
// sRGBToLinear transforms color value from sRGB to linear.
func sRGBToLinear(c float32) float32 {
// Formula from EXT_sRGB.
if c <= 0.04045 {
return c / 12.92
} else {
return float32(math.Pow(float64((c+0.055)/1.055), 2.4))
}
}
// MulAlpha applies the alpha to the color.
func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA {
c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF)
return c
}
// Disabled blends color towards the luminance and multiplies alpha.
// Blending towards luminance will desaturate the color.
// Multiplying alpha blends the color together more with the background.
func Disabled(c color.NRGBA) (d color.NRGBA) {
const r = 80 // blend ratio
lum := approxLuminance(c)
d = mix(c, color.NRGBA{A: c.A, R: lum, G: lum, B: lum}, r)
d = MulAlpha(d, 128+32)
return
}
// Hovered blends dark colors towards white, and light colors towards
// black. It is approximate because it operates in non-linear sRGB space.
func Hovered(c color.NRGBA) (h color.NRGBA) {
if c.A == 0 {
// Provide a reasonable default for transparent widgets.
return color.NRGBA{A: 0x44, R: 0x88, G: 0x88, B: 0x88}
}
const ratio = 0x20
m := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: c.A}
if approxLuminance(c) > 128 {
m = color.NRGBA{A: c.A}
}
return mix(m, c, ratio)
}
// mix mixes c1 and c2 weighted by (1 - a/256) and a/256 respectively.
func mix(c1, c2 color.NRGBA, a uint8) color.NRGBA {
ai := int(a)
return color.NRGBA{
R: byte((int(c1.R)*ai + int(c2.R)*(256-ai)) / 256),
G: byte((int(c1.G)*ai + int(c2.G)*(256-ai)) / 256),
B: byte((int(c1.B)*ai + int(c2.B)*(256-ai)) / 256),
A: byte((int(c1.A)*ai + int(c2.A)*(256-ai)) / 256),
}
}
// approxLuminance is a fast approximate version of RGBA.Luminance.
func approxLuminance(c color.NRGBA) byte {
const (
r = 13933 // 0.2126 * 256 * 256
g = 46871 // 0.7152 * 256 * 256
b = 4732 // 0.0722 * 256 * 256
t = r + g + b
)
return byte((r*int(c.R) + g*int(c.G) + b*int(c.B)) / t)
}

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Code generated by f32colorgen. DO NOT EDIT.
package f32color
// table corresponds to sRGBToLinear(float32(index)/0xff)
var srgb8ToLinear = [...]float32{
0, 0.000303527, 0.000607054, 0.000910581, 0.001214108, 0.001517635, 0.001821162, 0.0021246888, 0.002428216, 0.002731743, 0.00303527, 0.0033465363, 0.0036765079, 0.004024718, 0.004391443, 0.004776954,
0.005181518, 0.0056053926, 0.006048834, 0.0065120924, 0.0069954116, 0.007499033, 0.008023194, 0.008568126, 0.009134059, 0.00972122, 0.010329825, 0.010960096, 0.011612247, 0.012286489, 0.0129830325, 0.013702083,
0.014443846, 0.015208517, 0.015996296, 0.016807377, 0.017641956, 0.01850022, 0.019382365, 0.020288566, 0.021219013, 0.022173887, 0.023153368, 0.024157634, 0.025186861, 0.026241226, 0.027320895, 0.028426042,
0.029556837, 0.030713446, 0.031896036, 0.033104766, 0.03433981, 0.035601318, 0.03688945, 0.03820437, 0.039546244, 0.040915202, 0.042311415, 0.043735035, 0.04518621, 0.04666509, 0.048171826, 0.049706567,
0.05126947, 0.05286066, 0.05448029, 0.056128502, 0.05780544, 0.059511248, 0.06124608, 0.06301004, 0.06480329, 0.06662596, 0.06847819, 0.07036012, 0.07227187, 0.07421359, 0.0761854, 0.078187436,
0.080219835, 0.08228272, 0.08437622, 0.08650047, 0.08865561, 0.09084174, 0.09305899, 0.09530749, 0.09758737, 0.09989875, 0.102241755, 0.10461651, 0.10702312, 0.10946173, 0.11193245, 0.11443539,
0.11697068, 0.11953844, 0.122138806, 0.12477185, 0.12743771, 0.1301365, 0.13286835, 0.13563335, 0.13843164, 0.14126332, 0.14412849, 0.14702728, 0.1499598, 0.15292618, 0.15592648, 0.15896088,
0.16202942, 0.16513222, 0.16826941, 0.17144111, 0.1746474, 0.17788842, 0.18116425, 0.18447499, 0.18782078, 0.19120169, 0.19461782, 0.19806932, 0.20155625, 0.20507872, 0.20863685, 0.21223074,
0.21586055, 0.21952623, 0.223228, 0.2269659, 0.23074009, 0.23455067, 0.23839766, 0.24228121, 0.24620141, 0.25015837, 0.25415218, 0.25818294, 0.26225075, 0.2663557, 0.27049786, 0.2746774,
0.27889434, 0.28314883, 0.2874409, 0.29177073, 0.29613835, 0.30054384, 0.30498737, 0.30946898, 0.31398878, 0.31854683, 0.32314327, 0.32777816, 0.33245158, 0.33716366, 0.34191447, 0.3467041,
0.35153273, 0.35640025, 0.3613069, 0.36625272, 0.37123778, 0.37626222, 0.3813261, 0.38642955, 0.39157256, 0.39675534, 0.40197787, 0.4072403, 0.4125427, 0.41788515, 0.42326775, 0.42869058,
0.4341537, 0.43965724, 0.44520128, 0.45078585, 0.4564111, 0.46207705, 0.46778387, 0.47353154, 0.47932023, 0.48515, 0.4910209, 0.49693304, 0.5028866, 0.50888145, 0.5149178, 0.5209957,
0.5271153, 0.53327656, 0.5394796, 0.5457246, 0.55201155, 0.5583405, 0.56471163, 0.5711249, 0.5775806, 0.58407855, 0.59061897, 0.5972019, 0.6038274, 0.6104957, 0.61720663, 0.6239605,
0.6307572, 0.63759696, 0.64447975, 0.6514057, 0.6583749, 0.66538733, 0.6724432, 0.67954254, 0.6866855, 0.6938719, 0.7011021, 0.70837593, 0.71569365, 0.7230553, 0.7304609, 0.73791057,
0.74540436, 0.7529423, 0.76052463, 0.7681513, 0.77582234, 0.7835379, 0.79129803, 0.79910284, 0.80695236, 0.8148467, 0.82278585, 0.83076996, 0.8387991, 0.84687334, 0.8549927, 0.8631573,
0.8713672, 0.87962234, 0.8879232, 0.89626944, 0.90466136, 0.9130987, 0.92158204, 0.9301109, 0.9386859, 0.9473066, 0.9559735, 0.9646863, 0.9734455, 0.9822506, 0.9911022, 1,
}

View File

@ -0,0 +1,299 @@
// SPDX-License-Identifier: Unlicense OR MIT
package material
import (
"github.com/vsariola/sointu/tracker/gioui/patch"
"github.com/vsariola/sointu/tracker/gioui/patch/f32color"
"image"
"image/color"
"math"
"gioui.org/font"
"gioui.org/io/semantic"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type ButtonStyle struct {
Text string
// Color is the text color.
Color color.NRGBA
Font font.Font
TextSize unit.Sp
Background color.NRGBA
CornerRadius unit.Dp
Inset layout.Inset
Button *patch.Clickable
shaper *text.Shaper
}
type ButtonLayoutStyle struct {
Background color.NRGBA
CornerRadius unit.Dp
Button *patch.Clickable
}
type IconButtonStyle struct {
Background color.NRGBA
// Color is the icon color.
Color color.NRGBA
Icon *widget.Icon
// Size is the icon size.
Size unit.Dp
Inset layout.Inset
Button *patch.Clickable
Description string
}
func Button(th *material.Theme, button *patch.Clickable, txt string) ButtonStyle {
b := ButtonStyle{
Text: txt,
Color: th.Palette.ContrastFg,
CornerRadius: 4,
Background: th.Palette.ContrastBg,
TextSize: th.TextSize * 14.0 / 16.0,
Inset: layout.Inset{
Top: 10, Bottom: 10,
Left: 12, Right: 12,
},
Button: button,
shaper: th.Shaper,
}
b.Font.Typeface = th.Face
return b
}
func ButtonLayout(th *material.Theme, button *patch.Clickable) ButtonLayoutStyle {
return ButtonLayoutStyle{
Button: button,
Background: th.Palette.ContrastBg,
CornerRadius: 4,
}
}
func IconButton(th *material.Theme, button *patch.Clickable, icon *widget.Icon, description string) IconButtonStyle {
return IconButtonStyle{
Background: th.Palette.ContrastBg,
Color: th.Palette.ContrastFg,
Icon: icon,
Size: 24,
Inset: layout.UniformInset(12),
Button: button,
Description: description,
}
}
// Clickable lays out a rectangular clickable widget without further
// decoration.
func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions {
return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
if button.Hovered() || gtx.Focused(button) {
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
}
for _, c := range button.History() {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
},
w,
)
})
}
func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
return ButtonLayoutStyle{
Background: b.Background,
CornerRadius: b.CornerRadius,
Button: b.Button,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
colMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text, colMacro.Stop())
})
})
}
func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
min := gtx.Constraints.Min
return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
rr := gtx.Dp(b.CornerRadius)
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
background := b.Background
switch {
case !gtx.Enabled():
background = f32color.Disabled(b.Background)
case b.Button.Hovered() || gtx.Focused(b.Button):
background = f32color.Hovered(b.Background)
}
paint.Fill(gtx.Ops, background)
for _, c := range b.Button.History() {
drawInk(gtx, (widget.Press)(c))
}
return layout.Dimensions{Size: gtx.Constraints.Min}
},
func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min = min
return layout.Center.Layout(gtx, w)
},
)
})
}
func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
m := op.Record(gtx.Ops)
dims := b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
if d := b.Description; d != "" {
semantic.DescriptionOp(b.Description).Add(gtx.Ops)
}
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
background := b.Background
switch {
case !gtx.Enabled():
background = f32color.Disabled(b.Background)
case b.Button.Hovered() || gtx.Focused(b.Button):
background = f32color.Hovered(b.Background)
}
paint.Fill(gtx.Ops, background)
for _, c := range b.Button.History() {
drawInk(gtx, (widget.Press)(c))
}
return layout.Dimensions{Size: gtx.Constraints.Min}
},
func(gtx layout.Context) layout.Dimensions {
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
size := gtx.Dp(b.Size)
if b.Icon != nil {
gtx.Constraints.Min = image.Point{X: size}
b.Icon.Layout(gtx, b.Color)
}
return layout.Dimensions{
Size: image.Point{X: size, Y: size},
}
})
},
)
})
c := m.Stop()
bounds := image.Rectangle{Max: dims.Size}
defer clip.Ellipse(bounds).Push(gtx.Ops).Pop()
c.Add(gtx.Ops)
return dims
}
func drawInk(gtx layout.Context, c widget.Press) {
// duration is the number of seconds for the
// completed animation: expand while fading in, then
// out.
const (
expandDuration = float32(0.5)
fadeDuration = float32(0.9)
)
now := gtx.Now
t := float32(now.Sub(c.Start).Seconds())
end := c.End
if end.IsZero() {
// If the press hasn't ended, don't fade-out.
end = now
}
endt := float32(end.Sub(c.Start).Seconds())
// Compute the fade-in/out position in [0;1].
var alphat float32
{
var haste float32
if c.Cancelled {
// If the press was cancelled before the inkwell
// was fully faded in, fast forward the animation
// to match the fade-out.
if h := 0.5 - endt/fadeDuration; h > 0 {
haste = h
}
}
// Fade in.
half1 := t/fadeDuration + haste
if half1 > 0.5 {
half1 = 0.5
}
// Fade out.
half2 := float32(now.Sub(end).Seconds())
half2 /= fadeDuration
half2 += haste
if half2 > 0.5 {
// Too old.
return
}
alphat = half1 + half2
}
// Compute the expand position in [0;1].
sizet := t
if c.Cancelled {
// Freeze expansion of cancelled presses.
sizet = endt
}
sizet /= expandDuration
// Animate only ended presses, and presses that are fading in.
if !c.End.IsZero() || sizet <= 1.0 {
gtx.Execute(op.InvalidateCmd{})
}
if sizet > 1.0 {
sizet = 1.0
}
if alphat > .5 {
// Start fadeout after half the animation.
alphat = 1.0 - alphat
}
// Twice the speed to attain fully faded in at 0.5.
t2 := alphat * 2
// Beziér ease-in curve.
alphaBezier := t2 * t2 * (3.0 - 2.0*t2)
sizeBezier := sizet * sizet * (3.0 - 2.0*sizet)
size := gtx.Constraints.Min.X
if h := gtx.Constraints.Min.Y; h > size {
size = h
}
// Cover the entire constraints min rectangle and
// apply curve values to size and color.
size = int(float32(size) * 2 * float32(math.Sqrt(2)) * sizeBezier)
alpha := 0.7 * alphaBezier
const col = 0.8
ba, bc := byte(alpha*0xff), byte(col*0xff)
rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba)
ink := paint.ColorOp{Color: rgba}
ink.Add(gtx.Ops)
rr := size / 2
defer op.Offset(c.Position.Add(image.Point{
X: -rr,
Y: -rr,
})).Push(gtx.Ops).Pop()
defer clip.UniformRRect(image.Rectangle{Max: image.Pt(size, size)}, rr).Push(gtx.Ops).Pop()
paint.PaintOp{}.Add(gtx.Ops)
}