diff --git a/compiler/encoded_patch.go b/compiler/encoded_patch.go index 9920eea..6938fe5 100644 --- a/compiler/encoded_patch.go +++ b/compiler/encoded_patch.go @@ -33,6 +33,9 @@ func Encode(patch *sointu.Patch, featureSet FeatureSet) (*EncodedPatch, error) { return nil, errors.New("Each instrument must have at least 1 voice") } for _, unit := range instr.Units { + if unit.Type == "" { // empty units are just ignored & skipped + continue + } if unit.Type == "oscillator" && unit.Parameters["type"] == 4 { s := SampleOffset{Start: uint32(unit.Parameters["start"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])} index, ok := sampleOffsetMap[s] diff --git a/tracker/defaultsong.go b/tracker/defaultsong.go index 04cc936..6316461 100644 --- a/tracker/defaultsong.go +++ b/tracker/defaultsong.go @@ -1,14 +1,65 @@ package tracker -import "github.com/vsariola/sointu" +import ( + "sort" + + "github.com/vsariola/sointu" +) + +var defaultUnits = map[string]sointu.Unit{ + "envelope": {Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 64, "gain": 64}}, + "oscillator": {Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 64, "shape": 64, "gain": 64, "type": sointu.Sine}}, + "noise": {Type: "noise", Parameters: map[string]int{"stereo": 0, "shape": 64, "gain": 64}}, + "mulp": {Type: "mulp", Parameters: map[string]int{"stereo": 0}}, + "mul": {Type: "mul", Parameters: map[string]int{"stereo": 0}}, + "add": {Type: "add", Parameters: map[string]int{"stereo": 0}}, + "addp": {Type: "addp", Parameters: map[string]int{"stereo": 0}}, + "push": {Type: "push", Parameters: map[string]int{"stereo": 0}}, + "pop": {Type: "pop", Parameters: map[string]int{"stereo": 0}}, + "xch": {Type: "xch", Parameters: map[string]int{"stereo": 0}}, + "receive": {Type: "receive", Parameters: map[string]int{"stereo": 0}}, + "loadnote": {Type: "loadnote", Parameters: map[string]int{"stereo": 0}}, + "loadval": {Type: "loadval", Parameters: map[string]int{"stereo": 0, "value": 64}}, + "pan": {Type: "pan", Parameters: map[string]int{"stereo": 0, "panning": 64}}, + "gain": {Type: "gain", Parameters: map[string]int{"stereo": 0, "gain": 64}}, + "invgain": {Type: "invgain", Parameters: map[string]int{"stereo": 0, "invgain": 64}}, + "crush": {Type: "crush", Parameters: map[string]int{"stereo": 0, "resolution": 64}}, + "clip": {Type: "clip", Parameters: map[string]int{"stereo": 0}}, + "hold": {Type: "hold", Parameters: map[string]int{"stereo": 0, "holdfreq": 64}}, + "distort": {Type: "distort", Parameters: map[string]int{"stereo": 0, "drive": 64}}, + "filter": {Type: "filter", Parameters: map[string]int{"stereo": 0, "frequency": 64, "resonance": 64, "lowpass": 1, "bandpass": 0, "highpass": 0, "negbandpass": 0, "neghighpass": 0}}, + "out": {Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 64}}, + "outaux": {Type: "outaux", Parameters: map[string]int{"stereo": 1, "outgain": 64, "auxgain": 64}}, + "aux": {Type: "aux", Parameters: map[string]int{"stereo": 1, "gain": 64, "channel": 2}}, + "delay": {Type: "delay", + Parameters: map[string]int{"damp": 0, "dry": 128, "feedback": 96, "notetracking": 0, "pregain": 40, "stereo": 0}, + VarArgs: []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618, + 1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642, + }}, + "in": {Type: "in", Parameters: map[string]int{"stereo": 1, "channel": 2}}, + "speed": {Type: "speed", Parameters: map[string]int{}}, + "compressor": {Type: "compressor", Parameters: map[string]int{"stereo": 0, "attack": 64, "release": 64, "invgain": 64, "threshold": 64, "ratio": 64}}, +} + +var allUnits []string + +func init() { + allUnits = make([]string, 0, len(sointu.UnitTypes)) + for k := range sointu.UnitTypes { + allUnits = append(allUnits, k) + } + sort.Strings(allUnits) +} var defaultInstrument = sointu.Instrument{ NumVoices: 1, Units: []sointu.Unit{ - {Type: "envelope", Parameters: map[string]int{"stereo": 1, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 64}}, - {Type: "oscillator", Parameters: map[string]int{"stereo": 1, "transpose": 64, "detune": 64, "phase": 0, "color": 128, "shape": 64, "gain": 64, "type": sointu.Sine}}, - {Type: "mulp", Parameters: map[string]int{"stereo": 1}}, - {Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 64}}, + defaultUnits["envelope"], + defaultUnits["oscillator"], + defaultUnits["mulp"], + defaultUnits["delay"], + defaultUnits["pan"], + defaultUnits["out"], }, } @@ -20,16 +71,5 @@ var defaultSong = sointu.Song{ {NumVoices: 2, Sequence: []byte{0, 0, 0, 1}, Patterns: [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}, {64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 75, 0, 75, 0, 80, 0}}}, {NumVoices: 2, Sequence: []byte{0, 0, 0, 1}, Patterns: [][]byte{{0, 0, 64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0}, {32, 0, 64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 68, 0, 68, 0}}}, }, - Patch: sointu.Patch{ - Instruments: []sointu.Instrument{{NumVoices: 4, Units: []sointu.Unit{ - {Type: "envelope", Parameters: map[string]int{"stereo": 1, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 64}}, - {Type: "oscillator", Parameters: map[string]int{"stereo": 1, "transpose": 64, "detune": 64, "phase": 0, "color": 128, "shape": 64, "gain": 64, "type": sointu.Sine}}, - {Type: "mulp", Parameters: map[string]int{"stereo": 1}}, - {Type: "delay", - Parameters: map[string]int{"damp": 0, "dry": 128, "feedback": 96, "notetracking": 0, "pregain": 40, "stereo": 1}, - VarArgs: []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618, - 1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642, - }}, - {Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 64}}, - }}}}, + Patch: sointu.Patch{Instruments: []sointu.Instrument{{NumVoices: 4, Units: defaultInstrument.Units}}}, } diff --git a/tracker/instruments.go b/tracker/instruments.go index 61eee1a..ab8ae9f 100644 --- a/tracker/instruments.go +++ b/tracker/instruments.go @@ -2,7 +2,8 @@ package tracker import ( "fmt" - "sort" + "image" + "image/color" "gioui.org/layout" "gioui.org/op" @@ -52,7 +53,7 @@ func (t *Tracker) layoutInstruments() layout.Widget { func (t *Tracker) layoutInstrumentHeader() layout.Widget { headerBg := func(gtx C) D { - paint.FillShape(gtx.Ops, trackMenuSurfaceColor, clip.Rect{ + paint.FillShape(gtx.Ops, instrumentSurfaceColor, clip.Rect{ Max: gtx.Constraints.Min, }.Op()) return layout.Dimensions{Size: gtx.Constraints.Min} @@ -129,82 +130,65 @@ func (t *Tracker) layoutInstrumentNames() layout.Widget { } } func (t *Tracker) layoutInstrumentEditor() layout.Widget { + for t.AddUnitBtn.Clicked() { + t.AddUnit() + } + addUnitBtnStyle := material.IconButton(t.Theme, t.AddUnitBtn, widgetForIcon(icons.ContentAdd)) + addUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(4)) + margin := layout.UniformInset(unit.Dp(2)) + return func(gtx C) D { - paint.FillShape(gtx.Ops, instrumentSurfaceColor, clip.Rect{ - Max: gtx.Constraints.Max, - }.Op()) return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(t.layoutUnitList()), - layout.Rigid(t.layoutUnitControls())) + layout.Rigid(func(gtx C) D { + return layout.Stack{Alignment: layout.SE}.Layout(gtx, + layout.Expanded(t.layoutUnitList()), + layout.Stacked(func(gtx C) D { + return margin.Layout(gtx, addUnitBtnStyle.Layout) + })) + }), + layout.Rigid(t.layoutUnitEditor())) } } func (t *Tracker) layoutUnitList() layout.Widget { return func(gtx C) D { + paint.FillShape(gtx.Ops, unitListSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op()) + defer op.Save(gtx.Ops).Load() + + gtx.Constraints.Min.Y = gtx.Constraints.Max.Y units := t.song.Patch.Instruments[t.CurrentInstrument].Units count := len(units) - if len(t.UnitBtns) < count { - tail := make([]*widget.Clickable, count-len(t.UnitBtns)) - for t := range tail { - tail[t] = new(widget.Clickable) - } - t.UnitBtns = append(t.UnitBtns, tail...) + for len(t.UnitBtns) < count { + t.UnitBtns = append(t.UnitBtns, new(widget.Clickable)) } - children := make([]layout.FlexChild, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)) - for i, u := range t.song.Patch.Instruments[t.CurrentInstrument].Units { + + listElem := func(gtx C, i int) D { for t.UnitBtns[i].Clicked() { t.CurrentUnit = i + op.InvalidateOp{}.Add(gtx.Ops) } - i2 := i + u := t.song.Patch.Instruments[t.CurrentInstrument].Units[i] labelStyle := LabelStyle{Text: u.Type, ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)} - children[i] = layout.Rigid(func(gtx C) D { - dims := labelStyle.Layout(gtx) - gtx.Constraints = layout.Exact(dims.Size) - t.UnitBtns[i2].Layout(gtx) - return dims - }) + if labelStyle.Text == "" { + labelStyle.Text = "---" + labelStyle.Alignment = layout.Center + } + bg := func(gtx C) D { + gtx.Constraints = layout.Exact(image.Pt(120, 20)) + var color color.NRGBA + if t.CurrentUnit == i { + color = unitListSelectedColor + } else if t.UnitBtns[i].Hovered() { + color = unitListHighlightColor + } + paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) + return D{Size: gtx.Constraints.Min} + } + return layout.Stack{Alignment: layout.W}.Layout(gtx, + layout.Stacked(bg), + layout.Expanded(labelStyle.Layout), + layout.Expanded(t.UnitBtns[i].Layout)) } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) - } -} - -func (t *Tracker) layoutUnitControls() layout.Widget { - return func(gtx C) D { - params := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters - count := len(params) - children := make([]layout.FlexChild, 0, count) - if len(t.ParameterSliders) < count { - tail := make([]*widget.Float, count-len(t.ParameterSliders)) - for t := range tail { - tail[t] = new(widget.Float) - } - t.ParameterSliders = append(t.ParameterSliders, tail...) - } - keys := make([]string, 0, len(params)) - for k := range params { - keys = append(keys, k) - } - sort.Strings(keys) - for i, k := range keys { - for t.ParameterSliders[i].Changed() { - params[k] = int(t.ParameterSliders[i].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[i].Value = float32(params[k]) - sliderStyle := material.Slider(t.Theme, t.ParameterSliders[i], 0, 128) - sliderStyle.Color = t.Theme.Fg - k2 := k // avoid k changing in the closure - children = append(children, layout.Rigid(func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(Label(k2, white)), - layout.Rigid(func(gtx C) D { - gtx.Constraints.Min.X = 200 - return sliderStyle.Layout(gtx) - })) - })) - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) + return t.UnitList.Layout(gtx, len(t.song.Patch.Instruments[t.CurrentInstrument].Units), listElem) } } diff --git a/tracker/label.go b/tracker/label.go index 1010574..4b2ef6f 100644 --- a/tracker/label.go +++ b/tracker/label.go @@ -17,12 +17,13 @@ type LabelStyle struct { Text string Color color.NRGBA ShadeColor color.NRGBA + Alignment layout.Direction Font text.Font FontSize unit.Value } func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { - return layout.Stack{Alignment: layout.Center}.Layout(gtx, + return layout.Stack{Alignment: l.Alignment}.Layout(gtx, layout.Stacked(func(gtx layout.Context) layout.Dimensions { defer op.Save(gtx.Ops).Load() paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops) @@ -46,6 +47,6 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { ) } -func Label(text string, color color.NRGBA) layout.Widget { - return LabelStyle{Text: text, Color: color, ShadeColor: black, Font: labelDefaultFont, FontSize: labelDefaultFontSize}.Layout +func Label(str string, color color.NRGBA) layout.Widget { + return LabelStyle{Text: str, Color: color, ShadeColor: black, Font: labelDefaultFont, FontSize: labelDefaultFontSize, Alignment: layout.W}.Layout } diff --git a/tracker/popup.go b/tracker/popup.go index 286e2c8..8b5426c 100644 --- a/tracker/popup.go +++ b/tracker/popup.go @@ -56,6 +56,7 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D { } bg := func(gtx C) D { + pointer.PassOp{Pass: true}.Add(gtx.Ops) pointer.InputOp{Tag: s.Visible, Types: pointer.Press, }.Add(gtx.Ops) diff --git a/tracker/theme.go b/tracker/theme.go index f453e16..f0a9283 100644 --- a/tracker/theme.go +++ b/tracker/theme.go @@ -85,9 +85,16 @@ var patternSelectionColor = color.NRGBA{R: 19, G: 40, B: 60, A: 128} var inactiveBtnColor = color.NRGBA{R: 61, G: 55, B: 55, A: 255} -var instrumentSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, A: 255} +var instrumentSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255} var songSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, A: 255} -var popupSurfaceColor = color.NRGBA{R: 45, G: 45, B: 46, A: 255} +var popupSurfaceColor = color.NRGBA{R: 50, G: 50, B: 51, A: 255} var popupShadowColor = color.NRGBA{R: 0, G: 0, B: 0, A: 192} + +var unitListSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, A: 255} +var unitListSelectedColor = color.NRGBA{R: 55, G: 55, B: 61, A: 255} +var unitListHighlightColor = 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} diff --git a/tracker/tracker.go b/tracker/tracker.go index 8f6ec74..2cdefe3 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -25,6 +25,9 @@ type Tracker struct { CursorColumn int CurrentInstrument int CurrentUnit int + UnitGroupMenuVisible bool + UnitGroupMenuIndex int + UnitSubMenuIndex int NoteTracking bool Theme *material.Theme Octave *NumberInput @@ -47,7 +50,13 @@ type Tracker struct { FileMenuVisible bool ParameterSliders []*widget.Float UnitBtns []*widget.Clickable + UnitList *layout.List + DeleteUnitBtn *widget.Clickable + ClearUnitBtn *widget.Clickable + ChooseUnitTypeList *layout.List + ChooseUnitTypeBtns []*widget.Clickable InstrumentBtns []*widget.Clickable + AddUnitBtn *widget.Clickable InstrumentList *layout.List TrackHexCheckBoxes []*widget.Bool TrackShowHex []bool @@ -329,6 +338,51 @@ func (t *Tracker) SetRowsPerPattern(value int) { } } +func (t *Tracker) SetUnit(typ string) { + unit, ok := defaultUnits[typ] + if !ok { + return + } + if unit.Type == t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type { + return + } + t.SaveUndo() + t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit] = unit.Copy() + t.sequencer.SetPatch(t.song.Patch) +} + +func (t *Tracker) AddUnit() { + t.SaveUndo() + units := make([]sointu.Unit, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)+1) + copy(units, 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.CurrentUnit++ + t.sequencer.SetPatch(t.song.Patch) +} + +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.sequencer.SetPatch(t.song.Patch) +} + +func (t *Tracker) DeleteUnit() { + if len(t.song.Patch.Instruments[t.CurrentInstrument].Units) <= 1 { + return + } + t.SaveUndo() + units := make([]sointu.Unit, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)-1) + copy(units, t.song.Patch.Instruments[t.CurrentInstrument].Units[:t.CurrentUnit]) + copy(units[t.CurrentUnit:], t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit+1:]) + t.song.Patch.Instruments[t.CurrentInstrument].Units = units + if t.CurrentUnit > 0 { + t.CurrentUnit-- + } + t.sequencer.SetPatch(t.song.Patch) +} + func (t *Tracker) ClampPositions() { t.PlayPosition.Clamp(t.song) t.Cursor.Clamp(t.song) @@ -416,6 +470,10 @@ func New(audioContext sointu.AudioContext) *Tracker { SubtractSemitoneBtn: new(widget.Clickable), AddOctaveBtn: new(widget.Clickable), SubtractOctaveBtn: new(widget.Clickable), + AddUnitBtn: new(widget.Clickable), + DeleteUnitBtn: new(widget.Clickable), + ClearUnitBtn: new(widget.Clickable), + UnitList: &layout.List{Axis: layout.Vertical}, setPlaying: make(chan bool), rowJump: make(chan int), patternJump: make(chan int), @@ -427,12 +485,16 @@ func New(audioContext sointu.AudioContext) *Tracker { TopHorizontalSplit: new(Split), BottomHorizontalSplit: new(Split), VerticalSplit: new(Split), + ChooseUnitTypeList: &layout.List{Axis: layout.Vertical}, } t.Octave.Value = 4 t.VerticalSplit.Axis = layout.Vertical t.BottomHorizontalSplit.Ratio = -.5 t.Theme.Palette.Fg = primaryColor t.Theme.Palette.ContrastFg = black + for range allUnits { + t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable)) + } go t.sequencerLoop(t.closer) if err := t.LoadSong(defaultSong.Copy()); err != nil { panic(fmt.Errorf("cannot load default song: %w", err)) diff --git a/tracker/uniteditor.go b/tracker/uniteditor.go new file mode 100644 index 0000000..6f8b600 --- /dev/null +++ b/tracker/uniteditor.go @@ -0,0 +1,132 @@ +package tracker + +import ( + "image" + "image/color" + "sort" + + "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 (t *Tracker) layoutUnitEditor() layout.Widget { + editorFunc := t.layoutUnitSliders + if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type == "" { + editorFunc = t.layoutUnitTypeChooser + } + return func(gtx C) D { + 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()), + ) + } +} + +func (t *Tracker) layoutUnitSliders() layout.Widget { + return func(gtx C) D { + params := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters + count := len(params) + children := make([]layout.FlexChild, 0, count) + if len(t.ParameterSliders) < count { + tail := make([]*widget.Float, count-len(t.ParameterSliders)) + for t := range tail { + tail[t] = new(widget.Float) + } + t.ParameterSliders = append(t.ParameterSliders, tail...) + } + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + for t.ParameterSliders[i].Changed() { + params[k] = int(t.ParameterSliders[i].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[i].Value = float32(params[k]) + sliderStyle := material.Slider(t.Theme, t.ParameterSliders[i], 0, 128) + sliderStyle.Color = t.Theme.Fg + k2 := k // avoid k changing in the closure + children = append(children, layout.Rigid(func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(Label(k2, white)), + layout.Rigid(func(gtx C) D { + gtx.Constraints.Min.X = 200 + return sliderStyle.Layout(gtx) + })) + })) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) + } +} + +func (t *Tracker) layoutUnitFooter() layout.Widget { + return func(gtx C) D { + for t.ClearUnitBtn.Clicked() { + t.ClearUnit() + op.InvalidateOp{}.Add(gtx.Ops) + } + for t.DeleteUnitBtn.Clicked() { + t.DeleteUnit() + op.InvalidateOp{}.Add(gtx.Ops) + } + deleteUnitBtnStyle := material.IconButton(t.Theme, t.DeleteUnitBtn, widgetForIcon(icons.ActionDelete)) + deleteUnitBtnStyle.Background = transparent + deleteUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6)) + if len(t.song.Patch.Instruments[t.CurrentInstrument].Units) > 1 { + deleteUnitBtnStyle.Color = primaryColor + } else { + deleteUnitBtnStyle.Color = disabledTextColor + } + if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type == "" { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), + layout.Rigid(deleteUnitBtnStyle.Layout)) + } + clearUnitBtnStyle := material.IconButton(t.Theme, t.ClearUnitBtn, widgetForIcon(icons.ContentClear)) + clearUnitBtnStyle.Color = primaryColor + clearUnitBtnStyle.Background = transparent + clearUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6)) + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), + layout.Rigid(clearUnitBtnStyle.Layout), + layout.Rigid(deleteUnitBtnStyle.Layout)) + } +} + +func (t *Tracker) layoutUnitTypeChooser() layout.Widget { + return func(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() { + u := defaultUnits[allUnits[i]] + t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit] = (&u).Copy() + } + labelStyle := LabelStyle{Text: allUnits[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)} + bg := func(gtx C) D { + gtx.Constraints = layout.Exact(image.Pt(120, 20)) + var color color.NRGBA + if t.ChooseUnitTypeBtns[i].Hovered() { + color = unitTypeListHighlightColor + } + paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) + return D{Size: gtx.Constraints.Min} + } + return layout.Stack{Alignment: layout.W}.Layout(gtx, + layout.Stacked(bg), + layout.Expanded(labelStyle.Layout), + layout.Expanded(t.ChooseUnitTypeBtns[i].Layout)) + } + return t.ChooseUnitTypeList.Layout(gtx, len(allUnits), listElem) + } +}