feat(sointu, tracker,...): restructure domain & tracker models

send targets are now by ID and Song has "Score" part, which is the notes for it. also, moved the model part separate of the actual gioui dependend stuff.

sorry to my future self about the code bomb; ended up too far and did not find an easy way to rewrite the history to make the steps smaller, so in the end, just squashed everything.
This commit is contained in:
vsariola
2021-02-23 23:55:42 +02:00
parent fd1d018e82
commit adcf3ebce8
155 changed files with 5520 additions and 4914 deletions

116
tracker/gioui/alert.go Normal file
View File

@ -0,0 +1,116 @@
package gioui
import (
"image/color"
"time"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
)
type Alert struct {
message string
alertType AlertType
duration time.Duration
showMessage string
showAlertType AlertType
showDuration time.Duration
showTime time.Time
pos float64
lastUpdate time.Time
}
type AlertType int
const (
None AlertType = iota
Notify
Warning
Error
)
var alertSpeed = 150 * time.Millisecond
var alertMargin = layout.UniformInset(unit.Dp(6))
var alertInset = layout.UniformInset(unit.Dp(6))
func (a *Alert) Update(message string, alertType AlertType, duration time.Duration) {
if a.alertType < alertType {
a.message = message
a.alertType = alertType
a.duration = duration
}
}
func (a *Alert) Layout(gtx C) D {
now := time.Now()
if a.alertType != None {
a.showMessage = a.message
a.showAlertType = a.alertType
a.showTime = now
a.showDuration = a.duration
}
a.alertType = None
var targetPos float64 = 0.0
if now.Sub(a.showTime) <= a.showDuration {
targetPos = 1.0
}
delta := float64(now.Sub(a.lastUpdate)) / float64(alertSpeed)
if a.pos < targetPos {
a.pos += delta
if a.pos > targetPos {
a.pos = targetPos
} else {
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
}
} else if a.pos > targetPos {
a.pos -= delta
if a.pos < targetPos {
a.pos = targetPos
} else {
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
}
}
a.lastUpdate = now
var color, textColor, shadeColor color.NRGBA
switch a.showAlertType {
case Warning:
color = warningColor
textColor = black
case Error:
color = errorColor
textColor = black
default:
color = popupSurfaceColor
textColor = white
shadeColor = black
}
bgWidget := func(gtx C) D {
paint.FillShape(gtx.Ops, color, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
return D{Size: gtx.Constraints.Min}
}
labelStyle := LabelStyle{Text: a.showMessage, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Dp(16)}
return alertMargin.Layout(gtx, func(gtx C) D {
return layout.S.Layout(gtx, func(gtx C) D {
defer op.Save(gtx.Ops).Load()
gtx.Constraints.Min.X = gtx.Constraints.Max.X
recording := op.Record(gtx.Ops)
dims := layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Expanded(bgWidget),
layout.Stacked(func(gtx C) D {
return alertInset.Layout(gtx, labelStyle.Layout)
}),
)
macro := recording.Stop()
totalY := dims.Size.Y + gtx.Px(alertMargin.Bottom)
op.Offset(f32.Pt(0, float32((1-a.pos)*float64(totalY)))).Add((gtx.Ops))
macro.Add(gtx.Ops)
return dims
})
})
}

153
tracker/gioui/draglist.go Normal file
View File

@ -0,0 +1,153 @@
package gioui
import (
"image"
"image/color"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget/material"
)
type DragList struct {
SelectedItem int
HoverItem int
List *layout.List
drag bool
dragID pointer.ID
tags []bool
swapped bool
}
type FilledDragListStyle struct {
dragList *DragList
HoverColor color.NRGBA
SelectedColor color.NRGBA
Count int
element func(gtx C, i int) D
swap func(i, j int)
}
func FilledDragList(th *material.Theme, dragList *DragList, count int, element func(gtx C, i int) D, swap func(i, j int)) FilledDragListStyle {
return FilledDragListStyle{
dragList: dragList,
element: element,
swap: swap,
Count: count,
HoverColor: dragListHoverColor,
SelectedColor: dragListSelectedColor,
}
}
func (s *FilledDragListStyle) Layout(gtx C) D {
swap := 0
defer op.Save(gtx.Ops).Load()
if s.dragList.List.Axis == layout.Horizontal {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
} else {
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
}
listElem := func(gtx C, index int) D {
for len(s.dragList.tags) <= index {
s.dragList.tags = append(s.dragList.tags, false)
}
bg := func(gtx C) D {
var color color.NRGBA
if s.dragList.SelectedItem == index {
color = s.SelectedColor
} else if s.dragList.HoverItem == index {
color = s.HoverColor
}
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}
}
inputFg := func(gtx C) D {
defer op.Save(gtx.Ops).Load()
for _, ev := range gtx.Events(&s.dragList.tags[index]) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Enter:
s.dragList.HoverItem = index
case pointer.Leave:
if s.dragList.HoverItem == index {
s.dragList.HoverItem = -1
}
case pointer.Press:
if s.dragList.drag {
break
}
s.dragList.SelectedItem = index
}
}
rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(rect).Add(gtx.Ops)
pointer.InputOp{Tag: &s.dragList.tags[index],
Types: pointer.Press | pointer.Enter | pointer.Leave,
}.Add(gtx.Ops)
if index == s.dragList.SelectedItem {
for _, ev := range gtx.Events(s.dragList) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Press:
s.dragList.dragID = e.PointerID
case pointer.Drag:
if s.dragList.dragID != e.PointerID {
break
}
if s.dragList.List.Axis == layout.Horizontal {
if e.Position.X < 0 {
swap = -1
}
if e.Position.X > float32(gtx.Constraints.Min.X) {
swap = 1
}
} else {
if e.Position.Y < 0 {
swap = -1
}
if e.Position.Y > float32(gtx.Constraints.Min.Y) {
swap = 1
}
}
case pointer.Release:
fallthrough
case pointer.Cancel:
s.dragList.drag = false
}
}
pointer.InputOp{Tag: s.dragList,
Types: pointer.Drag | pointer.Press | pointer.Release,
Grab: s.dragList.drag,
}.Add(gtx.Ops)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}
return layout.Stack{Alignment: layout.W}.Layout(gtx,
layout.Expanded(bg),
layout.Stacked(func(gtx C) D {
return s.element(gtx, index)
}),
layout.Expanded(inputFg))
}
dims := s.dragList.List.Layout(gtx, s.Count, listElem)
if !s.dragList.swapped && swap != 0 && s.dragList.SelectedItem+swap >= 0 && s.dragList.SelectedItem+swap < s.Count {
s.swap(s.dragList.SelectedItem, s.dragList.SelectedItem+swap)
s.dragList.SelectedItem += swap
s.dragList.swapped = true
} else {
s.dragList.swapped = false
}
return dims
}

51
tracker/gioui/files.go Normal file
View File

@ -0,0 +1,51 @@
package gioui
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"gopkg.in/yaml.v3"
"github.com/sqweek/dialog"
"github.com/vsariola/sointu"
)
func (t *Tracker) LoadSongFile() {
filename, err := dialog.File().Filter("Sointu YAML song", "yml").Filter("Sointu JSON song", "json").Title("Load song").Load()
if err != nil {
return
}
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return
}
var song sointu.Song
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
return
}
}
t.SetSong(song)
}
func (t *Tracker) SaveSongFile() {
filename, err := dialog.File().Filter("Sointu YAML song", "yml").Filter("Sointu JSON song", "json").Title("Save song").Save()
if err != nil {
return
}
var extension = filepath.Ext(filename)
var contents []byte
if extension == "json" {
contents, err = json.Marshal(t.Song())
} else {
contents, err = yaml.Marshal(t.Song())
}
if err != nil {
return
}
if extension == "" {
filename = filename + ".yml"
}
ioutil.WriteFile(filename, contents, 0644)
}

View File

@ -0,0 +1,22 @@
package gioui
import (
"log"
"gioui.org/widget"
)
var iconCache = map[*byte]*widget.Icon{}
// widgetForIcon returns a widget for IconVG data, but caching the results
func widgetForIcon(icon []byte) *widget.Icon {
if widget, ok := iconCache[&icon[0]]; ok {
return widget
}
widget, err := widget.NewIcon(icon)
if err != nil {
log.Fatal(err)
}
iconCache[&icon[0]] = widget
return widget
}

View File

