feat(tracker): implement selecting a range in the tracker

also refactored a location in song to SongPoint, and a particular row in song into SongRow
This commit is contained in:
vsariola 2021-01-16 21:11:08 +02:00
parent 8f9bf75613
commit eb36a96e29
7 changed files with 228 additions and 143 deletions

View File

@ -60,7 +60,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
t.SetCurrentNote(0) t.SetCurrentNote(0)
return true return true
case key.NameDeleteForward: case key.NameDeleteForward:
t.SetCurrentNote(1) t.DeleteSelection()
return true return true
case key.NameEscape: case key.NameEscape:
os.Exit(0) os.Exit(0)
@ -77,7 +77,11 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModCtrl) {
delta = -t.song.PatternRows() delta = -t.song.PatternRows()
} }
t.moveCursor(delta) t.Cursor.Row += delta
t.Cursor.Clamp(t.song)
if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor
}
t.NoteTracking = false t.NoteTracking = false
return true return true
case key.NameDownArrow: case key.NameDownArrow:
@ -85,37 +89,41 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModCtrl) {
delta = t.song.PatternRows() delta = t.song.PatternRows()
} }
t.moveCursor(delta) t.Cursor.Row += delta
t.Cursor.Clamp(t.song)
if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor
}
t.NoteTracking = false t.NoteTracking = false
return true return true
case key.NameLeftArrow: case key.NameLeftArrow:
if t.CursorColumn == 0 || !t.TrackShowHex[t.ActiveTrack] || e.Modifiers.Contain(key.ModCtrl) { if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
t.ActiveTrack = (t.ActiveTrack + len(t.song.Tracks) - 1) % len(t.song.Tracks) t.Cursor.Track--
if t.TrackShowHex[t.ActiveTrack] { t.Cursor.Clamp(t.song)
if t.TrackShowHex[t.Cursor.Track] {
t.CursorColumn = 1 t.CursorColumn = 1
} else { } else {
t.CursorColumn = 0 t.CursorColumn = 0
} }
if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor
}
} else { } else {
t.CursorColumn-- t.CursorColumn--
} }
return true return true
case key.NameRightArrow: case key.NameRightArrow:
if t.CursorColumn == 1 || !t.TrackShowHex[t.ActiveTrack] || e.Modifiers.Contain(key.ModCtrl) { if t.CursorColumn == 1 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
t.ActiveTrack = (t.ActiveTrack + 1) % len(t.song.Tracks) t.Cursor.Track++
t.Cursor.Clamp(t.song)
if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor
}
t.CursorColumn = 0 t.CursorColumn = 0
} else { } else {
t.CursorColumn++ t.CursorColumn++
} }
return true return true
case key.NameTab:
if e.Modifiers.Contain(key.ModShift) {
t.ActiveTrack = (t.ActiveTrack + len(t.song.Tracks) - 1) % len(t.song.Tracks)
} else {
t.ActiveTrack = (t.ActiveTrack + 1) % len(t.song.Tracks)
}
t.CursorColumn = 0
return true
} }
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModCtrl) {
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
@ -123,7 +131,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
return true return true
} }
} else { } else {
if !t.TrackShowHex[t.ActiveTrack] { if !t.TrackShowHex[t.Cursor.Track] {
if val, ok := noteMap[e.Name]; ok { if val, ok := noteMap[e.Name]; ok {
t.NotePressed(val) t.NotePressed(val)
return true return true
@ -139,24 +147,9 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
return false return false
} }
func (t *Tracker) moveCursor(delta int) {
newRow := t.CursorRow + delta
remainder := (newRow + t.song.PatternRows()) % t.song.PatternRows()
t.DisplayPattern += (newRow - remainder) / t.song.PatternRows()
if t.DisplayPattern < 0 {
t.CursorRow = 0
t.DisplayPattern = 0
} else if t.DisplayPattern >= t.song.SequenceLength() {
t.CursorRow = t.song.PatternRows() - 1
t.DisplayPattern = t.song.SequenceLength() - 1
} else {
t.CursorRow = remainder
}
}
// getCurrent returns the current (note) value in current pattern under the cursor // getCurrent returns the current (note) value in current pattern under the cursor
func (t *Tracker) getCurrent() byte { func (t *Tracker) getCurrent() byte {
return t.song.Tracks[t.ActiveTrack].Patterns[t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern]][t.CursorRow] return t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row]
} }
// NotePressed handles incoming key presses while in the note column // NotePressed handles incoming key presses while in the note column

View File

@ -83,18 +83,8 @@ func (t *Tracker) Layout(gtx layout.Context) {
} }
func (t *Tracker) layoutTracksAndPatterns(gtx layout.Context) layout.Dimensions { func (t *Tracker) layoutTracksAndPatterns(gtx layout.Context) layout.Dimensions {
playPat := t.PlayPattern
if !t.Playing {
playPat = -1
}
return t.BottomHorizontalSplit.Layout(gtx, return t.BottomHorizontalSplit.Layout(gtx,
t.layoutPatterns( t.layoutPatterns,
t.song.Tracks,
t.ActiveTrack,
t.DisplayPattern,
t.CursorColumn,
playPat,
),
t.layoutTracks, t.layoutTracks,
) )
} }
@ -106,7 +96,7 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions {
t.playRowPatMutex.RLock() t.playRowPatMutex.RLock()
defer t.playRowPatMutex.RUnlock() defer t.playRowPatMutex.RUnlock()
playPat := t.PlayPattern playPat := t.PlayPosition.Pattern
if !t.Playing { if !t.Playing {
playPat = -1 playPat = -1
} }
@ -114,16 +104,15 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions {
rowMarkers := layout.Rigid(t.layoutRowMarkers( rowMarkers := layout.Rigid(t.layoutRowMarkers(
len(t.song.Tracks[0].Patterns[0]), len(t.song.Tracks[0].Patterns[0]),
len(t.song.Tracks[0].Sequence), len(t.song.Tracks[0].Sequence),
t.CursorRow, t.Cursor.Row,
t.DisplayPattern, t.Cursor.Pattern,
t.CursorColumn, t.CursorColumn,
t.PlayRow, t.PlayPosition.Row,
playPat, playPat,
)) ))
leftInset := layout.Inset{Left: unit.Dp(4)} leftInset := layout.Inset{Left: unit.Dp(4)}
for i, trk := range t.song.Tracks { for i := range t.song.Tracks {
i2 := i // avoids i being updated in the closure i2 := i // avoids i being updated in the closure
trk2 := trk // avoids trk being updated in the closure
if len(t.TrackHexCheckBoxes) <= i { if len(t.TrackHexCheckBoxes) <= i {
t.TrackHexCheckBoxes = append(t.TrackHexCheckBoxes, new(widget.Bool)) t.TrackHexCheckBoxes = append(t.TrackHexCheckBoxes, new(widget.Bool))
} }
@ -136,17 +125,7 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions {
cbStyle.Color = white cbStyle.Color = white
ret := layout.Stack{}.Layout(gtx, ret := layout.Stack{}.Layout(gtx,
layout.Stacked(func(gtx layout.Context) D { layout.Stacked(func(gtx layout.Context) D {
return leftInset.Layout(gtx, t.layoutTrack( return leftInset.Layout(gtx, t.layoutTrack(i2))
trk2.Patterns,
trk2.Sequence,
t.ActiveTrack == i2,
t.TrackShowHex[i2],
t.CursorRow,
t.DisplayPattern,
t.CursorColumn,
t.PlayRow,
playPat,
))
}), }),
layout.Stacked(cbStyle.Layout), layout.Stacked(cbStyle.Layout),
) )

View File

@ -11,7 +11,6 @@ import (
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/widget" "gioui.org/widget"
"github.com/vsariola/sointu"
) )
const patternCellHeight = 16 const patternCellHeight = 16
@ -19,34 +18,41 @@ const patternCellWidth = 16
const patternVisibleTracks = 8 const patternVisibleTracks = 8
const patternRowMarkerWidth = 30 const patternRowMarkerWidth = 30
func (t *Tracker) layoutPatterns(tracks []sointu.Track, activeTrack, cursorPattern, cursorCol, playingPattern int) layout.Widget { func (t *Tracker) layoutPatterns(gtx C) D {
return func(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth
gtx.Constraints.Min.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth gtx.Constraints.Max.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth
gtx.Constraints.Max.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth defer op.Push(gtx.Ops).Pop()
defer op.Push(gtx.Ops).Pop() clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) paint.FillShape(gtx.Ops, patternSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op())
paint.FillShape(gtx.Ops, patternSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op()) patternRect := SongRect{
for j := range tracks[0].Sequence { Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track},
if j == playingPattern { Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track},
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
}
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)))
stack := op.Push(gtx.Ops)
op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops)
for i, track := range tracks {
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, fmt.Sprintf("%d", track.Sequence[j]))
if activeTrack == i && j == cursorPattern {
paint.FillShape(gtx.Ops, patternCursorColor, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op())
}
op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops)
}
stack.Pop()
op.Offset(f32.Pt(0, patternCellHeight)).Add(gtx.Ops)
}
return layout.Dimensions{Size: gtx.Constraints.Max}
} }
for j := 0; j < t.song.SequenceLength(); j++ {
if j == t.PlayPosition.Pattern && t.Playing {
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
}
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)))
stack := op.Push(gtx.Ops)
op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops)
for i, track := range t.song.Tracks {
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, fmt.Sprintf("%d", track.Sequence[j]))
point := SongPoint{Track: i, SongRow: SongRow{Pattern: j}}
if patternRect.Contains(point) {
color := patternSelectionColor
if point.Pattern == t.Cursor.Pattern && point.Track == t.Cursor.Track {
color = patternCursorColor
}
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op())
}
op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops)
}
stack.Pop()
op.Offset(f32.Pt(0, patternCellHeight)).Add(gtx.Ops)
}
return layout.Dimensions{Size: gtx.Constraints.Max}
} }
func patternIndexToString(index byte) string { func patternIndexToString(index byte) string {

85
tracker/songpoint.go Normal file
View File

@ -0,0 +1,85 @@
package tracker
import "github.com/vsariola/sointu"
type SongRow struct {
Pattern int
Row int
}
type SongPoint struct {
Track int
SongRow
}
type SongRect struct {
Corner1 SongPoint
Corner2 SongPoint
}
func (r *SongRow) Wrap(song sointu.Song) {
totalRow := r.Pattern*song.PatternRows() + r.Row
r.Row = mod(totalRow, song.PatternRows())
r.Pattern = mod((totalRow-r.Row)/song.PatternRows(), song.SequenceLength())
}
func (r *SongRow) Clamp(song sointu.Song) {
totalRow := r.Pattern*song.PatternRows() + r.Row
if totalRow < 0 {
totalRow = 0
}
if totalRow >= song.TotalRows() {
totalRow = song.TotalRows() - 1
}
r.Row = totalRow % song.PatternRows()
r.Pattern = ((totalRow - r.Row) / song.PatternRows()) % song.SequenceLength()
}
func (p *SongPoint) Wrap(song sointu.Song) {
p.Track = mod(p.Track, len(song.Tracks))
p.SongRow.Wrap(song)
}
func (p *SongPoint) Clamp(song sointu.Song) {
if p.Track < 0 {
p.Track = 0
} else if l := len(song.Tracks); p.Track >= l {
p.Track = l - 1
}
p.SongRow.Clamp(song)
}
func (r *SongRect) Contains(p SongPoint) bool {
track1, track2 := r.Corner1.Track, r.Corner2.Track
if track2 < track1 {
track1, track2 = track2, track1
}
if p.Track < track1 || p.Track > track2 {
return false
}
pattern1, row1, pattern2, row2 := r.Corner1.Pattern, r.Corner1.Row, r.Corner2.Pattern, r.Corner2.Row
if pattern2 < pattern1 || (pattern1 == pattern2 && row2 < row1) {
pattern1, row1, pattern2, row2 = pattern2, row2, pattern1, row1
}
if p.Pattern < pattern1 || p.Pattern > pattern2 {
return false
}
if p.Pattern == pattern1 && p.Row < row1 {
return false
}
if p.Pattern == pattern2 && p.Row > row2 {
return false
}
return true
}
func mod(a, b int) int {
m := a % b
if a < 0 && b < 0 {
m -= b
}
if a < 0 && b > 0 {
m += b
}
return m
}

View File

@ -72,6 +72,7 @@ var trackerPatternRowTextColor = color.RGBA{R: 198, G: 198, B: 198, A: 255}
var trackerPlayColor = color.RGBA{R: 55, G: 55, B: 61, A: 255} var trackerPlayColor = color.RGBA{R: 55, G: 55, B: 61, A: 255}
var trackerPatMarker = primaryColor var trackerPatMarker = primaryColor
var trackerCursorColor = color.RGBA{R: 38, G: 79, B: 120, A: 64} var trackerCursorColor = color.RGBA{R: 38, G: 79, B: 120, A: 64}
var trackerSelectionColor = color.RGBA{R: 19, G: 40, B: 60, A: 128}
var trackerSurfaceColor = color.RGBA{R: 18, G: 18, B: 18, A: 18} var trackerSurfaceColor = color.RGBA{R: 18, G: 18, B: 18, A: 18}
var patternPlayColor = color.RGBA{R: 55, G: 55, B: 61, A: 255} var patternPlayColor = color.RGBA{R: 55, G: 55, B: 61, A: 255}
@ -80,6 +81,7 @@ var patternActiveTextColor = yellow
var patternFont = fontCollection[6].Font var patternFont = fontCollection[6].Font
var patternFontSize = unit.Px(12) var patternFontSize = unit.Px(12)
var patternCursorColor = color.RGBA{R: 38, G: 79, B: 120, A: 64} var patternCursorColor = color.RGBA{R: 38, G: 79, B: 120, A: 64}
var patternSelectionColor = color.RGBA{R: 19, G: 40, B: 60, A: 128}
var inactiveBtnColor = color.RGBA{R: 61, G: 55, B: 55, A: 255} var inactiveBtnColor = color.RGBA{R: 61, G: 55, B: 55, A: 255}

View File

@ -17,7 +17,7 @@ const trackRowHeight = 16
const trackWidth = 54 const trackWidth = 54
const patmarkWidth = 16 const patmarkWidth = 16
func (t *Tracker) layoutTrack(patterns [][]byte, sequence []byte, active bool, hex bool, cursorRow, cursorPattern, cursorCol, playRow, playPattern int) layout.Widget { func (t *Tracker) layoutTrack(trackNo int) layout.Widget {
return func(gtx layout.Context) layout.Dimensions { return func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.X = trackWidth gtx.Constraints.Min.X = trackWidth
gtx.Constraints.Max.X = trackWidth gtx.Constraints.Max.X = trackWidth
@ -26,29 +26,37 @@ func (t *Tracker) layoutTrack(patterns [][]byte, sequence []byte, active bool, h
op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y/2)-trackRowHeight)).Add(gtx.Ops) op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y/2)-trackRowHeight)).Add(gtx.Ops)
// TODO: this is a time bomb; as soon as one of the patterns is not the same length as rest. Find a solution // TODO: this is a time bomb; as soon as one of the patterns is not the same length as rest. Find a solution
// to fix the pattern lengths to a constant value // to fix the pattern lengths to a constant value
cursorSongRow := cursorPattern*len(patterns[0]) + cursorRow cursorSongRow := t.Cursor.Pattern*t.song.PatternRows() + t.Cursor.Row
playSongRow := playPattern*len(patterns[0]) + playRow
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops) op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
for i, s := range sequence { patternRect := SongRect{
if cursorPattern == i && active { Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track},
paint.FillShape(gtx.Ops, activeTrackColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight*len(patterns[0]))}.Op()) Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track},
}
pointRect := SongRect{
Corner1: t.Cursor,
Corner2: t.SelectionCorner,
}
for i, s := range t.song.Tracks[trackNo].Sequence {
if patternRect.Contains(SongPoint{Track: trackNo, SongRow: SongRow{Pattern: i}}) {
paint.FillShape(gtx.Ops, activeTrackColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight*t.song.PatternRows())}.Op())
} }
for j, c := range patterns[s] { for j, c := range t.song.Tracks[trackNo].Patterns[s] {
songRow := i*len(patterns[0]) + j songRow := SongRow{Pattern: i, Row: j}
if songRow == playSongRow { songPoint := SongPoint{Track: trackNo, SongRow: songRow}
if songRow == t.PlayPosition && t.Playing {
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight)}.Op()) paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight)}.Op())
} }
if j == 0 { if j == 0 {
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops) paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(s)) widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(s))
} }
if songRow == cursorSongRow { if songRow == t.Cursor.SongRow {
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops) paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
} else { } else {
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops) paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
} }
op.Offset(f32.Pt(patmarkWidth, 0)).Add(gtx.Ops) op.Offset(f32.Pt(patmarkWidth, 0)).Add(gtx.Ops)
if hex { if t.TrackShowHex[trackNo] {
var text string var text string
switch c { switch c {
case 0: case 0:
@ -59,13 +67,23 @@ func (t *Tracker) layoutTrack(patterns [][]byte, sequence []byte, active bool, h
text = fmt.Sprintf("%02x", c) text = fmt.Sprintf("%02x", c)
} }
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(text)) widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(text))
if active && songRow == cursorSongRow { if pointRect.Contains(songPoint) {
paint.FillShape(gtx.Ops, trackerCursorColor, clip.Rect{Min: image.Pt(cursorCol*10, 0), Max: image.Pt(cursorCol*10+10, trackRowHeight)}.Op()) for col := 0; col < 2; col++ {
color := trackerSelectionColor
if songPoint == t.Cursor && t.CursorColumn == col {
color = trackerCursorColor
}
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(col*10, 0), Max: image.Pt(col*10+10, trackRowHeight)}.Op())
}
} }
} else { } else {
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, valueAsNote(c)) widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, valueAsNote(c))
if active && cursorCol == 0 && songRow == cursorSongRow { if pointRect.Contains(songPoint) {
paint.FillShape(gtx.Ops, trackerCursorColor, clip.Rect{Max: image.Pt(30, trackRowHeight)}.Op()) color := trackerSelectionColor
if songPoint == t.Cursor {
color = trackerCursorColor
}
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(30, trackRowHeight)}.Op())
} }
} }
op.Offset(f32.Pt(-patmarkWidth, trackRowHeight)).Add(gtx.Ops) op.Offset(f32.Pt(-patmarkWidth, trackRowHeight)).Add(gtx.Ops)

