feat(tracker): implement edit modes, resembling tab stops

This commit is contained in:
vsariola 2021-02-08 20:15:37 +02:00
parent 7408956f77
commit 38008bdb87
13 changed files with 619 additions and 347 deletions

View File

@ -38,6 +38,10 @@ func Encode(patch *sointu.Patch, featureSet FeatureSet) (*EncodedPatch, error) {
} }
if unit.Type == "oscillator" && unit.Parameters["type"] == 4 { if unit.Type == "oscillator" && unit.Parameters["type"] == 4 {
s := SampleOffset{Start: uint32(unit.Parameters["samplestart"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])} s := SampleOffset{Start: uint32(unit.Parameters["samplestart"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
if s.LoopLength == 0 {
// hacky quick fix: looplength 0 causes div by zero so avoid crashing
s.LoopLength = 1
}
index, ok := sampleOffsetMap[s] index, ok := sampleOffsetMap[s]
if !ok { if !ok {
index = len(c.SampleOffsets) index = len(c.SampleOffsets)

View File

@ -255,7 +255,7 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false}, {Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false},
{Name: "samplestart", MinValue: 0, MaxValue: 1720329, CanSet: true, CanModulate: false}, {Name: "samplestart", MinValue: 0, MaxValue: 1720329, CanSet: true, CanModulate: false},
{Name: "loopstart", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}, {Name: "loopstart", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false},
{Name: "looplength", MinValue: 1, MaxValue: 65535, CanSet: true, CanModulate: false}}, {Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}},
"loadval": []UnitParameter{ "loadval": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, {Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},

View File

@ -24,7 +24,6 @@ type DragList struct {
type FilledDragListStyle struct { type FilledDragListStyle struct {
dragList *DragList dragList *DragList
SurfaceColor color.NRGBA
HoverColor color.NRGBA HoverColor color.NRGBA
SelectedColor color.NRGBA SelectedColor color.NRGBA
Count int Count int
@ -38,7 +37,6 @@ func FilledDragList(th *material.Theme, dragList *DragList, count int, element f
element: element, element: element,
swap: swap, swap: swap,
Count: count, Count: count,
SurfaceColor: dragListSurfaceColor,
HoverColor: dragListHoverColor, HoverColor: dragListHoverColor,
SelectedColor: dragListSelectedColor, SelectedColor: dragListSelectedColor,
} }
@ -47,7 +45,6 @@ func FilledDragList(th *material.Theme, dragList *DragList, count int, element f
func (s *FilledDragListStyle) Layout(gtx C) D { func (s *FilledDragListStyle) Layout(gtx C) D {
swap := 0 swap := 0
paint.FillShape(gtx.Ops, s.SurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op())
defer op.Save(gtx.Ops).Load() defer op.Save(gtx.Ops).Load()
if s.dragList.List.Axis == layout.Horizontal { if s.dragList.List.Axis == layout.Horizontal {

View File

@ -6,8 +6,6 @@ import (
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text" "gioui.org/text"
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget" "gioui.org/widget"
@ -50,12 +48,6 @@ func (t *Tracker) layoutInstruments(gtx C) D {
} }
func (t *Tracker) layoutInstrumentHeader(gtx C) D { func (t *Tracker) layoutInstrumentHeader(gtx C) D {
headerBg := func(gtx C) D {
paint.FillShape(gtx.Ops, instrumentSurfaceColor, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
return layout.Dimensions{Size: gtx.Constraints.Min}
}
header := func(gtx C) D { header := func(gtx C) D {
deleteInstrumentBtnStyle := material.IconButton(t.Theme, t.DeleteInstrumentBtn, widgetForIcon(icons.ActionDelete)) deleteInstrumentBtnStyle := material.IconButton(t.Theme, t.DeleteInstrumentBtn, widgetForIcon(icons.ActionDelete))
deleteInstrumentBtnStyle.Background = transparent deleteInstrumentBtnStyle.Background = transparent
@ -87,9 +79,7 @@ func (t *Tracker) layoutInstrumentHeader(gtx C) D {
for t.DeleteInstrumentBtn.Clicked() { for t.DeleteInstrumentBtn.Clicked() {
t.DeleteInstrument() t.DeleteInstrument()
} }
return layout.Stack{Alignment: layout.Center}.Layout(gtx, return Surface{Gray: 37, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, header)
layout.Expanded(headerBg),
layout.Stacked(header))
} }
func (t *Tracker) layoutInstrumentNames(gtx C) D { func (t *Tracker) layoutInstrumentNames(gtx C) D {
@ -135,10 +125,13 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D {
}) })
} }
color := inactiveLightSurfaceColor
if t.EditMode == EditUnits || t.EditMode == EditParameters {
color = activeLightSurfaceColor
}
instrumentList := FilledDragList(t.Theme, t.InstrumentDragList, len(t.song.Patch.Instruments), element, t.SwapInstruments) instrumentList := FilledDragList(t.Theme, t.InstrumentDragList, len(t.song.Patch.Instruments), element, t.SwapInstruments)
instrumentList.SelectedColor = instrumentSurfaceColor instrumentList.SelectedColor = color
instrumentList.HoverColor = instrumentHoverColor instrumentList.HoverColor = instrumentHoverColor
instrumentList.SurfaceColor = transparent
t.InstrumentDragList.SelectedItem = t.CurrentInstrument t.InstrumentDragList.SelectedItem = t.CurrentInstrument
defer op.Save(gtx.Ops).Load() defer op.Save(gtx.Ops).Load()
@ -174,21 +167,28 @@ func (t *Tracker) layoutInstrumentEditor(gtx C) D {
unitList := FilledDragList(t.Theme, t.UnitDragList, len(t.song.Patch.Instruments[t.CurrentInstrument].Units), element, t.SwapUnits) unitList := FilledDragList(t.Theme, t.UnitDragList, len(t.song.Patch.Instruments[t.CurrentInstrument].Units), element, t.SwapUnits)
if t.EditMode == EditUnits {
unitList.SelectedColor = cursorColor
}
t.UnitDragList.SelectedItem = t.CurrentUnit t.UnitDragList.SelectedItem = t.CurrentUnit
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, return Surface{Gray: 30, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, func(gtx C) D {
layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
return layout.Stack{Alignment: layout.SE}.Layout(gtx, layout.Rigid(func(gtx C) D {
layout.Expanded(func(gtx C) D { return layout.Stack{Alignment: layout.SE}.Layout(gtx,
dims := unitList.Layout(gtx) layout.Expanded(func(gtx C) D {
if t.CurrentUnit != t.UnitDragList.SelectedItem { dims := unitList.Layout(gtx)
t.CurrentUnit = t.UnitDragList.SelectedItem if t.CurrentUnit != t.UnitDragList.SelectedItem {
op.InvalidateOp{}.Add(gtx.Ops) t.CurrentUnit = t.UnitDragList.SelectedItem
} t.EditMode = EditUnits
return dims op.InvalidateOp{}.Add(gtx.Ops)
}), }
layout.Stacked(func(gtx C) D { return dims
return margin.Layout(gtx, addUnitBtnStyle.Layout) }),
})) layout.Stacked(func(gtx C) D {
}), return margin.Layout(gtx, addUnitBtnStyle.Layout)
layout.Rigid(t.layoutUnitEditor)) }))
}),
layout.Rigid(t.layoutUnitEditor))
})
} }

View File

@ -1,8 +1,8 @@
package tracker package tracker
import ( import (
"os"
"strconv" "strconv"
"strings"
"gioui.org/io/key" "gioui.org/io/key"
) )
@ -42,6 +42,38 @@ var noteMap = map[string]int{
"P": 16, "P": 16,
} }
var unitKeyMap = map[string]string{
"e": "envelope",
"o": "oscillator",
"m": "mulp",
"M": "mul",
"a": "addp",
"A": "add",
"p": "pan",
"S": "push",
"P": "pop",
"O": "out",
"l": "loadnote",
"L": "loadval",
"h": "xch",
"d": "delay",
"D": "distort",
"H": "hold",
"b": "crush",
"g": "gain",
"i": "invgain",
"f": "filter",
"I": "clip",
"E": "speed",
"r": "compressor",
"u": "outaux",
"U": "aux",
"s": "send",
"n": "noise",
"N": "in",
"R": "receive",
}
// KeyEvent handles incoming key events and returns true if repaint is needed. // KeyEvent handles incoming key events and returns true if repaint is needed.
func (t *Tracker) KeyEvent(e key.Event) bool { func (t *Tracker) KeyEvent(e key.Event) bool {
if e.State == key.Press { if e.State == key.Press {
@ -59,14 +91,15 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
t.Redo() t.Redo()
return true return true
} }
case "A":
t.SetCurrentNote(0)
return true
case key.NameDeleteForward: case key.NameDeleteForward:
t.DeleteSelection() switch t.EditMode {
return true case EditTracks:
case key.NameEscape: t.DeleteSelection()
os.Exit(0) return true
case EditUnits:
t.DeleteUnit()
return true
}
case "Space": case "Space":
t.TogglePlay() t.TogglePlay()
return true return true
@ -75,75 +108,161 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
return t.ChangeOctave(1) return t.ChangeOctave(1)
} }
return t.ChangeOctave(-1) return t.ChangeOctave(-1)
case key.NameTab:
if e.Modifiers.Contain(key.ModShift) {
t.EditMode = (t.EditMode - 1 + 4) % 4
} else {
t.EditMode = (t.EditMode + 1) % 4
}
return true
case key.NameUpArrow: case key.NameUpArrow:
delta := -1 switch t.EditMode {
if e.Modifiers.Contain(key.ModCtrl) { case EditPatterns:
delta = -t.song.RowsPerPattern if e.Modifiers.Contain(key.ModCtrl) {
t.Cursor.SongRow = SongRow{}
} else {
t.Cursor.Row -= t.song.RowsPerPattern
}
t.NoteTracking = false
case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) {
t.Cursor.Row -= t.song.RowsPerPattern
} else {
t.Cursor.Row--
}
t.NoteTracking = false
case EditUnits:
t.CurrentUnit--
case EditParameters:
t.CurrentParam--
} }
t.Cursor.Row += delta t.ClampPositions()
t.Cursor.Clamp(t.song)
if !e.Modifiers.Contain(key.ModShift) { if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor t.Unselect()
} }
t.NoteTracking = false
return true return true
case key.NameDownArrow: case key.NameDownArrow:
delta := 1 switch t.EditMode {
if e.Modifiers.Contain(key.ModCtrl) { case EditPatterns:
delta = t.song.RowsPerPattern if e.Modifiers.Contain(key.ModCtrl) {
t.Cursor.Row = t.song.TotalRows() - 1
} else {
t.Cursor.Row += t.song.RowsPerPattern
}
t.NoteTracking = false
case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) {
t.Cursor.Row += t.song.RowsPerPattern
} else {
t.Cursor.Row++
}
t.NoteTracking = false
case EditUnits:
t.CurrentUnit++
case EditParameters:
t.CurrentParam++
} }
t.Cursor.Row += delta t.ClampPositions()
t.Cursor.Clamp(t.song)
if !e.Modifiers.Contain(key.ModShift) { if !e.Modifiers.Contain(key.ModShift) {
t.SelectionCorner = t.Cursor t.Unselect()
} }
t.NoteTracking = false
return true return true
case key.NameLeftArrow: case key.NameLeftArrow:
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) { switch t.EditMode {
t.Cursor.Track-- case EditPatterns:
t.Cursor.Clamp(t.song) if e.Modifiers.Contain(key.ModCtrl) {
if t.TrackShowHex[t.Cursor.Track] { t.Cursor.Track = 0
} else {
t.Cursor.Track--
}
case EditTracks:
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
t.Cursor.Track--
t.CursorColumn = 1 t.CursorColumn = 1
} else { } else {
t.CursorColumn = 0 t.CursorColumn--
} }
if !e.Modifiers.Contain(key.ModShift) { case EditUnits:
t.SelectionCorner = t.Cursor t.CurrentInstrument--
case EditParameters:
if e.Modifiers.Contain(key.ModShift) {
t.SetUnitParam(t.GetUnitParam() - 16)
} else {
t.SetUnitParam(t.GetUnitParam() - 1)
} }
} else { }
t.CursorColumn-- t.ClampPositions()
if !e.Modifiers.Contain(key.ModShift) {
t.Unselect()
} }
return true return true
case key.NameRightArrow: case key.NameRightArrow:
if t.CursorColumn == 1 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) { switch t.EditMode {
t.Cursor.Track++ case EditPatterns:
t.Cursor.Clamp(t.song) if e.Modifiers.Contain(key.ModCtrl) {
if !e.Modifiers.Contain(key.ModShift) { t.Cursor.Track = len(t.song.Tracks) - 1
t.SelectionCorner = t.Cursor } else {
t.Cursor.Track++
} }
t.CursorColumn = 0 case EditTracks:
} else { if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
t.CursorColumn++ t.Cursor.Track++
t.CursorColumn = 0
} else {
t.CursorColumn++
}
case EditUnits:
t.CurrentInstrument++
case EditParameters:
if e.Modifiers.Contain(key.ModShift) {
t.SetUnitParam(t.GetUnitParam() + 16)
} else {
t.SetUnitParam(t.GetUnitParam() + 1)
}
}
t.ClampPositions()
if !e.Modifiers.Contain(key.ModShift) {
t.Unselect()
} }
return true return true
} }
if e.Modifiers.Contain(key.ModCtrl) { switch t.EditMode {
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { case EditPatterns:
if iv, err := strconv.Atoi(e.Name); err == nil {
t.SetCurrentPattern(byte(iv)) t.SetCurrentPattern(byte(iv))
return true return true
} }
} else { if b := byte(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
if !t.TrackShowHex[t.Cursor.Track] { t.SetCurrentPattern(b + 10)
if val, ok := noteMap[e.Name]; ok { return true
t.NotePressed(val) }
return true case EditTracks:
} if t.TrackShowHex[t.Cursor.Track] {
} else {
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
t.NumberPressed(byte(iv)) t.NumberPressed(byte(iv))
return true return true
} }
} else {
if e.Name == "A" {
t.SetCurrentNote(0)
return true
}
if val, ok := noteMap[e.Name]; ok {
t.NotePressed(val)
return true
}
}
case EditUnits:
name := e.Name
if !e.Modifiers.Contain(key.ModShift) {
name = strings.ToLower(name)
}
if val, ok := unitKeyMap[name]; ok {
if e.Modifiers.Contain(key.ModCtrl) {
t.AddUnit()
}
t.SetUnit(val)
return true
} }
} }
} }