@ -0,0 +1,264 @@
package gioui
import (
"fmt"
"image"
"strconv"
"time"
"gioui.org/io/clipboard"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
"gopkg.in/yaml.v3"
)
var instrumentPointerTag = false
func (t *Tracker) layoutInstruments(gtx C) D {
for _, ev := range gtx.Events(&instrumentPointerTag) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
if e.Type == pointer.Press && (t.EditMode() != tracker.EditUnits && t.EditMode() != tracker.EditParameters) {
t.SetEditMode(tracker.EditUnits)
}
}
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
pointer.Rect(rect).Add(gtx.Ops)
pointer.InputOp{Tag: &instrumentPointerTag,
Types: pointer.Press,
}.Add(gtx.Ops)
for t.NewInstrumentBtn.Clicked() {
t.AddInstrument(true)
}
btnStyle := material.IconButton(t.Theme, t.NewInstrumentBtn, widgetForIcon(icons.ContentAdd))
btnStyle.Background = transparent
btnStyle.Inset = layout.UniformInset(unit.Dp(6))
if t.CanAddInstrument() {
btnStyle.Color = primaryColor
} else {
btnStyle.Color = disabledTextColor
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Flex{}.Layout(
gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Stack{}.Layout(gtx,
layout.Stacked(t.layoutInstrumentNames),
layout.Expanded(func(gtx C) D {
return t.InstrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.Song().Patch), &t.InstrumentDragList.List.Position)
}),
)
}),
layout.Rigid(func(gtx C) D {
return layout.E.Layout(gtx, btnStyle.Layout)
}),
)
}),
layout.Rigid(t.layoutInstrumentHeader),
layout.Flexed(1, t.layoutInstrumentEditor))
}
func (t *Tracker) layoutInstrumentHeader(gtx C) D {
header := func(gtx C) D {
copyInstrumentBtnStyle := material.IconButton(t.Theme, t.CopyInstrumentBtn, widgetForIcon(icons.ContentContentCopy))
copyInstrumentBtnStyle.Background = transparent
copyInstrumentBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
copyInstrumentBtnStyle.Color = primaryColor
deleteInstrumentBtnStyle := material.IconButton(t.Theme, t.DeleteInstrumentBtn, widgetForIcon(icons.ActionDelete))
deleteInstrumentBtnStyle.Background = transparent
deleteInstrumentBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
if t.CanDeleteInstrument() {
deleteInstrumentBtnStyle.Color = primaryColor
} else {
deleteInstrumentBtnStyle.Color = disabledTextColor
}
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(Label("Voices: ", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
maxRemain := t.MaxInstrumentVoices()
t.InstrumentVoices.Value = t.Instrument().NumVoices
numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, 0, maxRemain)
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := numStyle.Layout(gtx)
t.SetInstrumentVoices(t.InstrumentVoices.Value)
return dims
}),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(copyInstrumentBtnStyle.Layout),
layout.Rigid(deleteInstrumentBtnStyle.Layout))
}
for t.CopyInstrumentBtn.Clicked() {
contents, err := yaml.Marshal(t.Instrument())
if err == nil {
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
t.Alert.Update("Instrument copied to clipboard", Notify, time.Second*3)
}
}
for t.DeleteInstrumentBtn.Clicked() {
t.DeleteInstrument(false)
}
return Surface{Gray: 37, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters}.Layout(gtx, header)
}
func (t *Tracker) layoutInstrumentNames(gtx C) D {
element := func(gtx C, i int) D {
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(36))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(30))
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center}
if i == t.InstrIndex() {
grabhandle.Text = ":::"
}
label := func(gtx C) D {
if i == t.InstrIndex() {
for _, ev := range t.InstrumentNameEditor.Events() {
_, ok := ev.(widget.SubmitEvent)
if ok {
t.InstrumentNameEditor = &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle} // TODO: is there any other way to defocus the editor
break
}
}
if n := t.Instrument().Name; n != t.InstrumentNameEditor.Text() {
t.InstrumentNameEditor.SetText(n)
}
editor := material.Editor(t.Theme, t.InstrumentNameEditor, "Instr")
editor.Color = instrumentNameColor
editor.HintColor = instrumentNameHintColor
editor.TextSize = unit.Dp(12)
dims := layout.Center.Layout(gtx, editor.Layout)
t.SetInstrumentName(t.InstrumentNameEditor.Text())
return dims
}
text := t.Song().Patch[i].Name
if text == "" {
text = "Instr"
}
labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: white, FontSize: unit.Sp(12)}
return layout.Center.Layout(gtx, labelStyle.Layout)
}
return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(grabhandle.Layout),
layout.Rigid(label),
)
})
}
color := inactiveLightSurfaceColor
if t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters {
color = activeLightSurfaceColor
}
instrumentList := FilledDragList(t.Theme, t.InstrumentDragList, len(t.Song().Patch), element, t.SwapInstruments)
instrumentList.SelectedColor = color
instrumentList.HoverColor = instrumentHoverColor
t.InstrumentDragList.SelectedItem = t.InstrIndex()
defer op.Save(gtx.Ops).Load()
pointer.PassOp{Pass: true}.Add(gtx.Ops)
dims := instrumentList.Layout(gtx)
if t.InstrIndex() != t.InstrumentDragList.SelectedItem {
t.SetInstrIndex(t.InstrumentDragList.SelectedItem)
op.InvalidateOp{}.Add(gtx.Ops)
}
return dims
}
func (t *Tracker) layoutInstrumentEditor(gtx C) D {
for t.AddUnitBtn.Clicked() {
t.AddUnit(true)
}
addUnitBtnStyle := material.IconButton(t.Theme, t.AddUnitBtn, widgetForIcon(icons.ContentAdd))
addUnitBtnStyle.Color = t.Theme.ContrastFg
addUnitBtnStyle.Background = t.Theme.Fg
addUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(4))
units := t.Instrument().Units
for len(t.StackUse) < len(units) {
t.StackUse = append(t.StackUse, 0)
}
stackHeight := 0
for i, u := range units {
stackHeight += u.StackChange()
t.StackUse[i] = stackHeight
}
element := func(gtx C, i int) D {
gtx.Constraints = layout.Exact(image.Pt(gtx.Px(unit.Dp(120)), gtx.Px(unit.Dp(20))))
u := units[i]
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)}
if unitNameLabel.Text == "" {
unitNameLabel.Text = "---"
unitNameLabel.Alignment = layout.Center
}
var stackText string
if i < len(t.StackUse) {
stackText = strconv.FormatInt(int64(t.StackUse[i]), 10)
var prevStackUse int
if i > 0 {
prevStackUse = t.StackUse[i-1]
}
if stackNeed := u.StackNeed(); stackNeed > prevStackUse {
unitNameLabel.Color = errorColor
typeString := u.Type
if u.Parameters["stereo"] == 1 {
typeString += " (stereo)"
}
t.Alert.Update(fmt.Sprintf("%v needs at least %v input signals, got %v", typeString, stackNeed, prevStackUse), Error, 0)
} else if i == len(units)-1 && t.StackUse[i] != 0 {
unitNameLabel.Color = warningColor
t.Alert.Update(fmt.Sprintf("Instrument leaves %v signal(s) on the stack", t.StackUse[i]), Warning, 0)
}
}
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12)}
rightMargin := layout.Inset{Right: unit.Dp(10)}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Flexed(1, unitNameLabel.Layout),
layout.Rigid(func(gtx C) D {
return rightMargin.Layout(gtx, stackLabel.Layout)
}),
)
}
unitList := FilledDragList(t.Theme, t.UnitDragList, len(units), element, t.SwapUnits)
if t.EditMode() == tracker.EditUnits {
unitList.SelectedColor = cursorColor
}
t.UnitDragList.SelectedItem = t.UnitIndex()
return Surface{Gray: 30, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.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.UnitIndex() != t.UnitDragList.SelectedItem {
t.SetUnitIndex(t.UnitDragList.SelectedItem)
t.SetEditMode(tracker.EditUnits)
op.InvalidateOp{}.Add(gtx.Ops)
}
return dims
}),
layout.Expanded(func(gtx C) D {
return t.UnitScrollBar.Layout(gtx, unit.Dp(10), len(t.Instrument().Units), &t.UnitDragList.List.Position)
}),
layout.Stacked(func(gtx C) D {
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
return margin.Layout(gtx, addUnitBtnStyle.Layout)
}))
}),
layout.Rigid(t.layoutUnitEditor))
})
}

454
tracker/gioui/keyevent.go Normal file
View File

