mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-19 05:24:48 -04:00
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:
116
tracker/gioui/alert.go
Normal file
116
tracker/gioui/alert.go
Normal 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
153
tracker/gioui/draglist.go
Normal 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
51
tracker/gioui/files.go
Normal 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)
|
||||
}
|
22
tracker/gioui/iconcache.go
Normal file
22
tracker/gioui/iconcache.go
Normal 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
|
||||
}
|
264
tracker/gioui/instruments.go
Normal file
264
tracker/gioui/instruments.go
Normal 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
454
tracker/gioui/keyevent.go
Normal 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
52
tracker/gioui/label.go
Normal 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
39
tracker/gioui/layout.go
Normal 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
151
tracker/gioui/menu.go
Normal 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,
|
||||
}
|
||||
}
|
194
tracker/gioui/numericupdown.go
Normal file
194
tracker/gioui/numericupdown.go
Normal 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
205
tracker/gioui/parameter.go
Normal 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
86
tracker/gioui/patterns.go
Normal 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
84
tracker/gioui/popup.go
Normal 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
|
||||
}
|
72
tracker/gioui/rowmarkers.go
Normal file
72
tracker/gioui/rowmarkers.go
Normal 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
69
tracker/gioui/run.go
Normal 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
128
tracker/gioui/scrollbar.go
Normal 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
195
tracker/gioui/songpanel.go
Normal 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
144
tracker/gioui/split.go
Normal 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
51
tracker/gioui/surface.go
Normal 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
75
tracker/gioui/theme.go
Normal 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
276
tracker/gioui/track.go
Normal 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
172
tracker/gioui/tracker.go
Normal 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
146
tracker/gioui/uniteditor.go
Normal 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, ¶m, 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
47
tracker/gioui/vumeter.go
Normal 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}
|
||||
}
|
Reference in New Issue
Block a user