This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-07-04 15:05:49 +03:00
parent 4e295a3a2f
commit 3c6c24c6af
9 changed files with 206 additions and 67 deletions

View File

@ -93,6 +93,16 @@ type (
*Model *Model
} }
ShowLicense Model ShowLicense Model
ChooseSendSource struct {
ID int
*Model
}
ChooseSendTarget struct {
ID int
Port int
*Model
}
) )
// Action methods // Action methods
@ -517,6 +527,40 @@ func (d DeleteOrderRow) Do() {
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
} }
// ChooseSendSource
func (m *Model) IsChoosingSendTarget() bool {
return m.d.SendSource > 0
}
func (m *Model) ChooseSendSource(id int) Action {
return MakeEnabledAction(ChooseSendSource{ID: id, Model: m})
}
func (s ChooseSendSource) Do() {
defer (*Model)(s.Model).change("ChooseSendSource", NoChange, MinorChange)()
s.Model.d.SendSource = s.ID
}
// ChooseSendTarget
func (m *Model) ChooseSendTarget(id int, port int) Action {
return MakeEnabledAction(ChooseSendTarget{ID: id, Port: port, Model: m})
}
func (s ChooseSendTarget) Do() {
defer (*Model)(s.Model).change("ChooseSendTarget", SongChange, MinorChange)()
sourceID := (*Model)(s.Model).d.SendSource
s.d.SendSource = 0
if sourceID <= 0 || s.ID <= 0 || s.Port < 0 || s.Port > 7 {
return
}
si, su, err := s.d.Song.Patch.FindUnit(sourceID)
if err != nil {
return
}
s.d.Song.Patch[si].Units[su].Parameters["target"] = s.ID
s.d.Song.Patch[si].Units[su].Parameters["port"] = s.Port
}
// NewSong // NewSong
func (m *Model) NewSong() Action { return MakeEnabledAction((*NewSong)(m)) } func (m *Model) NewSong() Action { return MakeEnabledAction((*NewSong)(m)) }

View File