@ -0,0 +1,454 @@
package gioui
import (
"strconv"
"strings"
"time"
"gioui.org/app"
"gioui.org/io/key"
"github.com/vsariola/sointu/tracker"
"gopkg.in/yaml.v3"
)
var noteMap = map[string]int{
"Z": -12,
"S": -11,
"X": -10,
"D": -9,
"C": -8,
"V": -7,
"G": -6,
"B": -5,
"H": -4,
"N": -3,
"J": -2,
"M": -1,
",": 0,
"L": 1,
".": 2,
"Q": 0,
"2": 1,
"W": 2,
"3": 3,
"E": 4,
"R": 5,
"5": 6,
"T": 7,
"6": 8,
"Y": 9,
"7": 10,
"U": 11,
"I": 12,
"9": 13,
"O": 14,
"0": 15,
"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(w *app.Window, e key.Event) bool {
if e.State == key.Press {
if t.InstrumentNameEditor.Focused() {
return false
}
switch e.Name {
case "C":
if e.Modifiers.Contain(key.ModShortcut) {
contents, err := yaml.Marshal(t.Song())
if err == nil {
w.WriteClipboard(string(contents))
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
}
return true
}
case "V":
if e.Modifiers.Contain(key.ModShortcut) {
w.ReadClipboard()
return true
}
case "Z":
if e.Modifiers.Contain(key.ModShortcut) {
t.Undo()
return true
}
case "Y":
if e.Modifiers.Contain(key.ModShortcut) {
t.Redo()
return true
}
case "N":
if e.Modifiers.Contain(key.ModShortcut) {
t.ResetSong()
return true
}
case "S":
if e.Modifiers.Contain(key.ModShortcut) {
t.SaveSongFile()
return false
}
case "O":
if e.Modifiers.Contain(key.ModShortcut) {
t.LoadSongFile()
return true
}
case "F1":
t.SetEditMode(tracker.EditPatterns)
return true
case "F2":
t.SetEditMode(tracker.EditTracks)
return true
case "F3":
t.SetEditMode(tracker.EditUnits)
return true
case "F4":
t.SetEditMode(tracker.EditParameters)
return true
case "F5":
t.SetNoteTracking(true)
startRow := t.Cursor().SongRow
if t.EditMode() == tracker.EditPatterns {
startRow.Row = 0
}
t.player.Play(startRow)
return true
case "F6":
t.SetNoteTracking(false)
startRow := t.Cursor().SongRow
if t.EditMode() == tracker.EditPatterns {
startRow.Row = 0
}
t.player.Play(startRow)
return true
case "F8":
t.player.Stop()
return true
case key.NameDeleteForward, key.NameDeleteBackward:
switch t.EditMode() {
case tracker.EditPatterns:
if e.Modifiers.Contain(key.ModShortcut) {
t.DeleteOrderRow(e.Name == key.NameDeleteForward)
} else {
t.DeletePatternSelection()
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
t.SetCursor(t.Cursor().AddPatterns(1))
t.SetSelectionCorner(t.Cursor())
}
}
return true
case tracker.EditTracks:
t.DeleteSelection()
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
t.SetSelectionCorner(t.Cursor())
}
return true
case tracker.EditUnits:
t.DeleteUnit(e.Name == key.NameDeleteForward)
return true
}
case "Space":
_, playing := t.player.Position()
if !playing {
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
startRow := t.Cursor().SongRow
if t.EditMode() == tracker.EditPatterns {
startRow.Row = 0
}
t.player.Play(startRow)
} else {
t.player.Stop()
}
return true
case `\`, `<`, `>`:
if e.Modifiers.Contain(key.ModShift) {
return t.SetOctave(t.Octave() + 1)
}
return t.SetOctave(t.Octave() - 1)
case key.NameTab:
if e.Modifiers.Contain(key.ModShift) {
t.SetEditMode((t.EditMode() - 1 + 4) % 4)
} else {
t.SetEditMode((t.EditMode() + 1) % 4)
}
return true
case key.NameReturn:
switch t.EditMode() {
case tracker.EditPatterns:
t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut))
case tracker.EditUnits:
t.AddUnit(!e.Modifiers.Contain(key.ModShortcut))
}
case key.NameUpArrow:
cursor := t.Cursor()
switch t.EditMode() {
case tracker.EditPatterns:
if e.Modifiers.Contain(key.ModShortcut) {
cursor.SongRow = tracker.SongRow{}
} else {
cursor.Row -= t.Song().Score.RowsPerPattern
}
t.SetNoteTracking(false)
case tracker.EditTracks:
if e.Modifiers.Contain(key.ModShortcut) {
cursor.Row -= t.Song().Score.RowsPerPattern
} else {
if t.Step.Value > 0 {
cursor.Row -= t.Step.Value
} else {
cursor.Row--
}
}
t.SetNoteTracking(false)
case tracker.EditUnits:
t.SetUnitIndex(t.UnitIndex() - 1)
case tracker.EditParameters:
t.SetParamIndex(t.ParamIndex() - 1)
}
t.SetCursor(cursor)
if !e.Modifiers.Contain(key.ModShift) {
t.SetSelectionCorner(t.Cursor())
}
return true
case key.NameDownArrow:
cursor := t.Cursor()
switch t.EditMode() {
case tracker.EditPatterns:
if e.Modifiers.Contain(key.ModShortcut) {
cursor.Row = t.Song().Score.LengthInRows() - 1
} else {
cursor.Row += t.Song().Score.RowsPerPattern
}
t.SetNoteTracking(false)
case tracker.EditTracks:
if e.Modifiers.Contain(key.ModShortcut) {
cursor.Row += t.Song().Score.RowsPerPattern
} else {
if t.Step.Value > 0 {
cursor.Row += t.Step.Value
} else {
cursor.Row++
}
}
t.SetNoteTracking(false)
case tracker.EditUnits:
t.SetUnitIndex(t.UnitIndex() + 1)
case tracker.EditParameters:
t.SetParamIndex(t.ParamIndex() + 1)
}
t.SetCursor(cursor)
if !e.Modifiers.Contain(key.ModShift) {
t.SetSelectionCorner(t.Cursor())
}
return true
case key.NameLeftArrow:
cursor := t.Cursor()
switch t.EditMode() {
case tracker.EditPatterns:
if e.Modifiers.Contain(key.ModShortcut) {
cursor.Track = 0
} else {
cursor.Track--
}
case tracker.EditTracks:
if !t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
cursor.Track--
t.SetLowNibble(true)
} else {
t.SetLowNibble(false)
}
case tracker.EditUnits:
t.SetInstrIndex(t.InstrIndex() - 1)
case tracker.EditParameters:
param, _ := t.Param(t.ParamIndex())
if e.Modifiers.Contain(key.ModShift) {
t.SetParam(param.Value - 16)
} else {
t.SetParam(param.Value - 1)
}
}
t.SetCursor(cursor)
if !e.Modifiers.Contain(key.ModShift) {
t.SetSelectionCorner(t.Cursor())
}
return true
case key.NameRightArrow:
switch t.EditMode() {
case tracker.EditPatterns:
cursor := t.Cursor()
if e.Modifiers.Contain(key.ModShortcut) {
cursor.Track = len(t.Song().Score.Tracks) - 1
} else {
cursor.Track++
}
t.SetCursor(cursor)
case tracker.EditTracks:
if t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
cursor := t.Cursor()
cursor.Track++
t.SetCursor(cursor)
t.SetLowNibble(false)
} else {
t.SetLowNibble(true)
}
case tracker.EditUnits:
t.SetInstrIndex(t.InstrIndex() + 1)
case tracker.EditParameters:
param, _ := t.Param(t.ParamIndex())
if e.Modifiers.Contain(key.ModShift) {
t.SetParam(param.Value + 16)
} else {
t.SetParam(param.Value + 1)
}
}
if !e.Modifiers.Contain(key.ModShift) {
t.SetSelectionCorner(t.Cursor())
}
return true
case "+":
switch t.EditMode() {
case tracker.EditTracks:
if e.Modifiers.Contain(key.ModShortcut) {
t.AdjustSelectionPitch(12)
} else {
t.AdjustSelectionPitch(1)
}
return true
}
case "-":
switch t.EditMode() {
case tracker.EditTracks:
if e.Modifiers.Contain(key.ModShortcut) {
t.AdjustSelectionPitch(-12)
} else {
t.AdjustSelectionPitch(-1)
}
return true
}
}
switch t.EditMode() {
case tracker.EditPatterns:
if iv, err := strconv.Atoi(e.Name); err == nil {
t.SetCurrentPattern(iv)
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
t.SetCursor(t.Cursor().AddPatterns(1))
t.SetSelectionCorner(t.Cursor())
}
return true
}
if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
t.SetCurrentPattern(b + 10)
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
t.SetCursor(t.Cursor().AddPatterns(1))
t.SetSelectionCorner(t.Cursor())
}
return true
}
case tracker.EditTracks:
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
t.NumberPressed(byte(iv))
}
} else {
if e.Name == "A" {
t.SetNote(0)
} else {
if val, ok := noteMap[e.Name]; ok {
if _, ok := t.KeyPlaying[e.Name]; !ok {
n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val)
t.SetNote(n)
trk := t.Cursor().Track
start := t.Song().Score.FirstVoiceForTrack(trk)
end := start + t.Song().Score.Tracks[trk].NumVoices
t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n)
}
}
}
}
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
t.SetSelectionCorner(t.Cursor())
}
return true
case tracker.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.ModShortcut) {
t.SetUnitType(val)
return true
}
}
fallthrough
case tracker.EditParameters:
if val, ok := noteMap[e.Name]; ok {
if _, ok := t.KeyPlaying[e.Name]; !ok {
n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val)
instr := t.InstrIndex()
start := t.Song().Patch.FirstVoiceForInstrument(instr)
end := start + t.Instrument().NumVoices
t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n)
return false
}
}
}
}
if e.State == key.Release {
if ID, ok := t.KeyPlaying[e.Name]; ok {
t.player.Release(ID)
delete(t.KeyPlaying, e.Name)
if _, playing := t.player.Position(); t.EditMode() == tracker.EditTracks && playing && t.Note() == 1 && t.NoteTracking() {
t.SetNote(0)
}
}
}
return false
}
// NumberPressed handles incoming presses while in either of the hex number columns
func (t *Tracker) NumberPressed(iv byte) {
val := t.Note()
if val == 1 {
val = 0
}
if t.LowNibble() {
val = (val & 0xF0) | (iv & 0xF)
} else {
val = ((iv & 0xF) << 4) | (val & 0xF)
}
t.SetNote(val)
}

52
tracker/gioui/label.go Normal file
View File

@ -0,0 +1,52 @@
package gioui
import (
"image"
"image/color"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
)
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: 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)
op.Offset(f32.Pt(2, 2)).Add(gtx.Ops)
dims := widget.Label{
Alignment: text.Start,
MaxLines: 1,
}.Layout(gtx, textShaper, l.Font, l.FontSize, l.Text)
return layout.Dimensions{
Size: dims.Size.Add(image.Pt(2, 2)),
Baseline: dims.Baseline,
}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
return widget.Label{
Alignment: text.Start,
MaxLines: 1,
}.Layout(gtx, textShaper, l.Font, l.FontSize, l.Text)
}),
)
}
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
}

39
tracker/gioui/layout.go Normal file
View File

@ -0,0 +1,39 @@
package gioui
import (
"image"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
"github.com/vsariola/sointu/tracker"
)
type C = layout.Context
type D = layout.Dimensions
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.layoutTop,
t.layoutBottom)
t.Alert.Layout(gtx)
}
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
return t.BottomHorizontalSplit.Layout(gtx,
func(gtx C) D {
return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditPatterns}.Layout(gtx, t.layoutPatterns)
},
func(gtx C) D {
return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditTracks}.Layout(gtx, t.layoutTracker)
},
)
}
func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
return t.TopHorizontalSplit.Layout(gtx,
t.layoutSongPanel,
t.layoutInstruments,
)
}

151
tracker/gioui/menu.go Normal file
View File

@ -0,0 +1,151 @@
package gioui
import (
"image"
"image/color"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type Menu struct {
Visible bool
clickable widget.Clickable
tags []bool
clicks []int
hover int
}
type MenuStyle struct {
Menu *Menu
Title string
IconColor color.NRGBA
TextColor color.NRGBA
ShortCutColor color.NRGBA
FontSize unit.Value
IconSize unit.Value
HoverColor color.NRGBA
}
type MenuItem struct {
IconBytes []byte
Text string
ShortcutText string
Disabled bool
}
func (m *Menu) Clicked() (int, bool) {
if len(m.clicks) == 0 {
return 0, false
}
first := m.clicks[0]
for i := 1; i < len(m.clicks); i++ {
m.clicks[i-1] = m.clicks[i]
}
m.clicks = m.clicks[:len(m.clicks)-1]
return first, true
}
func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
contents := func(gtx C) D {
flexChildren := make([]layout.FlexChild, len(items))
for i, item := range items {
// make sure we have a tag for every item
for len(m.Menu.tags) <= i {
m.Menu.tags = append(m.Menu.tags, false)
}
// handle pointer events for this item
for _, ev := range gtx.Events(&m.Menu.tags[i]) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Press:
m.Menu.clicks = append(m.Menu.clicks, i)
m.Menu.Visible = false
case pointer.Enter:
m.Menu.hover = i + 1
case pointer.Leave:
if m.Menu.hover == i+1 {
m.Menu.hover = 0
}
}
}
// layout contents for this item
i2 := i // avoid loop variable getting updated in closure
item2 := item
flexChildren[i] = layout.Rigid(func(gtx C) D {
defer op.Save(gtx.Ops).Load()
var macro op.MacroOp
if i2 == m.Menu.hover-1 && !item2.Disabled {
macro = op.Record(gtx.Ops)
}
icon := widgetForIcon(item2.IconBytes)
if !item2.Disabled {
icon.Color = m.IconColor
} else {
icon.Color = mediumEmphasisTextColor
}
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := LabelStyle{Text: item2.Text, FontSize: m.FontSize, Color: m.TextColor}
if item2.Disabled {
textLabel.Color = mediumEmphasisTextColor
}
shortcutLabel := LabelStyle{Text: item2.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor}
shortcutInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(12), Bottom: unit.Dp(2), Top: unit.Dp(2)}
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return iconInset.Layout(gtx, func(gtx C) D {
return icon.Layout(gtx, m.IconSize)
})
}),
layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }),
layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}),
)
if i2 == m.Menu.hover-1 && !item2.Disabled {
recording := macro.Stop()
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{
Max: image.Pt(dims.Size.X, dims.Size.Y),
}.Op())
recording.Add(gtx.Ops)
}
if !item2.Disabled {
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
pointer.Rect(rect).Add(gtx.Ops)
pointer.InputOp{Tag: &m.Menu.tags[i2],
Types: pointer.Press | pointer.Enter | pointer.Leave,
}.Add(gtx.Ops)
}
return dims
})
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, flexChildren...)
}
popup := Popup(&m.Menu.Visible)
popup.NE = unit.Dp(0)
popup.ShadowN = unit.Dp(0)
popup.NW = unit.Dp(0)
return popup.Layout(gtx, contents)
}
func PopupMenu(th *material.Theme, menu *Menu) MenuStyle {
return MenuStyle{
Menu: menu,
IconColor: white,
TextColor: white,
ShortCutColor: mediumEmphasisTextColor,
FontSize: unit.Dp(16),
IconSize: unit.Dp(16),
HoverColor: menuHoverColor,
}
}

View File

@ -0,0 +1,194 @@
package gioui
import (
"fmt"
"image"
"image/color"
"golang.org/x/exp/shiny/materialdesign/icons"
"gioui.org/f32"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget/material"
)
type NumberInput struct {
Value int
dragStartValue int
dragStartXY float32
clickDecrease gesture.Click
clickIncrease gesture.Click
}
type NumericUpDownStyle struct {
NumberInput *NumberInput
Min int
Max int
Color color.NRGBA
Font text.Font
TextSize unit.Value
BorderColor color.NRGBA
IconColor color.NRGBA
BackgroundColor color.NRGBA
CornerRadius unit.Value
Border unit.Value
ButtonWidth unit.Value
UnitsPerStep unit.Value
shaper text.Shaper
}
func NumericUpDown(th *material.Theme, number *NumberInput, min, max int) NumericUpDownStyle {
bgColor := th.Palette.Fg
bgColor.R /= 4
bgColor.G /= 4
bgColor.B /= 4
return NumericUpDownStyle{
NumberInput: number,
Min: min,
Max: max,
Color: white,
BorderColor: th.Palette.Fg,
IconColor: th.Palette.ContrastFg,
BackgroundColor: bgColor,
CornerRadius: unit.Dp(4),
ButtonWidth: unit.Dp(16),
Border: unit.Dp(1),
UnitsPerStep: unit.Dp(8),
TextSize: th.TextSize.Scale(14.0 / 16.0),
shaper: th.Shaper,
}
}
func (s NumericUpDownStyle) Layout(gtx C) D {
size := gtx.Constraints.Min
defer op.Save(gtx.Ops).Load()
rr := float32(gtx.Px(s.CornerRadius))
border := float32(gtx.Px(s.Border))
clip.UniformRRect(f32.Rectangle{Max: f32.Point{
X: float32(gtx.Constraints.Min.X),
Y: float32(gtx.Constraints.Min.Y),
}}, rr).Add(gtx.Ops)
paint.Fill(gtx.Ops, s.BorderColor)
op.Offset(f32.Pt(border, border)).Add(gtx.Ops)
clip.UniformRRect(f32.Rectangle{Max: f32.Point{
X: float32(gtx.Constraints.Min.X) - border*2,
Y: float32(gtx.Constraints.Min.Y) - border*2,
}}, rr-border).Add(gtx.Ops)
gtx.Constraints.Min.X -= int(border * 2)
gtx.Constraints.Min.Y -= int(border * 2)
gtx.Constraints.Max = gtx.Constraints.Min
layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(s.button(gtx.Constraints.Max.Y, widgetForIcon(icons.NavigationArrowBack), -1, &s.NumberInput.clickDecrease)),
layout.Flexed(1, s.layoutText),
layout.Rigid(s.button(gtx.Constraints.Max.Y, widgetForIcon(icons.NavigationArrowForward), 1, &s.NumberInput.clickIncrease)),
)
if s.NumberInput.Value < s.Min {
s.NumberInput.Value = s.Min
}
if s.NumberInput.Value > s.Max {
s.NumberInput.Value = s.Max
}
return layout.Dimensions{Size: size}
}
func (s NumericUpDownStyle) button(height int, icon *widget.Icon, delta int, click *gesture.Click) layout.Widget {
return func(gtx C) D {
btnWidth := gtx.Px(s.ButtonWidth)
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
//paint.FillShape(gtx.Ops, black, clip.Rect(image.Rect(0, 0, btnWidth, height)).Op())
return layout.Dimensions{Size: image.Point{X: btnWidth, Y: height}}
}),
layout.Expanded(func(gtx C) D {
size := btnWidth
if height < size {
size = height
}
if size < 1 {
size = 1
}
if icon != nil {
icon.Color = s.IconColor
return icon.Layout(gtx, unit.Px(float32(size)))
}
return layout.Dimensions{}
}),
layout.Expanded(func(gtx C) D {
return s.layoutClick(gtx, delta, click)
}),
)
}
}
func (s NumericUpDownStyle) layoutText(gtx C) D {
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Stacked(func(gtx C) D {
paint.FillShape(gtx.Ops, s.BackgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
return layout.Dimensions{Size: gtx.Constraints.Max}
}),
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: s.Color}.Add(gtx.Ops)
return widget.Label{Alignment: text.Middle}.Layout(gtx, s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Value))
}),
layout.Expanded(s.layoutDrag),
)
}
func (s NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions {
{ // handle dragging
pxPerStep := float32(gtx.Px(s.UnitsPerStep))
for _, ev := range gtx.Events(s.NumberInput) {
if e, ok := ev.(pointer.Event); ok {
switch e.Type {
case pointer.Press:
s.NumberInput.dragStartValue = s.NumberInput.Value
s.NumberInput.dragStartXY = e.Position.X - e.Position.Y
case pointer.Drag:
var deltaCoord float32
deltaCoord = e.Position.X - e.Position.Y - s.NumberInput.dragStartXY
s.NumberInput.Value = s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5)
}
}
}
// Avoid affecting the input tree with pointer events.
stack := op.Save(gtx.Ops)
// register for input
dragRect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(dragRect).Add(gtx.Ops)
pointer.InputOp{
Tag: s.NumberInput,
Types: pointer.Press | pointer.Drag | pointer.Release,
}.Add(gtx.Ops)
stack.Load()
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}
func (s NumericUpDownStyle) layoutClick(gtx layout.Context, delta int, click *gesture.Click) layout.Dimensions {
// handle clicking
for _, e := range click.Events(gtx) {
switch e.Type {
case gesture.TypeClick:
s.NumberInput.Value += delta
}
}
// Avoid affecting the input tree with pointer events.
stack := op.Save(gtx.Ops)
// register for input
clickRect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(clickRect).Add(gtx.Ops)
click.Add(gtx.Ops)
stack.Load()
return layout.Dimensions{Size: gtx.Constraints.Min}
}

205
tracker/gioui/parameter.go Normal file
View File

@ -0,0 +1,205 @@
package gioui
import (
"fmt"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
)
type ParameterWidget struct {
floatWidget widget.Float
boolWidget widget.Bool
labelBtn widget.Clickable
instrBtn widget.Clickable
instrMenu Menu
unitBtn widget.Clickable
unitMenu Menu
}
type ParameterStyle struct {
tracker *Tracker
Parameter *tracker.Parameter
ParameterWidget *ParameterWidget
Theme *material.Theme
Focus bool
}
func (t *Tracker) ParamStyle(th *material.Theme, param *tracker.Parameter, paramWidget *ParameterWidget) ParameterStyle {
return ParameterStyle{
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
Parameter: param,
Theme: th,
ParameterWidget: paramWidget,
}
}
func (p *ParameterWidget) Clicked() bool {
return p.labelBtn.Clicked()
}
func (p ParameterStyle) Layout(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Stack{}.Layout(gtx,
layout.Stacked(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Px(unit.Dp(110))
return layout.E.Layout(gtx, Label(p.Parameter.Name, white))
}),
layout.Expanded(p.ParameterWidget.labelBtn.Layout),
)
}),
layout.Rigid(func(gtx C) D {
switch p.Parameter.Type {
case tracker.IntegerParameter:
gtx.Constraints.Min.X = gtx.Px(unit.Dp(200))
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
if p.Focus {
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
}
if !p.ParameterWidget.floatWidget.Dragging() {
p.ParameterWidget.floatWidget.Value = float32(p.Parameter.Value)
}
sliderStyle := material.Slider(p.Theme, &p.ParameterWidget.floatWidget, float32(p.Parameter.Min), float32(p.Parameter.Max))
sliderStyle.Color = p.Theme.Fg
dims := sliderStyle.Layout(gtx)
p.Parameter.Value = int(p.ParameterWidget.floatWidget.Value + 0.5)
return dims
case tracker.BoolParameter:
gtx.Constraints.Min.X = gtx.Px(unit.Dp(60))
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
if p.Focus {
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
}
p.ParameterWidget.boolWidget.Value = p.Parameter.Value > p.Parameter.Min
boolStyle := material.Switch(p.Theme, &p.ParameterWidget.boolWidget)
boolStyle.Color.Disabled = p.Theme.Fg
boolStyle.Color.Enabled = white
dims := layout.Center.Layout(gtx, boolStyle.Layout)
if p.ParameterWidget.boolWidget.Value {
p.Parameter.Value = p.Parameter.Max
} else {
p.Parameter.Value = p.Parameter.Min
}
return dims
case tracker.IDParameter:
gtx.Constraints.Min.X = gtx.Px(unit.Dp(200))
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
if p.Focus {
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
Max: gtx.Constraints.Min,
}.Op())
}
for clickedItem, hasClicked := p.ParameterWidget.instrMenu.Clicked(); hasClicked; {
p.Parameter.Value = p.tracker.Song().Patch[clickedItem].Units[0].ID
clickedItem, hasClicked = p.ParameterWidget.instrMenu.Clicked()
}
instrItems := make([]MenuItem, len(p.tracker.Song().Patch))
for i, instr := range p.tracker.Song().Patch {
instrItems[i].Text = instr.Name
instrItems[i].IconBytes = icons.NavigationChevronRight
}
var unitItems []MenuItem
instrName := "<instr>"
unitName := "<unit>"
targetI, targetU, err := p.tracker.Song().Patch.FindSendTarget(p.Parameter.Value)
if err == nil {
targetInstrument := p.tracker.Song().Patch[targetI]
instrName = targetInstrument.Name
units := targetInstrument.Units
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
unitItems = make([]MenuItem, len(units))
for clickedItem, hasClicked := p.ParameterWidget.unitMenu.Clicked(); hasClicked; {
p.Parameter.Value = units[clickedItem].ID
clickedItem, hasClicked = p.ParameterWidget.unitMenu.Clicked()
}
for j, unit := range units {
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
unitItems[j].IconBytes = icons.NavigationChevronRight
}
}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(p.tracker.layoutMenu(instrName, &p.ParameterWidget.instrBtn, &p.ParameterWidget.instrMenu, unit.Dp(200),
instrItems...,
)),
layout.Rigid(p.tracker.layoutMenu(unitName, &p.ParameterWidget.unitBtn, &p.ParameterWidget.unitMenu, unit.Dp(200),
unitItems...,
)),
)
}
return D{}
}),
layout.Rigid(func(gtx C) D {
if p.Parameter.Type != tracker.IDParameter {
return Label(p.Parameter.Hint, white)(gtx)
}
return D{}
}),
)
}
/*
func (t *Tracker) layoutParameter(gtx C, index int) D {
u := t.Unit()
ut, _ := sointu.UnitTypes[u.Type]
params := u.Parameters
var name string
var value, min, max int
var valueText string
if u.Type == "oscillator" && index == len(ut) {
name = "sample"
key := compiler.SampleOffset{Start: uint32(params["samplestart"]), LoopStart: uint16(params["loopstart"]), LoopLength: uint16(params["looplength"])}
if v, ok := tracker.GmDlsEntryMap[key]; ok {
value = v + 1
valueText = fmt.Sprintf("%v / %v", value, tracker.GmDlsEntries[v].Name)
} else {
value = 0
valueText = "0 / custom"
}
min, max = 0, len(tracker.GmDlsEntries)
} else {
if ut[index].MaxValue < ut[index].MinValue {
return layout.Dimensions{}
}
name = ut[index].Name
if u.Type == "oscillator" && (name == "samplestart" || name == "loopstart" || name == "looplength") {
if params["type"] != sointu.Sample {
return layout.Dimensions{}
}
}
value = params[name]
min, max = ut[index].MinValue, ut[index].MaxValue
if u.Type == "send" && name == "voice" {
max = t.Song().Patch.NumVoices()
} else if u.Type == "send" && name == "unit" { // set the maximum values depending on the send target
instrIndex, _, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
if instrIndex != -1 {
max = len(t.Song().Patch[instrIndex].Units) - 1
}
} else if u.Type == "send" && name == "port" { // set the maximum values depending on the send target
instrIndex, unitIndex, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
if instrIndex != -1 && unitIndex != -1 {
max = len(sointu.Ports[t.Song().Patch[instrIndex].Units[unitIndex].Type]) - 1
}
}
hint := t.Song().Patch.ParamHintString(t.InstrIndex(), t.UnitIndex(), name)
if hint != "" {
valueText = fmt.Sprintf("%v / %v", value, hint)
} else {
valueText = fmt.Sprintf("%v", value)
}
}
}*/

86
tracker/gioui/patterns.go Normal file
View File

@ -0,0 +1,86 @@
package gioui
import (
"fmt"
"image"
"strings"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget"
"github.com/vsariola/sointu/tracker"
)
const patternCellHeight = 16
const patternCellWidth = 16
const patternRowMarkerWidth = 30
var patternPointerTag = false
func (t *Tracker) layoutPatterns(gtx C) D {
defer op.Save(gtx.Ops).Load()
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
for _, ev := range gtx.Events(&patternPointerTag) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
if e.Type == pointer.Press {
t.SetEditMode(tracker.EditPatterns)
}
}
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
pointer.Rect(rect).Add(gtx.Ops)
pointer.InputOp{Tag: &patternPointerTag,
Types: pointer.Press,
}.Add(gtx.Ops)
patternRect := tracker.SongRect{
Corner1: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.Cursor().Pattern}, Track: t.Cursor().Track},
Corner2: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.SelectionCorner().Pattern}, Track: t.SelectionCorner().Track},
}
for j := 0; j < t.Song().Score.Length; j++ {
if playPos, ok := t.player.Position(); ok && j == playPos.Pattern {
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
}
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)))
stack := op.Save(gtx.Ops)
op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops)
for i, track := range t.Song().Score.Tracks {
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
if j < len(track.Order) && track.Order[j] >= 0 {
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(track.Order[j]))
}
point := tracker.SongPoint{Track: i, SongRow: tracker.SongRow{Pattern: j}}
if t.EditMode() == tracker.EditPatterns || t.EditMode() == tracker.EditTracks {
if patternRect.Contains(point) {
color := inactiveSelectionColor
if t.EditMode() == tracker.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())
}
}
op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops)
}
stack.Load()
op.Offset(f32.Pt(0, patternCellHeight)).Add(gtx.Ops)
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}
func patternIndexToString(index int) string {
if index < 0 {
return ""
} else if index < 10 {
return string('0' + byte(index))
}
return string('A' + byte(index-10))
}

84
tracker/gioui/popup.go Normal file
View File

@ -0,0 +1,84 @@
package gioui
import (
"image/color"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
)
type PopupStyle struct {
Visible *bool
SurfaceColor color.NRGBA
ShadowColor color.NRGBA
ShadowN unit.Value
ShadowE unit.Value
ShadowW unit.Value
ShadowS unit.Value
SE, SW, NW, NE unit.Value
}
func Popup(visible *bool) PopupStyle {
return PopupStyle{
Visible: visible,
SurfaceColor: popupSurfaceColor,
ShadowColor: popupShadowColor,
ShadowN: unit.Dp(2),
ShadowE: unit.Dp(2),
ShadowS: unit.Dp(2),
ShadowW: unit.Dp(2),
SE: unit.Dp(6),
SW: unit.Dp(6),
NW: unit.Dp(6),
NE: unit.Dp(6),
}
}
func (s PopupStyle) Layout(gtx C, contents layout.Widget) D {
if !*s.Visible {
return D{}
}
for _, ev := range gtx.Events(s.Visible) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Press:
*s.Visible = false
}
}
bg := func(gtx C) D {
pointer.InputOp{Tag: s.Visible,
Types: pointer.Press,
}.Add(gtx.Ops)
rrect := clip.RRect{
Rect: f32.Rectangle{Max: f32.Pt(float32(gtx.Constraints.Min.X), float32(gtx.Constraints.Min.Y))},
SE: float32(gtx.Px(s.SE)),
SW: float32(gtx.Px(s.SW)),
NW: float32(gtx.Px(s.NW)),
NE: float32(gtx.Px(s.NE)),
}
rrect2 := rrect
rrect2.Rect.Min = rrect2.Rect.Min.Sub(f32.Pt(float32(gtx.Px(s.ShadowW)), float32(gtx.Px(s.ShadowN))))
rrect2.Rect.Max = rrect2.Rect.Max.Add(f32.Pt(float32(gtx.Px(s.ShadowE)), float32(gtx.Px(s.ShadowS))))
paint.FillShape(gtx.Ops, s.ShadowColor, rrect2.Op(gtx.Ops))
paint.FillShape(gtx.Ops, s.SurfaceColor, rrect.Op(gtx.Ops))
return D{Size: gtx.Constraints.Min}
}
macro := op.Record(gtx.Ops)
dims := layout.Stack{}.Layout(gtx,
layout.Expanded(bg),
layout.Stacked(contents),
)
callop := macro.Stop()
op.Defer(gtx.Ops, callop)
return dims
}

View File

@ -0,0 +1,72 @@
package gioui
import (
"fmt"
"image"
"strings"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget"
"github.com/vsariola/sointu/tracker"
)
const rowMarkerWidth = 50
func (t *Tracker) layoutRowMarkers(gtx C) D {
gtx.Constraints.Min.X = rowMarkerWidth
paint.FillShape(gtx.Ops, rowMarkerSurfaceColor, clip.Rect{
Max: gtx.Constraints.Max,
}.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-trackRowHeight)/2)).Add(gtx.Ops)
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
playPos, playing := t.player.Position()
playSongRow := playPos.Pattern*t.Song().Score.RowsPerPattern + playPos.Row
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
beatMarkerDensity := t.Song().RowsPerBeat
for beatMarkerDensity <= 2 {
beatMarkerDensity *= 2
}
for i := 0; i < t.Song().Score.Length; i++ {
for j := 0; j < t.Song().Score.RowsPerPattern; j++ {
songRow := i*t.Song().Score.RowsPerPattern + j
if mod(songRow, beatMarkerDensity*2) == 0 {
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
} else if mod(songRow, beatMarkerDensity) == 0 {
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
}
if playing && songRow == playSongRow {
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, 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 t.EditMode() == tracker.EditTracks && songRow == cursorSongRow {
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
} else {
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
}
op.Offset(f32.Pt(rowMarkerWidth/2, 0)).Add(gtx.Ops)
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)))
op.Offset(f32.Pt(-rowMarkerWidth/2, trackRowHeight)).Add(gtx.Ops)
}
}
return layout.Dimensions{Size: image.Pt(rowMarkerWidth, gtx.Constraints.Max.Y)}
}
func mod(a, b int) int {
m := a % b
if a < 0 && b < 0 {
m -= b
}
if a < 0 && b > 0 {
m += b
}
return m
}

69
tracker/gioui/run.go Normal file
View File

@ -0,0 +1,69 @@
package gioui
import (
"fmt"
"os"
"gioui.org/app"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
"github.com/vsariola/sointu"
)
func (t *Tracker) Run(w *app.Window) error {
var ops op.Ops
for {
if pos, playing := t.player.Position(); t.NoteTracking() && playing {
cursor := t.Cursor()
cursor.SongRow = pos
t.SetCursor(cursor)
t.SetSelectionCorner(cursor)
}
select {
case <-t.refresh:
w.Invalidate()
case v := <-t.volumeChan:
t.lastVolume = v
w.Invalidate()
case e := <-w.Events():
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case key.Event:
if t.KeyEvent(w, e) {
w.Invalidate()
}
case clipboard.Event:
err := t.UnmarshalContent([]byte(e.Text))
if err == nil {
w.Invalidate()
}
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
t.Layout(gtx)
e.Frame(gtx.Ops)
}
}
}
}
func Main(audioContext sointu.AudioContext, synthService sointu.SynthService) {
go func() {
w := app.NewWindow(
app.Size(unit.Dp(800), unit.Dp(600)),
app.Title("Sointu Tracker"),
)
t := New(audioContext, synthService)
defer t.Close()
if err := t.Run(w); err != nil {
fmt.Println(err)
os.Exit(1)
}
os.Exit(0)
}()
app.Main()
}

128
tracker/gioui/scrollbar.go Normal file
View File

@ -0,0 +1,128 @@
package gioui
import (
"image"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
)
type ScrollBar struct {
Axis layout.Axis
dragStart float32
hovering bool
dragging bool
}
func (s *ScrollBar) Layout(gtx C, width unit.Value, numItems int, pos *layout.Position) D {
defer op.Save(gtx.Ops).Load()
clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops)
gradientSize := gtx.Px(unit.Dp(4))
var totalPixelsEstimate, scrollBarRelLength float32
switch s.Axis {
case layout.Vertical:
if pos.First > 0 || pos.Offset > 0 {
paint.LinearGradientOp{Color1: black, Color2: transparent, Stop2: f32.Pt(0, float32(gradientSize))}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
}
if pos.BeforeEnd {
paint.LinearGradientOp{Color1: black, Color2: transparent, Stop1: f32.Pt(0, float32(gtx.Constraints.Min.Y)), Stop2: f32.Pt(0, float32(gtx.Constraints.Min.Y-gradientSize))}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
}
totalPixelsEstimate = float32(gtx.Constraints.Min.Y+pos.Offset-pos.OffsetLast) * float32(numItems) / float32(pos.Count)
scrollBarRelLength = float32(gtx.Constraints.Min.Y) / float32(totalPixelsEstimate)
case layout.Horizontal:
if pos.First > 0 || pos.Offset > 0 {
paint.LinearGradientOp{Color1: black, Color2: transparent, Stop2: f32.Pt(float32(gradientSize), 0)}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
}
if pos.BeforeEnd {
paint.LinearGradientOp{Color1: black, Color2: transparent, Stop1: f32.Pt(float32(gtx.Constraints.Min.X), 0), Stop2: f32.Pt(float32(gtx.Constraints.Min.X-gradientSize), 0)}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
}
totalPixelsEstimate = float32(gtx.Constraints.Min.X+pos.Offset-pos.OffsetLast) * float32(numItems) / float32(pos.Count)
scrollBarRelLength = float32(gtx.Constraints.Min.X) / float32(totalPixelsEstimate)
}
scrollBarRelStart := (float32(pos.First)*totalPixelsEstimate/float32(numItems) + float32(pos.Offset)) / totalPixelsEstimate
scrWidth := gtx.Px(width)
stack := op.Save(gtx.Ops)
switch s.Axis {
case layout.Vertical:
if scrollBarRelLength < 1 && (s.dragging || s.hovering) {
y1 := int(scrollBarRelStart * float32(gtx.Constraints.Min.Y))
y2 := int((scrollBarRelStart + scrollBarRelLength) * float32(gtx.Constraints.Min.Y))
paint.FillShape(gtx.Ops, scrollBarColor, clip.Rect{Min: image.Pt(gtx.Constraints.Min.X-scrWidth, y1), Max: image.Pt(gtx.Constraints.Min.X, y2)}.Op())
}
rect := image.Rect(gtx.Constraints.Min.X-scrWidth, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(rect).Add(gtx.Ops)
case layout.Horizontal:
if scrollBarRelLength < 1 && (s.dragging || s.hovering) {
x1 := int(scrollBarRelStart * float32(gtx.Constraints.Min.X))
x2 := int((scrollBarRelStart + scrollBarRelLength) * float32(gtx.Constraints.Min.X))
paint.FillShape(gtx.Ops, scrollBarColor, clip.Rect{Min: image.Pt(x1, gtx.Constraints.Min.Y-scrWidth), Max: image.Pt(x2, gtx.Constraints.Min.Y)}.Op())
}
rect := image.Rect(0, gtx.Constraints.Min.Y-scrWidth, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(rect).Add(gtx.Ops)
}
pointer.InputOp{Tag: &s.dragStart,
Types: pointer.Drag | pointer.Press | pointer.Cancel | pointer.Release,
}.Add(gtx.Ops)
stack.Load()
for _, ev := range gtx.Events(&s.dragStart) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Press:
if s.Axis == layout.Horizontal {
s.dragStart = e.Position.X
s.dragging = true
} else {
s.dragStart = e.Position.Y
s.dragging = true
}
case pointer.Drag:
if s.Axis == layout.Horizontal {
pos.Offset += int(e.Position.X - s.dragStart + 0.5)
s.dragStart = e.Position.X
} else {
pos.Offset += int(e.Position.Y - s.dragStart + 0.5)
s.dragStart = e.Position.Y
}
case pointer.Release, pointer.Cancel:
s.dragging = false
}
}
pointer.PassOp{Pass: true}.Add(gtx.Ops)
rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
pointer.Rect(rect).Add(gtx.Ops)
pointer.InputOp{Tag: s,
Types: pointer.Enter | pointer.Leave,
}.Add(gtx.Ops)
for _, ev := range gtx.Events(s) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Enter:
s.hovering = true
case pointer.Leave:
s.hovering = false
}
}
return D{Size: gtx.Constraints.Min}
}

195
tracker/gioui/songpanel.go Normal file
View File

@ -0,0 +1,195 @@
package gioui
import (
"image"
"math"
"runtime"
"time"
"gioui.org/f32"
"gioui.org/io/clipboard"
"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"
"gopkg.in/yaml.v3"
)
func (t *Tracker) layoutSongPanel(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(t.layoutMenuBar),
layout.Rigid(t.layoutSongOptions),
)
}
func (t *Tracker) layoutMenu(title string, clickable *widget.Clickable, menu *Menu, width unit.Value, items ...MenuItem) layout.Widget {
for clickable.Clicked() {
menu.Visible = true
}
m := PopupMenu(t.Theme, menu)
return func(gtx C) D {
defer op.Save(gtx.Ops).Load()
titleBtn := material.Button(t.Theme, clickable, title)
titleBtn.Color = white
titleBtn.Background = transparent
titleBtn.CornerRadius = unit.Dp(0)
dims := titleBtn.Layout(gtx)
op.Offset(f32.Pt(0, float32(dims.Size.Y))).Add(gtx.Ops)
gtx.Constraints.Max.X = gtx.Px(width)
gtx.Constraints.Max.Y = gtx.Px(unit.Dp(1000))
m.Layout(gtx, items...)
return dims
}
}
func (t *Tracker) layoutMenuBar(gtx C) D {
gtx.Constraints.Max.Y = gtx.Px(unit.Dp(36))
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(36))
for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; {
switch clickedItem {
case 0:
t.ResetSong()
case 1:
t.LoadSongFile()
case 2:
t.SaveSongFile()
}
clickedItem, hasClicked = t.Menus[0].Clicked()
}
for clickedItem, hasClicked := t.Menus[1].Clicked(); hasClicked; {
switch clickedItem {
case 0:
t.Undo()
case 1:
t.Redo()
case 2:
if contents, err := yaml.Marshal(t.Song()); err == nil {
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
}
case 3:
clipboard.ReadOp{Tag: &t.Menus[1]}.Add(gtx.Ops)
}
clickedItem, hasClicked = t.Menus[1].Clicked()
}
shortcutKey := "Ctrl+"
if runtime.GOOS == "darwin" {
shortcutKey = "Cmd+"
}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(t.layoutMenu("File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200),
MenuItem{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N"},
MenuItem{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
)),
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(160),
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: !t.CanUndo()},
MenuItem{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Disabled: !t.CanRedo()},
MenuItem{IconBytes: icons.ContentContentCopy, Text: "Copy", ShortcutText: shortcutKey + "C"},
MenuItem{IconBytes: icons.ContentContentPaste, Text: "Paste", ShortcutText: shortcutKey + "V"},
)),
)
}
func (t *Tracker) layoutSongOptions(gtx C) D {
paint.FillShape(gtx.Ops, songSurfaceColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
in := layout.UniformInset(unit.Dp(1))
panicBtnStyle := material.Button(t.Theme, t.PanicBtn, "Panic")
if t.player.Enabled() {
panicBtnStyle.Background = transparent
panicBtnStyle.Color = t.Theme.Palette.Fg
} else {
panicBtnStyle.Background = t.Theme.Palette.Fg
panicBtnStyle.Color = t.Theme.Palette.ContrastFg
}
for t.PanicBtn.Clicked() {
t.player.Disable()
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Label("LEN:", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
t.SongLength.Value = t.Song().Score.Length
numStyle := NumericUpDown(t.Theme, t.SongLength, 1, math.MaxInt32)
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
t.SetSongLength(t.SongLength.Value)
return dims
}),
)
}),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Label("BPM:", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
t.BPM.Value = t.Song().BPM
numStyle := NumericUpDown(t.Theme, t.BPM, 1, 999)
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
t.SetBPM(t.BPM.Value)
return dims
}),
)
}),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Label("RPP:", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
t.RowsPerPattern.Value = t.Song().Score.RowsPerPattern
numStyle := NumericUpDown(t.Theme, t.RowsPerPattern, 1, 255)
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
t.SetRowsPerPattern(t.RowsPerPattern.Value)
return dims
}),
)
}),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Label("RPB:", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
t.RowsPerBeat.Value = t.Song().RowsPerBeat
numStyle := NumericUpDown(t.Theme, t.RowsPerBeat, 1, 32)
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
t.SetRowsPerBeat(t.RowsPerBeat.Value)
return dims
}),
)
}),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Label("STP:", white)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
numStyle := NumericUpDown(t.Theme, t.Step, 0, 8)
numStyle.UnitsPerStep = unit.Dp(20)
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
return dims
}),
)
}),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min = image.Pt(0, 0)
return panicBtnStyle.Layout(gtx)
}),
layout.Rigid(VuMeter{Volume: t.lastVolume, Range: 100}.Layout),
)
}

144
tracker/gioui/split.go Normal file
View File

@ -0,0 +1,144 @@
package gioui
import (
"image"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
)
type Split struct {
// Ratio keeps the current layout.
// 0 is center, -1 completely to the left, 1 completely to the right.
Ratio float32
// Bar is the width for resizing the layout
Bar unit.Value
// Axis is the split direction: layout.Horizontal splits the view in left
// and right, layout.Vertical splits the view in top and bottom
Axis layout.Axis
drag bool
dragID pointer.ID
dragCoord float32
}
var defaultBarWidth = unit.Dp(10)
func (s *Split) Layout(gtx layout.Context, first, second layout.Widget) layout.Dimensions {
bar := gtx.Px(s.Bar)
if bar <= 1 {
bar = gtx.Px(defaultBarWidth)
}
var coord int
if s.Axis == layout.Horizontal {
coord = gtx.Constraints.Max.X
} else {
coord = gtx.Constraints.Max.Y
}
proportion := (s.Ratio + 1) / 2
firstSize := int(proportion*float32(coord) - float32(bar))
secondOffset := firstSize + bar
secondSize := coord - secondOffset
{ // handle input
// Avoid affecting the input tree with pointer events.
stack := op.Save(gtx.Ops)
for _, ev := range gtx.Events(s) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Press:
if s.drag {
break
}
s.dragID = e.PointerID
if s.Axis == layout.Horizontal {
s.dragCoord = e.Position.X
} else {
s.dragCoord = e.Position.Y
}
case pointer.Drag:
if s.dragID != e.PointerID {
break
}
var deltaCoord, deltaRatio float32
if s.Axis == layout.Horizontal {
deltaCoord = e.Position.X - s.dragCoord
s.dragCoord = e.Position.X
deltaRatio = deltaCoord * 2 / float32(gtx.Constraints.Max.X)
} else {
deltaCoord = e.Position.Y - s.dragCoord
s.dragCoord = e.Position.Y
deltaRatio = deltaCoord * 2 / float32(gtx.Constraints.Max.Y)
}
s.Ratio += deltaRatio
case pointer.Release:
fallthrough
case pointer.Cancel:
s.drag = false
}
}
// register for input
var barRect image.Rectangle
if s.Axis == layout.Horizontal {
barRect = image.Rect(firstSize, 0, secondOffset, gtx.Constraints.Max.Y)
} else {
barRect = image.Rect(0, firstSize, gtx.Constraints.Max.X, secondOffset)
}
pointer.Rect(barRect).Add(gtx.Ops)
pointer.InputOp{Tag: s,
Types: pointer.Press | pointer.Drag | pointer.Release,
Grab: s.drag,
}.Add(gtx.Ops)
stack.Load()
}
{
gtx := gtx
stack := op.Save(gtx.Ops)
if s.Axis == layout.Horizontal {
gtx.Constraints = layout.Exact(image.Pt(firstSize, gtx.Constraints.Max.Y))
} else {
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, firstSize))
}
first(gtx)
stack.Load()
}
{
gtx := gtx
stack := op.Save(gtx.Ops)
if s.Axis == layout.Horizontal {
op.Offset(f32.Pt(float32(secondOffset), 0)).Add(gtx.Ops)
gtx.Constraints = layout.Exact(image.Pt(secondSize, gtx.Constraints.Max.Y))
} else {
op.Offset(f32.Pt(0, float32(secondOffset))).Add(gtx.Ops)
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, secondSize))
}
second(gtx)
stack.Load()
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}

51
tracker/gioui/surface.go Normal file
View File

@ -0,0 +1,51 @@
package gioui
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)
}

75
tracker/gioui/theme.go Normal file
View File

@ -0,0 +1,75 @@
package gioui
import (
"image/color"
"gioui.org/font/gofont"
"gioui.org/text"
"gioui.org/unit"
)
var fontCollection []text.FontFace = gofont.Collection()
var textShaper = text.NewCache(fontCollection)
var white = color.NRGBA{R: 255, G: 255, B: 255, A: 255}
var black = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
var transparent = color.NRGBA{A: 0}
var primaryColor = color.NRGBA{R: 206, G: 147, B: 216, A: 255}
var secondaryColor = color.NRGBA{R: 128, G: 222, B: 234, A: 255}
var highEmphasisTextColor = color.NRGBA{R: 222, G: 222, B: 222, A: 222}
var mediumEmphasisTextColor = color.NRGBA{R: 153, G: 153, B: 153, A: 153}
var disabledTextColor = color.NRGBA{R: 255, G: 255, B: 255, A: 97}
var backgroundColor = color.NRGBA{R: 18, G: 18, B: 18, A: 255}
var labelDefaultColor = highEmphasisTextColor
var labelDefaultBgColor = transparent
var labelDefaultFont = fontCollection[6].Font
var labelDefaultFontSize = unit.Sp(18)
var rowMarkerSurfaceColor = color.NRGBA{R: 0, G: 0, B: 0, A: 0}
var rowMarkerPatternTextColor = secondaryColor
var rowMarkerRowTextColor = mediumEmphasisTextColor
var trackerFont = fontCollection[6].Font
var trackerFontSize = unit.Px(16)
var trackerInactiveTextColor = highEmphasisTextColor
var trackerActiveTextColor = color.NRGBA{R: 255, G: 255, B: 130, A: 255}
var trackerPlayColor = color.NRGBA{R: 55, G: 55, B: 61, A: 255}
var trackerPatMarker = primaryColor
var oneBeatHighlight = color.NRGBA{R: 31, G: 37, B: 38, A: 255}
var twoBeatHighlight = color.NRGBA{R: 31, G: 51, B: 53, A: 255}
var patternPlayColor = color.NRGBA{R: 55, G: 55, B: 61, A: 255}
var patternTextColor = primaryColor
var instrumentHoverColor = color.NRGBA{R: 30, G: 31, B: 38, A: 255}
var instrumentNameColor = color.NRGBA{R: 255, G: 255, B: 255, A: 255}
var instrumentNameHintColor = color.NRGBA{R: 200, G: 200, B: 200, A: 255}
var songSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, 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 dragListSelectedColor = color.NRGBA{R: 55, G: 55, B: 61, A: 255}
var dragListHoverColor = 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}
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
var menuHoverColor = color.NRGBA{R: 30, G: 31, B: 38, A: 255}
var scrollBarColor = color.NRGBA{R: 255, G: 255, B: 255, A: 32}
var warningColor = color.NRGBA{R: 251, G: 192, B: 45, A: 255}

276
tracker/gioui/track.go Normal file
View File

@ -0,0 +1,276 @@
package gioui
import (
"fmt"
"image"
"strings"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
)
const trackRowHeight = 16
const trackColWidth = 54
const patmarkWidth = 16
var trackPointerTag bool
var trackJumpPointerTag bool
func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
rowMarkers := layout.Rigid(t.layoutRowMarkers)
for t.NewTrackBtn.Clicked() {
t.AddTrack(true)
}
//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.CanAddTrack() {
newTrackBtnStyle.Color = primaryColor
} else {
newTrackBtnStyle.Color = disabledTextColor
}
in := layout.UniformInset(unit.Dp(1))
octave := func(gtx C) D {
t.OctaveNumberInput.Value = t.Octave()
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, 0, 9)
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
dims := in.Layout(gtx, numStyle.Layout)
t.SetOctave(t.OctaveNumberInput.Value)
return dims
}
n := t.Song().Score.Tracks[t.Cursor().Track].NumVoices
t.TrackVoices.Value = n
voiceUpDown := func(gtx C) D {
numStyle := NumericUpDown(t.Theme, t.TrackVoices, 1, t.MaxTrackVoices())
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
return in.Layout(gtx, numStyle.Layout)
}
t.TrackHexCheckBox.Value = t.Song().Score.Tracks[t.Cursor().Track].Effect
hexCheckBoxStyle := material.CheckBox(t.Theme, t.TrackHexCheckBox, "Hex")
dims := 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.Rigid(hexCheckBoxStyle.Layout),
layout.Rigid(Label(" Voices:", white)),
layout.Rigid(voiceUpDown),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(newTrackBtnStyle.Layout))
t.Song().Score.Tracks[t.Cursor().Track].Effect = t.TrackHexCheckBox.Value // TODO: we should not modify the model, but how should this be done
t.SetTrackVoices(t.TrackVoices.Value)
return dims
}
for _, ev := range gtx.Events(&trackPointerTag) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
if e.Type == pointer.Press {
t.SetEditMode(tracker.EditTracks)
}
}
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
pointer.Rect(rect).Add(gtx.Ops)
pointer.InputOp{Tag: &trackPointerTag,
Types: pointer.Press,
}.Add(gtx.Ops)
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return Surface{Gray: 37, Focus: t.EditMode() == tracker.EditTracks, FitSize: true}.Layout(gtx, menu)
}),
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
rowMarkers,
layout.Flexed(1, t.layoutTracks))
}),
)
}
func (t *Tracker) layoutTracks(gtx C) D {
defer op.Save(gtx.Ops).Load()
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
for _, ev := range gtx.Events(&trackJumpPointerTag) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
if e.Type == pointer.Press {
t.SetEditMode(tracker.EditTracks)
track := int(e.Position.X) / trackColWidth
row := int((e.Position.Y-float32(gtx.Constraints.Max.Y-trackRowHeight)/2)/trackRowHeight + float32(cursorSongRow))
cursor := tracker.SongPoint{Track: track, SongRow: tracker.SongRow{Row: row}}.Clamp(t.Song().Score)
t.SetCursor(cursor)
t.SetSelectionCorner(cursor)
cursorSongRow = cursor.Pattern*t.Song().Score.RowsPerPattern + cursor.Row
}
}
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
pointer.Rect(rect).Add(gtx.Ops)
pointer.InputOp{Tag: &trackJumpPointerTag,
Types: pointer.Press,
}.Add(gtx.Ops)
op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
if t.EditMode() == tracker.EditPatterns || t.EditMode() == tracker.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().Score.RowsPerPattern
x2 *= trackColWidth
y2 *= trackRowHeight * t.Song().Score.RowsPerPattern
paint.FillShape(gtx.Ops, inactiveSelectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
}
if t.EditMode() == tracker.EditTracks {
x1, y1 := t.Cursor().Track, t.Cursor().Pattern*t.Song().Score.RowsPerPattern+t.Cursor().Row
x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern*t.Song().Score.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().Score.RowsPerPattern + t.Cursor().Row) * trackRowHeight
cw := trackColWidth
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
cw /= 2
if t.LowNibble() {
cx += cw
}
}
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{Min: image.Pt(cx, cy), Max: image.Pt(cx+cw, 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().Score.LengthInRows(); lastRow >= l {
lastRow = l - 1
}
op.Offset(f32.Pt(0, float32(trackRowHeight*firstRow))).Add(gtx.Ops)
for _, trk := range t.Song().Score.Tracks {
stack := op.Save(gtx.Ops)
for row := firstRow; row <= lastRow; row++ {
pat := row / t.Song().Score.RowsPerPattern
patRow := row % t.Song().Score.RowsPerPattern
s := -1
if pat >= 0 && pat < len(trk.Order) {
s = trk.Order[pat]
}
if s < 0 {
op.Offset(f32.Pt(0, trackRowHeight)).Add(gtx.Ops)
continue
}
if s >= 0 && 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() == tracker.EditTracks && t.Cursor().Row == patRow && t.Cursor().Pattern == pat {
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
} else {
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
}
var c byte = 1
if s >= 0 && s < len(trk.Patterns) {
pattern := trk.Patterns[s]
if patRow >= 0 && patRow < len(pattern) {
c = pattern[patRow]
}
}
if trk.Effect {
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, tracker.NoteStr(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}
}

172
tracker/gioui/tracker.go Normal file
View File

@ -0,0 +1,172 @@
package gioui
import (
"encoding/json"
"errors"
"fmt"
"gioui.org/font/gofont"
"gioui.org/layout"
"gioui.org/text"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/tracker"
"gopkg.in/yaml.v3"
)
type Tracker struct {
Theme *material.Theme
MenuBar []widget.Clickable
Menus []Menu
OctaveNumberInput *NumberInput
BPM *NumberInput
RowsPerPattern *NumberInput
RowsPerBeat *NumberInput
Step *NumberInput
InstrumentVoices *NumberInput
TrackVoices *NumberInput
InstrumentNameEditor *widget.Editor
NewTrackBtn *widget.Clickable
NewInstrumentBtn *widget.Clickable
DeleteInstrumentBtn *widget.Clickable
AddSemitoneBtn *widget.Clickable
SubtractSemitoneBtn *widget.Clickable
AddOctaveBtn *widget.Clickable
SubtractOctaveBtn *widget.Clickable
SongLength *NumberInput
PanicBtn *widget.Clickable
CopyInstrumentBtn *widget.Clickable
ParameterList *layout.List
ParameterScrollBar *ScrollBar
Parameters []*ParameterWidget
UnitDragList *DragList
UnitScrollBar *ScrollBar
DeleteUnitBtn *widget.Clickable
ClearUnitBtn *widget.Clickable
ChooseUnitTypeList *layout.List
ChooseUnitScrollBar *ScrollBar
ChooseUnitTypeBtns []*widget.Clickable
AddUnitBtn *widget.Clickable
InstrumentDragList *DragList
InstrumentScrollBar *ScrollBar
TrackHexCheckBox *widget.Bool
TopHorizontalSplit *Split
BottomHorizontalSplit *Split
VerticalSplit *Split
StackUse []int
KeyPlaying map[string]uint32
Alert Alert
lastVolume tracker.Volume
volumeChan chan tracker.Volume
player *tracker.Player
refresh chan struct{}
playerCloser chan struct{}
audioContext sointu.AudioContext
*tracker.Model
}
func (t *Tracker) UnmarshalContent(bytes []byte) error {
var instr sointu.Instrument
if errJSON := json.Unmarshal(bytes, &instr); errJSON == nil {
if t.SetInstrument(instr) {
return nil
}
}
if errYaml := yaml.Unmarshal(bytes, &instr); errYaml == nil {
if t.SetInstrument(instr) {
return nil
}
}
var song sointu.Song
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
if song.BPM > 0 {
t.SetSong(song)
return nil
}
return errors.New("was able to unmarshal a song, but the bpm was 0")
}
func (t *Tracker) Close() {
t.playerCloser <- struct{}{}
t.audioContext.Close()
}
func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker {
t := &Tracker{
Theme: material.NewTheme(gofont.Collection()),
audioContext: audioContext,
BPM: new(NumberInput),
OctaveNumberInput: &NumberInput{Value: 4},
SongLength: new(NumberInput),
RowsPerPattern: new(NumberInput),
RowsPerBeat: new(NumberInput),
Step: &NumberInput{Value: 1},
InstrumentVoices: new(NumberInput),
TrackVoices: new(NumberInput),
InstrumentNameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
NewTrackBtn: new(widget.Clickable),
NewInstrumentBtn: new(widget.Clickable),
DeleteInstrumentBtn: new(widget.Clickable),
AddSemitoneBtn: new(widget.Clickable),
SubtractSemitoneBtn: new(widget.Clickable),
AddOctaveBtn: new(widget.Clickable),
SubtractOctaveBtn: new(widget.Clickable),
AddUnitBtn: new(widget.Clickable),
DeleteUnitBtn: new(widget.Clickable),
ClearUnitBtn: new(widget.Clickable),
PanicBtn: new(widget.Clickable),
CopyInstrumentBtn: new(widget.Clickable),
TrackHexCheckBox: new(widget.Bool),
Menus: make([]Menu, 2),
MenuBar: make([]widget.Clickable, 2),
UnitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}, HoverItem: -1},
UnitScrollBar: &ScrollBar{Axis: layout.Vertical},
refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
InstrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}, HoverItem: -1},
InstrumentScrollBar: &ScrollBar{Axis: layout.Horizontal},
ParameterList: &layout.List{Axis: layout.Vertical},
ParameterScrollBar: &ScrollBar{Axis: layout.Vertical},
TopHorizontalSplit: &Split{Ratio: -.6},
BottomHorizontalSplit: &Split{Ratio: -.6},
VerticalSplit: &Split{Axis: layout.Vertical},
ChooseUnitTypeList: &layout.List{Axis: layout.Vertical},
ChooseUnitScrollBar: &ScrollBar{Axis: layout.Vertical},
KeyPlaying: make(map[string]uint32),
volumeChan: make(chan tracker.Volume, 1),
playerCloser: make(chan struct{}),
}
t.Model = tracker.NewModel()
vuBufferObserver := make(chan []float32)
go tracker.VuAnalyzer(0.3, 1e-4, 1, -100, vuBufferObserver, t.volumeChan)
t.Theme.Palette.Fg = primaryColor
t.Theme.Palette.ContrastFg = black
t.SetEditMode(tracker.EditTracks)
for range tracker.UnitTypeNames {
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
}
t.SetOctave(4)
patchObserver := make(chan sointu.Patch, 16)
t.AddPatchObserver(patchObserver)
scoreObserver := make(chan sointu.Score, 16)
t.AddScoreObserver(scoreObserver)
sprObserver := make(chan int, 16)
t.AddSamplesPerRowObserver(sprObserver)
audioChannel := make(chan []float32)
t.player = tracker.NewPlayer(synthService, t.playerCloser, patchObserver, scoreObserver, sprObserver, t.refresh, audioChannel, vuBufferObserver)
audioOut := audioContext.Output()
go func() {
for buf := range audioChannel {
audioOut.WriteAudio(buf)
}
}()
t.ResetSong()
return t
}

146
tracker/gioui/uniteditor.go Normal file
View File

@ -0,0 +1,146 @@
package gioui
import (
"image"
"image/color"
"strings"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
)
func (t *Tracker) layoutUnitEditor(gtx C) D {
editorFunc := t.layoutUnitSliders
if t.Unit().Type == "" {
editorFunc = t.layoutUnitTypeChooser
}
return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.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 {
numItems := t.NumParams()
for len(t.Parameters) <= numItems {
t.Parameters = append(t.Parameters, new(ParameterWidget))
}
listItem := func(gtx C, index int) D {
for t.Parameters[index].Clicked() {
if t.EditMode() != tracker.EditParameters || t.ParamIndex() != index {
t.SetEditMode(tracker.EditParameters)
t.SetParamIndex(index)
} else {
t.ResetParam()
}
}
param, err := t.Param(index)
if err != nil {
return D{}
}
oldVal := param.Value
paramStyle := t.ParamStyle(t.Theme, &param, t.Parameters[index])
paramStyle.Focus = t.EditMode() == tracker.EditParameters && t.ParamIndex() == index
dims := paramStyle.Layout(gtx)
if oldVal != param.Value {
t.SetEditMode(tracker.EditParameters)
t.SetParamIndex(index)
t.SetParam(param.Value)
}
return dims
}
return layout.Stack{}.Layout(gtx,
layout.Stacked(func(gtx C) D {
return t.ParameterList.Layout(gtx, numItems, listItem)
}),
layout.Stacked(func(gtx C) D {
gtx.Constraints.Min = gtx.Constraints.Max
return t.ParameterScrollBar.Layout(gtx, unit.Dp(10), numItems, &t.ParameterList.Position)
}))
}
func (t *Tracker) layoutUnitFooter() layout.Widget {
return func(gtx C) D {
for t.ClearUnitBtn.Clicked() {
t.SetUnitType("")
op.InvalidateOp{}.Add(gtx.Ops)
}
for t.DeleteUnitBtn.Clicked() {
t.DeleteUnit(false)
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 t.CanDeleteUnit() {
deleteUnitBtnStyle.Color = primaryColor
} else {
deleteUnitBtnStyle.Color = disabledTextColor
}
text := t.Unit().Type
if text == "" {
text = "Choose unit type"
} else {
text = strings.Title(text)
}
hintText := Label(text, white)
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(deleteUnitBtnStyle.Layout),
layout.Rigid(func(gtx C) D {
var dims D
if t.Unit().Type != "" {
clearUnitBtnStyle := material.IconButton(t.Theme, t.ClearUnitBtn, widgetForIcon(icons.ContentClear))
clearUnitBtnStyle.Color = primaryColor
clearUnitBtnStyle.Background = transparent
clearUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
dims = clearUnitBtnStyle.Layout(gtx)
}
return D{Size: image.Pt(gtx.Px(unit.Dp(48)), dims.Size.Y)}
}),
layout.Flexed(1, hintText),
)
}
}
func (t *Tracker) layoutUnitTypeChooser(gtx C) D {
listElem := func(gtx C, i int) D {
for t.ChooseUnitTypeBtns[i].Clicked() {
t.SetUnitType(tracker.UnitTypeNames[i])
}
labelStyle := LabelStyle{Text: tracker.UnitTypeNames[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)}
bg := func(gtx C) D {
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, 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}
}
leftMargin := layout.Inset{Left: unit.Dp(10)}
return layout.Stack{Alignment: layout.W}.Layout(gtx,
layout.Stacked(bg),
layout.Expanded(func(gtx C) D {
return leftMargin.Layout(gtx, labelStyle.Layout)
}),
layout.Expanded(t.ChooseUnitTypeBtns[i].Layout))
}
return layout.Stack{}.Layout(gtx,
layout.Stacked(func(gtx C) D {
return t.ChooseUnitTypeList.Layout(gtx, len(tracker.UnitTypeNames), listElem)
}),
layout.Expanded(func(gtx C) D {
return t.ChooseUnitScrollBar.Layout(gtx, unit.Dp(10), len(tracker.UnitTypeNames), &t.ChooseUnitTypeList.Position)
}),
)
}

47
tracker/gioui/vumeter.go Normal file
View File

@ -0,0 +1,47 @@
package gioui
import (
"image"
"gioui.org/f32"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"github.com/vsariola/sointu/tracker"
)
type VuMeter struct {
Volume tracker.Volume
Range float32
}
func (v VuMeter) Layout(gtx C) D {
defer op.Save(gtx.Ops).Load()
gtx.Constraints.Max.Y = gtx.Px(unit.Dp(12))
height := gtx.Px(unit.Dp(6))
for j := 0; j < 2; j++ {
value := v.Volume.Average[j] + v.Range
if value > 0 {
x := int(value/v.Range*float32(gtx.Constraints.Max.X) + 0.5)
if x > gtx.Constraints.Max.X {
x = gtx.Constraints.Max.X
}
paint.FillShape(gtx.Ops, mediumEmphasisTextColor, clip.Rect(image.Rect(0, 0, x, height)).Op())
}
valueMax := v.Volume.Peak[j] + v.Range
if valueMax > 0 {
color := white
if valueMax >= v.Range {
color = errorColor
}
x := int(valueMax/v.Range*float32(gtx.Constraints.Max.X) + 0.5)
if x > gtx.Constraints.Max.X {
x = gtx.Constraints.Max.X
}
paint.FillShape(gtx.Ops, color, clip.Rect(image.Rect(x-1, 0, x, height)).Op())
}
op.Offset(f32.Pt(0, float32(height))).Add(gtx.Ops)
}
return D{Size: gtx.Constraints.Max}
}