mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-27 19:00:25 -04:00
257 lines
7.3 KiB
Go
257 lines
7.3 KiB
Go
package gioui
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"strings"
|
|
"time"
|
|
|
|
"gioui.org/io/clipboard"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"github.com/vsariola/sointu"
|
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type ParamEditor struct {
|
|
list *layout.List
|
|
scrollBar *ScrollBar
|
|
Parameters []*ParameterWidget
|
|
DeleteUnitBtn *TipClickable
|
|
CopyUnitBtn *TipClickable
|
|
ClearUnitBtn *TipClickable
|
|
ChooseUnitTypeBtns []*widget.Clickable
|
|
tag bool
|
|
focused bool
|
|
requestFocus bool
|
|
}
|
|
|
|
func (pe *ParamEditor) Focus() {
|
|
pe.requestFocus = true
|
|
}
|
|
|
|
func (pe *ParamEditor) Focused() bool {
|
|
return pe.focused
|
|
}
|
|
|
|
func NewParamEditor() *ParamEditor {
|
|
ret := &ParamEditor{
|
|
DeleteUnitBtn: new(TipClickable),
|
|
ClearUnitBtn: new(TipClickable),
|
|
CopyUnitBtn: new(TipClickable),
|
|
list: &layout.List{Axis: layout.Vertical},
|
|
scrollBar: &ScrollBar{Axis: layout.Vertical},
|
|
}
|
|
for range sointu.UnitNames {
|
|
ret.ChooseUnitTypeBtns = append(ret.ChooseUnitTypeBtns, new(widget.Clickable))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (pe *ParamEditor) Bind(t *Tracker) layout.Widget {
|
|
return func(gtx C) D {
|
|
for _, e := range gtx.Events(&pe.tag) {
|
|
switch e := e.(type) {
|
|
case key.FocusEvent:
|
|
pe.focused = e.Focus
|
|
case pointer.Event:
|
|
if e.Type == pointer.Press {
|
|
key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops)
|
|
}
|
|
case key.Event:
|
|
if e.Modifiers.Contain(key.ModShortcut) {
|
|
continue
|
|
}
|
|
switch e.State {
|
|
case key.Press:
|
|
switch e.Name {
|
|
case key.NameUpArrow:
|
|
t.SetParamIndex(t.ParamIndex() - 1)
|
|
case key.NameDownArrow:
|
|
t.SetParamIndex(t.ParamIndex() + 1)
|
|
case key.NameLeftArrow:
|
|
p, err := t.Param(t.ParamIndex())
|
|
if err != nil {
|
|
break
|
|
}
|
|
if e.Modifiers.Contain(key.ModShift) {
|
|
t.SetParam(p.Value - p.LargeStep)
|
|
} else {
|
|
t.SetParam(p.Value - 1)
|
|
}
|
|
case key.NameRightArrow:
|
|
p, err := t.Param(t.ParamIndex())
|
|
if err != nil {
|
|
break
|
|
}
|
|
if e.Modifiers.Contain(key.ModShift) {
|
|
t.SetParam(p.Value + p.LargeStep)
|
|
} else {
|
|
t.SetParam(p.Value + 1)
|
|
}
|
|
case key.NameEscape:
|
|
t.InstrumentEditor.unitDragList.Focus()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if pe.requestFocus {
|
|
pe.requestFocus = false
|
|
key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops)
|
|
}
|
|
editorFunc := pe.layoutUnitSliders
|
|
if y := t.Unit().Type; y == "" || y != t.InstrumentEditor.unitTypeEditor.Text() {
|
|
editorFunc = pe.layoutUnitTypeChooser
|
|
}
|
|
return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D {
|
|
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Flexed(1, func(gtx C) D {
|
|
return editorFunc(gtx, t)
|
|
}),
|
|
layout.Rigid(pe.layoutUnitFooter(t)))
|
|
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
|
area := clip.Rect(rect).Push(gtx.Ops)
|
|
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
|
pointer.InputOp{Tag: &pe.tag,
|
|
Types: pointer.Press,
|
|
}.Add(gtx.Ops)
|
|
key.InputOp{Tag: &pe.tag, Keys: "←|Shift-←|→|Shift-→|↑|↓|⎋"}.Add(gtx.Ops)
|
|
area.Pop()
|
|
return ret
|
|
})
|
|
}
|
|
}
|
|
|
|
func (pe *ParamEditor) layoutUnitSliders(gtx C, t *Tracker) D {
|
|
numItems := t.NumParams()
|
|
|
|
for len(pe.Parameters) <= numItems {
|
|
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
|
|
}
|
|
|
|
listItem := func(gtx C, index int) D {
|
|
for pe.Parameters[index].Clicked() {
|
|
if t.ParamIndex() != index {
|
|
t.SetParamIndex(index)
|
|
} else {
|
|
t.ResetParam()
|
|
}
|
|
pe.Focus()
|
|
}
|
|
param, err := t.Param(index)
|
|
if err != nil {
|
|
return D{}
|
|
}
|
|
oldVal := param.Value
|
|
paramStyle := t.ParamStyle(t.Theme, ¶m, pe.Parameters[index])
|
|
paramStyle.Focus = pe.focused && t.ParamIndex() == index
|
|
dims := paramStyle.Layout(gtx)
|
|
if oldVal != param.Value {
|
|
pe.Focus()
|
|
t.SetParamIndex(index)
|
|
t.SetParam(param.Value)
|
|
}
|
|
return dims
|
|
}
|
|
|
|
return layout.Stack{}.Layout(gtx,
|
|
layout.Stacked(func(gtx C) D {
|
|
return pe.list.Layout(gtx, numItems, listItem)
|
|
}),
|
|
layout.Stacked(func(gtx C) D {
|
|
gtx.Constraints.Min = gtx.Constraints.Max
|
|
return pe.scrollBar.Layout(gtx, unit.Dp(10), numItems, &pe.list.Position)
|
|
}))
|
|
}
|
|
|
|
func (pe *ParamEditor) layoutUnitFooter(t *Tracker) layout.Widget {
|
|
return func(gtx C) D {
|
|
for pe.ClearUnitBtn.Clickable.Clicked() {
|
|
t.SetUnitType("")
|
|
op.InvalidateOp{}.Add(gtx.Ops)
|
|
t.InstrumentEditor.unitDragList.Focus()
|
|
}
|
|
for pe.DeleteUnitBtn.Clickable.Clicked() {
|
|
t.DeleteUnits(false, t.UnitIndex(), t.UnitIndex())
|
|
op.InvalidateOp{}.Add(gtx.Ops)
|
|
t.InstrumentEditor.unitDragList.Focus()
|
|
}
|
|
for pe.CopyUnitBtn.Clickable.Clicked() {
|
|
op.InvalidateOp{}.Add(gtx.Ops)
|
|
contents, err := yaml.Marshal([]sointu.Unit{t.Unit()})
|
|
if err == nil {
|
|
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
|
t.Alert.Update("Unit copied to clipboard", Notify, time.Second*3)
|
|
}
|
|
}
|
|
copyUnitBtnStyle := IconButton(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, true, "Copy unit (Ctrl+C)")
|
|
deleteUnitBtnStyle := IconButton(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, t.CanDeleteUnit(), "Delete unit (Del)")
|
|
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(copyUnitBtnStyle.Layout),
|
|
layout.Rigid(func(gtx C) D {
|
|
var dims D
|
|
if t.Unit().Type != "" {
|
|
clearUnitBtnStyle := IconButton(t.Theme, pe.ClearUnitBtn, icons.ContentClear, true, "Clear unit")
|
|
dims = clearUnitBtnStyle.Layout(gtx)
|
|
}
|
|
return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)}
|
|
}),
|
|
layout.Flexed(1, hintText),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (pe *ParamEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D {
|
|
listElem := func(gtx C, i int) D {
|
|
for pe.ChooseUnitTypeBtns[i].Clicked() {
|
|
t.SetUnitType(sointu.UnitNames[i])
|
|
t.InstrumentEditor.unitTypeEditor.SetText(sointu.UnitNames[i])
|
|
}
|
|
text := sointu.UnitNames[i]
|
|
if t.InstrumentEditor.unitTypeEditor.Focused() && !strings.HasPrefix(text, t.InstrumentEditor.unitTypeEditor.Text()) {
|
|
return D{}
|
|
}
|
|
labelStyle := LabelStyle{Text: text, 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 pe.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 pe.ChooseUnitTypeBtns[i].Layout(gtx, func(gtx C) D {
|
|
return leftMargin.Layout(gtx, labelStyle.Layout)
|
|
})
|
|
}))
|
|
}
|
|
return layout.Stack{}.Layout(gtx,
|
|
layout.Stacked(func(gtx C) D {
|
|
return pe.list.Layout(gtx, len(sointu.UnitNames), listElem)
|
|
}),
|
|
layout.Expanded(func(gtx C) D {
|
|
return pe.scrollBar.Layout(gtx, unit.Dp(10), len(sointu.UnitNames), &pe.list.Position)
|
|
}),
|
|
)
|
|
}
|