View File

@ -19,12 +19,10 @@ type Tracker struct {
Playing bool Playing bool
// protects PlayPattern and PlayRow // protects PlayPattern and PlayRow
playRowPatMutex sync.RWMutex // protects song and playing playRowPatMutex sync.RWMutex // protects song and playing
PlayPattern int PlayPosition SongRow
PlayRow int SelectionCorner SongPoint
CursorRow int Cursor SongPoint
CursorColumn int CursorColumn int
DisplayPattern int
ActiveTrack int
CurrentInstrument int CurrentInstrument int
CurrentUnit int CurrentUnit int
NoteTracking bool NoteTracking bool
@ -73,21 +71,9 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
} else { } else {
t.synth = synth t.synth = synth
} }
if t.DisplayPattern >= song.SequenceLength() { t.PlayPosition.Clamp(song)
t.DisplayPattern = song.SequenceLength() - 1 t.Cursor.Clamp(song)
} t.SelectionCorner.Clamp(song)
if t.CursorRow >= song.PatternRows() {
t.CursorRow = song.PatternRows() - 1
}
if t.PlayPattern >= song.SequenceLength() {
t.PlayPattern = song.SequenceLength() - 1
}
if t.PlayRow >= song.PatternRows() {
t.PlayRow = song.PatternRows() - 1
}
if t.ActiveTrack >= len(song.Tracks) {
t.ActiveTrack = len(song.Tracks) - 1
}
if t.sequencer != nil { if t.sequencer != nil {
t.sequencer.SetSynth(t.synth) t.sequencer.SetSynth(t.synth)
} }
@ -105,8 +91,8 @@ func (t *Tracker) TogglePlay() {
t.Playing = !t.Playing t.Playing = !t.Playing
if t.Playing { if t.Playing {
t.NoteTracking = true t.NoteTracking = true
t.PlayPattern = t.DisplayPattern t.PlayPosition = t.Cursor.SongRow
t.PlayRow = t.CursorRow - 1 t.PlayPosition.Row-- // TODO: we advance soon to make up for this -1, but this is not very elegant way to do it
} }
} }
@ -124,22 +110,16 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
t.playRowPatMutex.Unlock() t.playRowPatMutex.Unlock()
return nil, false return nil, false
} }
t.PlayRow++ t.PlayPosition.Row++
if t.PlayRow >= t.song.PatternRows() { t.PlayPosition.Wrap(t.song)
t.PlayRow = 0
t.PlayPattern++
}
if t.PlayPattern >= t.song.SequenceLength() {
t.PlayPattern = 0
}
if t.NoteTracking { if t.NoteTracking {
t.DisplayPattern = t.PlayPattern t.Cursor.SongRow = t.PlayPosition
t.CursorRow = t.PlayRow t.SelectionCorner.SongRow = t.PlayPosition
} }
notes := make([]Note, 0, 32) notes := make([]Note, 0, 32)
for track := range t.song.Tracks { for track := range t.song.Tracks {
patternIndex := t.song.Tracks[track].Sequence[t.PlayPattern] patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayRow] note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row]
if note == 1 { // anything but hold causes an action. if note == 1 { // anything but hold causes an action.
continue continue
} }
@ -240,20 +220,20 @@ func (t *Tracker) AddInstrument() {
// SetCurrentNote sets the (note) value in current pattern under cursor to iv // SetCurrentNote sets the (note) value in current pattern under cursor to iv
func (t *Tracker) SetCurrentNote(iv byte) { func (t *Tracker) SetCurrentNote(iv byte) {
t.SaveUndo() t.SaveUndo()
t.song.Tracks[t.ActiveTrack].Patterns[t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern]][t.CursorRow] = iv t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row] = iv
} }
func (t *Tracker) SetCurrentPattern(pat byte) { func (t *Tracker) SetCurrentPattern(pat byte) {
t.SaveUndo() t.SaveUndo()
length := len(t.song.Tracks[t.ActiveTrack].Patterns) length := len(t.song.Tracks[t.Cursor.Track].Patterns)
if int(pat) >= length { if int(pat) >= length {
tail := make([][]byte, int(pat)-length+1) tail := make([][]byte, int(pat)-length+1)
for i := range tail { for i := range tail {
tail[i] = make([]byte, t.song.PatternRows()) tail[i] = make([]byte, t.song.PatternRows())
} }
t.song.Tracks[t.ActiveTrack].Patterns = append(t.song.Tracks[t.ActiveTrack].Patterns, tail...) t.song.Tracks[t.Cursor.Track].Patterns = append(t.song.Tracks[t.Cursor.Track].Patterns, tail...)
} }
t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern] = pat t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern] = pat
} }
func (t *Tracker) SetSongLength(value int) { func (t *Tracker) SetSongLength(value int) {
@ -276,6 +256,28 @@ func (t *Tracker) SetSongLength(value int) {
} }
} }
func (t *Tracker) DeleteSelection() {
t.SaveUndo()
r1 := t.Cursor.Pattern*t.song.PatternRows() + t.Cursor.Row
r2 := t.SelectionCorner.Pattern*t.song.PatternRows() + t.SelectionCorner.Row
if r2 < r1 {
r1, r2 = r2, r1
}
t1 := t.Cursor.Track
t2 := t.SelectionCorner.Track
if t2 < t1 {
t1, t2 = t2, t1
}
for r := r1; r <= r2; r++ {
for c := t1; c <= t2; c++ {
s := SongRow{Row: r}
s.Wrap(t.song)
p := t.song.Tracks[c].Sequence[s.Pattern]
t.song.Tracks[c].Patterns[p][s.Row] = 1
}
}
}
func New(audioContext sointu.AudioContext) *Tracker { func New(audioContext sointu.AudioContext) *Tracker {
t := &Tracker{ t := &Tracker{
Theme: material.NewTheme(gofont.Collection()), Theme: material.NewTheme(gofont.Collection()),