mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-13 11:43:19 -05: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