diff --git a/compiler/encoded_patch.go b/compiler/encoded_patch.go index a7a8d31..05e0ea6 100644 --- a/compiler/encoded_patch.go +++ b/compiler/encoded_patch.go @@ -38,6 +38,10 @@ func Encode(patch *sointu.Patch, featureSet FeatureSet) (*EncodedPatch, error) { } 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"])} + if s.LoopLength == 0 { + // hacky quick fix: looplength 0 causes div by zero so avoid crashing + s.LoopLength = 1 + } index, ok := sampleOffsetMap[s] if !ok { index = len(c.SampleOffsets) diff --git a/sointu.go b/sointu.go index 5caa589..c167c7d 100644 --- a/sointu.go +++ b/sointu.go @@ -255,7 +255,7 @@ var UnitTypes = map[string]([]UnitParameter){ {Name: "unison", MinValue: 0, MaxValue: 3, 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: "looplength", MinValue: 1, MaxValue: 65535, CanSet: true, CanModulate: false}}, + {Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}}, "loadval": []UnitParameter{ {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, diff --git a/tracker/draglist.go b/tracker/draglist.go index d93f5d4..31616a0 100644 --- a/tracker/draglist.go +++ b/tracker/draglist.go @@ -24,7 +24,6 @@ type DragList struct { type FilledDragListStyle struct { dragList *DragList - SurfaceColor color.NRGBA HoverColor color.NRGBA SelectedColor color.NRGBA Count int @@ -38,7 +37,6 @@ func FilledDragList(th *material.Theme, dragList *DragList, count int, element f element: element, swap: swap, Count: count, - SurfaceColor: dragListSurfaceColor, HoverColor: dragListHoverColor, SelectedColor: dragListSelectedColor, } @@ -47,7 +45,6 @@ func FilledDragList(th *material.Theme, dragList *DragList, count int, element f func (s *FilledDragListStyle) Layout(gtx C) D { 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() if s.dragList.List.Axis == layout.Horizontal { diff --git a/tracker/instruments.go b/tracker/instruments.go index 8e7b3c0..aa4c39f 100644 --- a/tracker/instruments.go +++ b/tracker/instruments.go @@ -6,8 +6,6 @@ import ( "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/op/paint" "gioui.org/text" "gioui.org/unit" "gioui.org/widget" @@ -50,12 +48,6 @@ func (t *Tracker) layoutInstruments(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 { deleteInstrumentBtnStyle := material.IconButton(t.Theme, t.DeleteInstrumentBtn, widgetForIcon(icons.ActionDelete)) deleteInstrumentBtnStyle.Background = transparent @@ -87,9 +79,7 @@ func (t *Tracker) layoutInstrumentHeader(gtx C) D { for t.DeleteInstrumentBtn.Clicked() { t.DeleteInstrument() } - return layout.Stack{Alignment: layout.Center}.Layout(gtx, - layout.Expanded(headerBg), - layout.Stacked(header)) + return Surface{Gray: 37, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, header) } 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.SelectedColor = instrumentSurfaceColor + instrumentList.SelectedColor = color instrumentList.HoverColor = instrumentHoverColor - instrumentList.SurfaceColor = transparent t.InstrumentDragList.SelectedItem = t.CurrentInstrument 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) + if t.EditMode == EditUnits { + unitList.SelectedColor = cursorColor + } + t.UnitDragList.SelectedItem = t.CurrentUnit - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Stack{Alignment: layout.SE}.Layout(gtx, - layout.Expanded(func(gtx C) D { - dims := unitList.Layout(gtx) - if t.CurrentUnit != t.UnitDragList.SelectedItem { - t.CurrentUnit = t.UnitDragList.SelectedItem - op.InvalidateOp{}.Add(gtx.Ops) - } - return dims - }), - layout.Stacked(func(gtx C) D { - return margin.Layout(gtx, addUnitBtnStyle.Layout) - })) - }), - layout.Rigid(t.layoutUnitEditor)) + return Surface{Gray: 30, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return layout.Stack{Alignment: layout.SE}.Layout(gtx, + layout.Expanded(func(gtx C) D { + dims := unitList.Layout(gtx) + if t.CurrentUnit != t.UnitDragList.SelectedItem { + t.CurrentUnit = t.UnitDragList.SelectedItem + t.EditMode = EditUnits + op.InvalidateOp{}.Add(gtx.Ops) + } + return dims + }), + layout.Stacked(func(gtx C) D { + return margin.Layout(gtx, addUnitBtnStyle.Layout) + })) + }), + layout.Rigid(t.layoutUnitEditor)) + }) } diff --git a/tracker/keyevent.go b/tracker/keyevent.go index 35fa0c6..6770042 100644 --- a/tracker/keyevent.go +++ b/tracker/keyevent.go @@ -1,8 +1,8 @@ package tracker import ( - "os" "strconv" + "strings" "gioui.org/io/key" ) @@ -42,6 +42,38 @@ var noteMap = map[string]int{ "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. func (t *Tracker) KeyEvent(e key.Event) bool { if e.State == key.Press { @@ -59,14 +91,15 @@ func (t *Tracker) KeyEvent(e key.Event) bool { t.Redo() return true } - case "A": - t.SetCurrentNote(0) - return true case key.NameDeleteForward: - t.DeleteSelection() - return true - case key.NameEscape: - os.Exit(0) + switch t.EditMode { + case EditTracks: + t.DeleteSelection() + return true + case EditUnits: + t.DeleteUnit() + return true + } case "Space": t.TogglePlay() return true @@ -75,75 +108,161 @@ func (t *Tracker) KeyEvent(e key.Event) bool { 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: - delta := -1 - if e.Modifiers.Contain(key.ModCtrl) { - delta = -t.song.RowsPerPattern + switch t.EditMode { + case EditPatterns: + 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.Cursor.Clamp(t.song) + t.ClampPositions() if !e.Modifiers.Contain(key.ModShift) { - t.SelectionCorner = t.Cursor + t.Unselect() } - t.NoteTracking = false return true case key.NameDownArrow: - delta := 1 - if e.Modifiers.Contain(key.ModCtrl) { - delta = t.song.RowsPerPattern + switch t.EditMode { + case EditPatterns: + 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.Cursor.Clamp(t.song) + t.ClampPositions() if !e.Modifiers.Contain(key.ModShift) { - t.SelectionCorner = t.Cursor + t.Unselect() } - t.NoteTracking = false return true case key.NameLeftArrow: - 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] { + switch t.EditMode { + case EditPatterns: + if e.Modifiers.Contain(key.ModCtrl) { + 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 } else { - t.CursorColumn = 0 + t.CursorColumn-- } - if !e.Modifiers.Contain(key.ModShift) { - t.SelectionCorner = t.Cursor + case EditUnits: + 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 case key.NameRightArrow: - 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 + switch t.EditMode { + case EditPatterns: + if e.Modifiers.Contain(key.ModCtrl) { + t.Cursor.Track = len(t.song.Tracks) - 1 + } else { + t.Cursor.Track++ } - t.CursorColumn = 0 - } else { - t.CursorColumn++ + case EditTracks: + if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) { + 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 } - if e.Modifiers.Contain(key.ModCtrl) { - if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { + switch t.EditMode { + case EditPatterns: + if iv, err := strconv.Atoi(e.Name); err == nil { t.SetCurrentPattern(byte(iv)) return true } - } else { - if !t.TrackShowHex[t.Cursor.Track] { - if val, ok := noteMap[e.Name]; ok { - t.NotePressed(val) - return true - } - } else { + if b := byte(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 { + t.SetCurrentPattern(b + 10) + return true + } + case EditTracks: + if t.TrackShowHex[t.Cursor.Track] { if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { t.NumberPressed(byte(iv)) 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 } } } diff --git a/tracker/layout.go b/tracker/layout.go index 619159a..50a7a5f 100644 --- a/tracker/layout.go +++ b/tracker/layout.go @@ -2,16 +2,13 @@ package tracker import ( "image" - "image/color" "gioui.org/layout" - "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" - "golang.org/x/exp/shiny/materialdesign/icons" ) 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) { 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.layoutControls, - t.layoutTracksAndPatterns) + t.layoutTop, + t.layoutBottom) 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, - t.layoutPatterns, - t.layoutTracks, + func(gtx C) D { + 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 { - 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 { +func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions { for t.NewInstrumentBtn.Clicked() { 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} - } -} diff --git a/tracker/patterns.go b/tracker/patterns.go index 76fecfc..dde8f26 100644 --- a/tracker/patterns.go +++ b/tracker/patterns.go @@ -20,7 +20,6 @@ const patternRowMarkerWidth = 30 func (t *Tracker) layoutPatterns(gtx C) D { defer op.Save(gtx.Ops).Load() 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}, @@ -35,14 +34,19 @@ func (t *Tracker) layoutPatterns(gtx C) D { 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])) + widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(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 + if t.EditMode == EditPatterns || t.EditMode == EditTracks { + if patternRect.Contains(point) { + color := inactiveSelectionColor + 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) } diff --git a/tracker/rowmarkers.go b/tracker/rowmarkers.go index 401eb43..fcb5a3d 100644 --- a/tracker/rowmarkers.go +++ b/tracker/rowmarkers.go @@ -24,7 +24,7 @@ func (t *Tracker) layoutRowMarkers(patternRows, sequenceLength, cursorRow, curso }.Op()) 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/2)-trackRowHeight)).Add(gtx.Ops) + op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops) cursorSongRow := cursorPattern*patternRows + cursorRow playSongRow := playPattern*patternRows + playRow 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++ { songRow := i*patternRows + j 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 { paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) 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) } else { paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops) diff --git a/tracker/surface.go b/tracker/surface.go new file mode 100644 index 0000000..582170f --- /dev/null +++ b/tracker/surface.go @@ -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) +} diff --git a/tracker/theme.go b/tracker/theme.go index 1a77fda..8586c44 100644 --- a/tracker/theme.go +++ b/tracker/theme.go @@ -56,6 +56,7 @@ var activeTrackColor = focusedContainerColor 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 patternSurfaceActiveColor = color.NRGBA{R: 30, G: 30, B: 30, A: 255} var rowMarkerSurfaceColor = color.NRGBA{R: 0, G: 0, B: 0, A: 0} 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 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} diff --git a/tracker/track.go b/tracker/track.go index ee2f01a..b5c2c2d 100644 --- a/tracker/track.go +++ b/tracker/track.go @@ -10,86 +10,238 @@ import ( "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" + "gioui.org/unit" "gioui.org/widget" + "gioui.org/widget/material" + "golang.org/x/exp/shiny/materialdesign/icons" ) const trackRowHeight = 16 -const trackWidth = 54 +const trackColWidth = 54 const patmarkWidth = 16 -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 - 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/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 := 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} +func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { + 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, + )) + + 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} } diff --git a/tracker/tracker.go b/tracker/tracker.go index d55d2ed..30292e3 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -13,6 +13,15 @@ import ( "github.com/vsariola/sointu" ) +type EditMode int + +const ( + EditPatterns EditMode = iota + EditTracks + EditUnits + EditParameters +) + type Tracker struct { QuitButton *widget.Clickable songPlayMutex sync.RWMutex // protects song and playing @@ -21,11 +30,13 @@ type Tracker struct { // protects PlayPattern and PlayRow playRowPatMutex sync.RWMutex // protects song and playing PlayPosition SongRow + EditMode EditMode SelectionCorner SongPoint Cursor SongPoint CursorColumn int CurrentInstrument int CurrentUnit int + CurrentParam int UnitGroupMenuVisible bool UnitGroupMenuIndex int UnitSubMenuIndex int @@ -85,12 +96,6 @@ func (t *Tracker) LoadSong(song sointu.Song) error { defer t.songPlayMutex.Unlock() t.song = song 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 { t.sequencer.SetPatch(song.Patch) t.sequencer.SetRowLength(song.SamplesPerRow()) @@ -98,6 +103,16 @@ func (t *Tracker) LoadSong(song sointu.Song) error { 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() { t.audioContext.Close() t.closer <- struct{}{} @@ -232,6 +247,7 @@ func (t *Tracker) AddInstrument() { copy(instr[t.CurrentInstrument+2:], t.song.Patch.Instruments[t.CurrentInstrument+1:]) t.song.Patch.Instruments = instr t.CurrentInstrument++ + t.ClampPositions() t.sequencer.SetPatch(t.song.Patch) } @@ -242,6 +258,7 @@ func (t *Tracker) SwapInstruments(i, j int) { t.SaveUndo() instruments := t.song.Patch.Instruments instruments[i], instruments[j] = instruments[j], instruments[i] + t.ClampPositions() t.sequencer.SetPatch(t.song.Patch) } @@ -254,6 +271,7 @@ func (t *Tracker) DeleteInstrument() { if t.CurrentInstrument >= len(t.song.Patch.Instruments) { t.CurrentInstrument = len(t.song.Patch.Instruments) - 1 } + t.ClampPositions() 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:]) t.song.Patch.Instruments[t.CurrentInstrument].Units = units t.CurrentUnit++ + t.ClampPositions() t.sequencer.SetPatch(t.song.Patch) } @@ -350,6 +369,7 @@ func (t *Tracker) ClearUnit() { t.SaveUndo() 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.ClampPositions() t.sequencer.SetPatch(t.song.Patch) } @@ -365,6 +385,30 @@ func (t *Tracker) DeleteUnit() { if t.CurrentUnit > 0 { 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) } @@ -375,6 +419,7 @@ func (t *Tracker) SwapUnits(i, j int) { t.SaveUndo() units := t.song.Patch.Instruments[t.CurrentInstrument].Units units[i], units[j] = units[j], units[i] + t.ClampPositions() t.sequencer.SetPatch(t.song.Patch) } @@ -382,6 +427,38 @@ func (t *Tracker) ClampPositions() { t.PlayPosition.Clamp(t.song) t.Cursor.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) { @@ -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 { t := &Tracker{ Theme: material.NewTheme(gofont.Collection()), @@ -484,6 +565,8 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr VerticalSplit: new(Split), ChooseUnitTypeList: &layout.List{Axis: layout.Vertical}, } + t.UnitDragList.HoverItem = -1 + t.InstrumentDragList.HoverItem = -1 t.Octave.Value = 4 t.VerticalSplit.Axis = layout.Vertical t.BottomHorizontalSplit.Ratio = -.5 diff --git a/tracker/uniteditor.go b/tracker/uniteditor.go index 674289c..6482620 100644 --- a/tracker/uniteditor.go +++ b/tracker/uniteditor.go @@ -21,11 +21,12 @@ func (t *Tracker) layoutUnitEditor(gtx C) D { if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type == "" { editorFunc = t.layoutUnitTypeChooser } - paint.FillShape(gtx.Ops, unitSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op()) - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Flexed(1, editorFunc), - layout.Rigid(t.layoutUnitFooter()), - ) + 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, + layout.Flexed(1, editorFunc), + layout.Rigid(t.layoutUnitFooter())) + }) + } 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)) } 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]) sliderStyle := material.Slider(t.Theme, t.ParameterSliders[index], float32(ut[index].MinValue), float32(ut[index].MaxValue)) sliderStyle.Color = t.Theme.Fg @@ -65,7 +60,19 @@ func (t *Tracker) layoutUnitSliders(gtx C) D { }), layout.Rigid(func(gtx C) D { 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)), ) @@ -108,7 +115,6 @@ func (t *Tracker) layoutUnitFooter() layout.Widget { } 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 { for t.ChooseUnitTypeBtns[i].Clicked() { t.SetUnit(allUnits[i])