View File

@ -2,16 +2,13 @@ package tracker
import ( import (
"image" "image"
"image/color"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
) )
func smallButton(icStyle material.IconButtonStyle) material.IconButtonStyle { func smallButton(icStyle material.IconButtonStyle) material.IconButtonStyle {
@ -40,156 +37,23 @@ func trackButton(t *material.Theme, w *widget.Clickable, text string, enabled bo
func (t *Tracker) Layout(gtx layout.Context) { func (t *Tracker) Layout(gtx layout.Context) {
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op()) paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
t.VerticalSplit.Layout(gtx, t.VerticalSplit.Layout(gtx,
t.layoutControls, t.layoutTop,
t.layoutTracksAndPatterns) t.layoutBottom)
t.updateInstrumentScroll() t.updateInstrumentScroll()
} }
func (t *Tracker) layoutTracksAndPatterns(gtx layout.Context) layout.Dimensions { func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
return t.BottomHorizontalSplit.Layout(gtx, return t.BottomHorizontalSplit.Layout(gtx,
t.layoutPatterns, func(gtx C) D {
t.layoutTracks, return Surface{Gray: 24, Focus: t.EditMode == 0}.Layout(gtx, t.layoutPatterns)
},
func(gtx C) D {
return Surface{Gray: 24, Focus: t.EditMode == 1}.Layout(gtx, t.layoutTracker)
},
) )
} }
func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions { func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
paint.FillShape(gtx.Ops, trackerSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op())
flexTracks := make([]layout.FlexChild, len(t.song.Tracks))
t.playRowPatMutex.RLock()
defer t.playRowPatMutex.RUnlock()
playPat := t.PlayPosition.Pattern
if !t.Playing {
playPat = -1
}
rowMarkers := layout.Rigid(t.layoutRowMarkers(
t.song.RowsPerPattern,
len(t.song.Tracks[0].Sequence),
t.Cursor.Row,
t.Cursor.Pattern,
t.CursorColumn,
t.PlayPosition.Row,
playPat,
))
leftInset := layout.Inset{Left: unit.Dp(4)}
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))
}
if len(t.TrackShowHex) <= i {
t.TrackShowHex = append(t.TrackShowHex, false)
}
flexTracks[i] = layout.Rigid(func(gtx layout.Context) layout.Dimensions {
t.TrackHexCheckBoxes[i2].Value = t.TrackShowHex[i2]
cbStyle := material.CheckBox(t.Theme, t.TrackHexCheckBoxes[i2], "hex")
cbStyle.Color = white
cbStyle.IconColor = t.Theme.Fg
ret := layout.Stack{}.Layout(gtx,
layout.Stacked(func(gtx layout.Context) D {
return leftInset.Layout(gtx, t.layoutTrack(i2))
}),
layout.Stacked(cbStyle.Layout),
)
t.TrackShowHex[i2] = t.TrackHexCheckBoxes[i2].Value
return ret
})
}
menuBg := func(gtx C) D {
paint.FillShape(gtx.Ops, trackMenuSurfaceColor, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
return layout.Dimensions{Size: gtx.Constraints.Min}
}
for t.AddSemitoneBtn.Clicked() {
t.AdjustSelectionPitch(1)
}
for t.SubtractSemitoneBtn.Clicked() {
t.AdjustSelectionPitch(-1)
}
for t.AddOctaveBtn.Clicked() {
t.AdjustSelectionPitch(12)
}
for t.SubtractOctaveBtn.Clicked() {
t.AdjustSelectionPitch(-12)
}
menu := func(gtx C) D {
addSemitoneBtnStyle := material.Button(t.Theme, t.AddSemitoneBtn, "+1")
addSemitoneBtnStyle.Color = primaryColor
addSemitoneBtnStyle.Background = transparent
addSemitoneBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
subtractSemitoneBtnStyle := material.Button(t.Theme, t.SubtractSemitoneBtn, "-1")
subtractSemitoneBtnStyle.Color = primaryColor
subtractSemitoneBtnStyle.Background = transparent
subtractSemitoneBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
addOctaveBtnStyle := material.Button(t.Theme, t.AddOctaveBtn, "+12")
addOctaveBtnStyle.Color = primaryColor
addOctaveBtnStyle.Background = transparent
addOctaveBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
subtractOctaveBtnStyle := material.Button(t.Theme, t.SubtractOctaveBtn, "-12")
subtractOctaveBtnStyle.Color = primaryColor
subtractOctaveBtnStyle.Background = transparent
subtractOctaveBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
newTrackBtnStyle := material.IconButton(t.Theme, t.NewTrackBtn, widgetForIcon(icons.ContentAdd))
newTrackBtnStyle.Background = transparent
newTrackBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
if t.song.TotalTrackVoices() < t.song.Patch.TotalVoices() {
newTrackBtnStyle.Color = primaryColor
} else {
newTrackBtnStyle.Color = disabledTextColor
}
in := layout.UniformInset(unit.Dp(1))
octave := func(gtx C) D {
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)
}
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(Label("OCT:", white)),
layout.Rigid(octave),
layout.Rigid(Label(" PITCH:", white)),
layout.Rigid(addSemitoneBtnStyle.Layout),
layout.Rigid(subtractSemitoneBtnStyle.Layout),
layout.Rigid(addOctaveBtnStyle.Layout),
layout.Rigid(subtractOctaveBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(newTrackBtnStyle.Layout))
}
for t.NewTrackBtn.Clicked() {
t.AddTrack()
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Expanded(menuBg),
layout.Stacked(menu),
)
}),
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
rowMarkers,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
defer op.Save(gtx.Ops).Load()
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
dims := layout.Flex{Axis: layout.Horizontal}.Layout(gtx, flexTracks...)
if dims.Size.X > gtx.Constraints.Max.X {
dims.Size.X = gtx.Constraints.Max.X
}
return dims
}))
}),
)
}
func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions {
for t.NewInstrumentBtn.Clicked() { for t.NewInstrumentBtn.Clicked() {
t.AddInstrument() t.AddInstrument()
} }
@ -200,19 +64,3 @@ func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions {
) )
} }
func (t *Tracker) line(horizontal bool, color color.NRGBA) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
if horizontal {
gtx.Constraints.Min.Y = 1
gtx.Constraints.Max.Y = 1
} else {
gtx.Constraints.Min.X = 1
gtx.Constraints.Max.X = 1
}
defer op.Save(gtx.Ops).Load()
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op())
return layout.Dimensions{Size: gtx.Constraints.Max}
}
}

