refactor(tracker/gioui): bind tracker.Int to NumericUpDown on Layout

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-21 11:45:31 +03:00
parent 289bfb0605
commit beef8fe1e0
8 changed files with 136 additions and 153 deletions

View File

@ -144,9 +144,9 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
octave := func(gtx C) D { octave := func(gtx C) D {
in := layout.UniformInset(unit.Dp(1)) in := layout.UniformInset(unit.Dp(1))
numStyle := NumUpDown(t.Theme, t.OctaveNumberInput, ie.octaveHint) return in.Layout(gtx, func(gtx C) D {
dims := in.Layout(gtx, numStyle.Layout) return t.OctaveNumberInput.Layout(gtx, t.Octave(), t.Theme, &t.Theme.NumericUpDown, "Octave")
return dims })
} }
newBtnStyle := ActionIcon(gtx, t.Theme, ie.newInstrumentBtn, icons.ContentAdd, ie.addInstrumentHint) newBtnStyle := ActionIcon(gtx, t.Theme, ie.newInstrumentBtn, icons.ContentAdd, ie.addInstrumentHint)
@ -231,9 +231,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
layout.Rigid(Label(t.Theme, &t.Theme.InstrumentEditor.Voices, "Voices").Layout), layout.Rigid(Label(t.Theme, &t.Theme.InstrumentEditor.Voices, "Voices").Layout),
layout.Rigid(layout.Spacer{Width: 4}.Layout), layout.Rigid(layout.Spacer{Width: 4}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
numStyle := NumUpDown(t.Theme, t.InstrumentVoices, "Number of voices for this instrument") return t.InstrumentVoices.Layout(gtx, t.Model.InstrumentVoices(), t.Theme, &t.Theme.NumericUpDown, "Number of voices for this instrument")
dims := numStyle.Layout(gtx)
return dims
}), }),
layout.Rigid(splitInstrumentBtnStyle.Layout), layout.Rigid(splitInstrumentBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),

View File

@ -293,7 +293,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
break break
} }
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected() instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
n := noteAsValue(t.OctaveNumberInput.Int.Value(), val-12) n := noteAsValue(t.Model.Octave().Value(), val-12)
t.KeyNoteMap.Press(e.Name, tracker.NoteEvent{Channel: instr, Note: n}) t.KeyNoteMap.Press(e.Name, tracker.NoteEvent{Channel: instr, Note: n})
} }
} }

View File

