feat!: display the parameters as knobs in a grid

Also removed the negbandpass & neghighpass parameters
and replaced them with bandpass & highpass set to -1, to
fit the switches better to the GUI.

Closes #51, closes #173
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-26 10:38:38 +03:00
parent c3caa8de11
commit 666af9433e
116 changed files with 3663 additions and 2049 deletions

View File

@ -111,6 +111,8 @@ func (s FilledDragListStyle) Layout(gtx C, element, bg func(gtx C, i int) D) D {
case key.FocusEvent:
if !ke.Focus {
s.dragList.TrackerList.SetSelected2(s.dragList.TrackerList.Selected())
} else {
s.dragList.EnsureVisible(s.dragList.TrackerList.Selected())
}
case key.Event:
if ke.State != key.Press {

View File

@ -93,3 +93,6 @@
- { key: "P", action: "Note28" }
- { key: "+", action: "Increase" }
- { key: "-", action: "Decrease" }
- { key: "+", shortcut: true, action: "IncreaseMore" } # increase a large step
- { key: "-", shortcut: true, action: "DecreaseMore" } # decrease a large step

View File

@ -120,7 +120,9 @@ func (oe *OrderEditor) Layout(gtx C) D {
table := FilledScrollTable(t.Theme, oe.scrollTable)
table.ColumnTitleHeight = orderTitleHeight
return table.Layout(gtx, cell, colTitle, rowTitle, nil, rowTitleBg)
return Surface{Gray: 24, Focus: oe.scrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
return table.Layout(gtx, cell, colTitle, rowTitle, nil, rowTitleBg)
})
}
func (oe *OrderEditor) handleEvents(gtx C, t *Tracker) {

428
tracker/gioui/param.go Normal file
View File

@ -0,0 +1,428 @@
package gioui
import (
"image"
"image/color"
"math"
"strconv"
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/event"
"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/x/stroke"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
)
type (
ParamState struct {
drag gesture.Drag
dragStartPt f32.Point // used to calculate the drag amount
dragStartVal int
tipArea TipArea
click gesture.Click
clickable Clickable
}
ParamWidget struct {
Parameter tracker.Parameter
State *ParamState
Theme *Theme
Focus bool
Disabled bool
}
PortStyle struct {
Diameter unit.Dp
StrokeWidth unit.Dp
Color color.NRGBA
}
PortWidget struct {
Theme *Theme
Style *PortStyle
State *ParamState
}
KnobStyle struct {
Diameter unit.Dp
StrokeWidth unit.Dp
Bg color.NRGBA
Pos struct {
Color color.NRGBA
Bg color.NRGBA
}
Neg struct {
Color color.NRGBA
Bg color.NRGBA
}
Indicator struct {
Color color.NRGBA
Width unit.Dp
InnerDiam unit.Dp
OuterDiam unit.Dp
}
Value LabelStyle
Title LabelStyle
}
KnobWidget struct {
Theme *Theme
Value tracker.Parameter
State *ParamState
Style *KnobStyle
Hint string
Scroll bool
}
SwitchStyle struct {
Neutral struct {
Fg color.NRGBA
Bg color.NRGBA
}
Pos struct {
Fg color.NRGBA
Bg color.NRGBA
}
Neg struct {
Fg color.NRGBA
Bg color.NRGBA
}
Width unit.Dp
Height unit.Dp
Outline unit.Dp
Handle unit.Dp
Icon unit.Dp
}
SwitchWidget struct {
Theme *Theme
Value tracker.Parameter
State *ParamState
Style *SwitchStyle
Hint string
Scroll bool
Disabled bool
}
)
// ParamState
func Param(Parameter tracker.Parameter, th *Theme, paramWidget *ParamState, focus, disabled bool) ParamWidget {
return ParamWidget{
Theme: th,
State: paramWidget,
Parameter: Parameter,
Focus: focus,
Disabled: disabled,
}
}
func (p ParamWidget) Layout(gtx C) D {
title := Label(p.Theme, &p.Theme.UnitEditor.Name, p.Parameter.Name())
t := TrackerFromContext(gtx)
widget := func(gtx C) D {
if port, ok := p.Parameter.Port(); t.IsChoosingSendTarget() && ok {
for p.State.clickable.Clicked(gtx) {
t.ChooseSendTarget(p.Parameter.UnitID(), port).Do()
}
k := Port(p.Theme, p.State)
return k.Layout(gtx)
}
switch p.Parameter.Type() {
case tracker.IntegerParameter:
k := Knob(p.Parameter, p.Theme, p.State, p.Parameter.Hint().Label, p.Focus, p.Disabled)
return k.Layout(gtx)
case tracker.BoolParameter:
s := Switch(p.Parameter, p.Theme, p.State, p.Parameter.Hint().Label, p.Focus, p.Disabled)
return s.Layout(gtx)
case tracker.IDParameter:
for p.State.clickable.Clicked(gtx) {
t.ChooseSendSource(p.Parameter.UnitID()).Do()
}
btn := Btn(t.Theme, &t.Theme.Button.Text, &p.State.clickable, "Set", p.Parameter.Hint().Label)
if p.Disabled {
btn.Style = &t.Theme.Button.Disabled
}
return btn.Layout(gtx)
}
if _, ok := p.Parameter.Port(); ok {
k := Port(p.Theme, p.State)
return k.Layout(gtx)
}
return D{}
}
title.Layout(gtx)
layout.Center.Layout(gtx, widget)
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
}
func (s *ParamState) update(gtx C, param tracker.Parameter, scroll bool) {
for {
p, ok := s.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
switch p.Kind {
case pointer.Press:
s.dragStartPt = p.Position
s.dragStartVal = param.Value()
case pointer.Drag:
// update the value based on the drag amount
m := param.Range()
d := p.Position.Sub(s.dragStartPt)
amount := float32(d.X-d.Y) / float32(gtx.Dp(128))
newValue := int(float32(s.dragStartVal) + amount*float32(m.Max-m.Min))
param.SetValue(newValue)
s.tipArea.Appear(gtx.Now)
}
}
for {
g, ok := s.click.Update(gtx.Source)
if !ok {
break
}
if g.Kind == gesture.KindClick && g.NumClicks > 1 {
param.Reset()
}
}
for scroll {
e, ok := gtx.Event(pointer.Filter{
Target: s,
Kinds: pointer.Scroll,
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
})
if !ok {
break
}
if ev, ok := e.(pointer.Event); ok && ev.Kind == pointer.Scroll {
delta := -int(math.Min(math.Max(float64(ev.Scroll.Y), -1), 1))
param.Add(delta, ev.Modifiers.Contain(key.ModShortcut))
s.tipArea.Appear(gtx.Now)
}
}
}
// KnobWidget
func Knob(v tracker.Parameter, th *Theme, state *ParamState, hint string, scroll, disabled bool) KnobWidget {
ret := KnobWidget{
Theme: th,
Value: v,
State: state,
Style: &th.Knob,
Hint: hint,
Scroll: scroll,
}
if disabled {
ret.Style = &th.DisabledKnob
}
return ret
}
func (k *KnobWidget) Layout(gtx C) D {
k.State.update(gtx, k.Value, k.Scroll)
knob := func(gtx C) D {
m := k.Value.Range()
amount := float32(k.Value.Value()-m.Min) / float32(m.Max-m.Min)
sw := gtx.Dp(k.Style.StrokeWidth)
d := gtx.Dp(k.Style.Diameter)
defer clip.Rect(image.Rectangle{Max: image.Pt(d, d)}).Push(gtx.Ops).Pop()
event.Op(gtx.Ops, k.State)
k.State.drag.Add(gtx.Ops)
k.State.click.Add(gtx.Ops)
middle := float32(k.Value.Neutral()-m.Min) / float32(m.Max-m.Min)
pos := max(amount, middle)
neg := min(amount, middle)
if middle > 0 {
k.strokeKnobArc(gtx, k.Style.Neg.Bg, sw, d, 0, neg)
}
if middle < 1 {
k.strokeKnobArc(gtx, k.Style.Pos.Bg, sw, d, pos, 1)
}
if pos > middle {
k.strokeKnobArc(gtx, k.Style.Pos.Color, sw, d, middle, pos)
}
if neg < middle {
k.strokeKnobArc(gtx, k.Style.Neg.Color, sw, d, neg, middle)
}
k.strokeIndicator(gtx, amount)
return D{Size: image.Pt(d, d)}
}
label := Label(k.Theme, &k.Style.Value, strconv.Itoa(k.Value.Value()))
w := func(gtx C) D {
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Stacked(knob),
layout.Stacked(label.Layout))
}
if k.Hint != "" {
c := gtx.Constraints
gtx.Constraints.Max = image.Pt(1e6, 1e6)
return k.State.tipArea.Layout(gtx, Tooltip(k.Theme, k.Hint), func(gtx C) D {
gtx.Constraints = c
return w(gtx)
})
}
return w(gtx)
}
func (k *KnobWidget) strokeKnobArc(gtx C, color color.NRGBA, strokeWidth, diameter int, start, end float32) {
rad := float32(diameter) / 2
end = min(max(end, 0), 1)
if end <= 0 {
return
}
startAngle := float64((start*8 + 1) / 10 * 2 * math.Pi)
deltaAngle := (end - start) * 8 * math.Pi / 5
center := f32.Point{X: rad, Y: rad}
r2 := rad - float32(strokeWidth)/2
startPt := f32.Point{X: rad - r2*float32(math.Sin(startAngle)), Y: rad + r2*float32(math.Cos(startAngle))}
segments := [...]stroke.Segment{
stroke.MoveTo(startPt),
stroke.ArcTo(center, deltaAngle),
}
s := stroke.Stroke{
Path: stroke.Path{Segments: segments[:]},
Width: float32(strokeWidth),
Cap: stroke.FlatCap,
}
paint.FillShape(gtx.Ops, color, s.Op(gtx.Ops))
}
func (k *KnobWidget) strokeIndicator(gtx C, amount float32) {
innerRad := float32(gtx.Dp(k.Style.Indicator.InnerDiam)) / 2
outerRad := float32(gtx.Dp(k.Style.Indicator.OuterDiam)) / 2
center := float32(gtx.Dp(k.Style.Diameter)) / 2
angle := (float64(amount)*8 + 1) / 10 * 2 * math.Pi
start := f32.Point{
X: center - innerRad*float32(math.Sin(angle)),
Y: center + innerRad*float32(math.Cos(angle)),
}
end := f32.Point{
X: center - outerRad*float32(math.Sin(angle)),
Y: center + outerRad*float32(math.Cos(angle)),
}
segments := [...]stroke.Segment{
stroke.MoveTo(start),
stroke.LineTo(end),
}
s := stroke.Stroke{
Path: stroke.Path{Segments: segments[:]},
Width: float32(k.Style.Indicator.Width),
Cap: stroke.FlatCap,
}
paint.FillShape(gtx.Ops, k.Style.Indicator.Color, s.Op(gtx.Ops))
}
// SwitchWidget
func Switch(v tracker.Parameter, th *Theme, state *ParamState, hint string, scroll, disabled bool) SwitchWidget {
return SwitchWidget{
Theme: th,
Value: v,
State: state,
Style: &th.Switch,
Hint: hint,
Scroll: scroll,
Disabled: disabled,
}
}
func (s *SwitchWidget) Layout(gtx C) D {
s.State.update(gtx, s.Value, s.Scroll)
width := gtx.Dp(s.Style.Width)
height := gtx.Dp(s.Style.Height)
var fg, bg color.NRGBA
o := 0
switch {
case s.Disabled || s.Value.Value() == 0:
fg = s.Style.Neutral.Fg
bg = s.Style.Neutral.Bg
o = gtx.Dp(s.Style.Outline)
case s.Value.Value() < 0:
fg = s.Style.Neg.Fg
bg = s.Style.Neg.Bg
case s.Value.Value() > 0:
fg = s.Style.Pos.Fg
bg = s.Style.Pos.Bg
}
r := min(width, height) / 2
fillRoundRect := func(ops *op.Ops, rect image.Rectangle, r int, c color.NRGBA) {
defer clip.UniformRRect(rect, r).Push(ops).Pop()
paint.ColorOp{Color: c}.Add(ops)
paint.PaintOp{}.Add(ops)
}
if o > 0 {
fillRoundRect(gtx.Ops, image.Rect(0, 0, width, height), r, fg)
}
fillRoundRect(gtx.Ops, image.Rect(o, o, width-o, height-o), r-o, bg)
a := r
b := width - r
p := a + (b-a)*(s.Value.Value()-s.Value.Range().Min)/(s.Value.Range().Max-s.Value.Range().Min)
circle := func(x, y, r int) clip.Op {
b := image.Rectangle{
Min: image.Pt(x-r, y-r),
Max: image.Pt(x+r, y+r),
}
return clip.Ellipse(b).Op(gtx.Ops)
}
paint.FillShape(gtx.Ops, fg, circle(p, height/2, gtx.Dp(s.Style.Handle)/2))
defer clip.Rect(image.Rectangle{Max: image.Pt(width, height)}).Push(gtx.Ops).Pop()
event.Op(gtx.Ops, s.State)
s.State.drag.Add(gtx.Ops)
s.State.click.Add(gtx.Ops)
icon := icons.NavigationClose
if s.Value.Range().Min < 0 {
if s.Value.Value() < 0 {
icon = icons.ImageExposureNeg1
} else if s.Value.Value() > 0 {
icon = icons.ImageExposurePlus1
}
} else if s.Value.Value() > 0 {
icon = icons.NavigationCheck
}
w := s.Theme.Icon(icon)
i := gtx.Dp(s.Style.Icon)
defer op.Offset(image.Pt(p-i/2, (height-i)/2)).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(i, i))
w.Layout(gtx, bg)
return D{Size: image.Pt(width, height)}
}
//
func Port(t *Theme, p *ParamState) PortWidget {
return PortWidget{Theme: t, Style: &t.Port, State: p}
}
func (p *PortWidget) Layout(gtx C) D {
return p.State.clickable.layout(p.State, gtx, func(gtx C) D {
d := gtx.Dp(p.Style.Diameter)
defer clip.Rect(image.Rectangle{Max: image.Pt(d, d)}).Push(gtx.Ops).Pop()
p.strokeCircle(gtx)
return D{Size: image.Pt(d, d)}
})
}
func (p *PortWidget) strokeCircle(gtx C) {
sw := float32(gtx.Dp(p.Style.StrokeWidth))
d := float32(gtx.Dp(p.Style.Diameter))
rad := d / 2
center := f32.Point{X: rad, Y: rad}
var path clip.Path
path.Begin(gtx.Ops)
path.MoveTo(f32.Pt(sw/2, rad))
path.ArcTo(center, center, float32(math.Pi*2))
paint.FillShape(gtx.Ops, p.Style.Color,
clip.Stroke{
Path: path.End(),
Width: sw,
}.Op())
}