View File

@ -20,7 +20,6 @@ const patternRowMarkerWidth = 30
func (t *Tracker) layoutPatterns(gtx C) D { func (t *Tracker) layoutPatterns(gtx C) D {
defer op.Save(gtx.Ops).Load() defer op.Save(gtx.Ops).Load()
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())
patternRect := SongRect{ patternRect := SongRect{
Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track}, Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track},
Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track}, Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track},
@ -35,14 +34,19 @@ func (t *Tracker) layoutPatterns(gtx C) D {
op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops) op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops)
for i, track := range t.song.Tracks { for i, track := range t.song.Tracks {
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops) paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, fmt.Sprintf("%d", track.Sequence[j])) widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(track.Sequence[j]))
point := SongPoint{Track: i, SongRow: SongRow{Pattern: j}} point := SongPoint{Track: i, SongRow: SongRow{Pattern: j}}
if patternRect.Contains(point) { if t.EditMode == EditPatterns || t.EditMode == EditTracks {
color := patternSelectionColor if patternRect.Contains(point) {
if point.Pattern == t.Cursor.Pattern && point.Track == t.Cursor.Track { color := inactiveSelectionColor
color = patternCursorColor if t.EditMode == EditPatterns {
color = selectionColor
if point.Pattern == t.Cursor.Pattern && point.Track == t.Cursor.Track {
color = cursorColor
}
}
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op())
} }
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op())
} }
op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops) op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops)
} }

