mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-19 13:34:34 -04:00
455 lines
11 KiB
Go
455 lines
11 KiB
Go
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
|
|
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)
|
|
speed := gtx.Dp(512)
|
|
if p.Modifiers.Contain(key.ModCtrl) {
|
|
speed = gtx.Dp(128)
|
|
}
|
|
amount := float32(d.X-d.Y) / float32(speed)
|
|
newValue := int(float32(s.dragStartVal) + amount*float32(m.Max-m.Min))
|
|
param.SetValue(newValue)
|
|
s.tipArea.Appear(gtx.Now)
|
|
}
|
|
}
|
|
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)
|
|
for k.Scroll {
|
|
ev, ok := gtx.Event(pointer.Filter{Target: k.State, Kinds: pointer.Press})
|
|
if !ok {
|
|
break
|
|
}
|
|
if pe, ok := ev.(pointer.Event); ok && pe.Kind == pointer.Press && pe.Buttons == pointer.ButtonSecondary {
|
|
k.Value.Reset()
|
|
k.State.tipArea.Appear(gtx.Now)
|
|
}
|
|
}
|
|
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)
|
|
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)
|
|
for s.Scroll {
|
|
ev, ok := gtx.Event(pointer.Filter{Target: s.State, Kinds: pointer.Press})
|
|
if !ok {
|
|
break
|
|
}
|
|
if pe, ok := ev.(pointer.Event); ok && pe.Kind == pointer.Press {
|
|
curVal := s.Value.Value()
|
|
if pe.Buttons == pointer.ButtonPrimary {
|
|
if curVal >= 1 {
|
|
s.Value.SetValue(0)
|
|
} else {
|
|
s.Value.SetValue(curVal + 1)
|
|
}
|
|
}
|
|
if pe.Buttons == pointer.ButtonSecondary {
|
|
if curVal <= -1 {
|
|
s.Value.SetValue(0)
|
|
} else {
|
|
s.Value.SetValue(curVal - 1)
|
|
}
|
|
}
|
|
s.State.tipArea.Appear(gtx.Now)
|
|
}
|
|
}
|
|
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)
|
|
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())
|
|
}
|