feat: UI work to facilitate future improvements in midi-into-track-input

This commit is contained in:
qm210 2024-11-22 13:48:25 +01:00
parent 4169356845
commit ad690c7697
12 changed files with 191 additions and 40 deletions

View File

@ -44,6 +44,14 @@ type (
TipArea component.TipArea TipArea component.TipArea
Bool tracker.Bool Bool tracker.Bool
} }
MenuClickable struct {
Clickable Clickable
menu Menu
Selected tracker.OptionalInt
TipArea component.TipArea
Tooltip component.Tooltip
}
) )
func NewActionClickable(a tracker.Action) *ActionClickable { func NewActionClickable(a tracker.Action) *ActionClickable {
@ -136,7 +144,10 @@ func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) Butt
ret := Button(th, &b.Clickable, text) ret := Button(th, &b.Clickable, text)
ret.Background = transparent ret.Background = transparent
ret.Inset = layout.UniformInset(unit.Dp(6)) ret.Inset = layout.UniformInset(unit.Dp(6))
if b.Bool.Value() { if !b.Bool.Enabled() {
ret.Color = disabledTextColor
ret.Background = transparent
} else if b.Bool.Value() {
ret.Color = th.Palette.ContrastFg ret.Color = th.Palette.ContrastFg
ret.Background = th.Palette.Fg ret.Background = th.Palette.Fg
} else { } else {
@ -287,6 +298,7 @@ type ButtonStyle struct {
Inset layout.Inset Inset layout.Inset
Button *Clickable Button *Clickable
shaper *text.Shaper shaper *text.Shaper
Hidden bool
} }
type ButtonLayoutStyle struct { type ButtonLayoutStyle struct {
@ -351,6 +363,9 @@ func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
CornerRadius: b.CornerRadius, CornerRadius: b.CornerRadius,
Button: b.Button, Button: b.Button,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { }.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
if b.Hidden {
return layout.Dimensions{}
}
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
colMacro := op.Record(gtx.Ops) colMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: b.Color}.Add(gtx.Ops) paint.ColorOp{Color: b.Color}.Add(gtx.Ops)

View File

@ -10,6 +10,9 @@ var iconCache = map[*byte]*widget.Icon{}
// widgetForIcon returns a widget for IconVG data, but caching the results // widgetForIcon returns a widget for IconVG data, but caching the results
func widgetForIcon(icon []byte) *widget.Icon { func widgetForIcon(icon []byte) *widget.Icon {
if icon == nil {
return nil
}
if widget, ok := iconCache[&icon[0]]; ok { if widget, ok := iconCache[&icon[0]]; ok {
return widget return widget
} }

View File

@ -46,5 +46,17 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
} }
func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget { func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget {
return LabelStyle{Text: str, Color: color, ShadeColor: black, Font: labelDefaultFont, FontSize: labelDefaultFontSize, Alignment: layout.W, Shaper: shaper}.Layout return SizedLabel(str, color, shaper, labelDefaultFontSize)
}
func SizedLabel(str string, color color.NRGBA, shaper *text.Shaper, fontSize unit.Sp) layout.Widget {
return LabelStyle{
Text: str,
Color: color,
ShadeColor: black,
Font: labelDefaultFont,
FontSize: fontSize,
Alignment: layout.W,
Shaper: shaper,
}.Layout
} }

View File

@ -12,8 +12,6 @@ import (
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/text" "gioui.org/text"
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker"
) )
@ -103,12 +101,12 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
} }
icon := widgetForIcon(item.IconBytes) icon := widgetForIcon(item.IconBytes)
iconColor := m.IconColor iconColor := m.IconColor
if !item.Doer.Allowed() {
iconColor = mediumEmphasisTextColor
}
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)} iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper} textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper}
if !item.Doer.Allowed() { if !item.Doer.Allowed() {
// note: might be a bug in gioui, but for iconColor = mediumEmphasisTextColor
// this does not render the icon at all. other colors seem to work fine.
iconColor = disabledTextColor
textLabel.Color = mediumEmphasisTextColor textLabel.Color = mediumEmphasisTextColor
} }
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper} shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper}
@ -116,13 +114,18 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return iconInset.Layout(gtx, func(gtx C) D { return iconInset.Layout(gtx, func(gtx C) D {
p := gtx.Dp(unit.Dp(m.IconSize)) p := gtx.Dp(m.IconSize)
gtx.Constraints.Min = image.Pt(p, p) gtx.Constraints.Min = image.Pt(p, p)
if icon == nil {
return D{Size: gtx.Constraints.Min}
}
return icon.Layout(gtx, iconColor) return icon.Layout(gtx, iconColor)
}) })
}), }),
layout.Rigid(textLabel.Layout), layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }), layout.Flexed(1, func(gtx C) D {
return D{Size: image.Pt(gtx.Constraints.Max.X, 1)}
}),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout) return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}), }),
@ -168,14 +171,14 @@ func PopupMenu(menu *Menu, shaper *text.Shaper) MenuStyle {
} }
} }
func (tr *Tracker) layoutMenu(gtx C, title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget { func (tr *Tracker) layoutMenu(gtx C, title string, clickable *Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
for clickable.Clicked(gtx) { for clickable.Clicked(gtx) {
menu.Visible = true menu.Visible = true
} }
m := PopupMenu(menu, tr.Theme.Shaper) m := PopupMenu(menu, tr.Theme.Shaper)
return func(gtx C) D { return func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
titleBtn := material.Button(tr.Theme, clickable, title) titleBtn := Button(tr.Theme, clickable, title)
titleBtn.Color = white titleBtn.Color = white
titleBtn.Background = transparent titleBtn.Background = transparent
titleBtn.CornerRadius = unit.Dp(0) titleBtn.CornerRadius = unit.Dp(0)

View File

@ -158,11 +158,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint) deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint) splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
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)) voiceUpDown := NumericUpDownPadded(t.Theme, te.TrackVoices, "Number of voices for this track", 1)
voiceUpDown := func(gtx C) D {
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
return in.Layout(gtx, numStyle.Layout)
}
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)
midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI") midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI")
@ -176,7 +172,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
layout.Rigid(effectBtnStyle.Layout), layout.Rigid(effectBtnStyle.Layout),
layout.Rigid(uniqueBtnStyle.Layout), layout.Rigid(uniqueBtnStyle.Layout),
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)), layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
layout.Rigid(voiceUpDown), layout.Rigid(voiceUpDown.Layout),
layout.Rigid(splitTrackBtnStyle.Layout), layout.Rigid(splitTrackBtnStyle.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} }),
layout.Rigid(midiInBtnStyle.Layout), layout.Rigid(midiInBtnStyle.Layout),
@ -287,15 +283,15 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
c = cursorColor c = cursorColor
} }
if hasTrackMidiIn { if hasTrackMidiIn {
c = cursorForTrackMidiInColor c = trackMidiInCurrentColor
} }
te.paintColumnCell(gtx, x, t, c) te.paintColumnCell(gtx, x, t, c, hasTrackMidiIn)
} }
// draw the corresponding "fake cursors" for instrument-track-groups (for polyphony) // draw the corresponding "fake cursors" for instrument-track-groups (for polyphony)
if hasTrackMidiIn { if hasTrackMidiIn && y == cursor.Y {
for _, trackIndex := range t.Model.TracksWithSameInstrumentAsCurrent() { for _, trackIndex := range t.Model.TracksWithSameInstrumentAsCurrent() {
if x == trackIndex && y == cursor.Y { if x == trackIndex {
te.paintColumnCell(gtx, x, t, cursorNeighborForTrackMidiInColor) te.paintColumnCell(gtx, x, t, trackMidiInAdditionalColor, hasTrackMidiIn)
} }
} }
} }
@ -334,10 +330,10 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
return table.Layout(gtx) return table.Layout(gtx)
} }
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA) { func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA, ignoreEffect bool) {
cw := gtx.Constraints.Min.X cw := gtx.Constraints.Min.X
cx := 0 cx := 0
if t.Model.Notes().Effect(x) { if t.Model.Notes().Effect(x) && !ignoreEffect {
cw /= 2 cw /= 2
if t.Model.Notes().LowNibble() { if t.Model.Notes().LowNibble() {
cx += cw cx += cw

View File

@ -48,7 +48,9 @@ type NumericUpDownStyle struct {
Tooltip component.Tooltip Tooltip component.Tooltip
Width unit.Dp Width unit.Dp
Height unit.Dp Height unit.Dp
Padding unit.Dp
shaper text.Shaper shaper text.Shaper
Hidden bool
} }
func NewNumberInput(v tracker.Int) *NumberInput { func NewNumberInput(v tracker.Int) *NumberInput {
@ -56,6 +58,10 @@ func NewNumberInput(v tracker.Int) *NumberInput {
} }
func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) NumericUpDownStyle { func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) NumericUpDownStyle {
return NumericUpDownPadded(th, number, tooltip, 0)
}
func NumericUpDownPadded(th *material.Theme, number *NumberInput, tooltip string, padding int) NumericUpDownStyle {
bgColor := th.Palette.Fg bgColor := th.Palette.Fg
bgColor.R /= 4 bgColor.R /= 4
bgColor.G /= 4 bgColor.G /= 4
@ -74,11 +80,22 @@ func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) Nume
Tooltip: Tooltip(th, tooltip), Tooltip: Tooltip(th, tooltip),
Width: unit.Dp(70), Width: unit.Dp(70),
Height: unit.Dp(20), Height: unit.Dp(20),
Padding: unit.Dp(padding),
shaper: *th.Shaper, shaper: *th.Shaper,
} }
} }
func (s *NumericUpDownStyle) Layout(gtx C) D { func (s *NumericUpDownStyle) Layout(gtx C) D {
if s.Hidden {
return D{}
}
if s.Padding <= 0 {
return s.layoutWithTooltip(gtx)
}
return layout.UniformInset(s.Padding).Layout(gtx, s.layoutWithTooltip)
}
func (s *NumericUpDownStyle) layoutWithTooltip(gtx C) D {
if s.Tooltip.Text.Text != "" { if s.Tooltip.Text.Text != "" {
return s.NumberInput.tipArea.Layout(gtx, s.Tooltip, s.actualLayout) return s.NumberInput.tipArea.Layout(gtx, s.Tooltip, s.actualLayout)
} }

View File

@ -7,14 +7,13 @@ import (
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget"
"github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/version" "github.com/vsariola/sointu/version"
"golang.org/x/exp/shiny/materialdesign/icons" "golang.org/x/exp/shiny/materialdesign/icons"
) )
type SongPanel struct { type SongPanel struct {
MenuBar []widget.Clickable MenuBar []Clickable
Menus []Menu Menus []Menu
BPM *NumberInput BPM *NumberInput
RowsPerPattern *NumberInput RowsPerPattern *NumberInput
@ -57,7 +56,7 @@ type SongPanel struct {
func NewSongPanel(model *tracker.Model) *SongPanel { func NewSongPanel(model *tracker.Model) *SongPanel {
ret := &SongPanel{ ret := &SongPanel{
MenuBar: make([]widget.Clickable, 3), MenuBar: make([]Clickable, 3),
Menus: make([]Menu, 3), Menus: make([]Menu, 3),
BPM: NewNumberInput(model.BPM().Int()), BPM: NewNumberInput(model.BPM().Int()),
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()), RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),

View File

@ -60,8 +60,10 @@ var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48} var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12} var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16} var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}
var cursorForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 48}
var cursorNeighborForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 24} var trackMidiInCurrentColor = color.NRGBA{R: 255, G: 100, B: 140, A: 48}
var trackMidiInAdditionalColor = withScaledAlpha(trackMidiInCurrentColor, 0.7)
var trackMidiVelInColor = withScaledAlpha(trackMidiInCurrentColor, 0.3)
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255} var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
@ -75,3 +77,13 @@ var dialogBgColor = color.NRGBA{R: 0, G: 0, B: 0, A: 224}
var paramIsSendTargetColor = color.NRGBA{R: 120, G: 120, B: 210, A: 255} var paramIsSendTargetColor = color.NRGBA{R: 120, G: 120, B: 210, A: 255}
var paramValueInvalidColor = color.NRGBA{R: 120, G: 120, B: 120, A: 190} var paramValueInvalidColor = color.NRGBA{R: 120, G: 120, B: 120, A: 190}
func withScaledAlpha(c color.NRGBA, factor float32) color.NRGBA {
A := factor * float32(c.A)
return color.NRGBA{
R: c.R,
G: c.G,
B: c.B,
A: uint8(A),
}
}