View File

@ -24,7 +24,7 @@ func (t *Tracker) layoutRowMarkers(patternRows, sequenceLength, cursorRow, curso
}.Op()) }.Op())
defer op.Save(gtx.Ops).Load() defer op.Save(gtx.Ops).Load()
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
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-trackRowHeight)/2)).Add(gtx.Ops)
cursorSongRow := cursorPattern*patternRows + cursorRow cursorSongRow := cursorPattern*patternRows + cursorRow
playSongRow := playPattern*patternRows + playRow playSongRow := playPattern*patternRows + 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)
@ -32,13 +32,13 @@ func (t *Tracker) layoutRowMarkers(patternRows, sequenceLength, cursorRow, curso
for j := 0; j < patternRows; j++ { for j := 0; j < patternRows; j++ {
songRow := i*patternRows + j songRow := i*patternRows + j
if songRow == playSongRow { if songRow == playSongRow {
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight)}.Op()) paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(trackColWidth, trackRowHeight)}.Op())
} }
if j == 0 { if j == 0 {
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i))) widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i)))
} }
if songRow == cursorSongRow { if t.EditMode == EditTracks && songRow == cursorSongRow {
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops) paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
} else { } else {
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops) paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)

51
tracker/surface.go Normal file
View File