@ -176,6 +176,7 @@ func (m *Model) deriveParams(unit *sointu.Unit) []Parameter {
if !ok { if !ok {
return ret return ret
} }
portIndex := 0
for i, up := range unitType { for i, up := range unitType {
if !up.CanSet && !up.CanModulate { if !up.CanSet && !up.CanModulate {
continue // skip parameters that cannot be set or modulated continue // skip parameters that cannot be set or modulated
@ -186,7 +187,12 @@ func (m *Model) deriveParams(unit *sointu.Unit) []Parameter {
if unit.Type == "send" && up.Name == "port" { if unit.Type == "send" && up.Name == "port" {
continue continue
} }
ret = append(ret, Parameter{m: m, unit: unit, up: &unitType[i], vtable: &namedParameter{}}) q := 0
if up.CanModulate {
portIndex++
q = portIndex
}
ret = append(ret, Parameter{m: m, unit: unit, up: &unitType[i], vtable: &namedParameter{}, port: q})
} }
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample { if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
ret = append(ret, Parameter{m: m, unit: unit, vtable: &gmDlsEntryParameter{}}) ret = append(ret, Parameter{m: m, unit: unit, vtable: &gmDlsEntryParameter{}})

View File

@ -149,15 +149,6 @@ func (k *KnobWidget) update(gtx C) {
} }
} }
func (k *KnobWidget) strokeBg(gtx C) {
diam := gtx.Dp(k.Style.Diameter)
circle := clip.Ellipse{
Min: image.Pt(0, 0),
Max: image.Pt(diam, diam),
}.Op(gtx.Ops)
paint.FillShape(gtx.Ops, k.Style.Bg, circle)
}
func (k *KnobWidget) strokeKnobArc(gtx C, color color.NRGBA, strokeWidth, diameter int, start, end float32) { func (k *KnobWidget) strokeKnobArc(gtx C, color color.NRGBA, strokeWidth, diameter int, start, end float32) {
rad := float32(diameter) / 2 rad := float32(diameter) / 2
end = min(max(end, 0), 1) end = min(max(end, 0), 1)

66
tracker/gioui/port.go Normal file
View File

@ -0,0 +1,66 @@
package gioui
import (
"image"
"image/color"
"math"
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
)
type (
PortState struct {
click gesture.Click
}
PortStyle struct {
Diameter unit.Dp
StrokeWidth unit.Dp
Color color.NRGBA
}
PortWidget struct {
Theme *Theme
Style *PortStyle
State *PortState
}
)
func Port(t *Theme, p *PortState) PortWidget {
return PortWidget{Theme: t, Style: &t.Port, State: p}
}
func (p *PortWidget) Layout(gtx C) D {
d := gtx.Dp(p.Style.Diameter)
defer clip.Rect(image.Rectangle{Max: image.Pt(d, d)}).Push(gtx.Ops).Pop()
event.Op(gtx.Ops, p.State)
p.State.click.Add(gtx.Ops)
p.strokeCircle(gtx)
return D{Size: image.Pt(d, d)}
}
func (p *PortState) Clicked(gtx C) bool {
ev, ok := p.click.Update(gtx.Source)
return ok && ev.Kind == gesture.KindClick
}
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

@ -85,16 +85,20 @@ type Theme struct {
} }
} }
UnitEditor struct { UnitEditor struct {
Name LabelStyle Name LabelStyle
Chooser LabelStyle Chooser LabelStyle
Hint LabelStyle Hint LabelStyle
InvalidParam color.NRGBA InvalidParam color.NRGBA
SendTarget color.NRGBA SendTarget color.NRGBA
Width unit.Dp Width unit.Dp
Height unit.Dp Height unit.Dp
RowTitle LabelStyle UnitList struct {
RowTitleWidth unit.Dp LabelWidth unit.Dp
Error color.NRGBA Name LabelStyle
Disabled LabelStyle
Error color.NRGBA
}
Error color.NRGBA
} }
Cursor CursorStyle Cursor CursorStyle
Selection CursorStyle Selection CursorStyle
@ -110,6 +114,7 @@ type Theme struct {
ScrollBar ScrollBarStyle ScrollBar ScrollBarStyle
Knob KnobStyle Knob KnobStyle
SignalRail SignalRailStyle SignalRail SignalRailStyle
Port PortStyle
// iconCache is used to cache the icons created from iconvg data // iconCache is used to cache the icons created from iconvg data
iconCache map[*byte]*widget.Icon iconCache map[*byte]*widget.Icon

View File

@ -214,9 +214,12 @@ uniteditor:
sendtarget: *secondarycolor sendtarget: *secondarycolor
width: 60 width: 60
height: 70 height: 70
rowtitle: { textsize: 12, color: *white, alignment: 2 } unitlist:
rowtitlewidth: 16 labelwidth: 16
error: *errorcolor name: { textsize: 12, color: *white, alignment: 2 }
disabled:
{ textsize: 12, color: *disabled, font: { style: 1 }, alignment: 2 }
error: *errorcolor
knob: knob:
diameter: 36 diameter: 36
value: { textsize: 12, color: *highemphasis } value: { textsize: 12, color: *highemphasis }
@ -231,3 +234,7 @@ signalrail:
linewidth: 2 linewidth: 2
portdiameter: 8 portdiameter: 8
portcolor: *primarycolor portcolor: *primarycolor
port:
diameter: 36
strokewidth: 4
color: *secondarycolor

View File

@ -31,7 +31,7 @@ type (
UnitEditor struct { UnitEditor struct {
paramTable *ScrollTable paramTable *ScrollTable
searchList *DragList searchList *DragList
Parameters [][]*ParameterWidget Parameters [][]*ParameterState
DeleteUnitBtn *Clickable DeleteUnitBtn *Clickable
CopyUnitBtn *Clickable CopyUnitBtn *Clickable
ClearUnitBtn *Clickable ClearUnitBtn *Clickable
@ -157,18 +157,18 @@ func (pe *UnitEditor) layoutRack(gtx C) D {
// create enough parameter widget to match the number of parameters // create enough parameter widget to match the number of parameters
width := pe.paramTable.Table.Width() width := pe.paramTable.Table.Width()
for len(pe.Parameters) < pe.paramTable.Table.Height() { for len(pe.Parameters) < pe.paramTable.Table.Height() {
pe.Parameters = append(pe.Parameters, make([]*ParameterWidget, 0)) pe.Parameters = append(pe.Parameters, make([]*ParameterState, 0))
} }
cellWidth := gtx.Dp(t.Theme.UnitEditor.Width) cellWidth := gtx.Dp(t.Theme.UnitEditor.Width)
cellHeight := gtx.Dp(t.Theme.UnitEditor.Height) cellHeight := gtx.Dp(t.Theme.UnitEditor.Height)
rowTitleLabelWidth := gtx.Dp(t.Theme.UnitEditor.RowTitleWidth) rowTitleLabelWidth := gtx.Dp(t.Theme.UnitEditor.UnitList.LabelWidth)
rowTitleSignalWidth := gtx.Dp(t.Theme.SignalRail.SignalWidth) * t.SignalRail().MaxWidth() rowTitleSignalWidth := gtx.Dp(t.Theme.SignalRail.SignalWidth) * t.SignalRail().MaxWidth()
rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth
signalError := t.SignalRail().Error() signalError := t.SignalRail().Error()
columnTitleHeight := gtx.Dp(0) columnTitleHeight := gtx.Dp(0)
for i := range pe.Parameters { for i := range pe.Parameters {
for len(pe.Parameters[i]) < width { for len(pe.Parameters[i]) < width {
pe.Parameters[i] = append(pe.Parameters[i], &ParameterWidget{}) pe.Parameters[i] = append(pe.Parameters[i], &ParameterState{})
} }
} }
coltitle := func(gtx C, x int) D { coltitle := func(gtx C, x int) D {
@ -184,9 +184,12 @@ func (pe *UnitEditor) layoutRack(gtx C) D {
} }
sr := SignalRail(t.Theme, t.SignalRail().Item(y)) sr := SignalRail(t.Theme, t.SignalRail().Item(y))
label := Label(t.Theme, &t.Theme.UnitEditor.RowTitle, t.Units().Item(y).Type) label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, t.Units().Item(y).Type)
if signalError.Err != nil && signalError.UnitIndex == y { switch {
label.Color = t.Theme.UnitEditor.Error case t.Units().Item(y).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)) gtx.Constraints = layout.Exact(image.Pt(rowTitleWidth, cellHeight))
sr.Layout(gtx) sr.Layout(gtx)
@ -210,10 +213,8 @@ func (pe *UnitEditor) layoutRack(gtx C) D {
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
} }
param := t.Model.Params().Item(tracker.Point{X: x, Y: y}) param := t.Model.Params().Item(point)
pe.Parameters[y][x].Parameter = param paramStyle := t.ParamStyle(param, t.Theme, pe.Parameters[y][x], pe.paramTable.Table.Cursor() == point)
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[y][x])
paramStyle.Focus = pe.paramTable.Table.Cursor() == tracker.Point{X: x, Y: y}
paramStyle.Layout(gtx) paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)} return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
} }
@ -351,63 +352,61 @@ func (t *UnitEditor) Tags(level int, yield TagYieldFunc) bool {
return yield(level, widget) && yield(level+1, &t.commentEditor.widgetEditor) return yield(level, widget) && yield(level+1, &t.commentEditor.widgetEditor)
} }
type ParameterWidget struct { type ParameterState struct {
knobState KnobState knobState KnobState
boolWidget widget.Bool boolWidget widget.Bool
instrBtn Clickable clickable Clickable
instrMenu MenuState portState PortState
unitBtn Clickable
unitMenu MenuState
Parameter tracker.Parameter
tipArea TipArea
} }
type ParameterStyle struct { type ParameterStyle struct {
tracker *Tracker Parameter tracker.Parameter
w *ParameterWidget State *ParameterState
Theme *Theme Theme *Theme
SendTargetTheme *material.Theme Focus bool
Focus bool
} }
func (t *Tracker) ParamStyle(th *Theme, paramWidget *ParameterWidget) ParameterStyle { func (t *Tracker) ParamStyle(Parameter tracker.Parameter, th *Theme, paramWidget *ParameterState, focus bool) 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{ return ParameterStyle{
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way Theme: th,
Theme: th, State: paramWidget,
SendTargetTheme: &sendTargetTheme, Parameter: Parameter,
w: paramWidget, Focus: focus,
} }
} }
func (p ParameterStyle) Layout(gtx C) D { func (p ParameterStyle) Layout(gtx C) D {
//_, _ := p.w.Parameter.Info() //_, _ := p.w.Parameter.Info()
title := Label(p.Theme, &p.Theme.UnitEditor.Name, p.w.Parameter.Name()) title := Label(p.Theme, &p.Theme.UnitEditor.Name, p.Parameter.Name())
t := TrackerFromContext(gtx)
widget := func(gtx C) D { widget := func(gtx C) D {
switch p.w.Parameter.Type() { if port, ok := p.Parameter.Port(); t.IsChoosingSendTarget() && ok {
for p.State.portState.Clicked(gtx) {
t.ChooseSendTarget(p.Parameter.UnitID(), port).Do()
}
k := Port(p.Theme, &p.State.portState)
return k.Layout(gtx)
}
switch p.Parameter.Type() {
case tracker.IntegerParameter: case tracker.IntegerParameter:
k := Knob(p.w.Parameter, p.Theme, &p.w.knobState, p.w.Parameter.Hint().Label, p.Focus) k := Knob(p.Parameter, p.Theme, &p.State.knobState, p.Parameter.Hint().Label, p.Focus)
return k.Layout(gtx) return k.Layout(gtx)
case tracker.BoolParameter: case tracker.BoolParameter:
ra := p.w.Parameter.Range() ra := p.Parameter.Range()
p.w.boolWidget.Value = p.w.Parameter.Value() > ra.Min p.State.boolWidget.Value = p.Parameter.Value() > ra.Min
boolStyle := material.Switch(&p.Theme.Material, &p.w.boolWidget, "Toggle boolean parameter") boolStyle := material.Switch(&p.Theme.Material, &p.State.boolWidget, "Toggle boolean parameter")
boolStyle.Color.Disabled = p.Theme.Material.Fg boolStyle.Color.Disabled = p.Theme.Material.Fg
defer pointer.PassOp{}.Push(gtx.Ops).Pop() defer pointer.PassOp{}.Push(gtx.Ops).Pop()
dims := layout.Center.Layout(gtx, boolStyle.Layout) dims := layout.Center.Layout(gtx, boolStyle.Layout)
if p.w.boolWidget.Value { if p.State.boolWidget.Value {
p.w.Parameter.SetValue(ra.Max) p.Parameter.SetValue(ra.Max)
} else { } else {
p.w.Parameter.SetValue(ra.Min) p.Parameter.SetValue(ra.Min)
} }
return dims return dims
case tracker.IDParameter: case tracker.IDParameter:
return drawCircle(gtx, gtx.Dp(p.Theme.Knob.Diameter), p.Theme.Knob.Pos.Bg) btn := ActionBtn(t.ChooseSendSource(p.Parameter.UnitID()), t.Theme, &p.State.clickable, "Set", p.Parameter.Hint().Label)
return btn.Layout(gtx)
/*instrItems := make([]ActionMenuItem, p.tracker.Instruments().Count()) /*instrItems := make([]ActionMenuItem, p.tracker.Instruments().Count())
for i := range instrItems { for i := range instrItems {
i := i i := i
@ -449,6 +448,10 @@ func (p ParameterStyle) Layout(gtx C) D {
}), }),
)*/ )*/
} }
if _, ok := p.Parameter.Port(); ok {
k := Port(p.Theme, &p.State.portState)
return k.Layout(gtx)
}
return D{} return D{}
} }
title.Layout(gtx) title.Layout(gtx)

View File

@ -240,6 +240,7 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
m.d.UnitIndex2 = clamp(m.d.UnitIndex2, 0, unitCount-1) m.d.UnitIndex2 = clamp(m.d.UnitIndex2, 0, unitCount-1)
m.d.UnitSearching = false // if we change anything in the patch, reset the unit searching m.d.UnitSearching = false // if we change anything in the patch, reset the unit searching
m.d.UnitSearchString = "" m.d.UnitSearchString = ""
m.d.SendSource = 0
m.updateDerivedPatchData() m.updateDerivedPatchData()
TrySend(m.broker.ToPlayer, any(m.d.Song.Patch.Copy())) TrySend(m.broker.ToPlayer, any(m.d.Song.Patch.Copy()))
} }

