diff --git a/tracker/keyevent.go b/tracker/keyevent.go index d147000..f1d2b85 100644 --- a/tracker/keyevent.go +++ b/tracker/keyevent.go @@ -69,9 +69,9 @@ func (t *Tracker) KeyEvent(e key.Event) bool { return true case `\`: 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: delta := -1 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 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 diff --git a/tracker/layout.go b/tracker/layout.go index 9ec419f..3a6e251 100644 --- a/tracker/layout.go +++ b/tracker/layout.go @@ -1,7 +1,6 @@ package tracker import ( - "fmt" "image" "image/color" "log" @@ -156,12 +155,6 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions { }) } 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 { newTrack := layout.Rigid(func(gtx layout.Context) layout.Dimensions { 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( gtx, 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) - }), - layout.Rigid(Label(fmt.Sprintf("%v", t.CurrentOctave), white)), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.OctaveDownBtn, downIcon)), t.CurrentOctave > 0).Layout) + numStyle := NumericUpDown(t.Theme, t.Octave, 0, 9) + gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20)) + gtx.Constraints.Min.X = gtx.Px(unit.Dp(70)) + return in.Layout(gtx, numStyle.Layout) }), ) }) @@ -207,25 +199,17 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions { func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions { go func() { - for t.BPMUpBtn.Clicked() { + /*for t.BPMUpBtn.Clicked() { t.ChangeBPM(1) } for t.BPMDownBtn.Clicked() { t.ChangeBPM(-1) - } + }*/ for t.NewInstrumentBtn.Clicked() { t.AddInstrument() } }() - for t.SongLengthUpBtn.Clicked() { - t.IncreaseSongLength() - } - - for t.SongLengthDownBtn.Clicked() { - t.DecreaseSongLength() - } - return t.TopHorizontalSplit.Layout(gtx, t.layoutSongPanel, t.layoutInstruments(), diff --git a/tracker/numericupdown.go b/tracker/numericupdown.go new file mode 100644 index 0000000..6cc365a --- /dev/null +++ b/tracker/numericupdown.go @@ -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) + } +} diff --git a/tracker/songpanel.go b/tracker/songpanel.go index 893f538..c195728 100644 --- a/tracker/songpanel.go +++ b/tracker/songpanel.go @@ -1,8 +1,8 @@ package tracker import ( - "fmt" "image" + "math" "gioui.org/layout" "gioui.org/op/clip" @@ -68,23 +68,30 @@ func (t *Tracker) layoutSongOptions(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { 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 { - return in.Layout(gtx, smallButton(material.IconButton(t.Theme, t.SongLengthUpBtn, upIcon)).Layout) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.SongLengthDownBtn, downIcon)), t.song.SequenceLength() > 1).Layout) + t.SongLength.Value = t.song.SequenceLength() + numStyle := NumericUpDown(t.Theme, t.SongLength, 1, math.MaxInt32) + gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20)) + 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 { 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 { - return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.BPMUpBtn, upIcon)), t.song.BPM < 999).Layout) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.BPMDownBtn, downIcon)), t.song.BPM > 1).Layout) + t.BPM.Value = t.song.BPM + numStyle := NumericUpDown(t.Theme, t.BPM, 1, 999) + gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20)) + 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) }), ) }), diff --git a/tracker/tracker.go b/tracker/tracker.go index f2241db..6054f07 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -27,19 +27,15 @@ type Tracker struct { ActiveTrack int CurrentInstrument int CurrentUnit int - CurrentOctave byte NoteTracking bool Theme *material.Theme - OctaveUpBtn *widget.Clickable - OctaveDownBtn *widget.Clickable - BPMUpBtn *widget.Clickable - BPMDownBtn *widget.Clickable + Octave *NumberInput + BPM *NumberInput NewTrackBtn *widget.Clickable NewInstrumentBtn *widget.Clickable LoadSongFileBtn *widget.Clickable NewSongFileBtn *widget.Clickable - SongLengthUpBtn *widget.Clickable - SongLengthDownBtn *widget.Clickable + SongLength *NumberInput SaveSongFileBtn *widget.Clickable ParameterSliders []*widget.Float UnitBtns []*widget.Clickable @@ -174,31 +170,30 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) { } func (t *Tracker) ChangeOctave(delta int) bool { - newOctave := int(t.CurrentOctave) + delta + newOctave := t.Octave.Value + delta if newOctave < 0 { newOctave = 0 } if newOctave > 9 { newOctave = 9 } - if newOctave != int(t.CurrentOctave) { - t.CurrentOctave = byte(newOctave) + if newOctave != t.Octave.Value { + t.Octave.Value = newOctave return true } return false } -func (t *Tracker) ChangeBPM(delta int) bool { - t.SaveUndo() - newBPM := t.song.BPM + delta - if newBPM < 1 { - newBPM = 1 +func (t *Tracker) SetBPM(value int) bool { + if value < 1 { + value = 1 } - if newBPM > 999 { - newBPM = 999 + if value > 999 { + value = 999 } - if newBPM != int(t.song.BPM) { - t.song.BPM = newBPM + if value != int(t.song.BPM) { + t.SaveUndo() + t.song.BPM = value t.sequencer.SetRowLength(44100 * 60 / (4 * t.song.BPM)) return true } @@ -261,19 +256,22 @@ func (t *Tracker) SetCurrentPattern(pat byte) { t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern] = pat } -func (t *Tracker) IncreaseSongLength() { - t.SaveUndo() - for i := range t.song.Tracks { - seq := t.song.Tracks[i].Sequence - t.song.Tracks[i].Sequence = append(seq, seq[len(seq)-1]) +func (t *Tracker) SetSongLength(value int) { + if value < 1 { + value = 1 } -} + if value != t.song.SequenceLength() { + t.SaveUndo() + for i := range t.song.Tracks { + 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]) + } + } -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{ Theme: material.NewTheme(gofont.Collection()), QuitButton: new(widget.Clickable), - CurrentOctave: 4, audioContext: audioContext, - OctaveUpBtn: new(widget.Clickable), - OctaveDownBtn: new(widget.Clickable), - BPMUpBtn: new(widget.Clickable), - BPMDownBtn: new(widget.Clickable), + BPM: new(NumberInput), + Octave: new(NumberInput), + SongLength: new(NumberInput), NewTrackBtn: new(widget.Clickable), NewInstrumentBtn: new(widget.Clickable), NewSongFileBtn: new(widget.Clickable), LoadSongFileBtn: new(widget.Clickable), SaveSongFileBtn: new(widget.Clickable), - SongLengthUpBtn: new(widget.Clickable), - SongLengthDownBtn: new(widget.Clickable), setPlaying: make(chan bool), rowJump: make(chan int), patternJump: make(chan int), @@ -307,6 +301,7 @@ func New(audioContext sointu.AudioContext) *Tracker { BottomHorizontalSplit: new(Split), VerticalSplit: new(Split), } + t.Octave.Value = 4 t.VerticalSplit.Axis = layout.Vertical t.BottomHorizontalSplit.Ratio = -.5 t.Theme.Color.Primary = primaryColor