This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-07-03 18:54:02 +03:00
parent c09a3f04db
commit 4e295a3a2f
8 changed files with 206 additions and 112 deletions

View File

@ -12,12 +12,13 @@ import (
"github.com/vsariola/sointu/tracker"
)
const numSignalsDrawn = 8
const maxSignalsDrawn = 16
type (
SignalRailStyle struct {
Color color.NRGBA
LineWidth unit.Dp
SignalWidth unit.Dp
PortDiameter unit.Dp
PortColor color.NRGBA
}
@ -25,7 +26,6 @@ type (
SignalRailWidget struct {
Style *SignalRailStyle
Signal tracker.Signal
Width unit.Dp
Height unit.Dp
}
)
@ -34,63 +34,69 @@ func SignalRail(th *Theme, signal tracker.Signal) SignalRailWidget {
return SignalRailWidget{
Style: &th.SignalRail,
Signal: signal,
Width: th.UnitEditor.Width,
Height: th.UnitEditor.Height,
}
}
func (s SignalRailWidget) Layout(gtx C) D {
w := gtx.Dp(s.Width)
sw := gtx.Dp(s.Style.SignalWidth)
h := gtx.Dp(s.Height)
l := gtx.Dp(s.Style.LineWidth)
d := gtx.Dp(s.Style.PortDiameter)
c := max(l, d) / 2
stride := (w - c*2) / numSignalsDrawn
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(numSignalsDrawn, s.Signal.PassThrough) {
x := float32(i*stride + c)
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), numSignalsDrawn-s.Signal.PassThrough) {
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)*stride + c)
x1 := float32((i+s.Signal.PassThrough)*sw + center)
for _, link := range input {
x2 := float32((link+s.Signal.PassThrough)*stride + c)
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) {
from := f32.Pt(float32((i+s.Signal.PassThrough)*sw+center), float32(h/2))
to := f32.Pt(float32(gtx.Constraints.Max.X), float32(h)-float32(sw/2))
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, numSignalsDrawn-s.Signal.PassThrough) {
x := float32((i+s.Signal.PassThrough)*stride + c)
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(l),
Width: float32(lw),
}.Op())
// Draw the circles on modified signals
for i := range min(len(s.Signal.StackUse.Modifies), numSignalsDrawn-s.Signal.PassThrough) {
// 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
}
var circle clip.Path
x := float32((i + s.Signal.PassThrough) * stride)
circle.Begin(gtx.Ops)
circle.MoveTo(f32.Pt(x, float32(h/2)))
f := f32.Pt(x+float32(c), float32(h/2))
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(w, h)}
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

@ -85,16 +85,16 @@ type Theme struct {
}
}
UnitEditor struct {
Name LabelStyle
Chooser LabelStyle
Hint LabelStyle
InvalidParam color.NRGBA
SendTarget color.NRGBA
Width unit.Dp
Height unit.Dp
RowTitleWidth unit.Dp
ColumnTitleHeight unit.Dp
RowTitle LabelStyle
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
}
Cursor CursorStyle
Selection CursorStyle

View File

@ -211,12 +211,12 @@ uniteditor:
name:
{ textsize: 12, alignment: 2, color: *highemphasis, shadowcolor: *black }
invalidparam: { r: 120, g: 120, b: 120, a: 190 }
sendtarget: { r: 120, g: 120, b: 210, a: 63 }
sendtarget: *secondarycolor
width: 60
height: 60
rowtitlewidth: 16
columntitleheight: 16
height: 70
rowtitle: { textsize: 12, color: *white, alignment: 2 }
rowtitlewidth: 16
error: *errorcolor
knob:
diameter: 36
value: { textsize: 12, color: *highemphasis }
@ -226,7 +226,8 @@ knob:
neg: { color: *secondarycolor, bg: { r: 32, g: 55, b: 58, a: 255 } }
indicator: { color: *white, width: 2, innerdiam: 24, outerdiam: 36 }
signalrail:
color: *primarycolor
color: *secondarycolor
signalwidth: 10
linewidth: 2
portdiameter: 8
portcolor: *secondarycolor
portcolor: *primarycolor

View File

