diff --git a/tracker/action.go b/tracker/action.go index 6faf6a7..f4b857c 100644 --- a/tracker/action.go +++ b/tracker/action.go @@ -93,6 +93,16 @@ type ( *Model } ShowLicense Model + + ChooseSendSource struct { + ID int + *Model + } + ChooseSendTarget struct { + ID int + Port int + *Model + } ) // Action methods @@ -517,6 +527,40 @@ func (d DeleteOrderRow) Do() { 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 func (m *Model) NewSong() Action { return MakeEnabledAction((*NewSong)(m)) } diff --git a/tracker/derived.go b/tracker/derived.go index cd20f47..16cf92a 100644 --- a/tracker/derived.go +++ b/tracker/derived.go @@ -176,6 +176,7 @@ func (m *Model) deriveParams(unit *sointu.Unit) []Parameter { if !ok { return ret } + portIndex := 0 for i, up := range unitType { if !up.CanSet && !up.CanModulate { 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" { 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 { ret = append(ret, Parameter{m: m, unit: unit, vtable: &gmDlsEntryParameter{}}) diff --git a/tracker/gioui/knob.go b/tracker/gioui/knob.go index 8710711..16e7404 100644 --- a/tracker/gioui/knob.go +++ b/tracker/gioui/knob.go @@ -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) { rad := float32(diameter) / 2 end = min(max(end, 0), 1) diff --git a/tracker/gioui/port.go b/tracker/gioui/port.go new file mode 100644 index 0000000..36b3b1b --- /dev/null +++ b/tracker/gioui/port.go @@ -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()) +} diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index f52552e..1d9f70e 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -85,16 +85,20 @@ type Theme struct { } } UnitEditor struct { - Name LabelStyle - Chooser LabelStyle - Hint LabelStyle - InvalidParam color.NRGBA - SendTarget color.NRGBA - Width unit.Dp - Height unit.Dp - RowTitle LabelStyle - RowTitleWidth unit.Dp - Error color.NRGBA + Name LabelStyle + Chooser LabelStyle + Hint LabelStyle + InvalidParam color.NRGBA + SendTarget color.NRGBA + Width unit.Dp + Height unit.Dp + UnitList struct { + LabelWidth unit.Dp + Name LabelStyle + Disabled LabelStyle + Error color.NRGBA + } + Error color.NRGBA } Cursor CursorStyle Selection CursorStyle @@ -110,6 +114,7 @@ type Theme struct { ScrollBar ScrollBarStyle Knob KnobStyle SignalRail SignalRailStyle + Port PortStyle // iconCache is used to cache the icons created from iconvg data iconCache map[*byte]*widget.Icon diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index 8574c0d..ad2659e 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -214,9 +214,12 @@ uniteditor: sendtarget: *secondarycolor width: 60 height: 70 - rowtitle: { textsize: 12, color: *white, alignment: 2 } - rowtitlewidth: 16 - error: *errorcolor + unitlist: + labelwidth: 16 + name: { textsize: 12, color: *white, alignment: 2 } + disabled: + { textsize: 12, color: *disabled, font: { style: 1 }, alignment: 2 } + error: *errorcolor knob: diameter: 36 value: { textsize: 12, color: *highemphasis } @@ -231,3 +234,7 @@ signalrail: linewidth: 2 portdiameter: 8 portcolor: *primarycolor +port: + diameter: 36 + strokewidth: 4 + color: *secondarycolor diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 381c96e..d055fb8 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -31,7 +31,7 @@ type ( UnitEditor struct { paramTable *ScrollTable searchList *DragList - Parameters [][]*ParameterWidget + Parameters [][]*ParameterState DeleteUnitBtn *Clickable CopyUnitBtn *Clickable ClearUnitBtn *Clickable @@ -157,18 +157,18 @@ func (pe *UnitEditor) layoutRack(gtx C) D { // create enough parameter widget to match the number of parameters width := pe.paramTable.Table.Width() 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) 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() rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth signalError := t.SignalRail().Error() columnTitleHeight := gtx.Dp(0) for i := range pe.Parameters { 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 { @@ -184,9 +184,12 @@ func (pe *UnitEditor) layoutRack(gtx C) D { } sr := SignalRail(t.Theme, t.SignalRail().Item(y)) - label := Label(t.Theme, &t.Theme.UnitEditor.RowTitle, t.Units().Item(y).Type) - if signalError.Err != nil && signalError.UnitIndex == y { - label.Color = t.Theme.UnitEditor.Error + label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, t.Units().Item(y).Type) + switch { + 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)) 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()) } - param := t.Model.Params().Item(tracker.Point{X: x, Y: y}) - pe.Parameters[y][x].Parameter = param - paramStyle := t.ParamStyle(t.Theme, pe.Parameters[y][x]) - paramStyle.Focus = pe.paramTable.Table.Cursor() == tracker.Point{X: x, Y: y} + param := t.Model.Params().Item(point) + paramStyle := t.ParamStyle(param, t.Theme, pe.Parameters[y][x], pe.paramTable.Table.Cursor() == point) paramStyle.Layout(gtx) 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) } -type ParameterWidget struct { +type ParameterState struct { knobState KnobState boolWidget widget.Bool - instrBtn Clickable - instrMenu MenuState - unitBtn Clickable - unitMenu MenuState - Parameter tracker.Parameter - tipArea TipArea + clickable Clickable + portState PortState } type ParameterStyle struct { - tracker *Tracker - w *ParameterWidget - Theme *Theme - SendTargetTheme *material.Theme - Focus bool + Parameter tracker.Parameter + State *ParameterState + Theme *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, - }) +func (t *Tracker) ParamStyle(Parameter tracker.Parameter, th *Theme, paramWidget *ParameterState, focus bool) ParameterStyle { 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, + Theme: th, + State: paramWidget, + Parameter: Parameter, + Focus: focus, } } func (p ParameterStyle) Layout(gtx C) D { //_, _ := 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 { - 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: - 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) case tracker.BoolParameter: - 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") + ra := p.Parameter.Range() + p.State.boolWidget.Value = p.Parameter.Value() > ra.Min + boolStyle := material.Switch(&p.Theme.Material, &p.State.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) + if p.State.boolWidget.Value { + p.Parameter.SetValue(ra.Max) } else { - p.w.Parameter.SetValue(ra.Min) + p.Parameter.SetValue(ra.Min) } return dims 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()) for i := range instrItems { 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{} } title.Layout(gtx) diff --git a/tracker/model.go b/tracker/model.go index 7766b9d..42e8089 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -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.UnitSearching = false // if we change anything in the patch, reset the unit searching m.d.UnitSearchString = "" + m.d.SendSource = 0 m.updateDerivedPatchData() TrySend(m.broker.ToPlayer, any(m.d.Song.Patch.Copy())) } diff --git a/tracker/params.go b/tracker/params.go index 400cc23..eff7ca8 100644 --- a/tracker/params.go +++ b/tracker/params.go @@ -20,6 +20,7 @@ type ( up *sointu.UnitParameter index int vtable parameterVtable + port int } parameterVtable interface { @@ -70,6 +71,12 @@ func (p *Parameter) Value() int { } 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 { if p.vtable == nil { return false @@ -123,6 +130,12 @@ func (p *Parameter) Reset() { } 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} } 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" { return IDParameter }