@ -0,0 +1,51 @@
package tracker
import (
"image/color"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
)
type Surface struct {
Gray int
Inset layout.Inset
FitSize bool
Focus bool
}
func (s Surface) Layout(gtx C, widget layout.Widget) D {
bg := func(gtx C) D {
grayInt := s.Gray
if s.Focus {
grayInt += 8
}
var grayUint8 uint8
if grayInt < 0 {
grayUint8 = 0
} else if grayInt > 255 {
grayUint8 = 255
} else {
grayUint8 = uint8(grayInt)
}
color := color.NRGBA{R: grayUint8, G: grayUint8, B: grayUint8, A: 255}
paint.FillShape(gtx.Ops, color, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
return D{Size: gtx.Constraints.Min}
}
fg := func(gtx C) D {
return s.Inset.Layout(gtx, widget)
}
if s.FitSize {
return layout.Stack{}.Layout(gtx,
layout.Expanded(bg),
layout.Stacked(fg),
)
}
gtxbg := gtx
gtxbg.Constraints.Min = gtxbg.Constraints.Max
bg(gtxbg)
return fg(gtx)
}

View File

@ -56,6 +56,7 @@ var activeTrackColor = focusedContainerColor
var trackSurfaceColor = color.NRGBA{R: 255, G: 255, B: 255, A: 31} var trackSurfaceColor = color.NRGBA{R: 255, G: 255, B: 255, A: 31}
var patternSurfaceColor = color.NRGBA{R: 24, G: 24, B: 24, A: 255} var patternSurfaceColor = color.NRGBA{R: 24, G: 24, B: 24, A: 255}
var patternSurfaceActiveColor = color.NRGBA{R: 30, G: 30, B: 30, A: 255}
var rowMarkerSurfaceColor = color.NRGBA{R: 0, G: 0, B: 0, A: 0} var rowMarkerSurfaceColor = color.NRGBA{R: 0, G: 0, B: 0, A: 0}
var rowMarkerPatternTextColor = secondaryColor var rowMarkerPatternTextColor = secondaryColor
@ -101,3 +102,10 @@ var dragListHoverColor = color.NRGBA{R: 42, G: 45, B: 61, A: 255}
var unitSurfaceColor = color.NRGBA{R: 30, G: 30, B: 30, A: 255} var unitSurfaceColor = color.NRGBA{R: 30, G: 30, B: 30, A: 255}
var unitTypeListHighlightColor = color.NRGBA{R: 42, G: 45, B: 61, A: 255} var unitTypeListHighlightColor = color.NRGBA{R: 42, G: 45, B: 61, A: 255}
var inactiveLightSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, A: 255}
var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 8}
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}

View File

