mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
feat(tracker/gioui): make buttons never have focus
The exception to the rule is the dialog buttons (which use still the default material buttons), because when there is a modal dialog on screen, there is not much else the user would want to do. Fixes #156
This commit is contained in:
parent
b08f5d4b1e
commit
65a7f060ec
@ -60,7 +60,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
wrong scaling ([#150][i150])
|
wrong scaling ([#150][i150])
|
||||||
- Empty patch should not crash the native synth ([#148][i148])
|
- Empty patch should not crash the native synth ([#148][i148])
|
||||||
- sointu-play does not default to the native synth yet, choose via `-tags=native`
|
- 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)
|
- Most buttons never gain focus, so that clicking a button does not stop
|
||||||
|
whatever the user was currently doing and so that the user does not
|
||||||
|
accidentally trigger the buttons by having them focused and e.g. hitting space
|
||||||
|
([#156][i156])
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The keyboard shortcuts are now again closer to what they were old trackers
|
- The keyboard shortcuts are now again closer to what they were old trackers
|
||||||
@ -280,6 +283,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[i150]: https://github.com/vsariola/sointu/issues/150
|
[i150]: https://github.com/vsariola/sointu/issues/150
|
||||||
[i151]: https://github.com/vsariola/sointu/issues/151
|
[i151]: https://github.com/vsariola/sointu/issues/151
|
||||||
[i154]: https://github.com/vsariola/sointu/issues/154
|
[i154]: https://github.com/vsariola/sointu/issues/154
|
||||||
|
[i156]: https://github.com/vsariola/sointu/issues/156
|
||||||
[i157]: https://github.com/vsariola/sointu/issues/157
|
[i157]: https://github.com/vsariola/sointu/issues/157
|
||||||
[i158]: https://github.com/vsariola/sointu/issues/158
|
[i158]: https://github.com/vsariola/sointu/issues/158
|
||||||
[i160]: https://github.com/vsariola/sointu/issues/160
|
[i160]: https://github.com/vsariola/sointu/issues/160
|
||||||
|
@ -1,18 +1,30 @@
|
|||||||
package gioui
|
package gioui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gioui.org/font"
|
||||||
|
"gioui.org/gesture"
|
||||||
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/semantic"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/op/clip"
|
||||||
|
"gioui.org/op/paint"
|
||||||
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
|
"gioui.org/widget"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
"gioui.org/x/component"
|
"gioui.org/x/component"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
"github.com/vsariola/sointu/tracker/gioui/patch"
|
|
||||||
patched "github.com/vsariola/sointu/tracker/gioui/patch/material"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
TipClickable struct {
|
TipClickable struct {
|
||||||
Clickable patch.Clickable
|
Clickable Clickable
|
||||||
TipArea component.TipArea
|
TipArea component.TipArea
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,12 +35,12 @@ type (
|
|||||||
|
|
||||||
TipIconButtonStyle struct {
|
TipIconButtonStyle struct {
|
||||||
TipArea *component.TipArea
|
TipArea *component.TipArea
|
||||||
IconButtonStyle patched.IconButtonStyle
|
IconButtonStyle IconButtonStyle
|
||||||
Tooltip component.Tooltip
|
Tooltip component.Tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
BoolClickable struct {
|
BoolClickable struct {
|
||||||
Clickable patch.Clickable
|
Clickable Clickable
|
||||||
TipArea component.TipArea
|
TipArea component.TipArea
|
||||||
Bool tracker.Bool
|
Bool tracker.Bool
|
||||||
}
|
}
|
||||||
@ -64,7 +76,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 {
|
func TipIcon(th *material.Theme, w *TipClickable, icon []byte, tip string) TipIconButtonStyle {
|
||||||
iconButtonStyle := patched.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
iconButtonStyle := IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||||
iconButtonStyle.Color = primaryColor
|
iconButtonStyle.Color = primaryColor
|
||||||
iconButtonStyle.Background = transparent
|
iconButtonStyle.Background = transparent
|
||||||
iconButtonStyle.Inset = layout.UniformInset(unit.Dp(6))
|
iconButtonStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||||
@ -85,7 +97,7 @@ func ToggleIcon(gtx C, th *material.Theme, w *BoolClickable, offIcon, onIcon []b
|
|||||||
for w.Clickable.Clicked(gtx) {
|
for w.Clickable.Clicked(gtx) {
|
||||||
w.Bool.Toggle()
|
w.Bool.Toggle()
|
||||||
}
|
}
|
||||||
ibStyle := patched.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
ibStyle := IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||||
ibStyle.Background = transparent
|
ibStyle.Background = transparent
|
||||||
ibStyle.Inset = layout.UniformInset(unit.Dp(6))
|
ibStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||||
ibStyle.Color = primaryColor
|
ibStyle.Color = primaryColor
|
||||||
@ -103,11 +115,11 @@ func (t *TipIconButtonStyle) Layout(gtx C) D {
|
|||||||
return t.TipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
return t.TipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) patched.ButtonStyle {
|
func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) ButtonStyle {
|
||||||
for w.Clickable.Clicked(gtx) {
|
for w.Clickable.Clicked(gtx) {
|
||||||
w.Action.Do()
|
w.Action.Do()
|
||||||
}
|
}
|
||||||
ret := patched.Button(th, &w.Clickable, text)
|
ret := Button(th, &w.Clickable, text)
|
||||||
ret.Color = th.Palette.Fg
|
ret.Color = th.Palette.Fg
|
||||||
if !w.Action.Allowed() {
|
if !w.Action.Allowed() {
|
||||||
ret.Color = disabledTextColor
|
ret.Color = disabledTextColor
|
||||||
@ -117,11 +129,11 @@ func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) pa
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) patched.ButtonStyle {
|
func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) ButtonStyle {
|
||||||
for b.Clickable.Clicked(gtx) {
|
for b.Clickable.Clicked(gtx) {
|
||||||
b.Bool.Toggle()
|
b.Bool.Toggle()
|
||||||
}
|
}
|
||||||
ret := patched.Button(th, &b.Clickable, text)
|
ret := Button(th, &b.Clickable, text)
|
||||||
ret.Background = transparent
|
ret.Background = transparent
|
||||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||||
if b.Bool.Value() {
|
if b.Bool.Value() {
|
||||||
@ -134,18 +146,409 @@ func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) patc
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func LowEmphasisButton(th *material.Theme, w *patch.Clickable, text string) patched.ButtonStyle {
|
func LowEmphasisButton(th *material.Theme, w *Clickable, text string) ButtonStyle {
|
||||||
ret := patched.Button(th, w, text)
|
ret := Button(th, w, text)
|
||||||
ret.Color = th.Palette.Fg
|
ret.Color = th.Palette.Fg
|
||||||
ret.Background = transparent
|
ret.Background = transparent
|
||||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func HighEmphasisButton(th *material.Theme, w *patch.Clickable, text string) patched.ButtonStyle {
|
func HighEmphasisButton(th *material.Theme, w *Clickable, text string) ButtonStyle {
|
||||||
ret := patched.Button(th, w, text)
|
ret := Button(th, w, text)
|
||||||
ret.Color = th.Palette.ContrastFg
|
ret.Color = th.Palette.ContrastFg
|
||||||
ret.Background = th.Palette.Fg
|
ret.Background = th.Palette.Fg
|
||||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clickable represents a clickable area.
|
||||||
|
type Clickable struct {
|
||||||
|
click gesture.Click
|
||||||
|
history []widget.Press
|
||||||
|
|
||||||
|
requestClicks int
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() []widget.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) (widget.Click, bool) {
|
||||||
|
return b.update(b, gtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Clickable) update(t event.Tag, gtx layout.Context) (widget.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 widget.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 widget.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:
|
||||||
|
b.history = append(b.history, widget.Press{
|
||||||
|
Position: e.Position,
|
||||||
|
Start: gtx.Now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widget.Click{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *Clickable
|
||||||
|
shaper *text.Shaper
|
||||||
|
}
|
||||||
|
|
||||||
|
type ButtonLayoutStyle struct {
|
||||||
|
Background color.NRGBA
|
||||||
|
CornerRadius unit.Dp
|
||||||
|
Button *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 *Clickable
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Button(th *material.Theme, button *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 *Clickable) ButtonLayoutStyle {
|
||||||
|
return ButtonLayoutStyle{
|
||||||
|
Button: button,
|
||||||
|
Background: th.Palette.ContrastBg,
|
||||||
|
CornerRadius: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IconButton(th *material.Theme, button *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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 b.Button.Hovered():
|
||||||
|
background = hoveredColor(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 b.Button.Hovered():
|
||||||
|
background = hoveredColor(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 := color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}
|
||||||
|
rgba.A = uint8(uint32(rgba.A) * uint32(ba) / 0xFF)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hoveredColor(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 int(c.R)+int(c.G)+int(c.B) > 384 {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,15 +6,17 @@ import (
|
|||||||
"gioui.org/op/paint"
|
"gioui.org/op/paint"
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
|
"gioui.org/widget"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
patched "github.com/vsariola/sointu/tracker/gioui/patch/material"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Dialog struct {
|
type Dialog struct {
|
||||||
BtnAlt *ActionClickable
|
BtnAlt widget.Clickable
|
||||||
BtnOk *ActionClickable
|
BtnOk widget.Clickable
|
||||||
BtnCancel *ActionClickable
|
BtnCancel widget.Clickable
|
||||||
|
|
||||||
|
ok, alt, cancel tracker.Action
|
||||||
}
|
}
|
||||||
|
|
||||||
type DialogStyle struct {
|
type DialogStyle struct {
|
||||||
@ -23,18 +25,14 @@ type DialogStyle struct {
|
|||||||
Text string
|
Text string
|
||||||
Inset layout.Inset
|
Inset layout.Inset
|
||||||
TextInset layout.Inset
|
TextInset layout.Inset
|
||||||
AltStyle patched.ButtonStyle
|
AltStyle material.ButtonStyle
|
||||||
OkStyle patched.ButtonStyle
|
OkStyle material.ButtonStyle
|
||||||
CancelStyle patched.ButtonStyle
|
CancelStyle material.ButtonStyle
|
||||||
Shaper *text.Shaper
|
Shaper *text.Shaper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDialog(ok, alt, cancel tracker.Action) *Dialog {
|
func NewDialog(ok, alt, cancel tracker.Action) *Dialog {
|
||||||
ret := &Dialog{
|
ret := &Dialog{ok: ok, alt: alt, cancel: cancel}
|
||||||
BtnOk: NewActionClickable(ok),
|
|
||||||
BtnAlt: NewActionClickable(alt),
|
|
||||||
BtnCancel: NewActionClickable(cancel),
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@ -46,21 +44,26 @@ func ConfirmDialog(gtx C, th *material.Theme, dialog *Dialog, title, text string
|
|||||||
Text: text,
|
Text: text,
|
||||||
Inset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12), Left: unit.Dp(20), Right: unit.Dp(20)},
|
Inset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12), Left: unit.Dp(20), Right: unit.Dp(20)},
|
||||||
TextInset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12)},
|
TextInset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12)},
|
||||||
AltStyle: ActionButton(gtx, th, dialog.BtnAlt, "Alt"),
|
AltStyle: material.Button(th, &dialog.BtnAlt, "Alt"),
|
||||||
OkStyle: ActionButton(gtx, th, dialog.BtnOk, "Ok"),
|
OkStyle: material.Button(th, &dialog.BtnOk, "Ok"),
|
||||||
CancelStyle: ActionButton(gtx, th, dialog.BtnCancel, "Cancel"),
|
CancelStyle: material.Button(th, &dialog.BtnCancel, "Cancel"),
|
||||||
Shaper: th.Shaper,
|
Shaper: th.Shaper,
|
||||||
}
|
}
|
||||||
|
for _, b := range [...]*material.ButtonStyle{&ret.AltStyle, &ret.OkStyle, &ret.CancelStyle} {
|
||||||
|
b.Background = transparent
|
||||||
|
b.Inset = layout.UniformInset(unit.Dp(6))
|
||||||
|
b.Color = th.Palette.Fg
|
||||||
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dialog) handleKeysForButton(gtx C, btn, next, prev *ActionClickable) {
|
func (d *Dialog) handleKeysForButton(gtx C, btn, next, prev *widget.Clickable) {
|
||||||
for {
|
for {
|
||||||
e, ok := gtx.Event(
|
e, ok := gtx.Event(
|
||||||
key.Filter{Focus: &btn.Clickable, Name: key.NameLeftArrow},
|
key.Filter{Focus: btn, Name: key.NameLeftArrow},
|
||||||
key.Filter{Focus: &btn.Clickable, Name: key.NameRightArrow},
|
key.Filter{Focus: btn, Name: key.NameRightArrow},
|
||||||
key.Filter{Focus: &btn.Clickable, Name: key.NameEscape},
|
key.Filter{Focus: btn, Name: key.NameEscape},
|
||||||
key.Filter{Focus: &btn.Clickable, Name: key.NameTab, Optional: key.ModShift},
|
key.Filter{Focus: btn, Name: key.NameTab, Optional: key.ModShift},
|
||||||
)
|
)
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
@ -68,30 +71,39 @@ func (d *Dialog) handleKeysForButton(gtx C, btn, next, prev *ActionClickable) {
|
|||||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||||
switch {
|
switch {
|
||||||
case e.Name == key.NameLeftArrow || (e.Name == key.NameTab && e.Modifiers.Contain(key.ModShift)):
|
case e.Name == key.NameLeftArrow || (e.Name == key.NameTab && e.Modifiers.Contain(key.ModShift)):
|
||||||
gtx.Execute(key.FocusCmd{Tag: &prev.Clickable})
|
gtx.Execute(key.FocusCmd{Tag: prev})
|
||||||
case e.Name == key.NameRightArrow || (e.Name == key.NameTab && !e.Modifiers.Contain(key.ModShift)):
|
case e.Name == key.NameRightArrow || (e.Name == key.NameTab && !e.Modifiers.Contain(key.ModShift)):
|
||||||
gtx.Execute(key.FocusCmd{Tag: &next.Clickable})
|
gtx.Execute(key.FocusCmd{Tag: next})
|
||||||
case e.Name == key.NameEscape:
|
case e.Name == key.NameEscape:
|
||||||
d.BtnCancel.Action.Do()
|
d.cancel.Do()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dialog) handleKeys(gtx C) {
|
func (d *Dialog) handleKeys(gtx C) {
|
||||||
if d.BtnAlt.Action.Allowed() {
|
for d.BtnOk.Clicked(gtx) {
|
||||||
d.handleKeysForButton(gtx, d.BtnAlt, d.BtnCancel, d.BtnOk)
|
d.ok.Do()
|
||||||
d.handleKeysForButton(gtx, d.BtnCancel, d.BtnOk, d.BtnAlt)
|
}
|
||||||
d.handleKeysForButton(gtx, d.BtnOk, d.BtnAlt, d.BtnCancel)
|
for d.BtnAlt.Clicked(gtx) {
|
||||||
|
d.alt.Do()
|
||||||
|
}
|
||||||
|
for d.BtnCancel.Clicked(gtx) {
|
||||||
|
d.cancel.Do()
|
||||||
|
}
|
||||||
|
if d.alt.Allowed() {
|
||||||
|
d.handleKeysForButton(gtx, &d.BtnAlt, &d.BtnCancel, &d.BtnOk)
|
||||||
|
d.handleKeysForButton(gtx, &d.BtnCancel, &d.BtnOk, &d.BtnAlt)
|
||||||
|
d.handleKeysForButton(gtx, &d.BtnOk, &d.BtnAlt, &d.BtnCancel)
|
||||||
} else {
|
} else {
|
||||||
d.handleKeysForButton(gtx, d.BtnOk, d.BtnCancel, d.BtnCancel)
|
d.handleKeysForButton(gtx, &d.BtnOk, &d.BtnCancel, &d.BtnCancel)
|
||||||
d.handleKeysForButton(gtx, d.BtnCancel, d.BtnOk, d.BtnOk)
|
d.handleKeysForButton(gtx, &d.BtnCancel, &d.BtnOk, &d.BtnOk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DialogStyle) Layout(gtx C) D {
|
func (d *DialogStyle) Layout(gtx C) D {
|
||||||
if !gtx.Source.Focused(&d.dialog.BtnOk.Clickable) && !gtx.Source.Focused(&d.dialog.BtnCancel.Clickable) && !gtx.Source.Focused(&d.dialog.BtnAlt.Clickable) {
|
if !gtx.Source.Focused(&d.dialog.BtnOk) && !gtx.Source.Focused(&d.dialog.BtnCancel) && !gtx.Source.Focused(&d.dialog.BtnAlt) {
|
||||||
gtx.Execute(key.FocusCmd{Tag: &d.dialog.BtnCancel.Clickable})
|
gtx.Execute(key.FocusCmd{Tag: &d.dialog.BtnCancel})
|
||||||
}
|
}
|
||||||
d.dialog.handleKeys(gtx)
|
d.dialog.handleKeys(gtx)
|
||||||
paint.Fill(gtx.Ops, dialogBgColor)
|
paint.Fill(gtx.Ops, dialogBgColor)
|
||||||
@ -108,7 +120,7 @@ func (d *DialogStyle) Layout(gtx C) D {
|
|||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layout.E.Layout(gtx, func(gtx C) D {
|
return layout.E.Layout(gtx, func(gtx C) D {
|
||||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(120))
|
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(120))
|
||||||
if d.dialog.BtnAlt.Action.Allowed() {
|
if d.dialog.alt.Allowed() {
|
||||||
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
|
||||||
layout.Rigid(d.OkStyle.Layout),
|
layout.Rigid(d.OkStyle.Layout),
|
||||||
layout.Rigid(d.AltStyle.Layout),
|
layout.Rigid(d.AltStyle.Layout),
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
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
|
|
@ -1,196 +0,0 @@
|
|||||||
////////////
|
|
||||||
// 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
|
|
||||||
}
|
|
@ -1,191 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
// 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,
|
|
||||||
}
|
|
@ -1,299 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user