View File

@ -20,6 +20,7 @@ type (
up *sointu.UnitParameter up *sointu.UnitParameter
index int index int
vtable parameterVtable vtable parameterVtable
port int
} }
parameterVtable interface { parameterVtable interface {
@ -70,6 +71,12 @@ func (p *Parameter) Value() int {
} }
return p.vtable.Value(p) return p.vtable.Value(p)
} }
func (p *Parameter) Port() (int, bool) {
if p.port <= 0 {
return 0, false
}
return p.port - 1, true
}
func (p *Parameter) SetValue(value int) bool { func (p *Parameter) SetValue(value int) bool {
if p.vtable == nil { if p.vtable == nil {
return false return false
@ -123,6 +130,12 @@ func (p *Parameter) Reset() {
} }
p.vtable.Reset(p) p.vtable.Reset(p)
} }
func (p *Parameter) UnitID() int {
if p.unit == nil {
return 0
}
return p.unit.ID
}
// //
@ -212,6 +225,9 @@ func (n *namedParameter) Range(p *Parameter) IntRange {
return IntRange{Min: p.up.MinValue, Max: p.up.MaxValue} return IntRange{Min: p.up.MinValue, Max: p.up.MaxValue}
} }
func (n *namedParameter) Type(p *Parameter) ParameterType { func (n *namedParameter) Type(p *Parameter) ParameterType {
if p.up == nil || !p.up.CanSet {
return NoParameter
}
if p.unit.Type == "send" && p.up.Name == "target" { if p.unit.Type == "send" && p.up.Name == "target" {
return IDParameter return IDParameter
} }