@ -10,86 +10,238 @@ import (
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
) )
const trackRowHeight = 16 const trackRowHeight = 16
const trackWidth = 54 const trackColWidth = 54
const patmarkWidth = 16 const patmarkWidth = 16
func (t *Tracker) layoutTrack(trackNo int) layout.Widget { func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
return func(gtx layout.Context) layout.Dimensions { t.playRowPatMutex.RLock()
gtx.Constraints.Min.X = trackWidth defer t.playRowPatMutex.RUnlock()
gtx.Constraints.Max.X = trackWidth
defer op.Save(gtx.Ops).Load() playPat := t.PlayPosition.Pattern
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) if !t.Playing {
op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y/2)-trackRowHeight)).Add(gtx.Ops) playPat = -1
// 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 := t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
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.RowsPerPattern)}.Op())
}
for j := 0; j < t.song.RowsPerPattern; j++ {
c := t.song.Tracks[trackNo].Patterns[s][j]
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 == 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 t.TrackShowHex[trackNo] {
var text string
switch c {
case 0:
text = "--"
case 1:
text = ".."
default:
text = fmt.Sprintf("%02x", c)
}
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(text))
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 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)
}
}
return layout.Dimensions{Size: gtx.Constraints.Max}
} }
rowMarkers := layout.Rigid(t.layoutRowMarkers(
t.song.RowsPerPattern,
len(t.song.Tracks[0].Sequence),
t.Cursor.Row,
t.Cursor.Pattern,
t.CursorColumn,
t.PlayPosition.Row,
playPat,
))
for t.NewTrackBtn.Clicked() {
t.AddTrack()
}
for len(t.TrackHexCheckBoxes) < len(t.song.Tracks) {
t.TrackHexCheckBoxes = append(t.TrackHexCheckBoxes, new(widget.Bool))
}
for len(t.TrackShowHex) < len(t.song.Tracks) {
t.TrackShowHex = append(t.TrackShowHex, false)
}
//t.TrackHexCheckBoxes[i2].Value = t.TrackShowHex[i2]
//cbStyle := material.CheckBox(t.Theme, t.TrackHexCheckBoxes[i2], "hex")
//cbStyle.Color = white
//cbStyle.IconColor = t.Theme.Fg
for t.AddSemitoneBtn.Clicked() {
t.AdjustSelectionPitch(1)
}
for t.SubtractSemitoneBtn.Clicked() {
t.AdjustSelectionPitch(-1)
}
for t.AddOctaveBtn.Clicked() {
t.AdjustSelectionPitch(12)
}
for t.SubtractOctaveBtn.Clicked() {
t.AdjustSelectionPitch(-12)
}
menu := func(gtx C) D {
addSemitoneBtnStyle := material.Button(t.Theme, t.AddSemitoneBtn, "+1")
addSemitoneBtnStyle.Color = primaryColor
addSemitoneBtnStyle.Background = transparent
addSemitoneBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
subtractSemitoneBtnStyle := material.Button(t.Theme, t.SubtractSemitoneBtn, "-1")
subtractSemitoneBtnStyle.Color = primaryColor
subtractSemitoneBtnStyle.Background = transparent
subtractSemitoneBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
addOctaveBtnStyle := material.Button(t.Theme, t.AddOctaveBtn, "+12")
addOctaveBtnStyle.Color = primaryColor
addOctaveBtnStyle.Background = transparent
addOctaveBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
subtractOctaveBtnStyle := material.Button(t.Theme, t.SubtractOctaveBtn, "-12")
subtractOctaveBtnStyle.Color = primaryColor
subtractOctaveBtnStyle.Background = transparent
subtractOctaveBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
newTrackBtnStyle := material.IconButton(t.Theme, t.NewTrackBtn, widgetForIcon(icons.ContentAdd))
newTrackBtnStyle.Background = transparent
newTrackBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
if t.song.TotalTrackVoices() < t.song.Patch.TotalVoices() {
newTrackBtnStyle.Color = primaryColor
} else {
newTrackBtnStyle.Color = disabledTextColor
}
in := layout.UniformInset(unit.Dp(1))
octave := func(gtx C) D {
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)
}
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(Label("OCT:", white)),
layout.Rigid(octave),
layout.Rigid(Label(" PITCH:", white)),
layout.Rigid(addSemitoneBtnStyle.Layout),
layout.Rigid(subtractSemitoneBtnStyle.Layout),
layout.Rigid(addOctaveBtnStyle.Layout),
layout.Rigid(subtractOctaveBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(newTrackBtnStyle.Layout))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return Surface{Gray: 37, Focus: t.EditMode == 1, FitSize: true}.Layout(gtx, menu)
}),
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
rowMarkers,
layout.Flexed(1, func(gtx C) D {
return layout.Stack{Alignment: layout.NW}.Layout(gtx,
layout.Stacked(t.layoutTracks),
layout.Stacked(t.layoutTrackTitles),
)
}))
}),
)
}
func (t *Tracker) layoutTrackTitles(gtx C) D {
defer op.Save(gtx.Ops).Load()
hexFlexChildren := make([]layout.FlexChild, len(t.song.Tracks))
for trkIndex := range t.song.Tracks {
trkIndex2 := trkIndex
hexFlexChildren[trkIndex] = layout.Rigid(func(gtx C) D {
t.TrackHexCheckBoxes[trkIndex2].Value = t.TrackShowHex[trkIndex2]
cbStyle := material.CheckBox(t.Theme, t.TrackHexCheckBoxes[trkIndex2], "hex")
dims := cbStyle.Layout(gtx)
t.TrackShowHex[trkIndex2] = t.TrackHexCheckBoxes[trkIndex2].Value
return layout.Dimensions{Size: image.Pt(trackColWidth, dims.Size.Y)}
})
}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, hexFlexChildren...)
}
func (t *Tracker) layoutTracks(gtx C) D {
defer op.Save(gtx.Ops).Load()
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
cursorSongRow := t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
if t.EditMode == EditPatterns || t.EditMode == EditTracks {
x1, y1 := t.Cursor.Track, t.Cursor.Pattern
x2, y2 := t.SelectionCorner.Track, t.SelectionCorner.Pattern
if x1 > x2 {
x1, x2 = x2, x1
}
if y1 > y2 {
y1, y2 = y2, y1
}
x2++
y2++
x1 *= trackColWidth
y1 *= trackRowHeight * t.song.RowsPerPattern
x2 *= trackColWidth
y2 *= trackRowHeight * t.song.RowsPerPattern
paint.FillShape(gtx.Ops, inactiveSelectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
}
if t.Playing {
py := trackRowHeight * (t.PlayPosition.Pattern*t.song.RowsPerPattern + t.PlayPosition.Row)
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Min: image.Pt(0, py), Max: image.Pt(gtx.Constraints.Max.X, py+trackRowHeight)}.Op())
}
if t.EditMode == EditTracks {
x1, y1 := t.Cursor.Track, t.Cursor.Pattern*t.song.RowsPerPattern+t.Cursor.Row
x2, y2 := t.SelectionCorner.Track, t.SelectionCorner.Pattern*t.song.RowsPerPattern+t.SelectionCorner.Row
if x1 > x2 {
x1, x2 = x2, x1
}
if y1 > y2 {
y1, y2 = y2, y1
}
x2++
y2++
x1 *= trackColWidth
y1 *= trackRowHeight
x2 *= trackColWidth
y2 *= trackRowHeight
paint.FillShape(gtx.Ops, selectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
cx := t.Cursor.Track * trackColWidth
cy := (t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row) * trackRowHeight
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{Min: image.Pt(cx, cy), Max: image.Pt(cx+trackColWidth, cy+trackRowHeight)}.Op())
}
delta := (gtx.Constraints.Max.Y/2 + trackRowHeight - 1) / trackRowHeight
firstRow := cursorSongRow - delta
lastRow := cursorSongRow + delta
if firstRow < 0 {
firstRow = 0
}
if l := t.song.TotalRows(); lastRow >= l {
lastRow = l - 1
}
op.Offset(f32.Pt(0, float32(trackRowHeight*firstRow))).Add(gtx.Ops)
for trkIndex, trk := range t.song.Tracks {
stack := op.Save(gtx.Ops)
for row := firstRow; row <= lastRow; row++ {
pat := row / t.song.RowsPerPattern
patRow := row % t.song.RowsPerPattern
s := trk.Sequence[pat]
if patRow == 0 {
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(s))
}
op.Offset(f32.Pt(patmarkWidth, 0)).Add(gtx.Ops)
if t.EditMode == EditTracks && t.Cursor.SongRow.Row == patRow && t.Cursor.SongRow.Pattern == pat {
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
} else {
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
}
c := trk.Patterns[s][patRow]
if t.TrackShowHex[trkIndex] {
var text string
switch c {
case 0:
text = "--"
case 1:
text = ".."
default:
text = fmt.Sprintf("%02x", c)
}
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(text))
} else {
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, valueAsNote(c))
}
op.Offset(f32.Pt(-patmarkWidth, trackRowHeight)).Add(gtx.Ops)
}
stack.Load()
op.Offset(f32.Pt(trackColWidth, 0)).Add(gtx.Ops)
}
return layout.Dimensions{Size: gtx.Constraints.Max}
} }

