feat(tracker): implement a numeric up down widget and use that for the numbers

This commit is contained in:
vsariola 2021-01-15 20:38:46 +02:00
parent f665a529e5
commit 80d87dea8c
5 changed files with 283 additions and 73 deletions

View File

@ -69,9 +69,9 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
return true return true
case `\`: case `\`:
if e.Modifiers.Contain(key.ModShift) { if e.Modifiers.Contain(key.ModShift) {
return t.ChangeBPM(1) return t.ChangeOctave(1)
} }
return t.ChangeBPM(-1) return t.ChangeOctave(-1)
case key.NameUpArrow: case key.NameUpArrow:
delta := -1 delta := -1
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModCtrl) {
@ -161,7 +161,7 @@ func (t *Tracker) getCurrent() byte {
// NotePressed handles incoming key presses while in the note column // NotePressed handles incoming key presses while in the note column
func (t *Tracker) NotePressed(val int) { func (t *Tracker) NotePressed(val int) {
t.SetCurrentNote(getNoteValue(int(t.CurrentOctave), val)) t.SetCurrentNote(getNoteValue(int(t.Octave.Value), val))
} }
// NumberPressed handles incoming presses while in either of the hex number columns // NumberPressed handles incoming presses while in either of the hex number columns

View File

@ -1,7 +1,6 @@
package tracker package tracker
import ( import (
"fmt"
"image" "image"
"image/color" "image/color"
"log" "log"
@ -156,12 +155,6 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions {
}) })
} }
in2 := layout.UniformInset(unit.Dp(8)) in2 := layout.UniformInset(unit.Dp(8))
for t.OctaveUpBtn.Clicked() {
t.ChangeOctave(1)
}
for t.OctaveDownBtn.Clicked() {
t.ChangeOctave(-1)
}
menu := layout.Rigid(func(gtx layout.Context) layout.Dimensions { menu := layout.Rigid(func(gtx layout.Context) layout.Dimensions {
newTrack := layout.Rigid(func(gtx layout.Context) layout.Dimensions { newTrack := layout.Rigid(func(gtx layout.Context) layout.Dimensions {
paint.FillShape(gtx.Ops, trackMenuSurfaceColor, clip.Rect{ paint.FillShape(gtx.Ops, trackMenuSurfaceColor, clip.Rect{
@ -175,11 +168,10 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Horizontal}.Layout( return layout.Flex{Axis: layout.Horizontal}.Layout(
gtx, gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.OctaveUpBtn, upIcon)), t.CurrentOctave < 9).Layout) numStyle := NumericUpDown(t.Theme, t.Octave, 0, 9)
}), gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
layout.Rigid(Label(fmt.Sprintf("%v", t.CurrentOctave), white)), gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return in.Layout(gtx, numStyle.Layout)
return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.OctaveDownBtn, downIcon)), t.CurrentOctave > 0).Layout)
}), }),
) )
}) })
@ -207,25 +199,17 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions {
func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions { func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions {
go func() { go func() {
for t.BPMUpBtn.Clicked() { /*for t.BPMUpBtn.Clicked() {
t.ChangeBPM(1) t.ChangeBPM(1)
} }
for t.BPMDownBtn.Clicked() { for t.BPMDownBtn.Clicked() {
t.ChangeBPM(-1) t.ChangeBPM(-1)
} }*/
for t.NewInstrumentBtn.Clicked() { for t.NewInstrumentBtn.Clicked() {
t.AddInstrument() t.AddInstrument()
} }
}() }()
for t.SongLengthUpBtn.Clicked() {
t.IncreaseSongLength()
}
for t.SongLengthDownBtn.Clicked() {
t.DecreaseSongLength()
}
return t.TopHorizontalSplit.Layout(gtx, return t.TopHorizontalSplit.Layout(gtx,
t.layoutSongPanel, t.layoutSongPanel,
t.layoutInstruments(), t.layoutInstruments(),

224
tracker/numericupdown.go Normal file
View File

@ -0,0 +1,224 @@
package tracker
import (
"fmt"
"image"
"image/color"
"log"
"golang.org/x/exp/shiny/materialdesign/icons"
"gioui.org/f32"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget/material"
)
var defaultNumericLeftIcon *widget.Icon
var defaultNumericRightIcon *widget.Icon
var defaultNumericUpIcon *widget.Icon
var defaultNumericDownIcon *widget.Icon
type NumberInput struct {
Value int
drag gesture.Drag
dragStartValue int
dragStartXY float32
clickDecrease gesture.Click
clickIncrease gesture.Click
}
type NumericUpDownStyle struct {
NumberInput *NumberInput
Min int
Max int
Axis layout.Axis
Color color.RGBA
Font text.Font
TextSize unit.Value
BorderColor color.RGBA
IconColor color.RGBA
BackgroundColor color.RGBA
CornerRadius unit.Value
Border unit.Value
ButtonWidth unit.Value
UnitsPerStep unit.Value
shaper text.Shaper
}
func NumericUpDown(th *material.Theme, number *NumberInput, min, max int) NumericUpDownStyle {
bgColor := th.Color.Primary
bgColor.R /= 4
bgColor.G /= 4
bgColor.B /= 4
return NumericUpDownStyle{
NumberInput: number,
Min: min,
Max: max,
Axis: layout.Horizontal,
Color: white,
BorderColor: th.Color.Primary,
IconColor: th.Color.InvText,
BackgroundColor: bgColor,
CornerRadius: unit.Dp(4),
ButtonWidth: unit.Dp(16),
Border: unit.Dp(1),
UnitsPerStep: unit.Dp(8),
TextSize: th.TextSize.Scale(14.0 / 16.0),
shaper: th.Shaper,
}
}
func (s NumericUpDownStyle) Layout(gtx C) D {
size := gtx.Constraints.Min
defer op.Push(gtx.Ops).Pop()
rr := float32(gtx.Px(s.CornerRadius))
border := float32(gtx.Px(s.Border))
clip.UniformRRect(f32.Rectangle{Max: f32.Point{
X: float32(gtx.Constraints.Min.X),
Y: float32(gtx.Constraints.Min.Y),
}}, rr).Add(gtx.Ops)
paint.Fill(gtx.Ops, s.BorderColor)
op.Offset(f32.Pt(border, border)).Add(gtx.Ops)
clip.UniformRRect(f32.Rectangle{Max: f32.Point{
X: float32(gtx.Constraints.Min.X) - border*2,
Y: float32(gtx.Constraints.Min.Y) - border*2,
}}, rr-border).Add(gtx.Ops)
gtx.Constraints.Min.X -= int(border * 2)
gtx.Constraints.Min.Y -= int(border * 2)
gtx.Constraints.Max = gtx.Constraints.Min
layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(s.button(gtx.Constraints.Max.Y, defaultNumericLeftIcon, -1, &s.NumberInput.clickDecrease)),
layout.Flexed(1, s.layoutText),
layout.Rigid(s.button(gtx.Constraints.Max.Y, defaultNumericRightIcon, 1, &s.NumberInput.clickIncrease)),
)
if s.NumberInput.Value < s.Min {
s.NumberInput.Value = s.Min
}
if s.NumberInput.Value > s.Max {
s.NumberInput.Value = s.Max
}
return layout.Dimensions{Size: size}
}
func (s NumericUpDownStyle) button(height int, icon *widget.Icon, delta int, click *gesture.Click) layout.Widget {
return func(gtx C) D {
btnWidth := gtx.Px(s.ButtonWidth)
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
//paint.FillShape(gtx.Ops, black, clip.Rect(image.Rect(0, 0, btnWidth, height)).Op())
return layout.Dimensions{Size: image.Point{X: btnWidth, Y: height}}
}),
layout.Expanded(func(gtx C) D {
size := btnWidth
if height < size {
size = height
}
if icon != nil {
icon.Color = s.IconColor
return icon.Layout(gtx, unit.Px(float32(size)))
}
return layout.Dimensions{}
}),
layout.Expanded(func(gtx C) D {
return s.layoutClick(gtx, delta, click)
}),
)
}
}
func (s NumericUpDownStyle) layoutText(gtx C) D {
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Stacked(func(gtx C) D {
paint.FillShape(gtx.Ops, s.BackgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
return layout.Dimensions{Size: gtx.Constraints.Max}
}),
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: s.Color}.Add(gtx.Ops)
return widget.Label{Alignment: text.Middle}.Layout(gtx, s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Value))
}),
layout.Expanded(s.layoutDrag),
)
}
func (s NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions {
{ // handle dragging
pxPerStep := float32(gtx.Px(s.UnitsPerStep))
for _, e := range s.NumberInput.drag.Events(gtx.Metric, gtx, gesture.Axis(s.Axis)) {
switch e.Type {
case pointer.Press:
s.NumberInput.dragStartValue = s.NumberInput.Value
if s.Axis == layout.Horizontal {
s.NumberInput.dragStartXY = e.Position.X
} else {
s.NumberInput.dragStartXY = e.Position.Y
}
case pointer.Drag:
var deltaCoord float32
if s.Axis == layout.Horizontal {
deltaCoord = e.Position.X - s.NumberInput.dragStartXY
} else {
deltaCoord = e.Position.Y - s.NumberInput.dragStartXY
}
s.NumberInput.Value = s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5)
}
}
// Avoid affecting the input tree with pointer events.
stack := op.Push(gtx.Ops)
// register for input
dragRect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(dragRect).Add(gtx.Ops)
s.NumberInput.drag.Add(gtx.Ops)
stack.Pop()
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}
func (s NumericUpDownStyle) layoutClick(gtx layout.Context, delta int, click *gesture.Click) layout.Dimensions {
// handle clicking
for _, e := range click.Events(gtx) {
switch e.Type {
case gesture.TypeClick:
s.NumberInput.Value += delta
}
}
// Avoid affecting the input tree with pointer events.
stack := op.Push(gtx.Ops)
// register for input
clickRect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(clickRect).Add(gtx.Ops)
click.Add(gtx.Ops)
stack.Pop()
return layout.Dimensions{Size: gtx.Constraints.Min}
}
func init() {
var err error
defaultNumericLeftIcon, err = widget.NewIcon(icons.NavigationArrowBack)
if err != nil {
log.Fatal(err)
}
defaultNumericRightIcon, err = widget.NewIcon(icons.NavigationArrowForward)
if err != nil {
log.Fatal(err)
}
defaultNumericUpIcon, err = widget.NewIcon(icons.NavigationArrowDropUp)
if err != nil {
log.Fatal(err)
}
defaultNumericDownIcon, err = widget.NewIcon(icons.NavigationArrowDropDown)
if err != nil {
log.Fatal(err)
}
}

View File

@ -1,8 +1,8 @@
package tracker package tracker
import ( import (
"fmt"
"image" "image"
"math"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op/clip" "gioui.org/op/clip"
@ -68,23 +68,30 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Label(fmt.Sprintf("LEN: %3v", t.song.SequenceLength()), white)), layout.Rigid(Label("LEN:", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, smallButton(material.IconButton(t.Theme, t.SongLengthUpBtn, upIcon)).Layout) t.SongLength.Value = t.song.SequenceLength()
}), numStyle := NumericUpDown(t.Theme, t.SongLength, 1, math.MaxInt32)
layout.Rigid(func(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.SongLengthDownBtn, downIcon)), t.song.SequenceLength() > 1).Layout) gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
t.SetSongLength(t.SongLength.Value)
return dims
}), }),
) )
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Label(fmt.Sprintf("BPM: %3v", t.song.BPM), white)), layout.Rigid(Label("BPM:", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.BPMUpBtn, upIcon)), t.song.BPM < 999).Layout) t.BPM.Value = t.song.BPM
}), numStyle := NumericUpDown(t.Theme, t.BPM, 1, 999)
layout.Rigid(func(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.BPMDownBtn, downIcon)), t.song.BPM > 1).Layout) gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
t.SetBPM(t.BPM.Value)
return dims
//return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.BPMUpBtn, upIcon)), t.song.BPM < 999).Layout)
}), }),
) )
}), }),

View File

@ -27,19 +27,15 @@ type Tracker struct {
ActiveTrack int ActiveTrack int
CurrentInstrument int CurrentInstrument int
CurrentUnit int CurrentUnit int
CurrentOctave byte
NoteTracking bool NoteTracking bool
Theme *material.Theme Theme *material.Theme
OctaveUpBtn *widget.Clickable Octave *NumberInput
OctaveDownBtn *widget.Clickable BPM *NumberInput
BPMUpBtn *widget.Clickable
BPMDownBtn *widget.Clickable
NewTrackBtn *widget.Clickable NewTrackBtn *widget.Clickable
NewInstrumentBtn *widget.Clickable NewInstrumentBtn *widget.Clickable
LoadSongFileBtn *widget.Clickable LoadSongFileBtn *widget.Clickable
NewSongFileBtn *widget.Clickable NewSongFileBtn *widget.Clickable
SongLengthUpBtn *widget.Clickable SongLength *NumberInput
SongLengthDownBtn *widget.Clickable
SaveSongFileBtn *widget.Clickable SaveSongFileBtn *widget.Clickable
ParameterSliders []*widget.Float ParameterSliders []*widget.Float
UnitBtns []*widget.Clickable UnitBtns []*widget.Clickable
@ -174,31 +170,30 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
} }
func (t *Tracker) ChangeOctave(delta int) bool { func (t *Tracker) ChangeOctave(delta int) bool {
newOctave := int(t.CurrentOctave) + delta newOctave := t.Octave.Value + delta
if newOctave < 0 { if newOctave < 0 {
newOctave = 0 newOctave = 0
} }
if newOctave > 9 { if newOctave > 9 {
newOctave = 9 newOctave = 9
} }
if newOctave != int(t.CurrentOctave) { if newOctave != t.Octave.Value {
t.CurrentOctave = byte(newOctave) t.Octave.Value = newOctave
return true return true
} }
return false return false
} }
func (t *Tracker) ChangeBPM(delta int) bool { func (t *Tracker) SetBPM(value int) bool {
if value < 1 {
value = 1
}
if value > 999 {
value = 999
}
if value != int(t.song.BPM) {
t.SaveUndo() t.SaveUndo()
newBPM := t.song.BPM + delta t.song.BPM = value
if newBPM < 1 {
newBPM = 1
}
if newBPM > 999 {
newBPM = 999
}
if newBPM != int(t.song.BPM) {
t.song.BPM = newBPM
t.sequencer.SetRowLength(44100 * 60 / (4 * t.song.BPM)) t.sequencer.SetRowLength(44100 * 60 / (4 * t.song.BPM))
return true return true
} }
@ -261,19 +256,22 @@ func (t *Tracker) SetCurrentPattern(pat byte) {
t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern] = pat t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern] = pat
} }
func (t *Tracker) IncreaseSongLength() { func (t *Tracker) SetSongLength(value int) {
if value < 1 {
value = 1
}
if value != t.song.SequenceLength() {
t.SaveUndo() t.SaveUndo()
for i := range t.song.Tracks { for i := range t.song.Tracks {
seq := t.song.Tracks[i].Sequence seq := t.song.Tracks[i].Sequence
if len(t.song.Tracks[i].Sequence) > value {
t.song.Tracks[i].Sequence = t.song.Tracks[i].Sequence[:value]
} else if len(t.song.Tracks[i].Sequence) < value {
for k := len(t.song.Tracks[i].Sequence); k < value; k++ {
t.song.Tracks[i].Sequence = append(seq, seq[len(seq)-1]) t.song.Tracks[i].Sequence = append(seq, seq[len(seq)-1])
} }
} }
func (t *Tracker) DecreaseSongLength() {
t.SaveUndo()
for i := range t.song.Tracks {
if len(t.song.Tracks[i].Sequence) > 0 {
t.song.Tracks[i].Sequence = t.song.Tracks[i].Sequence[0 : len(t.song.Tracks[i].Sequence)-1]
} }
} }
} }
@ -282,19 +280,15 @@ func New(audioContext sointu.AudioContext) *Tracker {
t := &Tracker{ t := &Tracker{
Theme: material.NewTheme(gofont.Collection()), Theme: material.NewTheme(gofont.Collection()),
QuitButton: new(widget.Clickable), QuitButton: new(widget.Clickable),
CurrentOctave: 4,
audioContext: audioContext, audioContext: audioContext,
OctaveUpBtn: new(widget.Clickable), BPM: new(NumberInput),
OctaveDownBtn: new(widget.Clickable), Octave: new(NumberInput),
BPMUpBtn: new(widget.Clickable), SongLength: new(NumberInput),
BPMDownBtn: new(widget.Clickable),
NewTrackBtn: new(widget.Clickable), NewTrackBtn: new(widget.Clickable),
NewInstrumentBtn: new(widget.Clickable), NewInstrumentBtn: new(widget.Clickable),
NewSongFileBtn: new(widget.Clickable), NewSongFileBtn: new(widget.Clickable),
LoadSongFileBtn: new(widget.Clickable), LoadSongFileBtn: new(widget.Clickable),
SaveSongFileBtn: new(widget.Clickable), SaveSongFileBtn: new(widget.Clickable),
SongLengthUpBtn: new(widget.Clickable),
SongLengthDownBtn: new(widget.Clickable),
setPlaying: make(chan bool), setPlaying: make(chan bool),
rowJump: make(chan int), rowJump: make(chan int),
patternJump: make(chan int), patternJump: make(chan int),
@ -307,6 +301,7 @@ func New(audioContext sointu.AudioContext) *Tracker {
BottomHorizontalSplit: new(Split), BottomHorizontalSplit: new(Split),
VerticalSplit: new(Split), VerticalSplit: new(Split),
} }
t.Octave.Value = 4
t.VerticalSplit.Axis = layout.Vertical t.VerticalSplit.Axis = layout.Vertical
t.BottomHorizontalSplit.Ratio = -.5 t.BottomHorizontalSplit.Ratio = -.5
t.Theme.Color.Primary = primaryColor t.Theme.Color.Primary = primaryColor