mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
385 lines
13 KiB
Go
385 lines
13 KiB
Go
package gioui
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gioui.org/io/key"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"github.com/vsariola/sointu/tracker"
|
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
|
)
|
|
|
|
const trackRowHeight = unit.Dp(16)
|
|
const trackColWidth = unit.Dp(54)
|
|
const trackColTitleHeight = unit.Dp(16)
|
|
const trackPatMarkWidth = unit.Dp(25)
|
|
const trackRowMarkWidth = unit.Dp(25)
|
|
|
|
var noteStr [256]string
|
|
var hexStr [256]string
|
|
|
|
func init() {
|
|
// initialize these strings once, so we don't have to do it every time we draw the note editor
|
|
hexStr[0] = "--"
|
|
hexStr[1] = ".."
|
|
noteStr[0] = "---"
|
|
noteStr[1] = "..."
|
|
for i := 2; i < 256; i++ {
|
|
hexStr[i] = fmt.Sprintf("%02x", i)
|
|
oNote := mod(i-baseNote, 12)
|
|
octave := (i - oNote - baseNote) / 12
|
|
switch {
|
|
case octave < 0:
|
|
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('Z'+1+octave)))
|
|
case octave >= 10:
|
|
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('A'+octave-10)))
|
|
default:
|
|
noteStr[i] = fmt.Sprintf("%s%d", notes[oNote], octave)
|
|
}
|
|
}
|
|
}
|
|
|
|
type NoteEditor struct {
|
|
TrackVoices *NumberInput
|
|
NewTrackBtn *ActionClickable
|
|
DeleteTrackBtn *ActionClickable
|
|
AddSemitoneBtn *ActionClickable
|
|
SubtractSemitoneBtn *ActionClickable
|
|
AddOctaveBtn *ActionClickable
|
|
SubtractOctaveBtn *ActionClickable
|
|
NoteOffBtn *ActionClickable
|
|
EffectBtn *BoolClickable
|
|
|
|
scrollTable *ScrollTable
|
|
tag struct{}
|
|
}
|
|
|
|
func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
|
return &NoteEditor{
|
|
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
|
|
NewTrackBtn: NewActionClickable(model.AddTrack()),
|
|
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
|
|
AddSemitoneBtn: NewActionClickable(model.AddSemitone()),
|
|
SubtractSemitoneBtn: NewActionClickable(model.SubtractSemitone()),
|
|
AddOctaveBtn: NewActionClickable(model.AddOctave()),
|
|
SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()),
|
|
NoteOffBtn: NewActionClickable(model.EditNoteOff()),
|
|
EffectBtn: NewBoolClickable(model.Effect().Bool()),
|
|
scrollTable: NewScrollTable(
|
|
model.Notes().Table(),
|
|
model.Tracks().List(),
|
|
model.NoteRows().List(),
|
|
),
|
|
}
|
|
}
|
|
|
|
func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
|
for {
|
|
e, ok := gtx.Event(
|
|
key.Filter{Focus: te.scrollTable, Name: "A"},
|
|
key.Filter{Focus: te.scrollTable, Name: "B"},
|
|
key.Filter{Focus: te.scrollTable, Name: "C"},
|
|
key.Filter{Focus: te.scrollTable, Name: "D"},
|
|
key.Filter{Focus: te.scrollTable, Name: "E"},
|
|
key.Filter{Focus: te.scrollTable, Name: "F"},
|
|
key.Filter{Focus: te.scrollTable, Name: "G"},
|
|
key.Filter{Focus: te.scrollTable, Name: "H"},
|
|
key.Filter{Focus: te.scrollTable, Name: "I"},
|
|
key.Filter{Focus: te.scrollTable, Name: "J"},
|
|
key.Filter{Focus: te.scrollTable, Name: "K"},
|
|
key.Filter{Focus: te.scrollTable, Name: "L"},
|
|
key.Filter{Focus: te.scrollTable, Name: "M"},
|
|
key.Filter{Focus: te.scrollTable, Name: "N"},
|
|
key.Filter{Focus: te.scrollTable, Name: "O"},
|
|
key.Filter{Focus: te.scrollTable, Name: "P"},
|
|
key.Filter{Focus: te.scrollTable, Name: "Q"},
|
|
key.Filter{Focus: te.scrollTable, Name: "R"},
|
|
key.Filter{Focus: te.scrollTable, Name: "S"},
|
|
key.Filter{Focus: te.scrollTable, Name: "T"},
|
|
key.Filter{Focus: te.scrollTable, Name: "U"},
|
|
key.Filter{Focus: te.scrollTable, Name: "V"},
|
|
key.Filter{Focus: te.scrollTable, Name: "W"},
|
|
key.Filter{Focus: te.scrollTable, Name: "X"},
|
|
key.Filter{Focus: te.scrollTable, Name: "Y"},
|
|
key.Filter{Focus: te.scrollTable, Name: "Z"},
|
|
key.Filter{Focus: te.scrollTable, Name: "0"},
|
|
key.Filter{Focus: te.scrollTable, Name: "1"},
|
|
key.Filter{Focus: te.scrollTable, Name: "2"},
|
|
key.Filter{Focus: te.scrollTable, Name: "3"},
|
|
key.Filter{Focus: te.scrollTable, Name: "4"},
|
|
key.Filter{Focus: te.scrollTable, Name: "5"},
|
|
key.Filter{Focus: te.scrollTable, Name: "6"},
|
|
key.Filter{Focus: te.scrollTable, Name: "7"},
|
|
key.Filter{Focus: te.scrollTable, Name: "8"},
|
|
key.Filter{Focus: te.scrollTable, Name: "9"},
|
|
key.Filter{Focus: te.scrollTable, Name: ","},
|
|
key.Filter{Focus: te.scrollTable, Name: "."},
|
|
)
|
|
if !ok {
|
|
break
|
|
}
|
|
switch e := e.(type) {
|
|
case key.Event:
|
|
if e.State == key.Release {
|
|
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
|
noteID.NoteOff()
|
|
delete(t.KeyPlaying, e.Name)
|
|
}
|
|
continue
|
|
}
|
|
te.command(gtx, t, e)
|
|
}
|
|
}
|
|
|
|
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
|
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
|
|
|
return Surface{Gray: 24, Focus: te.scrollTable.Focused()}.Layout(gtx, func(gtx C) D {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(func(gtx C) D {
|
|
return te.layoutButtons(gtx, t)
|
|
}),
|
|
layout.Flexed(1, func(gtx C) D {
|
|
return te.layoutTracks(gtx, t)
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
|
|
func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
|
return Surface{Gray: 37, Focus: te.scrollTable.Focused() || te.scrollTable.ChildFocused(), FitSize: true}.Layout(gtx, func(gtx C) D {
|
|
addSemitoneBtnStyle := ActionButton(gtx, t.Theme, te.AddSemitoneBtn, "+1")
|
|
subtractSemitoneBtnStyle := ActionButton(gtx, t.Theme, te.SubtractSemitoneBtn, "-1")
|
|
addOctaveBtnStyle := ActionButton(gtx, t.Theme, te.AddOctaveBtn, "+12")
|
|
subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, te.SubtractOctaveBtn, "-12")
|
|
noteOffBtnStyle := ActionButton(gtx, t.Theme, te.NoteOffBtn, "Note Off")
|
|
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, "Delete track\n(Ctrl+Shift+T)")
|
|
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, "Add track\n(Ctrl+T)")
|
|
in := layout.UniformInset(unit.Dp(1))
|
|
voiceUpDown := func(gtx C) D {
|
|
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
|
|
return in.Layout(gtx, numStyle.Layout)
|
|
}
|
|
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
|
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
|
layout.Rigid(addSemitoneBtnStyle.Layout),
|
|
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
|
layout.Rigid(addOctaveBtnStyle.Layout),
|
|
layout.Rigid(subtractOctaveBtnStyle.Layout),
|
|
layout.Rigid(noteOffBtnStyle.Layout),
|
|
layout.Rigid(effectBtnStyle.Layout),
|
|
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
|
|
layout.Rigid(voiceUpDown),
|
|
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
|
layout.Rigid(deleteTrackBtnStyle.Layout),
|
|
layout.Rigid(newTrackBtnStyle.Layout))
|
|
})
|
|
}
|
|
|
|
const baseNote = 24
|
|
|
|
var notes = []string{
|
|
"C-",
|
|
"C#",
|
|
"D-",
|
|
"D#",
|
|
"E-",
|
|
"F-",
|
|
"F#",
|
|
"G-",
|
|
"G#",
|
|
"A-",
|
|
"A#",
|
|
"B-",
|
|
}
|
|
|
|
func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
|
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
|
|
|
beatMarkerDensity := t.RowsPerBeat().Value()
|
|
switch beatMarkerDensity {
|
|
case 0, 1, 2:
|
|
beatMarkerDensity = 4
|
|
}
|
|
|
|
playSongRow := t.PlaySongRow()
|
|
pxWidth := gtx.Dp(trackColWidth)
|
|
pxHeight := gtx.Dp(trackRowHeight)
|
|
pxPatMarkWidth := gtx.Dp(trackPatMarkWidth)
|
|
pxRowMarkWidth := gtx.Dp(trackRowMarkWidth)
|
|
|
|
colTitle := func(gtx C, i int) D {
|
|
h := gtx.Dp(unit.Dp(trackColTitleHeight))
|
|
title := ((*tracker.Order)(t.Model)).Title(i)
|
|
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
|
|
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
|
return D{Size: image.Pt(pxWidth, h)}
|
|
}
|
|
|
|
rowTitleBg := func(gtx C, j int) D {
|
|
if mod(j, beatMarkerDensity*2) == 0 {
|
|
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
|
} else if mod(j, beatMarkerDensity) == 0 {
|
|
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
|
}
|
|
if t.SongPanel.PlayingBtn.Bool.Value() && j == playSongRow {
|
|
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
|
}
|
|
return D{}
|
|
}
|
|
|
|
rowTitle := func(gtx C, j int) D {
|
|
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
|
pat := j / rpp
|
|
row := j % rpp
|
|
w := pxPatMarkWidth + pxRowMarkWidth
|
|
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
|
if row == 0 {
|
|
color := rowMarkerPatternTextColor
|
|
if l := t.Loop(); pat >= l.Start && pat < l.Start+l.Length {
|
|
color = loopMarkerColor
|
|
}
|
|
paint.ColorOp{Color: color}.Add(gtx.Ops)
|
|
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", pat)), op.CallOp{})
|
|
}
|
|
defer op.Offset(image.Pt(pxPatMarkWidth, 0)).Push(gtx.Ops).Pop()
|
|
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
|
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", row)), op.CallOp{})
|
|
return D{Size: image.Pt(w, pxHeight)}
|
|
}
|
|
|
|
drawSelection := te.scrollTable.Table.Cursor() != te.scrollTable.Table.Cursor2()
|
|
selection := te.scrollTable.Table.Range()
|
|
|
|
cell := func(gtx C, x, y int) D {
|
|
// draw the background, to indicate selection
|
|
color := transparent
|
|
point := tracker.Point{X: x, Y: y}
|
|
if drawSelection && selection.Contains(point) {
|
|
color = inactiveSelectionColor
|
|
if te.scrollTable.Focused() {
|
|
color = selectionColor
|
|
}
|
|
}
|
|
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
|
// draw the cursor
|
|
if point == te.scrollTable.Table.Cursor() {
|
|
cw := gtx.Constraints.Min.X
|
|
cx := 0
|
|
if t.Model.Notes().Effect(x) {
|
|
cw /= 2
|
|
if t.Model.Notes().LowNibble() {
|
|
cx += cw
|
|
}
|
|
}
|
|
c := inactiveSelectionColor
|
|
if te.scrollTable.Focused() {
|
|
c = cursorColor
|
|
}
|
|
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
|
|
}
|
|
// draw the pattern marker
|
|
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
|
pat := y / rpp
|
|
row := y % rpp
|
|
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
|
s := t.Model.Order().Value(tracker.Point{X: x, Y: pat})
|
|
if row == 0 { // draw the pattern marker
|
|
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
|
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, patternIndexToString(s), op.CallOp{})
|
|
}
|
|
if row == 1 && t.Model.Notes().Unique(x, s) { // draw a * if the pattern is unique
|
|
paint.ColorOp{Color: mediumEmphasisTextColor}.Add(gtx.Ops)
|
|
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, "*", op.CallOp{})
|
|
}
|
|
if te.scrollTable.Table.Cursor() == point && te.scrollTable.Focused() {
|
|
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
|
} else {
|
|
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
|
|
}
|
|
val := noteStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
|
if t.Model.Notes().Effect(x) {
|
|
val = hexStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
|
}
|
|
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, val, op.CallOp{})
|
|
return D{Size: image.Pt(pxWidth, pxHeight)}
|
|
}
|
|
table := FilledScrollTable(t.Theme, te.scrollTable, cell, colTitle, rowTitle, nil, rowTitleBg)
|
|
table.RowTitleWidth = trackPatMarkWidth + trackRowMarkWidth
|
|
table.ColumnTitleHeight = trackColTitleHeight
|
|
table.CellWidth = trackColWidth
|
|
table.CellHeight = trackRowHeight
|
|
return table.Layout(gtx)
|
|
}
|
|
|
|
func mod(x, d int) int {
|
|
x = x % d
|
|
if x >= 0 {
|
|
return x
|
|
}
|
|
if d < 0 {
|
|
return x - d
|
|
}
|
|
return x + d
|
|
}
|
|
|
|
func noteAsValue(octave, note int) byte {
|
|
return byte(baseNote + (octave * 12) + note)
|
|
}
|
|
|
|
func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) {
|
|
if e.Name == "A" || e.Name == "1" {
|
|
t.Model.Notes().Table().Fill(0)
|
|
te.scrollTable.EnsureCursorVisible()
|
|
return
|
|
}
|
|
var n byte
|
|
if t.Model.Notes().Effect(te.scrollTable.Table.Cursor().X) {
|
|
if nibbleValue, err := strconv.ParseInt(string(e.Name), 16, 8); err == nil {
|
|
n = t.Model.Notes().Value(te.scrollTable.Table.Cursor())
|
|
t.Model.Notes().FillNibble(byte(nibbleValue), t.Model.Notes().LowNibble())
|
|
goto validNote
|
|
}
|
|
} else {
|
|
if val, ok := noteMap[e.Name]; ok {
|
|
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
|
t.Model.Notes().Table().Fill(int(n))
|
|
goto validNote
|
|
}
|
|
}
|
|
return
|
|
validNote:
|
|
te.scrollTable.EnsureCursorVisible()
|
|
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
|
trk := te.scrollTable.Table.Cursor().X
|
|
t.KeyPlaying[e.Name] = t.TrackNoteOn(trk, n)
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
case "+":
|
|
if e.Modifiers.Contain(key.ModShortcut) {
|
|
te.AddOctaveBtn.Action.Do()
|
|
} else {
|
|
te.AddSemitoneBtn.Action.Do()
|
|
}
|
|
case "-":
|
|
if e.Modifiers.Contain(key.ModShortcut) {
|
|
te.SubtractSemitoneBtn.Action.Do()
|
|
} else {
|
|
te.SubtractOctaveBtn.Action.Do()
|
|
}
|
|
}*/
|