View File

@ -13,6 +13,15 @@ import (
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
) )
type EditMode int
const (
EditPatterns EditMode = iota
EditTracks
EditUnits
EditParameters
)
type Tracker struct { type Tracker struct {
QuitButton *widget.Clickable QuitButton *widget.Clickable
songPlayMutex sync.RWMutex // protects song and playing songPlayMutex sync.RWMutex // protects song and playing
@ -21,11 +30,13 @@ type Tracker struct {
// protects PlayPattern and PlayRow // protects PlayPattern and PlayRow
playRowPatMutex sync.RWMutex // protects song and playing playRowPatMutex sync.RWMutex // protects song and playing
PlayPosition SongRow PlayPosition SongRow
EditMode EditMode
SelectionCorner SongPoint SelectionCorner SongPoint
Cursor SongPoint Cursor SongPoint
CursorColumn int CursorColumn int
CurrentInstrument int CurrentInstrument int
CurrentUnit int CurrentUnit int
CurrentParam int
UnitGroupMenuVisible bool UnitGroupMenuVisible bool
UnitGroupMenuIndex int UnitGroupMenuIndex int
UnitSubMenuIndex int UnitSubMenuIndex int
@ -85,12 +96,6 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
defer t.songPlayMutex.Unlock() defer t.songPlayMutex.Unlock()
t.song = song t.song = song
t.ClampPositions() t.ClampPositions()
if l := len(t.song.Patch.Instruments); t.CurrentInstrument >= l {
t.CurrentInstrument = l - 1
}
if l := len(t.song.Patch.Instruments[t.CurrentInstrument].Units); t.CurrentUnit >= l {
t.CurrentUnit = l - 1
}
if t.sequencer != nil { if t.sequencer != nil {
t.sequencer.SetPatch(song.Patch) t.sequencer.SetPatch(song.Patch)
t.sequencer.SetRowLength(song.SamplesPerRow()) t.sequencer.SetRowLength(song.SamplesPerRow())
@ -98,6 +103,16 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
return nil return nil
} }
func clamp(a, min, max int) int {
if a < min {
return min
}
if a >= max {
return max - 1
}
return a
}
func (t *Tracker) Close() { func (t *Tracker) Close() {
t.audioContext.Close() t.audioContext.Close()
t.closer <- struct{}{} t.closer <- struct{}{}
@ -232,6 +247,7 @@ func (t *Tracker) AddInstrument() {
copy(instr[t.CurrentInstrument+2:], t.song.Patch.Instruments[t.CurrentInstrument+1:]) copy(instr[t.CurrentInstrument+2:], t.song.Patch.Instruments[t.CurrentInstrument+1:])
t.song.Patch.Instruments = instr t.song.Patch.Instruments = instr
t.CurrentInstrument++ t.CurrentInstrument++
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch) t.sequencer.SetPatch(t.song.Patch)
} }
@ -242,6 +258,7 @@ func (t *Tracker) SwapInstruments(i, j int) {
t.SaveUndo() t.SaveUndo()
instruments := t.song.Patch.Instruments instruments := t.song.Patch.Instruments
instruments[i], instruments[j] = instruments[j], instruments[i] instruments[i], instruments[j] = instruments[j], instruments[i]
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch) t.sequencer.SetPatch(t.song.Patch)
} }
@ -254,6 +271,7 @@ func (t *Tracker) DeleteInstrument() {
if t.CurrentInstrument >= len(t.song.Patch.Instruments) { if t.CurrentInstrument >= len(t.song.Patch.Instruments) {
t.CurrentInstrument = len(t.song.Patch.Instruments) - 1 t.CurrentInstrument = len(t.song.Patch.Instruments) - 1
} }
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch) t.sequencer.SetPatch(t.song.Patch)
} }
@ -343,6 +361,7 @@ func (t *Tracker) AddUnit() {
copy(units[t.CurrentUnit+2:], t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit+1:]) copy(units[t.CurrentUnit+2:], t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit+1:])
t.song.Patch.Instruments[t.CurrentInstrument].Units = units t.song.Patch.Instruments[t.CurrentInstrument].Units = units
t.CurrentUnit++ t.CurrentUnit++
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch) t.sequencer.SetPatch(t.song.Patch)
} }
@ -350,6 +369,7 @@ func (t *Tracker) ClearUnit() {
t.SaveUndo() t.SaveUndo()
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type = "" t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type = ""
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters = make(map[string]int) t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters = make(map[string]int)
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch) t.sequencer.SetPatch(t.song.Patch)
} }
@ -365,6 +385,30 @@ func (t *Tracker) DeleteUnit() {
if t.CurrentUnit > 0 { if t.CurrentUnit > 0 {
t.CurrentUnit-- t.CurrentUnit--
} }
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch)
}
func (t *Tracker) GetUnitParam() int {
unit := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit]
paramtype := sointu.UnitTypes[unit.Type][t.CurrentParam]
return unit.Parameters[paramtype.Name]
}
func (t *Tracker) SetUnitParam(value int) {
unit := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit]
unittype := sointu.UnitTypes[unit.Type][t.CurrentParam]
if value < unittype.MinValue {
value = unittype.MinValue
} else if value > unittype.MaxValue {
value = unittype.MaxValue
}
if unit.Parameters[unittype.Name] == value {
return
}
t.SaveUndo()
unit.Parameters[unittype.Name] = value
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch) t.sequencer.SetPatch(t.song.Patch)
} }
@ -375,6 +419,7 @@ func (t *Tracker) SwapUnits(i, j int) {
t.SaveUndo() t.SaveUndo()
units := t.song.Patch.Instruments[t.CurrentInstrument].Units units := t.song.Patch.Instruments[t.CurrentInstrument].Units
units[i], units[j] = units[j], units[i] units[i], units[j] = units[j], units[i]
t.ClampPositions()
t.sequencer.SetPatch(t.song.Patch) t.sequencer.SetPatch(t.song.Patch)
} }
@ -382,6 +427,38 @@ func (t *Tracker) ClampPositions() {
t.PlayPosition.Clamp(t.song) t.PlayPosition.Clamp(t.song)
t.Cursor.Clamp(t.song) t.Cursor.Clamp(t.song)
t.SelectionCorner.Clamp(t.song) t.SelectionCorner.Clamp(t.song)
if t.Cursor.Track >= len(t.TrackShowHex) || !t.TrackShowHex[t.Cursor.Track] {
t.CursorColumn = 0
}
t.CurrentInstrument = clamp(t.CurrentInstrument, 0, len(t.song.Patch.Instruments))
t.CurrentUnit = clamp(t.CurrentUnit, 0, len(t.song.Patch.Instruments[t.CurrentInstrument].Units))
numSettableParams := 0
for _, t := range sointu.UnitTypes[t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type] {
if t.CanSet {
numSettableParams++
}
}
if t.CurrentParam < 0 && t.CurrentUnit > 0 {
t.CurrentUnit--
numSettableParams = 0
for _, t := range sointu.UnitTypes[t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type] {
if t.CanSet {
numSettableParams++
}
}
t.CurrentParam = numSettableParams - 1
}
if t.CurrentParam >= numSettableParams && t.CurrentUnit < len(t.song.Patch.Instruments[t.CurrentInstrument].Units)-1 {
t.CurrentUnit++
numSettableParams = 0
for _, t := range sointu.UnitTypes[t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type] {
if t.CanSet {
numSettableParams++
}
}
t.CurrentParam = 0
}
t.CurrentParam = clamp(t.CurrentParam, 0, numSettableParams)
} }
func (t *Tracker) getSelectionRange() (int, int, int, int) { func (t *Tracker) getSelectionRange() (int, int, int, int) {
@ -443,6 +520,10 @@ func (t *Tracker) DeleteSelection() {
} }
} }
func (t *Tracker) Unselect() {
t.SelectionCorner = t.Cursor
}
func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker { func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker {
t := &Tracker{ t := &Tracker{
Theme: material.NewTheme(gofont.Collection()), Theme: material.NewTheme(gofont.Collection()),
@ -484,6 +565,8 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
VerticalSplit: new(Split), VerticalSplit: new(Split),
ChooseUnitTypeList: &layout.List{Axis: layout.Vertical}, ChooseUnitTypeList: &layout.List{Axis: layout.Vertical},
} }
t.UnitDragList.HoverItem = -1
t.InstrumentDragList.HoverItem = -1
t.Octave.Value = 4 t.Octave.Value = 4
t.VerticalSplit.Axis = layout.Vertical t.VerticalSplit.Axis = layout.Vertical
t.BottomHorizontalSplit.Ratio = -.5 t.BottomHorizontalSplit.Ratio = -.5

View File

@ -21,11 +21,12 @@ func (t *Tracker) layoutUnitEditor(gtx C) D {
if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type == "" { if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type == "" {
editorFunc = t.layoutUnitTypeChooser editorFunc = t.layoutUnitTypeChooser
} }
paint.FillShape(gtx.Ops, unitSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op()) return Surface{Gray: 24, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, editorFunc), layout.Flexed(1, editorFunc),
layout.Rigid(t.layoutUnitFooter()), layout.Rigid(t.layoutUnitFooter()))
) })
} }
func (t *Tracker) layoutUnitSliders(gtx C) D { func (t *Tracker) layoutUnitSliders(gtx C) D {
@ -41,12 +42,6 @@ func (t *Tracker) layoutUnitSliders(gtx C) D {
t.ParameterSliders = append(t.ParameterSliders, new(widget.Float)) t.ParameterSliders = append(t.ParameterSliders, new(widget.Float))
} }
params := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters params := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters
for t.ParameterSliders[index].Changed() {
params[ut[index].Name] = int(t.ParameterSliders[index].Value)
// TODO: tracker should have functions to update parameters and
// to do this efficiently i.e. not compile the whole patch again
t.LoadSong(t.song)
}
t.ParameterSliders[index].Value = float32(params[ut[index].Name]) t.ParameterSliders[index].Value = float32(params[ut[index].Name])
sliderStyle := material.Slider(t.Theme, t.ParameterSliders[index], float32(ut[index].MinValue), float32(ut[index].MaxValue)) sliderStyle := material.Slider(t.Theme, t.ParameterSliders[index], float32(ut[index].MinValue), float32(ut[index].MaxValue))
sliderStyle.Color = t.Theme.Fg sliderStyle.Color = t.Theme.Fg
@ -65,7 +60,19 @@ func (t *Tracker) layoutUnitSliders(gtx C) D {
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Px(unit.Dp(200)) gtx.Constraints.Min.X = gtx.Px(unit.Dp(200))
return sliderStyle.Layout(gtx) gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
if t.EditMode == EditParameters && t.CurrentParam == index {
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
}
dims := sliderStyle.Layout(gtx)
for sliderStyle.Float.Changed() {
t.EditMode = EditParameters
t.CurrentParam = index
t.SetUnitParam(int(t.ParameterSliders[index].Value))
}
return dims
}), }),
layout.Rigid(Label(valueText, white)), layout.Rigid(Label(valueText, white)),
) )
@ -108,7 +115,6 @@ func (t *Tracker) layoutUnitFooter() layout.Widget {
} }
func (t *Tracker) layoutUnitTypeChooser(gtx C) D { func (t *Tracker) layoutUnitTypeChooser(gtx C) D {
paint.FillShape(gtx.Ops, unitSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op())
listElem := func(gtx C, i int) D { listElem := func(gtx C, i int) D {
for t.ChooseUnitTypeBtns[i].Clicked() { for t.ChooseUnitTypeBtns[i].Clicked() {
t.SetUnit(allUnits[i]) t.SetUnit(allUnits[i])