@ -71,7 +71,7 @@ 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
}
@ -152,7 +152,7 @@ func (pe *UnitEditor) ChooseUnitType(t *Tracker) {
}
}
func (pe *UnitEditor) layoutSliders(gtx C) D {
func (pe *UnitEditor) layoutRack(gtx C) D {
t := TrackerFromContext(gtx)
// create enough parameter widget to match the number of parameters
width := pe.paramTable.Table.Width()
@ -161,8 +161,11 @@ func (pe *UnitEditor) layoutSliders(gtx C) D {
}
cellWidth := gtx.Dp(t.Theme.UnitEditor.Width)
cellHeight := gtx.Dp(t.Theme.UnitEditor.Height)
rowTitleWidth := gtx.Dp(t.Theme.UnitEditor.RowTitleWidth)
columnTitleHeight := gtx.Dp(t.Theme.UnitEditor.ColumnTitleHeight)
rowTitleLabelWidth := gtx.Dp(t.Theme.UnitEditor.RowTitleWidth)
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{})
@ -171,20 +174,29 @@ func (pe *UnitEditor) layoutSliders(gtx C) D {
coltitle := func(gtx C, x int) D {
return D{Size: image.Pt(cellWidth, columnTitleHeight)}
}
rowTitleBg := func(gtx C, j int) D {
paint.FillShape(gtx.Ops, t.Theme.NoteEditor.Play, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, 1)}.Op())
return D{}
}
rowtitle := func(gtx C, y int) D {
//defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(cellHeight)})).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(cellHeight, rowTitleWidth))
Label(t.Theme, &t.Theme.UnitEditor.RowTitle, t.Units().Item(y).Type).Layout(gtx)
if y < 0 || y >= len(pe.Parameters) {
return 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
}
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 {
if x == 0 {
sr := SignalRail(t.Theme, t.SignalRail().Item(y))
return sr.Layout(gtx)
}
x--
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]) {
@ -206,63 +218,76 @@ func (pe *UnitEditor) layoutSliders(gtx C) D {
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
}
table := FilledScrollTable(t.Theme, pe.paramTable)
table.RowTitleWidth = t.Theme.UnitEditor.RowTitleWidth
table.ColumnTitleHeight = t.Theme.UnitEditor.ColumnTitleHeight
table.RowTitleWidth = gtx.Metric.PxToDp(rowTitleWidth)
table.ColumnTitleHeight = 0
table.CellWidth = t.Theme.UnitEditor.Width
table.CellHeight = t.Theme.UnitEditor.Height
pe.drawSignals(gtx)
dims := table.Layout(gtx, cell, coltitle, rowtitle, nil, nil)
pe.drawSignals(gtx, rowTitleWidth)
dims := table.Layout(gtx, cell, coltitle, rowtitle, nil, rowTitleBg)
return dims
}
func (pe *UnitEditor) drawSignals(gtx C) {
func (pe *UnitEditor) drawSignals(gtx C, rowTitleWidth int) {
t := TrackerFromContext(gtx)
units := t.Units()
colP := pe.paramTable.ColTitleList.List.Position
rowP := pe.paramTable.RowTitleList.List.Position
p := image.Pt(gtx.Dp(t.Theme.UnitEditor.RowTitleWidth), gtx.Dp(t.Theme.UnitEditor.ColumnTitleHeight))
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 i := 0; i < units.Count(); i++ {
item := units.Item(i)
if item.TargetUnit > 0 {
pe.drawSignal(gtx, 3-colP.First, i-rowP.First, item.TargetPort-colP.First, item.TargetUnit-1-rowP.First)
if item.TargetOk {
pe.drawSignal(gtx, i-rowP.First, item.TargetX-colP.First, item.TargetY-rowP.First)
}
}
}
func (pe *UnitEditor) drawSignal(gtx C, sx, sy, ex, ey int) {
func (pe *UnitEditor) drawSignal(gtx C, sy, ex, ey int) {
t := TrackerFromContext(gtx)
width := float32(gtx.Dp(t.Theme.UnitEditor.Width))
height := float32(gtx.Dp(t.Theme.UnitEditor.Height))
diam := gtx.Dp(t.Theme.Knob.Diameter)
from := f32.Pt((float32(sx)+.5)*width, (float32(sy)+.6)*height)
to := f32.Pt((float32(ex)+.5)*width, (float32(ey)+.6)*height)
var c1, c2 f32.Point
if sy < ey {
from.Y += float32(diam) / 2
to.Y -= float32(diam) / 2
c1 = from.Add(f32.Pt(0, height/2))
c2 = to.Sub(f32.Pt(0, height/2))
} else {
from.Y -= float32(diam) / 2
to.Y += float32(diam) / 2
c1 = from.Sub(f32.Pt(0, height/2))
c2 = to.Add(f32.Pt(0, height/2))
from := f32.Pt(0, float32((sy+1)*gtx.Dp(t.Theme.UnitEditor.Height))-float32(gtx.Dp(t.Theme.SignalRail.SignalWidth)/2))
corner := f32.Pt(1, 1)
if ex > 0 {
corner.X = -corner.X
}
if sy < ey {
corner.Y = -corner.Y
}
c := float32(diam) / 2 / float32(math.Sqrt2)
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
//toTan := mulVec(corner, f32.Pt(-k, -k))
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(c1, c2, to)
path.CubeTo(from.Add(fromTan), p1.Sub(p1Tan), p1)
path.CubeTo(p1.Add(p1Tan), p2, to)
paint.FillShape(gtx.Ops, t.Theme.UnitEditor.SendTarget,
clip.Stroke{
Path: path.End(),
Width: float32(gtx.Dp(4)),
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()
@ -426,9 +451,9 @@ func (p ParameterStyle) Layout(gtx C) D {
}
return D{}
}
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(title.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Center.Layout(gtx, widget) }),
title.Layout(gtx)
layout.Center.Layout(gtx, widget)
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
/* layout.Rigid(func(gtx C) D {
if p.w.Parameter.Type() != tracker.IDParameter {
hint := p.w.Parameter.Hint()
@ -445,7 +470,6 @@ func (p ParameterStyle) Layout(gtx C) D {
}
return D{}
}),*/
)
}
func drawCircle(gtx C, i int, nRGBA color.NRGBA) D {