View File

@ -101,9 +101,7 @@ func NewTracker(model *tracker.Model) *Tracker {
func (t *Tracker) Main() { func (t *Tracker) Main() {
titleFooter := "" titleFooter := ""
w := new(app.Window) w := NewWindow()
w.Option(app.Title("Sointu Tracker"))
w.Option(app.Size(unit.Dp(800), unit.Dp(600)))
t.InstrumentEditor.Focus() t.InstrumentEditor.Focus()
recoveryTicker := time.NewTicker(time.Second * 30) recoveryTicker := time.NewTicker(time.Second * 30)
t.Explorer = explorer.NewExplorer(w) t.Explorer = explorer.NewExplorer(w)
@ -127,9 +125,7 @@ func (t *Tracker) Main() {
} }
if !t.Quitted() { if !t.Quitted() {
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog // TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
w = new(app.Window) w = NewWindow()
w.Option(app.Title("Sointu Tracker"))
w.Option(app.Size(unit.Dp(800), unit.Dp(600)))
t.Explorer = explorer.NewExplorer(w) t.Explorer = explorer.NewExplorer(w)
go eventLoop(w, events, acks) go eventLoop(w, events, acks)
} }
@ -165,6 +161,16 @@ func (t *Tracker) Main() {
t.quitWG.Done() t.quitWG.Done()
} }
func NewWindow() *app.Window {
w := new(app.Window)
w.Option(app.Title("Sointu Tracker"))
w.Option(
app.Size(unit.Dp(800), unit.Dp(600)),
app.Fullscreen.Option(),
)
return w
}
func eventLoop(w *app.Window, events chan<- event.Event, acks <-chan struct{}) { func eventLoop(w *app.Window, events chan<- event.Event, acks <-chan struct{}) {
// Iterate window events, sending each to the old event loop and waiting for // Iterate window events, sending each to the old event loop and waiting for
// a signal that processing is complete before iterating again. // a signal that processing is complete before iterating again.

View File

@ -32,7 +32,7 @@ type UnitEditor struct {
CopyUnitBtn *TipClickable CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable SelectTypeBtn *Clickable
commentEditor *Editor commentEditor *Editor
caser cases.Caser caser cases.Caser
@ -47,7 +47,7 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
ClearUnitBtn: NewActionClickable(m.ClearUnit()), ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()), DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable), CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable), SelectTypeBtn: new(Clickable),
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}), commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
sliderList: NewDragList(m.Params().List(), layout.Vertical), sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical), searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
@ -236,9 +236,9 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) {
type ParameterWidget struct { type ParameterWidget struct {
floatWidget widget.Float floatWidget widget.Float
boolWidget widget.Bool boolWidget widget.Bool
instrBtn widget.Clickable instrBtn Clickable
instrMenu Menu instrMenu Menu
unitBtn widget.Clickable unitBtn Clickable
unitMenu Menu unitMenu Menu
Parameter tracker.Parameter Parameter tracker.Parameter
tipArea component.TipArea tipArea component.TipArea
@ -332,7 +332,6 @@ func (p ParameterStyle) Layout(gtx C) D {
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40)) gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
instrItems := make([]MenuItem, p.tracker.Instruments().Count()) instrItems := make([]MenuItem, p.tracker.Instruments().Count())
for i := range instrItems { for i := range instrItems {
i := i
name, _, _, _ := p.tracker.Instruments().Item(i) name, _, _, _ := p.tracker.Instruments().Item(i)
instrItems[i].Text = name instrItems[i].Text = name
instrItems[i].IconBytes = icons.NavigationChevronRight instrItems[i].IconBytes = icons.NavigationChevronRight

42
tracker/optional_int.go Normal file
View File

@ -0,0 +1,42 @@
package tracker
type (
// OptionalInt tries to follow the same convention as e.g. Int{...} or Bool{...}
// Do not confuse with types.OptionalInteger, which you might use as a model,
// but don't necessarily have to.
OptionalInt struct {
optionalIntData
}
optionalIntData interface {
Unpack() (int, bool)
Value() int
Range() intRange
setValue(int)
unsetValue()
change(kind string) func()
}
TrackMidiVelIn Model
)
func (v OptionalInt) Set(value int, present bool) (ok bool) {
if !present {
v.unsetValue()
return true
}
// TODO: can we deduplicate this by referencing Int{...}.Set(value) ?
r := v.Range()
if v.Equals(value, present) || value < r.Min || value > r.Max {
return false
}
defer v.change("Set")()
v.setValue(value)
return true
}
func (v OptionalInt) Equals(value int, present bool) bool {
oldValue, oldPresent := v.Unpack()
return value == oldValue && present == oldPresent
}

47
tracker/types/optional.go Normal file
View File

@ -0,0 +1,47 @@
package types
type (
// OptionalInteger is the simple struct, not to be confused with tracker.OptionalInt.
// It implements the tracker.optionalIntData interface, without needing to know so.
OptionalInteger struct {
value int
exists bool
}
)
func NewOptionalInteger(value int, exists bool) OptionalInteger {
return OptionalInteger{value, exists}
}
func NewOptionalIntegerOf(value int) OptionalInteger {
return OptionalInteger{
value: value,
exists: true,
}
}
func NewEmptyOptionalInteger() OptionalInteger {
// could also just use OptionalInteger{}
return OptionalInteger{
exists: false,
}
}
func (i OptionalInteger) Unpack() (int, bool) {
return i.value, i.exists
}
func (i OptionalInteger) Value() int {
if !i.exists {
panic("Access value of empty OptionalInteger")
}
return i.value
}
func (i OptionalInteger) Empty() bool {
return !i.exists
}
func (i OptionalInteger) Equals(value int) bool {
return i.exists && i.value == value
}