From 65a7f060ecb59a4abb717b56343385912fd7e6e7 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:57:09 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 6 +- tracker/gioui/buttons.go | 433 ++++++++++++++++++++++++- tracker/gioui/dialog.go | 76 +++-- tracker/gioui/patch/WHAT_IS_THIS.md | 22 -- tracker/gioui/patch/button.go | 196 ----------- tracker/gioui/patch/f32color/rgba.go | 191 ----------- tracker/gioui/patch/f32color/tables.go | 25 -- tracker/gioui/patch/material/button.go | 299 ----------------- 8 files changed, 467 insertions(+), 781 deletions(-) delete mode 100644 tracker/gioui/patch/WHAT_IS_THIS.md delete mode 100644 tracker/gioui/patch/button.go delete mode 100644 tracker/gioui/patch/f32color/rgba.go delete mode 100644 tracker/gioui/patch/f32color/tables.go delete mode 100644 tracker/gioui/patch/material/button.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f75298..42e7c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,10 @@ 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) +- 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 - 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 [i151]: https://github.com/vsariola/sointu/issues/151 [i154]: https://github.com/vsariola/sointu/issues/154 +[i156]: https://github.com/vsariola/sointu/issues/156 [i157]: https://github.com/vsariola/sointu/issues/157 [i158]: https://github.com/vsariola/sointu/issues/158 [i160]: https://github.com/vsariola/sointu/issues/160 diff --git a/tracker/gioui/buttons.go b/tracker/gioui/buttons.go index 3b31304..024ab10 100644 --- a/tracker/gioui/buttons.go +++ b/tracker/gioui/buttons.go @@ -1,18 +1,30 @@ package gioui 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/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" "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 patch.Clickable + Clickable Clickable TipArea component.TipArea } @@ -23,12 +35,12 @@ type ( TipIconButtonStyle struct { TipArea *component.TipArea - IconButtonStyle patched.IconButtonStyle + IconButtonStyle IconButtonStyle Tooltip component.Tooltip } BoolClickable struct { - Clickable patch.Clickable + Clickable Clickable TipArea component.TipArea 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 { - iconButtonStyle := patched.IconButton(th, &w.Clickable, widgetForIcon(icon), "") + iconButtonStyle := IconButton(th, &w.Clickable, widgetForIcon(icon), "") iconButtonStyle.Color = primaryColor iconButtonStyle.Background = transparent 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) { w.Bool.Toggle() } - ibStyle := patched.IconButton(th, &w.Clickable, widgetForIcon(icon), "") + ibStyle := IconButton(th, &w.Clickable, widgetForIcon(icon), "") ibStyle.Background = transparent ibStyle.Inset = layout.UniformInset(unit.Dp(6)) ibStyle.Color = primaryColor @@ -103,11 +115,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) patched.ButtonStyle { +func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) ButtonStyle { for w.Clickable.Clicked(gtx) { w.Action.Do() } - ret := patched.Button(th, &w.Clickable, text) + ret := Button(th, &w.Clickable, text) ret.Color = th.Palette.Fg if !w.Action.Allowed() { ret.Color = disabledTextColor @@ -117,11 +129,11 @@ func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) pa 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) { b.Bool.Toggle() } - ret := patched.Button(th, &b.Clickable, text) + ret := Button(th, &b.Clickable, text) ret.Background = transparent ret.Inset = layout.UniformInset(unit.Dp(6)) if b.Bool.Value() { @@ -134,18 +146,409 @@ func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) patc return ret } -func LowEmphasisButton(th *material.Theme, w *patch.Clickable, text string) patched.ButtonStyle { - ret := patched.Button(th, w, text) +func LowEmphasisButton(th *material.Theme, w *Clickable, text string) ButtonStyle { + ret := 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 *patch.Clickable, text string) patched.ButtonStyle { - ret := patched.Button(th, w, text) +func HighEmphasisButton(th *material.Theme, w *Clickable, text string) ButtonStyle { + ret := Button(th, w, text) ret.Color = th.Palette.ContrastFg ret.Background = th.Palette.Fg ret.Inset = layout.UniformInset(unit.Dp(6)) 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), + } +} diff --git a/tracker/gioui/dialog.go b/tracker/gioui/dialog.go index a84acc1..2577a67 100644 --- a/tracker/gioui/dialog.go +++ b/tracker/gioui/dialog.go @@ -6,15 +6,17 @@ import ( "gioui.org/op/paint" "gioui.org/text" "gioui.org/unit" + "gioui.org/widget" "gioui.org/widget/material" "github.com/vsariola/sointu/tracker" - patched "github.com/vsariola/sointu/tracker/gioui/patch/material" ) type Dialog struct { - BtnAlt *ActionClickable - BtnOk *ActionClickable - BtnCancel *ActionClickable + BtnAlt widget.Clickable + BtnOk widget.Clickable + BtnCancel widget.Clickable + + ok, alt, cancel tracker.Action } type DialogStyle struct { @@ -23,18 +25,14 @@ type DialogStyle struct { Text string Inset layout.Inset TextInset layout.Inset - AltStyle patched.ButtonStyle - OkStyle patched.ButtonStyle - CancelStyle patched.ButtonStyle + AltStyle material.ButtonStyle + OkStyle material.ButtonStyle + CancelStyle material.ButtonStyle Shaper *text.Shaper } func NewDialog(ok, alt, cancel tracker.Action) *Dialog { - ret := &Dialog{ - BtnOk: NewActionClickable(ok), - BtnAlt: NewActionClickable(alt), - BtnCancel: NewActionClickable(cancel), - } + ret := &Dialog{ok: ok, alt: alt, cancel: cancel} return ret } @@ -46,21 +44,26 @@ func ConfirmDialog(gtx C, th *material.Theme, dialog *Dialog, title, text string Text: text, 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)}, - AltStyle: ActionButton(gtx, th, dialog.BtnAlt, "Alt"), - OkStyle: ActionButton(gtx, th, dialog.BtnOk, "Ok"), - CancelStyle: ActionButton(gtx, th, dialog.BtnCancel, "Cancel"), + AltStyle: material.Button(th, &dialog.BtnAlt, "Alt"), + OkStyle: material.Button(th, &dialog.BtnOk, "Ok"), + CancelStyle: material.Button(th, &dialog.BtnCancel, "Cancel"), 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 } -func (d *Dialog) handleKeysForButton(gtx C, btn, next, prev *ActionClickable) { +func (d *Dialog) handleKeysForButton(gtx C, btn, next, prev *widget.Clickable) { for { e, ok := gtx.Event( - key.Filter{Focus: &btn.Clickable, Name: key.NameLeftArrow}, - key.Filter{Focus: &btn.Clickable, Name: key.NameRightArrow}, - key.Filter{Focus: &btn.Clickable, Name: key.NameEscape}, - key.Filter{Focus: &btn.Clickable, Name: key.NameTab, Optional: key.ModShift}, + key.Filter{Focus: btn, Name: key.NameLeftArrow}, + key.Filter{Focus: btn, Name: key.NameRightArrow}, + key.Filter{Focus: btn, Name: key.NameEscape}, + key.Filter{Focus: btn, Name: key.NameTab, Optional: key.ModShift}, ) if !ok { 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 { switch { 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)): - gtx.Execute(key.FocusCmd{Tag: &next.Clickable}) + gtx.Execute(key.FocusCmd{Tag: next}) case e.Name == key.NameEscape: - d.BtnCancel.Action.Do() + d.cancel.Do() } } } } func (d *Dialog) handleKeys(gtx C) { - if d.BtnAlt.Action.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) + for d.BtnOk.Clicked(gtx) { + d.ok.Do() + } + 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 { - d.handleKeysForButton(gtx, d.BtnOk, d.BtnCancel, d.BtnCancel) - d.handleKeysForButton(gtx, d.BtnCancel, d.BtnOk, d.BtnOk) + d.handleKeysForButton(gtx, &d.BtnOk, &d.BtnCancel, &d.BtnCancel) + d.handleKeysForButton(gtx, &d.BtnCancel, &d.BtnOk, &d.BtnOk) } } 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) { - gtx.Execute(key.FocusCmd{Tag: &d.dialog.BtnCancel.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}) } d.dialog.handleKeys(gtx) paint.Fill(gtx.Ops, dialogBgColor) @@ -108,7 +120,7 @@ func (d *DialogStyle) Layout(gtx C) D { layout.Rigid(func(gtx C) D { return layout.E.Layout(gtx, func(gtx C) D { 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, layout.Rigid(d.OkStyle.Layout), layout.Rigid(d.AltStyle.Layout), diff --git a/tracker/gioui/patch/WHAT_IS_THIS.md b/tracker/gioui/patch/WHAT_IS_THIS.md deleted file mode 100644 index 83d2415..0000000 --- a/tracker/gioui/patch/WHAT_IS_THIS.md +++ /dev/null @@ -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 diff --git a/tracker/gioui/patch/button.go b/tracker/gioui/patch/button.go deleted file mode 100644 index 9fedfe1..0000000 --- a/tracker/gioui/patch/button.go +++ /dev/null @@ -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 -} diff --git a/tracker/gioui/patch/f32color/rgba.go b/tracker/gioui/patch/f32color/rgba.go deleted file mode 100644 index 5488a0c..0000000 --- a/tracker/gioui/patch/f32color/rgba.go +++ /dev/null @@ -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) -} diff --git a/tracker/gioui/patch/f32color/tables.go b/tracker/gioui/patch/f32color/tables.go deleted file mode 100644 index 137d05b..0000000 --- a/tracker/gioui/patch/f32color/tables.go +++ /dev/null @@ -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, -} \ No newline at end of file diff --git a/tracker/gioui/patch/material/button.go b/tracker/gioui/patch/material/button.go deleted file mode 100644 index db28813..0000000 --- a/tracker/gioui/patch/material/button.go +++ /dev/null @@ -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) -}