@ -51,7 +51,7 @@ func init() {
} }
type NoteEditor struct { type NoteEditor struct {
TrackVoices *NumberInput TrackVoices *NumericUpDown
NewTrackBtn *ActionClickable NewTrackBtn *ActionClickable
DeleteTrackBtn *ActionClickable DeleteTrackBtn *ActionClickable
SplitTrackBtn *ActionClickable SplitTrackBtn *ActionClickable
@ -76,7 +76,7 @@ type NoteEditor struct {
func NewNoteEditor(model *tracker.Model) *NoteEditor { func NewNoteEditor(model *tracker.Model) *NoteEditor {
ret := &NoteEditor{ ret := &NoteEditor{
TrackVoices: NewNumberInput(model.TrackVoices()), TrackVoices: NewNumericUpDown(),
NewTrackBtn: NewActionClickable(model.AddTrack()), NewTrackBtn: NewActionClickable(model.AddTrack()),
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()), DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
SplitTrackBtn: NewActionClickable(model.SplitTrack()), SplitTrackBtn: NewActionClickable(model.SplitTrack()),
@ -174,8 +174,9 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint) newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
in := layout.UniformInset(unit.Dp(1)) in := layout.UniformInset(unit.Dp(1))
voiceUpDown := func(gtx C) D { voiceUpDown := func(gtx C) D {
numStyle := NumUpDown(t.Theme, te.TrackVoices, "Track voices") return in.Layout(gtx, func(gtx C) D {
return in.Layout(gtx, numStyle.Layout) return te.TrackVoices.Layout(gtx, t.Model.TrackVoices(), t.Theme, &t.Theme.NumericUpDown, "Track voices")
})
} }
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex") effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip) uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
@ -390,7 +391,7 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
if err != nil { if err != nil {
return return
} }
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val-12) n = noteAsValue(t.Octave().Value(), val-12)
ev := t.Model.Notes().Input(n) ev := t.Model.Notes().Input(n)
t.KeyNoteMap.Press(e.Name, ev) t.KeyNoteMap.Press(e.Name, ev)
} }

View File

@ -23,8 +23,9 @@ import (
"gioui.org/text" "gioui.org/text"
) )
type NumberInput struct { type NumericUpDown struct {
Int tracker.Int DpPerStep unit.Dp
dragStartValue int dragStartValue int
dragStartXY float32 dragStartXY float32
clickDecrease gesture.Click clickDecrease gesture.Click
@ -41,36 +42,19 @@ type NumericUpDownStyle struct {
Width unit.Dp Width unit.Dp
Height unit.Dp Height unit.Dp
TextSize unit.Sp TextSize unit.Sp
DpPerStep unit.Dp Font font.Font
} }
type NumericUpDown struct { func NewNumericUpDown() *NumericUpDown {
NumberInput *NumberInput return &NumericUpDown{DpPerStep: unit.Dp(8)}
Tooltip component.Tooltip
Theme *Theme
Font font.Font
NumericUpDownStyle
} }
func NewNumberInput(v tracker.Int) *NumberInput { func (s *NumericUpDown) Update(gtx layout.Context, v tracker.Int) {
return &NumberInput{Int: v}
}
func NumUpDown(th *Theme, number *NumberInput, tooltip string) NumericUpDown {
return NumericUpDown{
NumberInput: number,
Theme: th,
Tooltip: Tooltip(th, tooltip),
NumericUpDownStyle: th.NumericUpDown,
}
}
func (s *NumericUpDown) Update(gtx layout.Context) {
// handle dragging // handle dragging
pxPerStep := float32(gtx.Dp(s.DpPerStep)) pxPerStep := float32(gtx.Dp(s.DpPerStep))
for { for {
ev, ok := gtx.Event(pointer.Filter{ ev, ok := gtx.Event(pointer.Filter{
Target: s.NumberInput, Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release, Kinds: pointer.Press | pointer.Drag | pointer.Release,
}) })
if !ok { if !ok {
@ -79,46 +63,54 @@ func (s *NumericUpDown) Update(gtx layout.Context) {
if e, ok := ev.(pointer.Event); ok { if e, ok := ev.(pointer.Event); ok {
switch e.Kind { switch e.Kind {
case pointer.Press: case pointer.Press:
s.NumberInput.dragStartValue = s.NumberInput.Int.Value() s.dragStartValue = v.Value()
s.NumberInput.dragStartXY = e.Position.X - e.Position.Y s.dragStartXY = e.Position.X - e.Position.Y
case pointer.Drag: case pointer.Drag:
var deltaCoord float32 var deltaCoord float32
deltaCoord = e.Position.X - e.Position.Y - s.NumberInput.dragStartXY deltaCoord = e.Position.X - e.Position.Y - s.dragStartXY
s.NumberInput.Int.SetValue(s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5)) v.SetValue(s.dragStartValue + int(deltaCoord/pxPerStep+0.5))
} }
} }
} }
// handle decrease clicks // handle decrease clicks
for ev, ok := s.NumberInput.clickDecrease.Update(gtx.Source); ok; ev, ok = s.NumberInput.clickDecrease.Update(gtx.Source) { for ev, ok := s.clickDecrease.Update(gtx.Source); ok; ev, ok = s.clickDecrease.Update(gtx.Source) {
if ev.Kind == gesture.KindClick { if ev.Kind == gesture.KindClick {
s.NumberInput.Int.Add(-1) v.Add(-1)
} }
} }
// handle increase clicks // handle increase clicks
for ev, ok := s.NumberInput.clickIncrease.Update(gtx.Source); ok; ev, ok = s.NumberInput.clickIncrease.Update(gtx.Source) { for ev, ok := s.clickIncrease.Update(gtx.Source); ok; ev, ok = s.clickIncrease.Update(gtx.Source) {
if ev.Kind == gesture.KindClick { if ev.Kind == gesture.KindClick {
s.NumberInput.Int.Add(1) v.Add(1)
} }
} }
} }
func (s NumericUpDown) Layout(gtx C) D { func (s *NumericUpDown) Widget(v tracker.Int, th *Theme, st *NumericUpDownStyle, tooltip string) func(gtx C) D {
if s.Tooltip.Text.Text != "" { return func(gtx C) D {
return s.NumberInput.tipArea.Layout(gtx, s.Tooltip, s.actualLayout) return s.Layout(gtx, v, th, st, tooltip)
} }
return s.actualLayout(gtx)
} }
func (s *NumericUpDown) actualLayout(gtx C) D { func (s *NumericUpDown) Layout(gtx C, v tracker.Int, th *Theme, st *NumericUpDownStyle, tooltip string) D {
s.Update(gtx) s.Update(gtx, v)
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(s.Width), gtx.Dp(s.Height))) if tooltip != "" {
width := gtx.Dp(s.ButtonWidth) return s.tipArea.Layout(gtx, Tooltip(th, tooltip), func(gtx C) D {
height := gtx.Dp(s.Height) return s.actualLayout(gtx, v, th, st)
})
}
return s.actualLayout(gtx, v, th, st)
}
func (s *NumericUpDown) actualLayout(gtx C, v tracker.Int, th *Theme, st *NumericUpDownStyle) D {
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(st.Width), gtx.Dp(st.Height)))
width := gtx.Dp(st.ButtonWidth)
height := gtx.Dp(st.Height)
return layout.Background{}.Layout(gtx, return layout.Background{}.Layout(gtx,
func(gtx C) D { func(gtx C) D {
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, gtx.Dp(s.CornerRadius)).Push(gtx.Ops).Pop() defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, gtx.Dp(st.CornerRadius)).Push(gtx.Ops).Pop()
paint.Fill(gtx.Ops, s.BgColor) paint.Fill(gtx.Ops, st.BgColor)
event.Op(gtx.Ops, s.NumberInput) // register drag inputs, if not hitting the clicks event.Op(gtx.Ops, s) // register drag inputs, if not hitting the clicks
return D{Size: gtx.Constraints.Min} return D{Size: gtx.Constraints.Min}
}, },
func(gtx C) D { func(gtx C) D {
@ -128,25 +120,25 @@ func (s *NumericUpDown) actualLayout(gtx C) D {
return layout.Background{}.Layout(gtx, return layout.Background{}.Layout(gtx,
func(gtx C) D { func(gtx C) D {
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
s.NumberInput.clickDecrease.Add(gtx.Ops) s.clickDecrease.Add(gtx.Ops)
return D{Size: gtx.Constraints.Min} return D{Size: gtx.Constraints.Min}
}, },
func(gtx C) D { return s.Theme.Icon(icons.ContentRemove).Layout(gtx, s.IconColor) }, func(gtx C) D { return th.Icon(icons.ContentRemove).Layout(gtx, st.IconColor) },
) )
}), }),
layout.Flexed(1, func(gtx C) D { layout.Flexed(1, func(gtx C) D {
paint.ColorOp{Color: s.TextColor}.Add(gtx.Ops) paint.ColorOp{Color: st.TextColor}.Add(gtx.Ops)
return widget.Label{Alignment: text.Middle}.Layout(gtx, s.Theme.Material.Shaper, s.Font, s.TextSize, strconv.Itoa(s.NumberInput.Int.Value()), op.CallOp{}) return widget.Label{Alignment: text.Middle}.Layout(gtx, th.Material.Shaper, st.Font, st.TextSize, strconv.Itoa(v.Value()), op.CallOp{})
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
gtx.Constraints = layout.Exact(image.Pt(width, height)) gtx.Constraints = layout.Exact(image.Pt(width, height))
return layout.Background{}.Layout(gtx, return layout.Background{}.Layout(gtx,
func(gtx C) D { func(gtx C) D {
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
s.NumberInput.clickIncrease.Add(gtx.Ops) s.clickIncrease.Add(gtx.Ops)
return D{Size: gtx.Constraints.Min} return D{Size: gtx.Constraints.Min}
}, },
func(gtx C) D { return s.Theme.Icon(icons.ContentAdd).Layout(gtx, s.IconColor) }, func(gtx C) D { return th.Icon(icons.ContentAdd).Layout(gtx, st.IconColor) },
) )
}), }),
) )

