Merge 243396c3010c462b7a1440218779449057ad2476 into 416935684582fc25c4cc2f1fae3b9ddb0229877e

This commit is contained in:
qm210 2024-11-22 14:46:54 +00:00 committed by GitHub
commit d7df2707f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 202 additions and 41 deletions

View File

@ -19,6 +19,7 @@ import (
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"gioui.org/x/component" "gioui.org/x/component"
"github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker"
) )
@ -44,6 +45,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 +145,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 {

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
} }

16
tracker/gioui/layout.go Normal file
View File

@ -0,0 +1,16 @@
package gioui
import "gioui.org/layout"
// general helpers for layout that do not belong to any specific widget
func EmptyWidget() layout.Spacer {
return layout.Spacer{}
}
func OnlyIf(condition bool, widget layout.Widget) layout.Widget {
if condition {
return widget
}
return EmptyWidget().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,8 @@ 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 := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
voiceUpDown := func(gtx C) D { voiceUpDown.Padding = unit.Dp(1)
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,10 +173,10 @@ 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(OnlyIf(t.HasAnyMidiInput(), midiInBtnStyle.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(deleteTrackBtnStyle.Layout), layout.Rigid(deleteTrackBtnStyle.Layout),
layout.Rigid(newTrackBtnStyle.Layout)) layout.Rigid(newTrackBtnStyle.Layout))
@ -287,15 +284,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 +331,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,6 +48,7 @@ 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
} }
@ -74,11 +75,19 @@ 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(0),
shaper: *th.Shaper, shaper: *th.Shaper,
} }
} }
func (s *NumericUpDownStyle) Layout(gtx C) D { func (s *NumericUpDownStyle) Layout(gtx C) 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,13 @@ 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)))
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.
@ -344,3 +347,10 @@ func (t *Tracker) removeFromMidiNotePlaying(note byte) {
} }
} }
} }
func (t *Tracker) HasAnyMidiInput() bool {
for _ = range t.Model.MIDI.InputDevices {
return true
}
return false
}

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
}