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)
return true
case key.NameDeleteForward:
t.SetCurrentNote(1)
t.DeleteSelection()
return true
case key.NameEscape:
os.Exit(0)
@ -77,7 +77,11 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
if e.Modifiers.Contain(key.ModCtrl) {
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
return true
case key.NameDownArrow:
@ -85,37 +89,41 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
if e.Modifiers.Contain(key.ModCtrl) {
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
return true
case key.NameLeftArrow:
if t.CursorColumn == 0 || !t.TrackShowHex[t.ActiveTrack] || e.Modifiers.Contain(key.ModCtrl) {
t.ActiveTrack = (t.ActiveTrack + len(t.song.Tracks) - 1) % len(t.song.Tracks)
if t.TrackShowHex[t.ActiveTrack] {
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
t.Cursor.Track--
t.Cursor.Clamp(t.song)
if t.TrackShowHex[t.Cursor.Track] {
t.CursorColumn = 1
} else {
t.CursorColumn = 0
}
if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor
}
} else {
t.CursorColumn--
}
return true
case key.NameRightArrow:
if t.CursorColumn == 1 || !t.TrackShowHex[t.ActiveTrack] || e.Modifiers.Contain(key.ModCtrl) {
t.ActiveTrack = (t.ActiveTrack + 1) % len(t.song.Tracks)
if t.CursorColumn == 1 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
t.Cursor.Track++
t.Cursor.Clamp(t.song)
if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor
}
t.CursorColumn = 0
} else {
t.CursorColumn++
}
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 iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
@ -123,7 +131,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
return true
}
} else {
if !t.TrackShowHex[t.ActiveTrack] {
if !t.TrackShowHex[t.Cursor.Track] {
if val, ok := noteMap[e.Name]; ok {
t.NotePressed(val)
return true
@ -139,24 +147,9 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
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
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

View File

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

View File

@ -11,7 +11,6 @@ import (
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget"
"github.com/vsariola/sointu"
)
const patternCellHeight = 16
@ -19,34 +18,41 @@ const patternCellWidth = 16
const patternVisibleTracks = 8
const patternRowMarkerWidth = 30
func (t *Tracker) layoutPatterns(tracks []sointu.Track, activeTrack, cursorPattern, cursorCol, playingPattern int) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth
gtx.Constraints.Max.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth
defer op.Push(gtx.Ops).Pop()
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())
for j := range tracks[0].Sequence {
if j == playingPattern {
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}
func (t *Tracker) layoutPatterns(gtx C) D {
gtx.Constraints.Min.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth
gtx.Constraints.Max.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth
defer op.Push(gtx.Ops).Pop()
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())
patternRect := SongRect{
Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track},
Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track},
}
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 {

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 trackerPatMarker = primaryColor
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 patternPlayColor = color.RGBA{R: 55, G: 55, B: 61, A: 255}
@ -80,6 +81,7 @@ var patternActiveTextColor = yellow
var patternFont = fontCollection[6].Font
var patternFontSize = unit.Px(12)
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}

View File

@ -17,7 +17,7 @@ const trackRowHeight = 16
const trackWidth = 54
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 {
gtx.Constraints.Min.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)
// 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
cursorSongRow := cursorPattern*len(patterns[0]) + cursorRow
playSongRow := playPattern*len(patterns[0]) + playRow
cursorSongRow := t.Cursor.Pattern*t.song.PatternRows() + t.Cursor.Row
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
for i, s := range sequence {
if cursorPattern == i && active {
paint.FillShape(gtx.Ops, activeTrackColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight*len(patterns[0]))}.Op())
patternRect := SongRect{
Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track},
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] {
songRow := i*len(patterns[0]) + j
if songRow == playSongRow {
for j, c := range t.song.Tracks[trackNo].Patterns[s] {
songRow := SongRow{Pattern: i, Row: j}
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())
}
if j == 0 {
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(s))
}
if songRow == cursorSongRow {
if songRow == t.Cursor.SongRow {
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
} else {
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
}
op.Offset(f32.Pt(patmarkWidth, 0)).Add(gtx.Ops)
if hex {
if t.TrackShowHex[trackNo] {
var text string
switch c {
case 0:
@ -59,13 +67,23 @@ func (t *Tracker) layoutTrack(patterns [][]byte, sequence []byte, active bool, h
text = fmt.Sprintf("%02x", c)
}
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(text))
if active && songRow == cursorSongRow {
paint.FillShape(gtx.Ops, trackerCursorColor, clip.Rect{Min: image.Pt(cursorCol*10, 0), Max: image.Pt(cursorCol*10+10, trackRowHeight)}.Op())
if pointRect.Contains(songPoint) {
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 {
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, valueAsNote(c))
if active && cursorCol == 0 && songRow == cursorSongRow {
paint.FillShape(gtx.Ops, trackerCursorColor, clip.Rect{Max: image.Pt(30, trackRowHeight)}.Op())
if pointRect.Contains(songPoint) {
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)

View File

@ -19,12 +19,10 @@ type Tracker struct {
Playing bool
// protects PlayPattern and PlayRow
playRowPatMutex sync.RWMutex // protects song and playing
PlayPattern int
PlayRow int
CursorRow int
PlayPosition SongRow
SelectionCorner SongPoint
Cursor SongPoint
CursorColumn int
DisplayPattern int
ActiveTrack int
CurrentInstrument int
CurrentUnit int
NoteTracking bool
@ -73,21 +71,9 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
} else {
t.synth = synth
}
if t.DisplayPattern >= song.SequenceLength() {
t.DisplayPattern = song.SequenceLength() - 1
}
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
}
t.PlayPosition.Clamp(song)
t.Cursor.Clamp(song)
t.SelectionCorner.Clamp(song)
if t.sequencer != nil {
t.sequencer.SetSynth(t.synth)
}
@ -105,8 +91,8 @@ func (t *Tracker) TogglePlay() {
t.Playing = !t.Playing
if t.Playing {
t.NoteTracking = true
t.PlayPattern = t.DisplayPattern
t.PlayRow = t.CursorRow - 1
t.PlayPosition = t.Cursor.SongRow
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()
return nil, false
}
t.PlayRow++
if t.PlayRow >= t.song.PatternRows() {
t.PlayRow = 0
t.PlayPattern++
}
if t.PlayPattern >= t.song.SequenceLength() {
t.PlayPattern = 0
}
t.PlayPosition.Row++
t.PlayPosition.Wrap(t.song)
if t.NoteTracking {
t.DisplayPattern = t.PlayPattern
t.CursorRow = t.PlayRow
t.Cursor.SongRow = t.PlayPosition
t.SelectionCorner.SongRow = t.PlayPosition
}
notes := make([]Note, 0, 32)
for track := range t.song.Tracks {
patternIndex := t.song.Tracks[track].Sequence[t.PlayPattern]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayRow]
patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row]
if note == 1 { // anything but hold causes an action.
continue
}
@ -240,20 +220,20 @@ func (t *Tracker) AddInstrument() {
// SetCurrentNote sets the (note) value in current pattern under cursor to iv
func (t *Tracker) SetCurrentNote(iv byte) {
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) {
t.SaveUndo()
length := len(t.song.Tracks[t.ActiveTrack].Patterns)
length := len(t.song.Tracks[t.Cursor.Track].Patterns)
if int(pat) >= length {
tail := make([][]byte, int(pat)-length+1)
for i := range tail {
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) {
@ -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 {
t := &Tracker{
Theme: material.NewTheme(gofont.Collection()),