View File

@ -19,8 +19,8 @@ type (
OscilloscopeState struct { OscilloscopeState struct {
onceBtn *BoolClickable onceBtn *BoolClickable
wrapBtn *BoolClickable wrapBtn *BoolClickable
lengthInBeatsNumber *NumberInput lengthInBeatsNumber *NumericUpDown
triggerChannelNumber *NumberInput triggerChannelNumber *NumericUpDown
xScale int xScale int
xOffset float32 xOffset float32
yScale float64 yScale float64
@ -34,98 +34,89 @@ type (
LimitColor color.NRGBA `yaml:",flow"` LimitColor color.NRGBA `yaml:",flow"`
CursorColor color.NRGBA `yaml:",flow"` CursorColor color.NRGBA `yaml:",flow"`
} }
Oscilloscope struct {
State *OscilloscopeState
Wave tracker.RingBuffer[[2]float32]
Theme *Theme
OscilloscopeStyle
}
) )
func NewOscilloscope(model *tracker.Model) *OscilloscopeState { func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
return &OscilloscopeState{ return &OscilloscopeState{
onceBtn: NewBoolClickable(model.SignalAnalyzer().Once()), onceBtn: NewBoolClickable(model.SignalAnalyzer().Once()),
wrapBtn: NewBoolClickable(model.SignalAnalyzer().Wrap()), wrapBtn: NewBoolClickable(model.SignalAnalyzer().Wrap()),
lengthInBeatsNumber: NewNumberInput(model.SignalAnalyzer().LengthInBeats()), lengthInBeatsNumber: NewNumericUpDown(),
triggerChannelNumber: NewNumberInput(model.SignalAnalyzer().TriggerChannel()), triggerChannelNumber: NewNumericUpDown(),
} }
} }
func Scope(s *OscilloscopeState, wave tracker.RingBuffer[[2]float32], th *Theme) *Oscilloscope { func (s *OscilloscopeState) Layout(gtx C, vtrig, vlen tracker.Int, wave tracker.RingBuffer[[2]float32], th *Theme, st *OscilloscopeStyle) D {
return &Oscilloscope{State: s, Wave: wave, Theme: th} wrapBtnStyle := ToggleButton(gtx, th, s.wrapBtn, "Wrap")
} onceBtnStyle := ToggleButton(gtx, th, s.onceBtn, "Once")
func (s *Oscilloscope) Layout(gtx C) D {
wrapBtnStyle := ToggleButton(gtx, s.Theme, s.State.wrapBtn, "Wrap")
onceBtnStyle := ToggleButton(gtx, s.Theme, s.State.onceBtn, "Once")
triggerChannelStyle := NumUpDown(s.Theme, s.State.triggerChannelNumber, "Trigger channel")
lengthNumberStyle := NumUpDown(s.Theme, s.State.lengthInBeatsNumber, "Buffer length in beats")
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D { return s.layoutWave(gtx) }), layout.Flexed(1, func(gtx C) D { return s.layoutWave(gtx, wave, th) }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(leftSpacer), layout.Rigid(leftSpacer),
layout.Rigid(Label(s.Theme, &s.Theme.SongPanel.RowHeader, "Trigger").Layout), layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Trigger").Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
layout.Rigid(onceBtnStyle.Layout), layout.Rigid(onceBtnStyle.Layout),
layout.Rigid(triggerChannelStyle.Layout), layout.Rigid(func(gtx C) D {
return s.triggerChannelNumber.Layout(gtx, vtrig, th, &th.NumericUpDown, "Trigger channel")
}),
layout.Rigid(rightSpacer), layout.Rigid(rightSpacer),
) )
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(leftSpacer), layout.Rigid(leftSpacer),
layout.Rigid(Label(s.Theme, &s.Theme.SongPanel.RowHeader, "Buffer").Layout), layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Buffer").Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
layout.Rigid(wrapBtnStyle.Layout), layout.Rigid(wrapBtnStyle.Layout),
layout.Rigid(lengthNumberStyle.Layout), layout.Rigid(func(gtx C) D {
return s.lengthInBeatsNumber.Layout(gtx, vlen, th, &th.NumericUpDown, "Buffer length in beats")
}),
layout.Rigid(rightSpacer), layout.Rigid(rightSpacer),
) )
}), }),
) )
} }
func (s *Oscilloscope) layoutWave(gtx C) D { func (s *OscilloscopeState) layoutWave(gtx C, wave tracker.RingBuffer[[2]float32], th *Theme) D {
s.update(gtx) s.update(gtx, wave)
if gtx.Constraints.Max.X == 0 || gtx.Constraints.Max.Y == 0 { if gtx.Constraints.Max.X == 0 || gtx.Constraints.Max.Y == 0 {
return D{} return D{}
} }
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
event.Op(gtx.Ops, s.State) event.Op(gtx.Ops, s)
paint.ColorOp{Color: s.Theme.Oscilloscope.CursorColor}.Add(gtx.Ops) paint.ColorOp{Color: th.Oscilloscope.CursorColor}.Add(gtx.Ops)
cursorX := int(s.sampleToPx(gtx, float32(s.Wave.Cursor))) cursorX := int(s.sampleToPx(gtx, float32(wave.Cursor), wave))
fillRect(gtx, clip.Rect{Min: image.Pt(cursorX, 0), Max: image.Pt(cursorX+1, gtx.Constraints.Max.Y)}) fillRect(gtx, clip.Rect{Min: image.Pt(cursorX, 0), Max: image.Pt(cursorX+1, gtx.Constraints.Max.Y)})
paint.ColorOp{Color: s.Theme.Oscilloscope.LimitColor}.Add(gtx.Ops) paint.ColorOp{Color: th.Oscilloscope.LimitColor}.Add(gtx.Ops)
minusOneY := int(s.ampToY(gtx, -1)) minusOneY := int(s.ampToY(gtx, -1))
fillRect(gtx, clip.Rect{Min: image.Pt(0, minusOneY), Max: image.Pt(gtx.Constraints.Max.X, minusOneY+1)}) fillRect(gtx, clip.Rect{Min: image.Pt(0, minusOneY), Max: image.Pt(gtx.Constraints.Max.X, minusOneY+1)})
plusOneY := int(s.ampToY(gtx, 1)) plusOneY := int(s.ampToY(gtx, 1))
fillRect(gtx, clip.Rect{Min: image.Pt(0, plusOneY), Max: image.Pt(gtx.Constraints.Max.X, plusOneY+1)}) fillRect(gtx, clip.Rect{Min: image.Pt(0, plusOneY), Max: image.Pt(gtx.Constraints.Max.X, plusOneY+1)})
leftX := int(s.sampleToPx(gtx, 0)) leftX := int(s.sampleToPx(gtx, 0, wave))
fillRect(gtx, clip.Rect{Min: image.Pt(leftX, 0), Max: image.Pt(leftX+1, gtx.Constraints.Max.Y)}) fillRect(gtx, clip.Rect{Min: image.Pt(leftX, 0), Max: image.Pt(leftX+1, gtx.Constraints.Max.Y)})
rightX := int(s.sampleToPx(gtx, float32(len(s.Wave.Buffer)-1))) rightX := int(s.sampleToPx(gtx, float32(len(wave.Buffer)-1), wave))
fillRect(gtx, clip.Rect{Min: image.Pt(rightX, 0), Max: image.Pt(rightX+1, gtx.Constraints.Max.Y)}) fillRect(gtx, clip.Rect{Min: image.Pt(rightX, 0), Max: image.Pt(rightX+1, gtx.Constraints.Max.Y)})
for chn := range 2 { for chn := range 2 {
paint.ColorOp{Color: s.Theme.Oscilloscope.CurveColors[chn]}.Add(gtx.Ops) paint.ColorOp{Color: th.Oscilloscope.CurveColors[chn]}.Add(gtx.Ops)
for px := range gtx.Constraints.Max.X { for px := range gtx.Constraints.Max.X {
// left and right is the sample range covered by the pixel // left and right is the sample range covered by the pixel
left := int(s.pxToSample(gtx, float32(px)-0.5)) left := int(s.pxToSample(gtx, float32(px)-0.5, wave))
right := int(s.pxToSample(gtx, float32(px)+0.5)) right := int(s.pxToSample(gtx, float32(px)+0.5, wave))
if right < 0 || left >= len(s.Wave.Buffer) { if right < 0 || left >= len(wave.Buffer) {
continue continue
} }
right = min(right, len(s.Wave.Buffer)-1) right = min(right, len(wave.Buffer)-1)
left = max(left, 0) left = max(left, 0)
// smin and smax are the smallest and largest sample values in the pixel range // smin and smax are the smallest and largest sample values in the pixel range
smax := float32(math.Inf(-1)) smax := float32(math.Inf(-1))
smin := float32(math.Inf(1)) smin := float32(math.Inf(1))
for x := left; x <= right; x++ { for x := left; x <= right; x++ {
smax = max(smax, s.Wave.Buffer[x][chn]) smax = max(smax, wave.Buffer[x][chn])
smin = min(smin, s.Wave.Buffer[x][chn]) smin = min(smin, wave.Buffer[x][chn])
} }
// y1 and y2 are the pixel range covered by the sample value // y1 and y2 are the pixel range covered by the sample value
y1 := min(max(int(s.ampToY(gtx, smax)+0.5), 0), gtx.Constraints.Max.Y-1) y1 := min(max(int(s.ampToY(gtx, smax)+0.5), 0), gtx.Constraints.Max.Y-1)
@ -142,10 +133,10 @@ func fillRect(gtx C, rect clip.Rect) {
stack.Pop() stack.Pop()
} }
func (o *Oscilloscope) update(gtx C) { func (o *OscilloscopeState) update(gtx C, wave tracker.RingBuffer[[2]float32]) {
for { for {
ev, ok := gtx.Event(pointer.Filter{ ev, ok := gtx.Event(pointer.Filter{
Target: o.State, Target: o,
Kinds: pointer.Scroll | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel, Kinds: pointer.Scroll | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6}, ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
}) })
@ -155,59 +146,59 @@ func (o *Oscilloscope) update(gtx C) {
if e, ok := ev.(pointer.Event); ok { if e, ok := ev.(pointer.Event); ok {
switch e.Kind { switch e.Kind {
case pointer.Scroll: case pointer.Scroll:
s1 := o.pxToSample(gtx, e.Position.X) s1 := o.pxToSample(gtx, e.Position.X, wave)
o.State.xScale += min(max(-1, int(e.Scroll.Y)), 1) o.xScale += min(max(-1, int(e.Scroll.Y)), 1)
s2 := o.pxToSample(gtx, e.Position.X) s2 := o.pxToSample(gtx, e.Position.X, wave)
o.State.xOffset -= s1 - s2 o.xOffset -= s1 - s2
case pointer.Press: case pointer.Press:
if e.Buttons&pointer.ButtonSecondary != 0 { if e.Buttons&pointer.ButtonSecondary != 0 {
o.State.xOffset = 0 o.xOffset = 0
o.State.xScale = 0 o.xScale = 0
o.State.yScale = 0 o.yScale = 0
} }
if e.Buttons&pointer.ButtonPrimary != 0 { if e.Buttons&pointer.ButtonPrimary != 0 {
o.State.dragging = true o.dragging = true
o.State.dragId = e.PointerID o.dragId = e.PointerID
o.State.dragStartPoint = e.Position o.dragStartPoint = e.Position
} }
case pointer.Drag: case pointer.Drag:
if e.Buttons&pointer.ButtonPrimary != 0 && o.State.dragging && e.PointerID == o.State.dragId { if e.Buttons&pointer.ButtonPrimary != 0 && o.dragging && e.PointerID == o.dragId {
deltaX := o.pxToSample(gtx, e.Position.X) - o.pxToSample(gtx, o.State.dragStartPoint.X) deltaX := o.pxToSample(gtx, e.Position.X, wave) - o.pxToSample(gtx, o.dragStartPoint.X, wave)
o.State.xOffset += deltaX o.xOffset += deltaX
num := o.yToAmp(gtx, e.Position.Y) num := o.yToAmp(gtx, e.Position.Y)
den := o.yToAmp(gtx, o.State.dragStartPoint.Y) den := o.yToAmp(gtx, o.dragStartPoint.Y)
if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 { if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 {
o.State.yScale += math.Log(l) o.yScale += math.Log(l)
o.State.yScale = min(max(o.State.yScale, -1e3), 1e3) o.yScale = min(max(o.yScale, -1e3), 1e3)
} }
o.State.dragStartPoint = e.Position o.dragStartPoint = e.Position
} }
case pointer.Release | pointer.Cancel: case pointer.Release | pointer.Cancel:
o.State.dragging = false o.dragging = false
} }
} }
} }
} }
func (o *Oscilloscope) scaleFactor() float32 { func (o *OscilloscopeState) scaleFactor() float32 {
return float32(math.Pow(1.1, float64(o.State.xScale))) return float32(math.Pow(1.1, float64(o.xScale)))
} }
func (s *Oscilloscope) pxToSample(gtx C, px float32) float32 { func (s *OscilloscopeState) pxToSample(gtx C, px float32, wave tracker.RingBuffer[[2]float32]) float32 {
return px*s.scaleFactor()*float32(len(s.Wave.Buffer))/float32(gtx.Constraints.Max.X) - s.State.xOffset return px*s.scaleFactor()*float32(len(wave.Buffer))/float32(gtx.Constraints.Max.X) - s.xOffset
} }
func (s *Oscilloscope) sampleToPx(gtx C, sample float32) float32 { func (s *OscilloscopeState) sampleToPx(gtx C, sample float32, wave tracker.RingBuffer[[2]float32]) float32 {
return (sample + s.State.xOffset) * float32(gtx.Constraints.Max.X) / float32(len(s.Wave.Buffer)) / s.scaleFactor() return (sample + s.xOffset) * float32(gtx.Constraints.Max.X) / float32(len(wave.Buffer)) / s.scaleFactor()
} }
func (s *Oscilloscope) ampToY(gtx C, amp float32) float32 { func (s *OscilloscopeState) ampToY(gtx C, amp float32) float32 {
scale := float32(math.Exp(s.State.yScale)) scale := float32(math.Exp(s.yScale))
return (1 - amp*scale) / 2 * float32(gtx.Constraints.Max.Y-1) return (1 - amp*scale) / 2 * float32(gtx.Constraints.Max.Y-1)
} }
func (s *Oscilloscope) yToAmp(gtx C, y float32) float32 { func (s *OscilloscopeState) yToAmp(gtx C, y float32) float32 {
scale := float32(math.Exp(s.State.yScale)) scale := float32(math.Exp(s.yScale))
return (1 - y/float32(gtx.Constraints.Max.Y-1)*2) / scale return (1 - y/float32(gtx.Constraints.Max.Y-1)*2) / scale
} }

View File

@ -25,11 +25,11 @@ type SongPanel struct {
WeightingTypeBtn *Clickable WeightingTypeBtn *Clickable
OversamplingBtn *Clickable OversamplingBtn *Clickable
BPM *NumberInput BPM *NumericUpDown
RowsPerPattern *NumberInput RowsPerPattern *NumericUpDown
RowsPerBeat *NumberInput RowsPerBeat *NumericUpDown
Step *NumberInput Step *NumericUpDown
SongLength *NumberInput SongLength *NumericUpDown
Scope *OscilloscopeState Scope *OscilloscopeState
@ -39,11 +39,11 @@ type SongPanel struct {
func NewSongPanel(model *tracker.Model) *SongPanel { func NewSongPanel(model *tracker.Model) *SongPanel {
ret := &SongPanel{ ret := &SongPanel{
BPM: NewNumberInput(model.BPM()), BPM: NewNumericUpDown(),
RowsPerPattern: NewNumberInput(model.RowsPerPattern()), RowsPerPattern: NewNumericUpDown(),
RowsPerBeat: NewNumberInput(model.RowsPerBeat()), RowsPerBeat: NewNumericUpDown(),
Step: NewNumberInput(model.Step()), Step: NewNumericUpDown(),
SongLength: NewNumberInput(model.SongLength()), SongLength: NewNumericUpDown(),
Scope: NewOscilloscope(model), Scope: NewOscilloscope(model),
MenuBar: NewMenuBar(model), MenuBar: NewMenuBar(model),
PlayBar: NewPlayBar(model), PlayBar: NewPlayBar(model),
@ -115,19 +115,19 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
func(gtx C) D { func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layoutSongOptionRow(gtx, tr.Theme, "BPM", NumUpDown(tr.Theme, t.BPM, "BPM").Layout) return layoutSongOptionRow(gtx, tr.Theme, "BPM", t.BPM.Widget(tr.Model.BPM(), tr.Theme, &tr.Theme.NumericUpDown, "BPM"))
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layoutSongOptionRow(gtx, tr.Theme, "Song length", NumUpDown(tr.Theme, t.SongLength, "Song Length").Layout) return layoutSongOptionRow(gtx, tr.Theme, "Song length", t.SongLength.Widget(tr.Model.SongLength(), tr.Theme, &tr.Theme.NumericUpDown, "Song length"))
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layoutSongOptionRow(gtx, tr.Theme, "Rows per pat", NumUpDown(tr.Theme, t.RowsPerPattern, "Rows per pattern").Layout) return layoutSongOptionRow(gtx, tr.Theme, "Rows per pat", t.RowsPerPattern.Widget(tr.Model.RowsPerPattern(), tr.Theme, &tr.Theme.NumericUpDown, "Rows per pattern"))
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layoutSongOptionRow(gtx, tr.Theme, "Rows per beat", NumUpDown(tr.Theme, t.RowsPerBeat, "Rows per beat").Layout) return layoutSongOptionRow(gtx, tr.Theme, "Rows per beat", t.RowsPerBeat.Widget(tr.Model.RowsPerBeat(), tr.Theme, &tr.Theme.NumericUpDown, "Rows per beat"))
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layoutSongOptionRow(gtx, tr.Theme, "Cursor step", NumUpDown(tr.Theme, t.Step, "Cursor step").Layout) return layoutSongOptionRow(gtx, tr.Theme, "Cursor step", t.Step.Widget(tr.Model.Step(), tr.Theme, &tr.Theme.NumericUpDown, "Cursor step"))
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
cpuload := tr.Model.CPULoad() cpuload := tr.Model.CPULoad()
@ -200,7 +200,9 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
) )
}), }),
layout.Flexed(1, func(gtx C) D { layout.Flexed(1, func(gtx C) D {
return t.ScopeExpander.Layout(gtx, tr.Theme, "Oscilloscope", func(gtx C) D { return D{} }, Scope(t.Scope, tr.SignalAnalyzer().Waveform(), tr.Theme).Layout) return t.ScopeExpander.Layout(gtx, tr.Theme, "Oscilloscope", func(gtx C) D { return D{} }, func(gtx C) D {
return t.Scope.Layout(gtx, tr.Model.SignalAnalyzer().TriggerChannel(), tr.Model.SignalAnalyzer().LengthInBeats(), tr.Model.SignalAnalyzer().Waveform(), tr.Theme, &tr.Theme.Oscilloscope)
})
}), }),
layout.Rigid(Label(tr.Theme, &tr.Theme.SongPanel.Version, version.VersionOrHash).Layout), layout.Rigid(Label(tr.Theme, &tr.Theme.SongPanel.Version, version.VersionOrHash).Layout),
) )

View File

@ -64,7 +64,6 @@ numericupdown:
iconcolor: *primarycolor iconcolor: *primarycolor
cornerradius: 4 cornerradius: 4
buttonwidth: 16 buttonwidth: 16
dpperstep: 8
textsize: 14 textsize: 14
width: 70 width: 70
height: 20 height: 20

View File

@ -28,8 +28,8 @@ var canQuit = true // set to false in init() if plugin tag is enabled
type ( type (
Tracker struct { Tracker struct {
Theme *Theme Theme *Theme
OctaveNumberInput *NumberInput OctaveNumberInput *NumericUpDown
InstrumentVoices *NumberInput InstrumentVoices *NumericUpDown
TopHorizontalSplit *Split TopHorizontalSplit *Split
BottomHorizontalSplit *Split BottomHorizontalSplit *Split
VerticalSplit *Split VerticalSplit *Split
@ -70,8 +70,8 @@ var ZoomFactors = []float32{.25, 1. / 3, .5, 2. / 3, .75, .8, 1, 1.1, 1.25, 1.5,
func NewTracker(model *tracker.Model) *Tracker { func NewTracker(model *tracker.Model) *Tracker {
t := &Tracker{ t := &Tracker{
OctaveNumberInput: NewNumberInput(model.Octave()), OctaveNumberInput: NewNumericUpDown(),
InstrumentVoices: NewNumberInput(model.InstrumentVoices()), InstrumentVoices: NewNumericUpDown(),
TopHorizontalSplit: &Split{Ratio: -.5, MinSize1: 180, MinSize2: 180}, TopHorizontalSplit: &Split{Ratio: -.5, MinSize1: 180, MinSize2: 180},
BottomHorizontalSplit: &Split{Ratio: -.6, MinSize1: 180, MinSize2: 180}, BottomHorizontalSplit: &Split{Ratio: -.6, MinSize1: 180, MinSize2: 180},