This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-07-06 21:38:42 +03:00
parent e1aa9c0d26
commit 57926d4b0e
91 changed files with 454 additions and 270 deletions

View File

@ -12,11 +12,13 @@ import (
"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 (
@ -58,8 +60,87 @@ type (
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 *KnobState
Style *SwitchStyle
Hint string
Scroll bool
}
)
// KnobState
func (s *KnobState) 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)
}
}
}
// Knob
func Knob(v tracker.Parameter, th *Theme, state *KnobState, hint string, scroll bool) KnobWidget {
return KnobWidget{
Theme: th,
@ -72,7 +153,7 @@ func Knob(v tracker.Parameter, th *Theme, state *KnobState, hint string, scroll
}
func (k *KnobWidget) Layout(gtx C) D {
k.update(gtx)
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)
@ -82,8 +163,21 @@ func (k *KnobWidget) Layout(gtx C) D {
event.Op(gtx.Ops, k.State)
k.State.drag.Add(gtx.Ops)
k.State.click.Add(gtx.Ops)
k.strokeKnobArc(gtx, k.Style.Pos.Bg, sw, d, amount, 1)
k.strokeKnobArc(gtx, k.Style.Pos.Color, sw, d, 0, amount)
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)}
}
@ -104,52 +198,6 @@ func (k *KnobWidget) Layout(gtx C) D {
return w(gtx)
}
func (k *KnobWidget) update(gtx C) {
for {
p, ok := k.State.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
switch p.Kind {
case pointer.Press:
k.State.dragStartPt = p.Position
k.State.dragStartVal = k.Value.Value()
case pointer.Drag:
// update the value based on the drag amount
m := k.Value.Range()
d := p.Position.Sub(k.State.dragStartPt)
amount := float32(d.X-d.Y) / float32(gtx.Dp(k.Style.Diameter)) / 4
newValue := int(float32(k.State.dragStartVal) + amount*float32(m.Max-m.Min))
k.Value.SetValue(newValue)
k.State.tipArea.Appear(gtx.Now)
}
}
for {
g, ok := k.State.click.Update(gtx.Source)
if !ok {
break
}
if g.Kind == gesture.KindClick && g.NumClicks > 1 {
k.Value.Reset()
}
}
for k.Scroll {
e, ok := gtx.Event(pointer.Filter{
Target: k.State,
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))
k.Value.Add(delta, ev.Modifiers.Contain(key.ModShortcut))
k.State.tipArea.Appear(gtx.Now)
}
}
}
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)
@ -197,3 +245,77 @@ func (k *KnobWidget) strokeIndicator(gtx C, amount float32) {
}
paint.FillShape(gtx.Ops, k.Style.Indicator.Color, s.Op(gtx.Ops))
}
// Switch
func Switch(v tracker.Parameter, th *Theme, state *KnobState, hint string, scroll bool) SwitchWidget {
return SwitchWidget{
Theme: th,
Value: v,
State: state,
Style: &th.Switch,
Hint: hint,
Scroll: scroll,
}
}
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.Value.Value() < 0:
fg = s.Style.Neg.Fg
bg = s.Style.Neg.Bg
case 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.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)}
}

View File

@ -67,8 +67,9 @@ func (s SignalRailWidget) Layout(gtx C) D {
}
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(sw/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)

View File

@ -114,6 +114,7 @@ type Theme struct {
Split SplitStyle
ScrollBar ScrollBarStyle
Knob KnobStyle
Switch SwitchStyle
SignalRail SignalRailStyle
Port PortStyle

View File

@ -239,3 +239,18 @@ 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

@ -11,14 +11,12 @@ import (
"gioui.org/f32"
"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/text"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
@ -103,12 +101,19 @@ func (pe *UnitEditor) update(gtx C, t *Tracker) {
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 {
@ -135,12 +140,28 @@ func (pe *UnitEditor) update(gtx C, t *Tracker) {
}
}
for {
e, ok := gtx.Event(key.Filter{Focus: pe.paramTable.RowTitleList, Name: key.NameLeftArrow})
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},
)
if !ok {
break
}
if e, ok := e.(key.Event); ok && e.State == key.Press {
t.PatchPanel.unitList.dragList.Focus()
switch e.Name {
case key.NameLeftArrow:
t.PatchPanel.unitList.dragList.Focus()
case key.NameDeleteBackward:
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()
}
}
}
}
@ -148,7 +169,7 @@ 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()
}
}
@ -259,7 +280,7 @@ func (pe *UnitEditor) drawBackGround(gtx C) {
func (pe *UnitEditor) drawRemoteSendSignal(gtx C, wire tracker.Wire, col, row int) {
sy := wire.From - row
t := TrackerFromContext(gtx)
defer op.Offset(image.Pt(0, (sy+1)*gtx.Dp(t.Theme.UnitEditor.Height)-gtx.Dp(16))).Push(gtx.Ops).Pop()
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)
}
@ -300,7 +321,7 @@ func (pe *UnitEditor) drawSignal(gtx C, wire tracker.Wire, col, row int) {
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(t.Theme.SignalRail.SignalWidth)/2))
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
@ -438,18 +459,8 @@ func (p ParameterStyle) Layout(gtx C) D {
k := Knob(p.Parameter, p.Theme, &p.State.knobState, p.Parameter.Hint().Label, p.Focus)
return k.Layout(gtx)
case tracker.BoolParameter:
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.State.boolWidget.Value {
p.Parameter.SetValue(ra.Max)
} else {
p.Parameter.SetValue(ra.Min)
}
return dims
s := Switch(p.Parameter, p.Theme, &p.State.knobState, p.Parameter.Hint().Label, p.Focus)
return s.Layout(gtx)
case tracker.IDParameter:
btn := ActionBtn(t.ChooseSendSource(p.Parameter.UnitID()), t.Theme, &p.State.clickable, "Set", p.Parameter.Hint().Label)
return btn.Layout(gtx)