View File

@ -2,7 +2,6 @@ package gioui
import (
"bytes"
"fmt"
"image"
"image/color"
"io"
@ -371,79 +370,45 @@ func MakeUnitList(m *tracker.Model) UnitList {
func (ul *UnitList) Layout(gtx C) D {
t := TrackerFromContext(gtx)
ul.update(gtx, t)
var units [256]tracker.UnitListItem
for i, item := range (*tracker.Units)(t.Model).Iterate {
if i >= 256 {
break
}
units[i] = item
}
count := min(ul.dragList.TrackerList.Count(), 256)
element := func(gtx C, i int) D {
gtx.Constraints.Max.Y = gtx.Dp(20)
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
if i < 0 || i > 255 {
return layout.Dimensions{Size: gtx.Constraints.Min}
}
u := units[i]
u := t.Units().Item(i)
editorStyle := t.Theme.InstrumentEditor.UnitList.Name
if u.Disabled {
signalError := t.RailError()
switch {
case u.Disabled:
editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled
}
stackText := strconv.FormatInt(int64(u.StackAfter), 10)
if u.StackNeed > u.StackBefore {
case signalError.Err != nil && signalError.UnitIndex == i:
editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Error
(*tracker.Alerts)(t.Model).AddNamed("UnitNeedsInputs", fmt.Sprintf("%v needs at least %v input signals, got %v", u.Type, u.StackNeed, u.StackBefore), tracker.Error)
} else if i == count-1 && u.StackAfter != 0 {
editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Warning
(*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning)
}
stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText)
rightMargin := layout.Inset{Right: unit.Dp(10)}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(func(gtx C) D {
if i == ul.dragList.TrackerList.Selected() {
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
str := t.Model.UnitSearch()
for ev := ul.searchEditor.Update(gtx, str); ev != EditorEventNone; ev = ul.searchEditor.Update(gtx, str) {
if ev == EditorEventSubmit {
if str.Value() != "" {
for _, n := range sointu.UnitNames {
if strings.HasPrefix(n, str.Value()) {
t.Units().SetSelectedType(n)
break
}
}
} else {
t.Units().SetSelectedType("")
}
}
ul.dragList.Focus()
t.UnitSearching().SetValue(false)
}
return ul.searchEditor.Layout(gtx, str, t.Theme, &editorStyle, "---")
} else {
text := u.Type
if text == "" {
text = "---"
}
l := editorStyle.AsLabelStyle()
return Label(t.Theme, &l, text).Layout(gtx)
unitName := func(gtx C) D {
if i == ul.dragList.TrackerList.Selected() {
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
return ul.searchEditor.Layout(gtx, t.Model.UnitSearch(), t.Theme, &editorStyle, "---")
} else {
text := u.Type
if text == "" {
text = "---"
}
}),
layout.Flexed(1, func(gtx C) D {
unitNameLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment)
inset := layout.Inset{Left: unit.Dp(5)}
return inset.Layout(gtx, unitNameLabel.Layout)
}),
layout.Rigid(func(gtx C) D {
return rightMargin.Layout(gtx, stackLabel.Layout)
}),
l := editorStyle.AsLabelStyle()
return Label(t.Theme, &l, text).Layout(gtx)
}
}
stackText := strconv.FormatInt(int64(u.Signals.StackAfter()), 10)
commentLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment)
stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText)
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(unitName),
layout.Rigid(layout.Spacer{Width: 5}.Layout),
layout.Flexed(1, commentLabel.Layout),
layout.Rigid(stackLabel.Layout),
layout.Rigid(layout.Spacer{Width: 10}.Layout),
)
}
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
unitList := FilledDragList(t.Theme, ul.dragList)
return Surface{Gray: 30, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
surface := func(gtx C) D {
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
layout.Expanded(func(gtx C) D {
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
@ -458,7 +423,8 @@ func (ul *UnitList) Layout(gtx C) D {
return margin.Layout(gtx, addUnitBtn.Layout)
}),
)
})
}
return Surface{Gray: 30, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, surface)
}
func (ul *UnitList) update(gtx C, t *Tracker) {
@ -478,7 +444,7 @@ func (ul *UnitList) update(gtx C, t *Tracker) {
if e, ok := event.(key.Event); ok && e.State == key.Press {
switch e.Name {
case key.NameRightArrow:
t.PatchPanel.unitEditor.sliderList.Focus()
t.PatchPanel.unitEditor.paramTable.RowTitleList.Focus()
case key.NameDeleteBackward:
t.Units().SetSelectedType("")
t.UnitSearching().SetValue(true)
@ -490,6 +456,23 @@ func (ul *UnitList) update(gtx C, t *Tracker) {
}
}
}
str := t.Model.UnitSearch()
for ev := ul.searchEditor.Update(gtx, str); ev != EditorEventNone; ev = ul.searchEditor.Update(gtx, str) {
if ev == EditorEventSubmit {
if str.Value() != "" {
for _, n := range sointu.UnitNames {
if strings.HasPrefix(n, str.Value()) {
t.Units().SetSelectedType(n)
break
}
}
} else {
t.Units().SetSelectedType("")
}
}
ul.dragList.Focus()
t.UnitSearching().SetValue(false)
}
}
func (ul *UnitList) Tags(curLevel int, yield TagYieldFunc) bool {

View File

@ -62,7 +62,7 @@ func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *Scro
}
for k, a := range keyBindingMap {
switch a {
case "Copy", "Paste", "Cut", "Increase", "Decrease":
case "Copy", "Paste", "Cut", "Increase", "Decrease", "IncreaseMore", "DecreaseMore":
ret.eventFilters = append(ret.eventFilters, key.Filter{Focus: ret, Name: k.Name, Required: k.Modifiers})
}
}
@ -117,18 +117,18 @@ func (s ScrollTableStyle) Layout(gtx C, element func(gtx C, x, y int) D, colTitl
p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight))
s.handleEvents(gtx, p)
return Surface{Gray: 24, Focus: s.ScrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
dims := gtx.Constraints.Max
s.layoutColTitles(gtx, p, colTitle, colTitleBg)
s.layoutRowTitles(gtx, p, rowTitle, rowTitleBg)
defer op.Offset(p).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
s.layoutTable(gtx, element)
s.RowTitleStyle.LayoutScrollBar(gtx)
s.ColTitleStyle.LayoutScrollBar(gtx)
return D{Size: dims}
})
//return Surface{Gray: 24, Focus: s.ScrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
dims := gtx.Constraints.Max
s.layoutColTitles(gtx, p, colTitle, colTitleBg)
s.layoutRowTitles(gtx, p, rowTitle, rowTitleBg)
defer op.Offset(p).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
s.layoutTable(gtx, element)
s.RowTitleStyle.LayoutScrollBar(gtx)
s.ColTitleStyle.LayoutScrollBar(gtx)
return D{Size: dims}
//})
}
func (s *ScrollTableStyle) handleEvents(gtx layout.Context, p image.Point) {
@ -176,12 +176,17 @@ func (s *ScrollTableStyle) handleEvents(gtx layout.Context, p image.Point) {
}
case key.Event:
if e.State == key.Press {
s.ScrollTable.command(gtx, e)
s.ScrollTable.command(gtx, e, p)
}
case transfer.DataEvent:
if b, err := io.ReadAll(e.Open()); err == nil {
s.ScrollTable.Table.Paste(b)
}
case key.FocusEvent:
if e.Focus {
s.ScrollTable.ColTitleList.EnsureVisible(s.ScrollTable.Table.Cursor().X)
s.ScrollTable.RowTitleList.EnsureVisible(s.ScrollTable.Table.Cursor().Y)
}
}
}
@ -250,7 +255,7 @@ func (s *ScrollTableStyle) layoutColTitles(gtx C, p image.Point, fg, bg func(gtx
s.ColTitleStyle.Layout(gtx, fg, bg)
}
func (s *ScrollTable) command(gtx C, e key.Event) {
func (s *ScrollTable) command(gtx C, e key.Event, p image.Point) {
stepX := 1
stepY := 1
if e.Modifiers.Contain(key.ModAlt) {
@ -265,13 +270,13 @@ func (s *ScrollTable) command(gtx C, e key.Event) {
s.Table.Clear()
return
case key.NameUpArrow:
if !s.Table.MoveCursor(0, -stepY) && stepY == 1 {
if !s.Table.MoveCursor(0, -stepY) && stepY == 1 && p.Y > 0 {
s.ColTitleList.Focus()
}
case key.NameDownArrow:
s.Table.MoveCursor(0, stepY)
case key.NameLeftArrow:
if !s.Table.MoveCursor(-stepX, 0) && stepX == 1 {
if !s.Table.MoveCursor(-stepX, 0) && stepX == 1 && p.X > 0 {
s.RowTitleList.Focus()
}
case key.NameRightArrow:
@ -300,11 +305,11 @@ func (s *ScrollTable) command(gtx C, e key.Event) {
case "Paste":
gtx.Execute(clipboard.ReadCmd{Tag: s})
return
case "Increase":
s.Table.Add(1)
case "Increase", "IncreaseMore":
s.Table.Add(1, a == "IncreaseMore")
return
case "Decrease":
s.Table.Add(-1)
case "Decrease", "DecreaseMore":
s.Table.Add(-1, a == "DecreaseMore")
return
}
}

View File

@ -0,0 +1,106 @@
package gioui
import (
"image"
"image/color"
"math"
"gioui.org/f32"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"github.com/vsariola/sointu/tracker"
)
const maxSignalsDrawn = 16
type (
RailStyle struct {
Color color.NRGBA
LineWidth unit.Dp
SignalWidth unit.Dp
PortDiameter unit.Dp
PortColor color.NRGBA
}
RailWidget struct {
Style *RailStyle
Signal tracker.Rail
Height unit.Dp
}
)
func Rail(th *Theme, signal tracker.Rail) RailWidget {
return RailWidget{
Style: &th.SignalRail,
Signal: signal,
Height: th.UnitEditor.Height,
}
}
func (s RailWidget) Layout(gtx C) D {
sw := gtx.Dp(s.Style.SignalWidth)
h := gtx.Dp(s.Height)
if s.Signal.PassThrough == 0 && len(s.Signal.StackUse.Inputs) == 0 && s.Signal.StackUse.NumOutputs == 0 {
return D{Size: image.Pt(sw, h)}
}
lw := gtx.Dp(s.Style.LineWidth)
pd := gtx.Dp(s.Style.PortDiameter)
center := sw / 2
var path clip.Path
path.Begin(gtx.Ops)
// Draw pass through signals
for i := range min(maxSignalsDrawn, s.Signal.PassThrough) {
x := float32(i*sw + center)
path.MoveTo(f32.Pt(x, 0))
path.LineTo(f32.Pt(x, float32(h)))
}
// Draw the routing of input signals
for i := range min(len(s.Signal.StackUse.Inputs), maxSignalsDrawn-s.Signal.PassThrough) {
input := s.Signal.StackUse.Inputs[i]
x1 := float32((i+s.Signal.PassThrough)*sw + center)
for _, link := range input {
x2 := float32((link+s.Signal.PassThrough)*sw + center)
path.MoveTo(f32.Pt(x1, 0))
path.LineTo(f32.Pt(x2, float32(h/2)))
}
}
if s.Signal.Send {
for i := range min(len(s.Signal.StackUse.Inputs), maxSignalsDrawn-s.Signal.PassThrough) {
d := gtx.Dp(8)
from := f32.Pt(float32((i+s.Signal.PassThrough)*sw+center), float32(h/2))
to := f32.Pt(float32(gtx.Constraints.Max.X), float32(h)-float32(d))
ctrl := f32.Pt(from.X, to.Y)
path.MoveTo(from)
path.QuadTo(ctrl, to)
}
}
// Draw the routing of output signals
for i := range min(s.Signal.StackUse.NumOutputs, maxSignalsDrawn-s.Signal.PassThrough) {
x := float32((i+s.Signal.PassThrough)*sw + center)
path.MoveTo(f32.Pt(x, float32(h/2)))
path.LineTo(f32.Pt(x, float32(h)))
}
// Signal paths finished
paint.FillShape(gtx.Ops, s.Style.Color,
clip.Stroke{
Path: path.End(),
Width: float32(lw),
}.Op())
// Draw the circles on signals that get modified
var circle clip.Path
circle.Begin(gtx.Ops)
for i := range min(len(s.Signal.StackUse.Modifies), maxSignalsDrawn-s.Signal.PassThrough) {
if !s.Signal.StackUse.Modifies[i] {
continue
}
f := f32.Pt(float32((i+s.Signal.PassThrough)*sw+center), float32(h/2))
circle.MoveTo(f32.Pt(f.X-float32(pd/2), float32(h/2)))
circle.ArcTo(f, f, float32(2*math.Pi))
}
p := clip.Outline{Path: circle.End()}.Op().Push(gtx.Ops)
paint.ColorOp{Color: s.Style.PortColor}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
p.Pop()
return D{Size: image.Pt(sw, h)}
}

View File

@ -5,6 +5,7 @@ import (
"image/color"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
@ -84,11 +85,23 @@ type Theme struct {
}
}
UnitEditor struct {
Hint LabelStyle
Name LabelStyle
Chooser LabelStyle
ParameterName LabelStyle
InvalidParam color.NRGBA
SendTarget color.NRGBA
Hint LabelStyle
WireColor color.NRGBA
WireHint LabelStyle
WireHighlight color.NRGBA
Width unit.Dp
Height unit.Dp
RackComment LabelStyle
UnitList struct {
LabelWidth unit.Dp
Name LabelStyle
Disabled LabelStyle
Error color.NRGBA
}
Error color.NRGBA
Divider color.NRGBA
}
Cursor CursorStyle
Selection CursorStyle
@ -100,8 +113,13 @@ type Theme struct {
Menu PopupStyle
Dialog PopupStyle
}
Split SplitStyle
ScrollBar ScrollBarStyle
Split SplitStyle
ScrollBar ScrollBarStyle
Knob KnobStyle
DisabledKnob KnobStyle
Switch SwitchStyle
SignalRail RailStyle
Port PortStyle
// iconCache is used to cache the icons created from iconvg data
iconCache map[*byte]*widget.Icon

View File

@ -178,12 +178,6 @@ instrumenteditor:
disabled: { textsize: 12, color: *disabled }
warning: *warningcolor
error: *errorcolor
uniteditor:
hint: { textsize: 16, color: *highemphasis, shadowcolor: *black }
chooser: { textsize: 12, color: *white, shadowcolor: *black }
parametername: { textsize: 16, color: *white, shadowcolor: *black }
invalidparam: { r: 120, g: 120, b: 120, a: 190 }
sendtarget: { r: 120, g: 120, b: 210, a: 255 }
cursor:
active: { r: 100, g: 140, b: 255, a: 48 }
activealt: { r: 255, g: 100, b: 140, a: 48 }
@ -211,3 +205,62 @@ dialog:
textinset: { top: 12, bottom: 12, left: 20, right: 20 }
buttons: *textbutton
split: { bar: 10, minsize1: 180, minsize2: 180 }
uniteditor:
hint: { textsize: 16, color: *highemphasis, shadowcolor: *black }
chooser: { textsize: 12, color: *white, shadowcolor: *black }
name:
{ textsize: 12, alignment: 2, color: *highemphasis, shadowcolor: *black }
wirecolor: *secondarycolor
wirehighlight: *white
wirehint: { textsize: 12, color: *disabled, shadowcolor: *black }
width: 60
height: 70
unitlist:
labelwidth: 16
name: { textsize: 12, color: *white, alignment: 2 }
disabled:
{ textsize: 12, color: *disabled, font: { style: 1 }, alignment: 2 }
error: *errorcolor
divider: { r: 255, g: 255, b: 255, a: 5 }
rackcomment: { textsize: 16, color: *mediumemphasis, shadowcolor: *black }
knob:
diameter: 36
value: { textsize: 12, color: *highemphasis }
strokewidth: 4
bg: { r: 40, g: 40, b: 40, a: 255 }
pos: { color: *primarycolor, bg: { r: 51, g: 36, b: 54, a: 255 } }
neg: { color: *secondarycolor, bg: { r: 32, g: 55, b: 58, a: 255 } }
indicator: { color: *white, width: 2, innerdiam: 24, outerdiam: 36 }
disabledknob:
diameter: 36
value: { textsize: 12, color: { r: 147, g: 143, b: 153, a: 255 }}
strokewidth: 4
bg: { r: 40, g: 40, b: 40, a: 255 }
pos: { color: { r: 147, g: 143, b: 153, a: 255 }, bg: { r: 54, g: 52, b: 59, a: 255 } }
neg: { color: { r: 147, g: 143, b: 153, a: 255 }, bg: { r: 54, g: 52, b: 59, a: 255 } }
indicator: { color: { r: 147, g: 143, b: 153, a: 255 }, width: 2, innerdiam: 24, outerdiam: 36 }
signalrail:
color: *secondarycolor
signalwidth: 10
linewidth: 2
portdiameter: 8
portcolor: *primarycolor
port:
diameter: 36
strokewidth: 4
color: { r: 32, g: 55, b: 58, a: 255 }
switch:
width: 36
height: 20
handle: 16
neutral:
fg: { r: 147, g: 143, b: 153, a: 255 }
bg: { r: 54, g: 52, b: 59, a: 255 }
pos:
fg: *white
bg: { r: 125, g: 87, b: 128, a: 255 }
neg:
fg: *white
bg: { r: 70, g: 128, b: 131, a: 255 }
icon: 10
outline: 1

View File

@ -2,47 +2,46 @@ package gioui
import (
"bytes"
"fmt"
"image"
"image/color"
"io"
"math"
"time"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type UnitEditor struct {
sliderList *DragList
searchList *DragList
Parameters []*ParameterWidget
DeleteUnitBtn *Clickable
CopyUnitBtn *Clickable
ClearUnitBtn *Clickable
DisableUnitBtn *Clickable
SelectTypeBtn *Clickable
commentEditor *Editor
caser cases.Caser
type (
UnitEditor struct {
paramTable *ScrollTable
searchList *DragList
Parameters [][]*ParamState
DeleteUnitBtn *Clickable
CopyUnitBtn *Clickable
ClearUnitBtn *Clickable
DisableUnitBtn *Clickable
SelectTypeBtn *Clickable
commentEditor *Editor
caser cases.Caser
copyHint string
disableUnitHint string
enableUnitHint string
copyHint string
disableUnitHint string
enableUnitHint string
searching tracker.Bool
}
searching tracker.Bool
}
)
func NewUnitEditor(m *tracker.Model) *UnitEditor {
ret := &UnitEditor{
@ -52,7 +51,7 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
CopyUnitBtn: new(Clickable),
SelectTypeBtn: new(Clickable),
commentEditor: NewEditor(true, true, text.Start),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
paramTable: NewScrollTable(m.Params().Table(), m.ParamVertList().List(), m.Units().List()),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
searching: m.UnitSearching(),
}
@ -66,8 +65,7 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
func (pe *UnitEditor) Layout(gtx C) D {
t := TrackerFromContext(gtx)
pe.update(gtx, t)
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
editorFunc := pe.layoutSliders
editorFunc := pe.layoutRack
if pe.showingChooser() {
editorFunc = pe.layoutUnitTypeChooser
}
@ -80,7 +78,7 @@ func (pe *UnitEditor) Layout(gtx C) D {
}
func (pe *UnitEditor) showingChooser() bool {
return pe.searching.Value() || pe.sliderList.TrackerList.Count() == 0
return pe.searching.Value()
}
func (pe *UnitEditor) update(gtx C, t *Tracker) {
@ -96,46 +94,82 @@ func (pe *UnitEditor) update(gtx C, t *Tracker) {
for pe.commentEditor.Update(gtx, t.UnitComment()) != EditorEventNone {
t.FocusPrev(gtx, false)
}
for pe.ClearUnitBtn.Clicked(gtx) {
t.ClearUnit().Do()
t.UnitSearch().SetValue("")
t.UnitSearching().SetValue(true)
pe.searchList.Focus()
}
for {
e, ok := gtx.Event(
key.Filter{Focus: pe.searchList, Name: key.NameEnter},
key.Filter{Focus: pe.searchList, Name: key.NameReturn},
key.Filter{Focus: pe.searchList, Name: key.NameEscape},
)
if !ok {
break
}
if e, ok := e.(key.Event); ok && e.State == key.Press {
pe.ChooseUnitType(t)
switch e.Name {
case key.NameEscape:
t.UnitSearching().SetValue(false)
pe.paramTable.RowTitleList.Focus()
case key.NameEnter, key.NameReturn:
pe.ChooseUnitType(t)
}
}
}
for {
e, ok := gtx.Event(
key.Filter{Focus: pe.sliderList, Name: key.NameLeftArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderList, Name: key.NameRightArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderList, Name: key.NameDeleteBackward},
key.Filter{Focus: pe.sliderList, Name: key.NameDeleteForward},
key.Filter{Focus: pe.paramTable, Name: key.NameLeftArrow, Required: key.ModShift, Optional: key.ModShortcut},
key.Filter{Focus: pe.paramTable, Name: key.NameRightArrow, Required: key.ModShift, Optional: key.ModShortcut},
key.Filter{Focus: pe.paramTable, Name: key.NameDeleteBackward},
key.Filter{Focus: pe.paramTable, Name: key.NameDeleteForward},
)
if !ok {
break
}
if e, ok := e.(key.Event); ok && e.State == key.Press {
params := t.Model.Params()
item := params.SelectedItem()
switch e.Name {
case key.NameLeftArrow:
if e.Modifiers.Contain(key.ModShift) {
item.SetValue(item.Value() - item.LargeStep())
} else {
item.SetValue(item.Value() - 1)
}
t.Model.Params().Table().Add(-1, e.Modifiers.Contain(key.ModShortcut))
case key.NameRightArrow:
if e.Modifiers.Contain(key.ModShift) {
item.SetValue(item.Value() + item.LargeStep())
} else {
item.SetValue(item.Value() + 1)
}
t.Model.Params().Table().Add(1, e.Modifiers.Contain(key.ModShortcut))
case key.NameDeleteBackward, key.NameDeleteForward:
item.Reset()
t.Model.Params().Table().Clear()
}
c := t.Model.Params().Cursor()
if c.X >= 0 && c.Y >= 0 && c.Y < len(pe.Parameters) && c.X < len(pe.Parameters[c.Y]) {
ta := &pe.Parameters[c.Y][c.X].tipArea
ta.Appear(gtx.Now)
ta.Exit.SetTarget(gtx.Now.Add(ta.ExitDuration))
}
}
}
for {
e, ok := gtx.Event(
key.Filter{Focus: pe.paramTable.RowTitleList, Name: key.NameEnter},
key.Filter{Focus: pe.paramTable.RowTitleList, Name: key.NameReturn},
key.Filter{Focus: pe.paramTable.RowTitleList, Name: key.NameLeftArrow},
key.Filter{Focus: pe.paramTable.RowTitleList, Name: key.NameDeleteBackward},
)
if !ok {
break
}
if e, ok := e.(key.Event); ok && e.State == key.Press {
switch e.Name {
case key.NameLeftArrow:
t.PatchPanel.unitList.dragList.Focus()
case key.NameDeleteBackward:
t.ClearUnit().Do()
t.UnitSearch().SetValue("")
t.UnitSearching().SetValue(true)
pe.searchList.Focus()
case key.NameEnter, key.NameReturn:
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
t.UnitSearch().SetValue("")
t.UnitSearching().SetValue(true)
pe.searchList.Focus()
}
}
}
@ -144,67 +178,237 @@ func (pe *UnitEditor) update(gtx C, t *Tracker) {
func (pe *UnitEditor) ChooseUnitType(t *Tracker) {
if ut, ok := t.SearchResults().Item(pe.searchList.TrackerList.Selected()); ok {
t.Units().SetSelectedType(ut)
t.PatchPanel.unitList.dragList.Focus()
pe.paramTable.RowTitleList.Focus()
}
}
func (pe *UnitEditor) layoutSliders(gtx C) D {
func (pe *UnitEditor) layoutRack(gtx C) D {
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
t := TrackerFromContext(gtx)
numItems := pe.sliderList.TrackerList.Count()
// create enough parameter widget to match the number of parameters
for len(pe.Parameters) < numItems {
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
width := pe.paramTable.Table.Width()
for len(pe.Parameters) < pe.paramTable.Table.Height() {
pe.Parameters = append(pe.Parameters, make([]*ParamState, 0))
}
index := 0
for param := range t.Model.Params().Iterate {
pe.Parameters[index].Parameter = param
index++
cellWidth := gtx.Dp(t.Theme.UnitEditor.Width)
cellHeight := gtx.Dp(t.Theme.UnitEditor.Height)
rowTitleLabelWidth := gtx.Dp(t.Theme.UnitEditor.UnitList.LabelWidth)
rowTitleSignalWidth := gtx.Dp(t.Theme.SignalRail.SignalWidth) * t.RailWidth()
rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth
signalError := t.RailError()
columnTitleHeight := gtx.Dp(0)
for i := range pe.Parameters {
for len(pe.Parameters[i]) < width {
pe.Parameters[i] = append(pe.Parameters[i], &ParamState{tipArea: TipArea{ExitDuration: time.Second * 2}})
}
}
element := func(gtx C, index int) D {
if index < 0 || index >= numItems {
coltitle := func(gtx C, x int) D {
return D{Size: image.Pt(cellWidth, columnTitleHeight)}
}
rowtitle := func(gtx C, y int) D {
if y < 0 || y >= len(pe.Parameters) {
return D{}
}
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index])
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index
dims := paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
item := t.Units().Item(y)
sr := Rail(t.Theme, item.Signals)
label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, item.Type)
switch {
case item.Disabled:
label.LabelStyle = t.Theme.UnitEditor.UnitList.Disabled
case signalError.Err != nil && signalError.UnitIndex == y:
label.Color = t.Theme.UnitEditor.UnitList.Error
}
gtx.Constraints = layout.Exact(image.Pt(rowTitleWidth, cellHeight))
sr.Layout(gtx)
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: float32(rowTitleSignalWidth), Y: float32(cellHeight)})).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(cellHeight, rowTitleLabelWidth))
label.Layout(gtx)
return D{Size: image.Pt(rowTitleWidth, cellHeight)}
}
cursor := t.Model.Params().Cursor()
cell := func(gtx C, x, y int) D {
gtx.Constraints = layout.Exact(image.Pt(cellWidth, cellHeight))
point := tracker.Point{X: x, Y: y}
if y < 0 || y >= len(pe.Parameters) || x < 0 || x >= len(pe.Parameters[y]) {
return D{}
}
selection := pe.paramTable.Table.Range()
if selection.Contains(point) {
color := t.Theme.Selection.Inactive
if gtx.Focused(pe.paramTable) {
color = t.Theme.Selection.Active
}
if point == cursor {
color = t.Theme.Cursor.Inactive
if gtx.Focused(pe.paramTable) {
color = t.Theme.Cursor.Active
}
}
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
}
fdl := FilledDragList(t.Theme, pe.sliderList)
dims := fdl.Layout(gtx, element, nil)
gtx.Constraints = layout.Exact(dims.Size)
fdl.LayoutScrollBar(gtx)
param := t.Model.Params().Item(point)
paramStyle := Param(param, t.Theme, pe.Parameters[y][x], pe.paramTable.Table.Cursor() == point, t.Units().Item(y).Disabled)
paramStyle.Layout(gtx)
comment := t.Units().Item(y).Comment
if comment != "" && x == t.Model.Params().RowWidth(y) {
label := Label(t.Theme, &t.Theme.UnitEditor.RackComment, comment)
return layout.W.Layout(gtx, func(gtx C) D {
gtx.Constraints.Max.X = 1e6
gtx.Constraints.Min.Y = 0
return label.Layout(gtx)
})
}
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
}
table := FilledScrollTable(t.Theme, pe.paramTable)
table.RowTitleWidth = gtx.Metric.PxToDp(rowTitleWidth)
table.ColumnTitleHeight = 0
table.CellWidth = t.Theme.UnitEditor.Width
table.CellHeight = t.Theme.UnitEditor.Height
pe.drawBackGround(gtx)
pe.drawSignals(gtx, rowTitleWidth)
dims := table.Layout(gtx, cell, coltitle, rowtitle, nil, nil)
return dims
}
func (pe *UnitEditor) drawSignals(gtx C, rowTitleWidth int) {
t := TrackerFromContext(gtx)
colP := pe.paramTable.ColTitleList.List.Position
rowP := pe.paramTable.RowTitleList.List.Position
p := image.Pt(rowTitleWidth, 0)
defer op.Offset(p).Push(gtx.Ops).Pop()
gtx.Constraints.Max = gtx.Constraints.Max.Sub(p)
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
for wire := range t.Wires {
clr := t.Theme.UnitEditor.WireColor
if wire.Highlight {
clr = t.Theme.UnitEditor.WireHighlight
}
switch {
case wire.FromSet && !wire.ToSet:
pe.drawRemoteSendSignal(gtx, wire, colP.First, rowP.First)
case !wire.FromSet && wire.ToSet:
pe.drawRemoteReceiveSignal(gtx, wire, colP.First, rowP.First, clr)
case wire.FromSet && wire.ToSet:
pe.drawSignal(gtx, wire, colP.First, rowP.First, clr)
}
}
}
func (pe *UnitEditor) drawBackGround(gtx C) {
t := TrackerFromContext(gtx)
rowP := pe.paramTable.RowTitleList.List.Position
defer op.Offset(image.Pt(0, -rowP.Offset)).Push(gtx.Ops).Pop()
for range pe.paramTable.RowTitleList.List.Position.Count + 1 {
paint.FillShape(gtx.Ops, t.Theme.UnitEditor.Divider, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, 1)}.Op())
op.Offset(image.Pt(0, gtx.Dp(t.Theme.UnitEditor.Height))).Add(gtx.Ops)
}
}
func (pe *UnitEditor) drawRemoteSendSignal(gtx C, wire tracker.Wire, col, row int) {
sy := wire.From - row
t := TrackerFromContext(gtx)
defer op.Offset(image.Pt(gtx.Dp(5), (sy+1)*gtx.Dp(t.Theme.UnitEditor.Height)-gtx.Dp(16))).Push(gtx.Ops).Pop()
Label(t.Theme, &t.Theme.UnitEditor.WireHint, wire.Hint).Layout(gtx)
}
func (pe *UnitEditor) drawRemoteReceiveSignal(gtx C, wire tracker.Wire, col, row int, clr color.NRGBA) {
ex := wire.To.X - col
ey := wire.To.Y - row
t := TrackerFromContext(gtx)
width := float32(gtx.Dp(t.Theme.UnitEditor.Width))
height := float32(gtx.Dp(t.Theme.UnitEditor.Height))
topLeft := f32.Pt(float32(ex)*width, float32(ey)*height)
center := topLeft.Add(f32.Pt(width/2, height/2))
c := float32(gtx.Dp(t.Theme.Knob.Diameter)) / 2 / float32(math.Sqrt2)
from := f32.Pt(c, c).Add(center)
q := c
c1 := f32.Pt(c+q, c+q).Add(center)
o := float32(gtx.Dp(8))
c2 := f32.Pt(width-q, height-o).Add(topLeft)
to := f32.Pt(width, height-o).Add(topLeft)
var path clip.Path
path.Begin(gtx.Ops)
path.MoveTo(from)
path.CubeTo(c1, c2, to)
paint.FillShape(gtx.Ops, clr,
clip.Stroke{
Path: path.End(),
Width: float32(gtx.Dp(t.Theme.SignalRail.LineWidth)),
}.Op())
defer op.Offset(image.Pt((ex+1)*gtx.Dp(t.Theme.UnitEditor.Width)+gtx.Dp(5), (ey+1)*gtx.Dp(t.Theme.UnitEditor.Height)-gtx.Dp(16))).Push(gtx.Ops).Pop()
Label(t.Theme, &t.Theme.UnitEditor.WireHint, wire.Hint).Layout(gtx)
}
func (pe *UnitEditor) drawSignal(gtx C, wire tracker.Wire, col, row int, clr color.NRGBA) {
sy := wire.From - row
ex := wire.To.X - col
ey := wire.To.Y - row
t := TrackerFromContext(gtx)
diam := gtx.Dp(t.Theme.Knob.Diameter)
c := float32(diam) / 2 / float32(math.Sqrt2)
width := float32(gtx.Dp(t.Theme.UnitEditor.Width))
height := float32(gtx.Dp(t.Theme.UnitEditor.Height))
from := f32.Pt(0, float32((sy+1)*gtx.Dp(t.Theme.UnitEditor.Height))-float32(gtx.Dp(8)))
corner := f32.Pt(1, 1)
if ex > 0 {
corner.X = -corner.X
}
if sy < ey {
corner.Y = -corner.Y
}
topLeft := f32.Pt(float32(ex)*width, float32(ey)*height)
center := topLeft.Add(f32.Pt(width/2, height/2))
to := mulVec(corner, f32.Pt(c, c)).Add(center)
p2 := mulVec(corner, f32.Pt(width/2, height/2)).Add(center)
p1 := f32.Pt(p2.X, float32((sy+1)*gtx.Dp(t.Theme.UnitEditor.Height)))
if sy > ey {
p1 = f32.Pt(p2.X, (float32(sy)+0.5)*float32(gtx.Dp(t.Theme.UnitEditor.Height))+float32(diam)/2)
}
k := float32(width) / 4
p2Tan := mulVec(corner, f32.Pt(-k, -k))
p1Tan := f32.Pt(k, p2Tan.Y)
fromTan := f32.Pt(k, 0)
var path clip.Path
path.Begin(gtx.Ops)
path.MoveTo(from)
path.CubeTo(from.Add(fromTan), p1.Sub(p1Tan), p1)
path.CubeTo(p1.Add(p1Tan), p2, to)
paint.FillShape(gtx.Ops, clr,
clip.Stroke{
Path: path.End(),
Width: float32(gtx.Dp(t.Theme.SignalRail.LineWidth)),
}.Op())
}
func mulVec(a, b f32.Point) f32.Point {
return f32.Pt(a.X*b.X, a.Y*b.Y)
}
func (pe *UnitEditor) layoutFooter(gtx C) D {
t := TrackerFromContext(gtx)
st := t.Units().SelectedType()
text := "Choose unit type"
if st != "" {
text = pe.caser.String(st)
if !t.UnitSearching().Value() {
text = pe.caser.String(t.Units().SelectedType())
}
hintText := Label(t.Theme, &t.Theme.UnitEditor.Hint, text)
deleteUnitBtn := ActionIconBtn(t.DeleteUnit(), t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
copyUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint)
disableUnitBtn := ToggleIconBtn(t.UnitDisabled(), t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint)
w := layout.Spacer{Width: t.Theme.IconButton.Enabled.Size}.Layout
if st != "" {
clearUnitBtn := ActionIconBtn(t.ClearUnit(), t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
w = clearUnitBtn.Layout
}
clearUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(deleteUnitBtn.Layout),
layout.Rigid(copyUnitBtn.Layout),
layout.Rigid(disableUnitBtn.Layout),
layout.Rigid(w),
layout.Rigid(clearUnitBtn.Layout),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(120)
gtx.Constraints.Min.X = gtx.Dp(130)
return hintText.Layout(gtx)
}),
layout.Flexed(1, func(gtx C) D {
return pe.commentEditor.Layout(gtx, t.UnitComment(), t.Theme, &t.Theme.InstrumentEditor.UnitComment, "---")
return pe.commentEditor.Layout(gtx, t.UnitComment(), t.Theme, &t.Theme.InstrumentEditor.UnitComment, "Comment")
}),
)
}
@ -234,173 +438,8 @@ func (pe *UnitEditor) layoutUnitTypeChooser(gtx C) D {
}
func (t *UnitEditor) Tags(level int, yield TagYieldFunc) bool {
widget := t.sliderList
if t.showingChooser() {
widget = t.searchList
return yield(level, t.searchList) && yield(level+1, &t.commentEditor.widgetEditor)
}
return yield(level, widget) && yield(level+1, &t.commentEditor.widgetEditor)
}
type ParameterWidget struct {
floatWidget widget.Float
boolWidget widget.Bool
instrBtn Clickable
instrMenu MenuState
unitBtn Clickable
unitMenu MenuState
Parameter tracker.Parameter
tipArea TipArea
}
type ParameterStyle struct {
tracker *Tracker
w *ParameterWidget
Theme *Theme
SendTargetTheme *material.Theme
Focus bool
}
func (t *Tracker) ParamStyle(th *Theme, paramWidget *ParameterWidget) ParameterStyle {
sendTargetTheme := th.Material.WithPalette(material.Palette{
Bg: th.Material.Bg,
Fg: th.UnitEditor.SendTarget,
ContrastBg: th.Material.ContrastBg,
ContrastFg: th.Material.ContrastFg,
})
return ParameterStyle{
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
Theme: th,
SendTargetTheme: &sendTargetTheme,
w: paramWidget,
}
}
func (p ParameterStyle) Layout(gtx C) D {
info, infoOk := p.w.Parameter.Info()
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
return layout.E.Layout(gtx, Label(p.Theme, &p.Theme.UnitEditor.ParameterName, p.w.Parameter.Name()).Layout)
}),
layout.Rigid(func(gtx C) D {
switch p.w.Parameter.Type() {
case tracker.IntegerParameter:
for p.Focus {
e, ok := gtx.Event(pointer.Filter{
Target: &p.w.floatWidget,
Kinds: pointer.Scroll,
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
})
if !ok {
break
}
if ev, ok := e.(pointer.Event); ok && ev.Kind == pointer.Scroll {
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
p.w.Parameter.SetValue(p.w.Parameter.Value() - int(delta))
}
}
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
ra := p.w.Parameter.Range()
if !p.w.floatWidget.Dragging() {
p.w.floatWidget.Value = (float32(p.w.Parameter.Value()) - float32(ra.Min)) / float32(ra.Max-ra.Min)
}
sliderStyle := material.Slider(&p.Theme.Material, &p.w.floatWidget)
if infoOk {
sliderStyle.Color = p.Theme.UnitEditor.SendTarget
}
r := image.Rectangle{Max: gtx.Constraints.Min}
defer clip.Rect(r).Push(gtx.Ops).Pop()
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
if p.Focus {
event.Op(gtx.Ops, &p.w.floatWidget)
}
dims := sliderStyle.Layout(gtx)
p.w.Parameter.SetValue(int(p.w.floatWidget.Value*float32(ra.Max-ra.Min) + float32(ra.Min) + 0.5))
return dims
case tracker.BoolParameter:
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60))
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
ra := p.w.Parameter.Range()
p.w.boolWidget.Value = p.w.Parameter.Value() > ra.Min
boolStyle := material.Switch(&p.Theme.Material, &p.w.boolWidget, "Toggle boolean parameter")
boolStyle.Color.Disabled = p.Theme.Material.Fg
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
dims := layout.Center.Layout(gtx, boolStyle.Layout)
if p.w.boolWidget.Value {
p.w.Parameter.SetValue(ra.Max)
} else {
p.w.Parameter.SetValue(ra.Min)
}
return dims
case tracker.IDParameter:
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
instrItems := make([]ActionMenuItem, p.tracker.Instruments().Count())
for i := range instrItems {
i := i
name, _, _, _ := p.tracker.Instruments().Item(i)
instrItems[i].Text = name
instrItems[i].Icon = icons.NavigationChevronRight
instrItems[i].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
if id, ok := p.tracker.Instruments().FirstID(i); ok {
p.w.Parameter.SetValue(id)
}
}))
}
var unitItems []ActionMenuItem
instrName := "<instr>"
unitName := "<unit>"
targetInstrName, units, targetUnitIndex, ok := p.tracker.UnitInfo(p.w.Parameter.Value())
if ok {
instrName = targetInstrName
unitName = buildUnitLabel(targetUnitIndex, units[targetUnitIndex])
unitItems = make([]ActionMenuItem, len(units))
for j, unit := range units {
id := unit.ID
unitItems[j].Text = buildUnitLabel(j, unit)
unitItems[j].Icon = icons.NavigationChevronRight
unitItems[j].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
p.w.Parameter.SetValue(id)
}))
}
}
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
instrBtn := MenuBtn(p.tracker.Theme, &p.w.instrMenu, &p.w.instrBtn, instrName)
unitBtn := MenuBtn(p.tracker.Theme, &p.w.unitMenu, &p.w.unitBtn, unitName)
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return instrBtn.Layout(gtx, instrItems...)
}),
layout.Rigid(func(gtx C) D {
return unitBtn.Layout(gtx, unitItems...)
}),
)
}
return D{}
}),
layout.Rigid(func(gtx C) D {
if p.w.Parameter.Type() != tracker.IDParameter {
hint := p.w.Parameter.Hint()
label := Label(p.tracker.Theme, &p.tracker.Theme.UnitEditor.Hint, hint.Label)
if !hint.Valid {
label.Color = p.tracker.Theme.UnitEditor.InvalidParam
}
if info == "" {
return label.Layout(gtx)
}
tooltip := component.PlatformTooltip(p.SendTargetTheme, info)
return p.w.tipArea.Layout(gtx, tooltip, label.Layout)
}
return D{}
}),
)
}
func buildUnitLabel(index int, u sointu.Unit) string {
text := u.Type
if u.Comment != "" {
text = fmt.Sprintf("%s \"%s\"", text, u.Comment)
}
return fmt.Sprintf("%d: %s", index, text)
return yield(level+1, t.paramTable.RowTitleList) && yield(level, t.paramTable) && yield(level+1, &t.commentEditor.widgetEditor)
}