From db2ccf977d40f27afba489203a777c8078df1374 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:49:07 +0300 Subject: [PATCH] refactor(tracker/gioui): rewrote Button(s) to bind to Model during layout The old mechanism made it difficult to follow exactly what happens when a button was clicked, because the Action/Bool that gets executed / toggled was declared ages ago, in the constructor. In the new mechanism, the Action / Bool is bound to the button at the last minute, right before Layout. ActionButton, ToggleButton, ActionIconButton and ToggleIconButton were done to avoid heap escapes: if the corresponding functions woudl've returned layout.Widget, a heap allocation would've been needed. --- tracker/gioui/buttons.go | 483 +++++++++++++++-------------- tracker/gioui/instrument_editor.go | 130 ++++---- tracker/gioui/keybindings.go | 4 +- tracker/gioui/menu.go | 5 +- tracker/gioui/note_editor.go | 91 +++--- tracker/gioui/oscilloscope.go | 20 +- tracker/gioui/songpanel.go | 70 ++--- tracker/gioui/theme.go | 6 + tracker/gioui/theme.yml | 18 ++ tracker/gioui/tracker.go | 2 +- tracker/gioui/unit_editor.go | 35 ++- 11 files changed, 453 insertions(+), 411 deletions(-) diff --git a/tracker/gioui/buttons.go b/tracker/gioui/buttons.go index 8bf45b8..ca2bd10 100644 --- a/tracker/gioui/buttons.go +++ b/tracker/gioui/buttons.go @@ -22,41 +22,273 @@ import ( ) type ( - TipClickable struct { - Clickable Clickable - TipArea component.TipArea + Clickable struct { + click gesture.Click + history []widget.Press + + requestClicks int + TipArea component.TipArea // since almost all buttons have tooltips, we include the state for a tooltip here for convenience } - ActionClickable struct { - Action tracker.Action - TipClickable + ButtonStyle struct { + // Color is the text color. + Color color.NRGBA + Font font.Font + TextSize unit.Sp + Background color.NRGBA + CornerRadius unit.Dp + Height unit.Dp + Inset layout.Inset } - TipIconButtonStyle struct { - TipArea *component.TipArea - IconButtonStyle IconButtonStyle - Tooltip component.Tooltip + IconButtonStyle struct { + Background color.NRGBA + // Color is the icon color. + Color color.NRGBA + // Size is the icon size. + Size unit.Dp + Inset layout.Inset } - BoolClickable struct { - Clickable Clickable - TipArea component.TipArea - Bool tracker.Bool + // Button is a text button + Button struct { + Theme *Theme + Style *ButtonStyle + Text string + Tip string + Clickable *Clickable + } + + // ActionButton is a text button that executes an action when clicked. + ActionButton struct { + Action tracker.Action + DisabledStyle *ButtonStyle + Button + } + + // ToggleButton is a text button that toggles a boolean value when clicked. + ToggleButton struct { + Bool tracker.Bool + DisabledStyle *ButtonStyle + OffStyle *ButtonStyle + Button + } + + // IconButton is a button with an icon. + IconButton struct { + Theme *Theme + Style *IconButtonStyle + Icon *widget.Icon + Tip string + Clickable *Clickable + } + + // ActionIconButton is an icon button that executes an action when clicked. + ActionIconButton struct { + Action tracker.Action + DisabledStyle *IconButtonStyle + IconButton + } + + // ToggleIconButton is an icon button that toggles a boolean value when clicked. + ToggleIconButton struct { + Bool tracker.Bool + DisabledStyle *IconButtonStyle + OffIcon *widget.Icon + OffTip string + IconButton } ) -func NewActionClickable(a tracker.Action) *ActionClickable { - return &ActionClickable{ - Action: a, +func Btn(th *Theme, st *ButtonStyle, c *Clickable, txt string, tip string) Button { + return Button{ + Theme: th, + Style: st, + Clickable: c, + Text: txt, + Tip: tip, } } -func NewBoolClickable(b tracker.Bool) *BoolClickable { - return &BoolClickable{ - Bool: b, +func ActionBtn(act tracker.Action, th *Theme, c *Clickable, txt string, tip string) ActionButton { + return ActionButton{ + Action: act, + DisabledStyle: &th.Button.Disabled, + Button: Btn(th, &th.Button.Text, c, txt, tip), } } +func ToggleBtn(b tracker.Bool, th *Theme, c *Clickable, text string, tip string) ToggleButton { + return ToggleButton{ + Bool: b, + DisabledStyle: &th.Button.Disabled, + OffStyle: &th.Button.Text, + Button: Btn(th, &th.Button.Filled, c, text, tip), + } +} + +func IconBtn(th *Theme, st *IconButtonStyle, c *Clickable, icon []byte, tip string) IconButton { + return IconButton{ + Theme: th, + Style: st, + Clickable: c, + Icon: th.Icon(icon), + Tip: tip, + } +} + +func ActionIconBtn(act tracker.Action, th *Theme, c *Clickable, icon []byte, tip string) ActionIconButton { + return ActionIconButton{ + Action: act, + DisabledStyle: &th.IconButton.Disabled, + IconButton: IconBtn(th, &th.IconButton.Enabled, c, icon, tip), + } +} + +func ToggleIconBtn(b tracker.Bool, th *Theme, c *Clickable, offIcon, onIcon []byte, offTip, onTip string) ToggleIconButton { + return ToggleIconButton{ + Bool: b, + DisabledStyle: &th.IconButton.Disabled, + OffIcon: th.Icon(offIcon), + OffTip: offTip, + IconButton: IconBtn(th, &th.IconButton.Enabled, c, onIcon, onTip), + } +} + +func (b *Button) Layout(gtx C) D { + if b.Tip != "" { + return b.Clickable.TipArea.Layout(gtx, Tooltip(b.Theme, b.Tip), b.actualLayout) + } + return b.actualLayout(gtx) +} + +func (b *Button) actualLayout(gtx C) D { + min := gtx.Constraints.Min + min.Y = gtx.Dp(b.Style.Height) + return b.Clickable.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + semantic.Button.Add(gtx.Ops) + return layout.Background{}.Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + rr := gtx.Dp(b.Style.CornerRadius) + defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() + background := b.Style.Background + switch { + case b.Clickable.Hovered(): + background = hoveredColor(background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Clickable.History() { + drawInk(gtx, (widget.Press)(c)) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }, + func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min = min + return layout.Center.Layout(gtx, func(gtx C) D { + return b.Style.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + colMacro := op.Record(gtx.Ops) + paint.ColorOp{Color: b.Style.Color}.Add(gtx.Ops) + return widget.Label{Alignment: text.Middle}.Layout(gtx, b.Theme.Material.Shaper, b.Style.Font, b.Style.TextSize, b.Text, colMacro.Stop()) + }) + }) + }, + ) + }) +} + +func (b *ActionButton) Layout(gtx C) D { + for b.Clickable.Clicked(gtx) { + b.Action.Do() + } + if !b.Action.Enabled() { + b.Style = b.DisabledStyle + } + return b.Button.Layout(gtx) +} + +func (b *ToggleButton) Layout(gtx C) D { + for b.Clickable.Clicked(gtx) { + b.Bool.Toggle() + } + if !b.Bool.Enabled() { + b.Style = b.DisabledStyle + } else if !b.Bool.Value() { + b.Style = b.OffStyle + } + return b.Button.Layout(gtx) +} + +func (b *IconButton) Layout(gtx C) D { + if b.Tip != "" { + return b.Clickable.TipArea.Layout(gtx, Tooltip(b.Theme, b.Tip), b.actualLayout) + } + return b.actualLayout(gtx) +} + +func (b *IconButton) actualLayout(gtx C) D { + m := op.Record(gtx.Ops) + dims := b.Clickable.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + semantic.Button.Add(gtx.Ops) + return layout.Background{}.Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4 + defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() + background := b.Style.Background + switch { + case b.Clickable.Hovered(): + background = hoveredColor(background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Clickable.History() { + drawInk(gtx, (widget.Press)(c)) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }, + func(gtx layout.Context) layout.Dimensions { + return b.Style.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + size := gtx.Dp(b.Style.Size) + if b.Icon != nil { + gtx.Constraints.Min = image.Point{X: size} + b.Icon.Layout(gtx, b.Style.Color) + } + return layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + }) + }, + ) + }) + c := m.Stop() + bounds := image.Rectangle{Max: dims.Size} + defer clip.Ellipse(bounds).Push(gtx.Ops).Pop() + c.Add(gtx.Ops) + return dims +} + +func (b *ActionIconButton) Layout(gtx C) D { + for b.Clickable.Clicked(gtx) { + b.Action.Do() + } + if !b.Action.Enabled() { + b.Style = b.DisabledStyle + } + return b.IconButton.Layout(gtx) +} + +func (b *ToggleIconButton) Layout(gtx C) D { + for b.Clickable.Clicked(gtx) { + b.Bool.Toggle() + } + if !b.Bool.Enabled() { + b.Style = b.DisabledStyle + } + if !b.Bool.Value() { + b.Icon = b.OffIcon + b.Tip = b.OffTip + } + return b.IconButton.Layout(gtx) +} + func Tooltip(th *Theme, tip string) component.Tooltip { tooltip := component.PlatformTooltip(&th.Material, tip) tooltip.Bg = th.Tooltip.Bg @@ -64,88 +296,6 @@ func Tooltip(th *Theme, tip string) component.Tooltip { return tooltip } -func ActionIcon(gtx C, th *Theme, w *ActionClickable, icon []byte, tip string) TipIconButtonStyle { - ret := TipIcon(th, &w.TipClickable, icon, tip) - for w.Clickable.Clicked(gtx) { - w.Action.Do() - } - if !w.Action.Enabled() { - ret.IconButtonStyle.Color = th.Button.Disabled.Color - } - return ret -} - -func TipIcon(th *Theme, w *TipClickable, icon []byte, tip string) TipIconButtonStyle { - iconButtonStyle := IconButton(th, &w.Clickable, th.Icon(icon), "") - iconButtonStyle.Color = th.Material.Palette.ContrastBg - iconButtonStyle.Background = color.NRGBA{} - iconButtonStyle.Inset = layout.UniformInset(unit.Dp(6)) - return TipIconButtonStyle{ - TipArea: &w.TipArea, - IconButtonStyle: iconButtonStyle, - Tooltip: Tooltip(th, tip), - } -} - -func ToggleIcon(gtx C, th *Theme, w *BoolClickable, offIcon, onIcon []byte, offTip, onTip string) TipIconButtonStyle { - icon := offIcon - tip := offTip - if w.Bool.Value() { - icon = onIcon - tip = onTip - } - for w.Clickable.Clicked(gtx) { - w.Bool.Toggle() - } - ibStyle := IconButton(th, &w.Clickable, th.Icon(icon), "") - ibStyle.Background = color.NRGBA{} - ibStyle.Inset = layout.UniformInset(unit.Dp(6)) - ibStyle.Color = th.Material.Palette.ContrastBg - if !w.Bool.Enabled() { - ibStyle.Color = th.Button.Disabled.Color - } - return TipIconButtonStyle{ - TipArea: &w.TipArea, - IconButtonStyle: ibStyle, - Tooltip: Tooltip(th, tip), - } -} - -func (t *TipIconButtonStyle) Layout(gtx C) D { - return t.TipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout) -} - -func ActionButton(gtx C, th *Theme, style *ButtonStyle, w *ActionClickable, text string) Button { - for w.Clickable.Clicked(gtx) { - w.Action.Do() - } - if !w.Action.Enabled() { - return Btn(th, &th.Button.Disabled, &w.Clickable, text) - } - return Btn(th, style, &w.Clickable, text) -} - -func ToggleButton(gtx C, th *Theme, b *BoolClickable, text string) Button { - for b.Clickable.Clicked(gtx) { - b.Bool.Toggle() - } - if !b.Bool.Enabled() { - return Btn(th, &th.Button.Disabled, &b.Clickable, text) - } - if b.Bool.Value() { - return Btn(th, &th.Button.Filled, &b.Clickable, text) - } - return Btn(th, &th.Button.Text, &b.Clickable, text) -} - -// Clickable represents a clickable area. -type Clickable struct { - click gesture.Click - history []widget.Press - - requestClicks int -} - // Click executes a simple programmatic click. func (b *Clickable) Click() { b.requestClicks++ @@ -252,135 +402,6 @@ func (b *Clickable) update(_ event.Tag, gtx layout.Context) (widget.Click, bool) return widget.Click{}, false } -type ButtonStyle struct { - // Color is the text color. - Color color.NRGBA - Font font.Font - TextSize unit.Sp - Background color.NRGBA - CornerRadius unit.Dp - Height unit.Dp - Inset layout.Inset -} - -type Button struct { - Text string - Button *Clickable - shaper *text.Shaper - ButtonStyle -} - -type IconButtonStyle struct { - Background color.NRGBA - // Color is the icon color. - Color color.NRGBA - Icon *widget.Icon - // Size is the icon size. - Size unit.Dp - Inset layout.Inset - Button *Clickable - Description string -} - -func Btn(th *Theme, style *ButtonStyle, button *Clickable, txt string) Button { - b := Button{ - Text: txt, - ButtonStyle: *style, - Button: button, - shaper: th.Material.Shaper, - } - return b -} - -func IconButton(th *Theme, button *Clickable, icon *widget.Icon, description string) IconButtonStyle { - return IconButtonStyle{ - Background: th.Material.Palette.ContrastBg, - Color: th.Material.Palette.ContrastFg, - Icon: icon, - Size: 24, - Inset: layout.UniformInset(12), - Button: button, - Description: description, - } -} - -func (b *Button) Layout(gtx layout.Context) layout.Dimensions { - min := gtx.Constraints.Min - min.Y = gtx.Dp(b.Height) - return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.Button.Add(gtx.Ops) - return layout.Background{}.Layout(gtx, - func(gtx layout.Context) layout.Dimensions { - rr := gtx.Dp(b.CornerRadius) - defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() - background := b.Background - switch { - case b.Button.Hovered(): - background = hoveredColor(background) - } - paint.Fill(gtx.Ops, background) - for _, c := range b.Button.History() { - drawInk(gtx, (widget.Press)(c)) - } - return layout.Dimensions{Size: gtx.Constraints.Min} - }, - func(gtx layout.Context) layout.Dimensions { - gtx.Constraints.Min = min - return layout.Center.Layout(gtx, func(gtx C) D { - return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - colMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: b.Color}.Add(gtx.Ops) - return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text, colMacro.Stop()) - }) - }) - }, - ) - }) -} - -func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions { - m := op.Record(gtx.Ops) - dims := b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.Button.Add(gtx.Ops) - if d := b.Description; d != "" { - semantic.DescriptionOp(b.Description).Add(gtx.Ops) - } - return layout.Background{}.Layout(gtx, - func(gtx layout.Context) layout.Dimensions { - rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4 - defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() - background := b.Background - switch { - case b.Button.Hovered(): - background = hoveredColor(background) - } - paint.Fill(gtx.Ops, background) - for _, c := range b.Button.History() { - drawInk(gtx, (widget.Press)(c)) - } - return layout.Dimensions{Size: gtx.Constraints.Min} - }, - func(gtx layout.Context) layout.Dimensions { - return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(b.Size) - if b.Icon != nil { - gtx.Constraints.Min = image.Point{X: size} - b.Icon.Layout(gtx, b.Color) - } - return layout.Dimensions{ - Size: image.Point{X: size, Y: size}, - } - }) - }, - ) - }) - c := m.Stop() - bounds := image.Rectangle{Max: dims.Size} - defer clip.Ellipse(bounds).Push(gtx.Ops).Pop() - c.Add(gtx.Ops) - return dims -} - func drawInk(gtx layout.Context, c widget.Press) { // duration is the number of seconds for the // completed animation: expand while fading in, then diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index 35bf3f6..5663623 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -23,19 +23,19 @@ import ( type ( InstrumentEditor struct { - newInstrumentBtn *ActionClickable - enlargeBtn *BoolClickable - deleteInstrumentBtn *ActionClickable - linkInstrTrackBtn *BoolClickable - splitInstrumentBtn *ActionClickable - copyInstrumentBtn *TipClickable - saveInstrumentBtn *TipClickable - loadInstrumentBtn *TipClickable - addUnitBtn *ActionClickable - presetMenuBtn *TipClickable - commentExpandBtn *BoolClickable - soloBtn *BoolClickable - muteBtn *BoolClickable + newInstrumentBtn *Clickable + enlargeBtn *Clickable + deleteInstrumentBtn *Clickable + linkInstrTrackBtn *Clickable + splitInstrumentBtn *Clickable + copyInstrumentBtn *Clickable + saveInstrumentBtn *Clickable + loadInstrumentBtn *Clickable + addUnitBtn *Clickable + presetMenuBtn *Clickable + commentExpandBtn *Clickable + soloBtn *Clickable + muteBtn *Clickable commentEditor *Editor commentString tracker.String nameEditor *Editor @@ -68,18 +68,19 @@ type ( func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { ret := &InstrumentEditor{ - newInstrumentBtn: NewActionClickable(model.AddInstrument()), - enlargeBtn: NewBoolClickable(model.InstrEnlarged()), - deleteInstrumentBtn: NewActionClickable(model.DeleteInstrument()), - linkInstrTrackBtn: NewBoolClickable(model.LinkInstrTrack()), - splitInstrumentBtn: NewActionClickable(model.SplitInstrument()), - copyInstrumentBtn: new(TipClickable), - saveInstrumentBtn: new(TipClickable), - loadInstrumentBtn: new(TipClickable), - commentExpandBtn: NewBoolClickable(model.CommentExpanded()), - presetMenuBtn: new(TipClickable), - soloBtn: NewBoolClickable(model.Solo()), - muteBtn: NewBoolClickable(model.Mute()), + newInstrumentBtn: new(Clickable), + enlargeBtn: new(Clickable), + deleteInstrumentBtn: new(Clickable), + linkInstrTrackBtn: new(Clickable), + splitInstrumentBtn: new(Clickable), + copyInstrumentBtn: new(Clickable), + saveInstrumentBtn: new(Clickable), + loadInstrumentBtn: new(Clickable), + commentExpandBtn: new(Clickable), + presetMenuBtn: new(Clickable), + soloBtn: new(Clickable), + muteBtn: new(Clickable), + addUnitBtn: new(Clickable), commentEditor: NewEditor(false, false, text.Start), nameEditor: NewEditor(true, true, text.Middle), searchEditor: NewEditor(true, true, text.Start), @@ -95,7 +96,6 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { return true }) ret.addUnit = model.AddUnit(false) - ret.addUnitBtn = NewActionClickable(tracker.MakeEnabledAction(ret.AddUnitThenFocus())) ret.enlargeHint = makeHint("Enlarge", " (%s)", "InstrEnlargedToggle") ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle") ret.addInstrumentHint = makeHint("Add\ninstrument", "\n(%s)", "AddInstrument") @@ -133,14 +133,12 @@ func (ie *InstrumentEditor) Focused(gtx C) bool { func (ie *InstrumentEditor) childFocused(gtx C) bool { return ie.unitEditor.sliderList.Focused(gtx) || ie.instrumentDragList.Focused(gtx) || gtx.Source.Focused(ie.commentEditor) || gtx.Source.Focused(ie.nameEditor) || gtx.Source.Focused(ie.searchEditor) || - gtx.Source.Focused(ie.addUnitBtn.Clickable) || gtx.Source.Focused(ie.commentExpandBtn.Clickable) || gtx.Source.Focused(ie.presetMenuBtn.Clickable) || - gtx.Source.Focused(ie.deleteInstrumentBtn.Clickable) || gtx.Source.Focused(ie.copyInstrumentBtn.Clickable) + gtx.Source.Focused(ie.addUnitBtn) || gtx.Source.Focused(ie.commentExpandBtn) || gtx.Source.Focused(ie.presetMenuBtn) || + gtx.Source.Focused(ie.deleteInstrumentBtn) || gtx.Source.Focused(ie.copyInstrumentBtn) } func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { ie.wasFocused = ie.Focused(gtx) || ie.childFocused(gtx) - fullscreenBtnStyle := ToggleIcon(gtx, t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, ie.enlargeHint, ie.shrinkHint) - linkBtnStyle := ToggleIcon(gtx, t.Theme, ie.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, ie.linkDisabledHint, ie.linkEnabledHint) octave := func(gtx C) D { in := layout.UniformInset(unit.Dp(1)) @@ -149,7 +147,6 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { }) } - newBtnStyle := ActionIcon(gtx, t.Theme, ie.newInstrumentBtn, icons.ContentAdd, ie.addInstrumentHint) ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout( @@ -162,13 +159,16 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { layout.Rigid(layout.Spacer{Width: 4}.Layout), layout.Rigid(octave), layout.Rigid(func(gtx C) D { - return layout.E.Layout(gtx, linkBtnStyle.Layout) + linkInstrTrackBtn := ToggleIconBtn(t.Model.LinkInstrTrack(), t.Theme, ie.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, ie.linkDisabledHint, ie.linkEnabledHint) + return layout.E.Layout(gtx, linkInstrTrackBtn.Layout) }), layout.Rigid(func(gtx C) D { - return layout.E.Layout(gtx, fullscreenBtnStyle.Layout) + instrEnlargedBtn := ToggleIconBtn(t.Model.InstrEnlarged(), t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, ie.enlargeHint, ie.shrinkHint) + return layout.E.Layout(gtx, instrEnlargedBtn.Layout) }), layout.Rigid(func(gtx C) D { - return layout.E.Layout(gtx, newBtnStyle.Layout) + addInstrumentBtn := ActionIconBtn(t.Model.AddInstrument(), t.Theme, ie.newInstrumentBtn, icons.ContentAdd, ie.addInstrumentHint) + return layout.E.Layout(gtx, addInstrumentBtn.Layout) }), ) }), @@ -190,26 +190,16 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { header := func(gtx C) D { - commentExpandBtnStyle := ToggleIcon(gtx, t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, ie.expandCommentHint, ie.collapseCommentHint) - presetMenuBtnStyle := TipIcon(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, "Load preset") - copyInstrumentBtnStyle := TipIcon(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument") - saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument") - loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument") - deleteInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, ie.deleteInstrumentHint) - splitInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.splitInstrumentBtn, icons.CommunicationCallSplit, ie.splitInstrumentHint) - soloBtnStyle := ToggleIcon(gtx, t.Theme, ie.soloBtn, icons.SocialGroup, icons.SocialPerson, ie.soloHint, ie.unsoloHint) - muteBtnStyle := ToggleIcon(gtx, t.Theme, ie.muteBtn, icons.AVVolumeUp, icons.AVVolumeOff, ie.muteHint, ie.unmuteHint) - m := PopupMenu(t.Theme, &t.Theme.Menu.Text, &ie.presetMenu) - for ie.copyInstrumentBtn.Clickable.Clicked(gtx) { + for ie.copyInstrumentBtn.Clicked(gtx) { if contents, ok := t.Instruments().List().CopyElements(); ok { gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) t.Alerts().Add("Instrument copied to clipboard", tracker.Info) } } - for ie.saveInstrumentBtn.Clickable.Clicked(gtx) { + for ie.saveInstrumentBtn.Clicked(gtx) { writer, err := t.Explorer.CreateFile(t.InstrumentName().Value() + ".yml") if err != nil { continue @@ -217,7 +207,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { t.SaveInstrument(writer) } - for ie.loadInstrumentBtn.Clickable.Clicked(gtx) { + for ie.loadInstrumentBtn.Clicked(gtx) { reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp") if err != nil { continue @@ -225,6 +215,15 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { t.LoadInstrument(reader) } + splitInstrumentBtn := ActionIconBtn(t.SplitInstrument(), t.Theme, ie.splitInstrumentBtn, icons.CommunicationCallSplit, ie.splitInstrumentHint) + commentExpandedBtn := ToggleIconBtn(t.CommentExpanded(), t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, ie.expandCommentHint, ie.collapseCommentHint) + soloBtn := ToggleIconBtn(t.Solo(), t.Theme, ie.soloBtn, icons.SocialGroup, icons.SocialPerson, ie.soloHint, ie.unsoloHint) + muteBtn := ToggleIconBtn(t.Mute(), t.Theme, ie.muteBtn, icons.AVVolumeUp, icons.AVVolumeOff, ie.muteHint, ie.unmuteHint) + saveInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument") + loadInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument") + copyInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument") + deleteInstrumentBtn := ActionIconBtn(t.DeleteInstrument(), t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, ie.deleteInstrumentHint) + header := func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(layout.Spacer{Width: 6}.Layout), @@ -233,31 +232,32 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return t.InstrumentVoices.Layout(gtx, t.Model.InstrumentVoices(), t.Theme, &t.Theme.NumericUpDown, "Number of voices for this instrument") }), - layout.Rigid(splitInstrumentBtnStyle.Layout), + layout.Rigid(splitInstrumentBtn.Layout), layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), - layout.Rigid(commentExpandBtnStyle.Layout), - layout.Rigid(soloBtnStyle.Layout), - layout.Rigid(muteBtnStyle.Layout), + layout.Rigid(commentExpandedBtn.Layout), + layout.Rigid(soloBtn.Layout), + layout.Rigid(muteBtn.Layout), layout.Rigid(func(gtx C) D { - //defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() - dims := presetMenuBtnStyle.Layout(gtx) + presetBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.presetMenuBtn, icons.NavigationMenu, "Load preset") + dims := presetBtn.Layout(gtx) op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops) gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500)) gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180)) m.Layout(gtx, ie.presetMenuItems...) return dims }), - layout.Rigid(saveInstrumentBtnStyle.Layout), - layout.Rigid(loadInstrumentBtnStyle.Layout), - layout.Rigid(copyInstrumentBtnStyle.Layout), - layout.Rigid(deleteInstrumentBtnStyle.Layout)) + layout.Rigid(saveInstrumentBtn.Layout), + layout.Rigid(loadInstrumentBtn.Layout), + layout.Rigid(copyInstrumentBtn.Layout), + layout.Rigid(deleteInstrumentBtn.Layout), + ) } - for ie.presetMenuBtn.Clickable.Clicked(gtx) { + for ie.presetMenuBtn.Clicked(gtx) { ie.presetMenu.Visible = true } - if ie.commentExpandBtn.Bool.Value() || gtx.Source.Focused(ie.commentEditor) { // we draw once the widget after it manages to lose focus + if t.CommentExpanded().Value() || gtx.Source.Focused(ie.commentEditor) { // we draw once the widget after it manages to lose focus ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(header), layout.Rigid(func(gtx C) D { @@ -356,12 +356,6 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D { } func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { - // TODO: how to ie.unitDragList.Focus() - addUnitBtnStyle := ActionIcon(gtx, t.Theme, ie.addUnitBtn, icons.ContentAdd, "Add unit (Enter)") - addUnitBtnStyle.IconButtonStyle.Color = t.Theme.Material.ContrastFg - addUnitBtnStyle.IconButtonStyle.Background = t.Theme.Material.ContrastBg - addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4)) - var units [256]tracker.UnitListItem for i, item := range (*tracker.Units)(t.Model).Iterate { if i >= 256 { @@ -482,8 +476,12 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { return dims }), layout.Stacked(func(gtx C) D { + for ie.addUnitBtn.Clicked(gtx) { + t.AddUnit(false).Do() + } margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)} - return margin.Layout(gtx, addUnitBtnStyle.Layout) + addUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Emphasis, ie.addUnitBtn, icons.ContentAdd, "Add unit (Enter)") + return margin.Layout(gtx, addUnitBtn.Layout) }), ) }) diff --git a/tracker/gioui/keybindings.go b/tracker/gioui/keybindings.go index 9afdb81..5e6e93a 100644 --- a/tracker/gioui/keybindings.go +++ b/tracker/gioui/keybindings.go @@ -263,7 +263,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) { case t.TrackEditor.scrollTable.Focused(gtx): t.OrderEditor.scrollTable.Focus() case t.InstrumentEditor.Focused(gtx): - if t.InstrumentEditor.enlargeBtn.Bool.Value() { + if t.InstrEnlarged().Value() { t.InstrumentEditor.unitEditor.sliderList.Focus() } else { t.TrackEditor.scrollTable.Focus() @@ -280,7 +280,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) { case t.InstrumentEditor.Focused(gtx): t.InstrumentEditor.unitEditor.sliderList.Focus() default: - if t.InstrumentEditor.enlargeBtn.Bool.Value() { + if t.InstrEnlarged().Value() { t.InstrumentEditor.Focus() } else { t.OrderEditor.scrollTable.Focus() diff --git a/tracker/gioui/menu.go b/tracker/gioui/menu.go index d5ccbe1..29e2f2a 100644 --- a/tracker/gioui/menu.go +++ b/tracker/gioui/menu.go @@ -168,9 +168,8 @@ func (tr *Tracker) layoutMenu(gtx C, title string, clickable *Clickable, menu *M m := PopupMenu(tr.Theme, &tr.Theme.Menu.Text, menu) return func(gtx C) D { defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() - titleBtn := Btn(tr.Theme, &tr.Theme.Button.Menu, clickable, title) - titleBtn.CornerRadius = unit.Dp(0) - dims := titleBtn.Layout(gtx) + btn := Btn(tr.Theme, &tr.Theme.Button.Menu, clickable, title, "") + dims := btn.Layout(gtx) op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops) gtx.Constraints.Max.X = gtx.Dp(width) gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(300)) diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index 7ba62dc..ebc983c 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -52,18 +52,18 @@ func init() { type NoteEditor struct { TrackVoices *NumericUpDown - NewTrackBtn *ActionClickable - DeleteTrackBtn *ActionClickable - SplitTrackBtn *ActionClickable + NewTrackBtn *Clickable + DeleteTrackBtn *Clickable + SplitTrackBtn *Clickable - AddSemitoneBtn *ActionClickable - SubtractSemitoneBtn *ActionClickable - AddOctaveBtn *ActionClickable - SubtractOctaveBtn *ActionClickable - NoteOffBtn *ActionClickable - EffectBtn *BoolClickable - UniqueBtn *BoolClickable - TrackMidiInBtn *BoolClickable + AddSemitoneBtn *Clickable + SubtractSemitoneBtn *Clickable + AddOctaveBtn *Clickable + SubtractOctaveBtn *Clickable + NoteOffBtn *Clickable + EffectBtn *Clickable + UniqueBtn *Clickable + TrackMidiInBtn *Clickable scrollTable *ScrollTable eventFilters []event.Filter @@ -77,17 +77,17 @@ type NoteEditor struct { func NewNoteEditor(model *tracker.Model) *NoteEditor { ret := &NoteEditor{ TrackVoices: NewNumericUpDown(), - NewTrackBtn: NewActionClickable(model.AddTrack()), - DeleteTrackBtn: NewActionClickable(model.DeleteTrack()), - SplitTrackBtn: NewActionClickable(model.SplitTrack()), - AddSemitoneBtn: NewActionClickable(model.AddSemitone()), - SubtractSemitoneBtn: NewActionClickable(model.SubtractSemitone()), - AddOctaveBtn: NewActionClickable(model.AddOctave()), - SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()), - NoteOffBtn: NewActionClickable(model.EditNoteOff()), - EffectBtn: NewBoolClickable(model.Effect()), - UniqueBtn: NewBoolClickable(model.UniquePatterns()), - TrackMidiInBtn: NewBoolClickable(model.TrackMidiIn()), + NewTrackBtn: new(Clickable), + DeleteTrackBtn: new(Clickable), + SplitTrackBtn: new(Clickable), + AddSemitoneBtn: new(Clickable), + SubtractSemitoneBtn: new(Clickable), + AddOctaveBtn: new(Clickable), + SubtractOctaveBtn: new(Clickable), + NoteOffBtn: new(Clickable), + EffectBtn: new(Clickable), + UniqueBtn: new(Clickable), + TrackMidiInBtn: new(Clickable), scrollTable: NewScrollTable( model.Notes().Table(), model.Tracks().List(), @@ -164,42 +164,41 @@ func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D { return Surface{Gray: 37, Focus: te.scrollTable.Focused(gtx) || te.scrollTable.ChildFocused(gtx)}.Layout(gtx, func(gtx C) D { - addSemitoneBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.AddSemitoneBtn, "+1") - subtractSemitoneBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.SubtractSemitoneBtn, "-1") - addOctaveBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.AddOctaveBtn, "+12") - subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.SubtractOctaveBtn, "-12") - noteOffBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.NoteOffBtn, "Note Off") - deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint) - splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint) - newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint) + addSemitoneBtn := ActionBtn(t.AddSemitone(), t.Theme, te.AddSemitoneBtn, "+1", "Add semitone") + subtractSemitoneBtn := ActionBtn(t.SubtractSemitone(), t.Theme, te.SubtractSemitoneBtn, "-1", "Subtract semitone") + addOctaveBtn := ActionBtn(t.AddOctave(), t.Theme, te.AddOctaveBtn, "+12", "Add octave") + subtractOctaveBtn := ActionBtn(t.SubtractOctave(), t.Theme, te.SubtractOctaveBtn, "-12", "Subtract octave") + noteOffBtn := ActionBtn(t.EditNoteOff(), t.Theme, te.NoteOffBtn, "Note Off", "") + deleteTrackBtn := ActionIconBtn(t.DeleteTrack(), t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint) + splitTrackBtn := ActionIconBtn(t.SplitTrack(), t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint) + newTrackBtn := ActionIconBtn(t.AddTrack(), t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint) in := layout.UniformInset(unit.Dp(1)) voiceUpDown := func(gtx C) D { return in.Layout(gtx, func(gtx C) D { return te.TrackVoices.Layout(gtx, t.Model.TrackVoices(), t.Theme, &t.Theme.NumericUpDown, "Track voices") }) } - effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex") - uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip) - midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI") + effectBtn := ToggleBtn(t.Effect(), t.Theme, te.EffectBtn, "Hex", "Input notes as hex values") + uniqueBtn := ToggleIconBtn(t.UniquePatterns(), t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip) + midiInBtn := ToggleBtn(t.TrackMidiIn(), t.Theme, te.TrackMidiInBtn, "MIDI", "Input notes from MIDI keyboard") return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }), - layout.Rigid(addSemitoneBtnStyle.Layout), - layout.Rigid(subtractSemitoneBtnStyle.Layout), - layout.Rigid(addOctaveBtnStyle.Layout), - layout.Rigid(subtractOctaveBtnStyle.Layout), - layout.Rigid(noteOffBtnStyle.Layout), - layout.Rigid(effectBtnStyle.Layout), - layout.Rigid(uniqueBtnStyle.Layout), + layout.Rigid(addSemitoneBtn.Layout), + layout.Rigid(subtractSemitoneBtn.Layout), + layout.Rigid(addOctaveBtn.Layout), + layout.Rigid(subtractOctaveBtn.Layout), + layout.Rigid(noteOffBtn.Layout), + layout.Rigid(effectBtn.Layout), + layout.Rigid(uniqueBtn.Layout), layout.Rigid(layout.Spacer{Width: 10}.Layout), layout.Rigid(Label(t.Theme, &t.Theme.NoteEditor.Header, "Voices").Layout), layout.Rigid(layout.Spacer{Width: 4}.Layout), layout.Rigid(voiceUpDown), - layout.Rigid(splitTrackBtnStyle.Layout), + layout.Rigid(splitTrackBtn.Layout), + layout.Rigid(midiInBtn.Layout), layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), - layout.Rigid(midiInBtnStyle.Layout), - layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), - layout.Rigid(deleteTrackBtnStyle.Layout), - layout.Rigid(newTrackBtnStyle.Layout)) + layout.Rigid(deleteTrackBtn.Layout), + layout.Rigid(newTrackBtn.Layout)) }) } @@ -280,7 +279,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D { cursor := te.scrollTable.Table.Cursor() drawSelection := cursor != te.scrollTable.Table.Cursor2() selection := te.scrollTable.Table.Range() - hasTrackMidiIn := te.TrackMidiInBtn.Bool.Value() + hasTrackMidiIn := t.Model.TrackMidiIn().Value() patternNoOp := colorOp(gtx, t.Theme.NoteEditor.PatternNo.Color) uniqueOp := colorOp(gtx, t.Theme.NoteEditor.Unique.Color) diff --git a/tracker/gioui/oscilloscope.go b/tracker/gioui/oscilloscope.go index 0fa3256..568efa0 100644 --- a/tracker/gioui/oscilloscope.go +++ b/tracker/gioui/oscilloscope.go @@ -17,8 +17,8 @@ import ( type ( OscilloscopeState struct { - onceBtn *BoolClickable - wrapBtn *BoolClickable + onceBtn *Clickable + wrapBtn *Clickable lengthInBeatsNumber *NumericUpDown triggerChannelNumber *NumericUpDown xScale int @@ -38,20 +38,20 @@ type ( func NewOscilloscope(model *tracker.Model) *OscilloscopeState { return &OscilloscopeState{ - onceBtn: NewBoolClickable(model.SignalAnalyzer().Once()), - wrapBtn: NewBoolClickable(model.SignalAnalyzer().Wrap()), + onceBtn: new(Clickable), + wrapBtn: new(Clickable), lengthInBeatsNumber: NewNumericUpDown(), triggerChannelNumber: NewNumericUpDown(), } } -func (s *OscilloscopeState) Layout(gtx C, vtrig, vlen tracker.Int, wave tracker.RingBuffer[[2]float32], th *Theme, st *OscilloscopeStyle) D { - wrapBtnStyle := ToggleButton(gtx, th, s.wrapBtn, "Wrap") - onceBtnStyle := ToggleButton(gtx, th, s.onceBtn, "Once") - +func (s *OscilloscopeState) Layout(gtx C, vtrig, vlen tracker.Int, once, wrap tracker.Bool, wave tracker.RingBuffer[[2]float32], th *Theme, st *OscilloscopeStyle) D { leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout + onceBtn := ToggleBtn(once, th, s.onceBtn, "Once", "Trigger once on next event") + wrapBtn := ToggleBtn(wrap, th, s.wrapBtn, "Wrap", "Wrap buffer when full") + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Flexed(1, func(gtx C) D { return s.layoutWave(gtx, wave, th) }), layout.Rigid(func(gtx C) D { @@ -59,7 +59,7 @@ func (s *OscilloscopeState) Layout(gtx C, vtrig, vlen tracker.Int, wave tracker. layout.Rigid(leftSpacer), layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Trigger").Layout), layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), - layout.Rigid(onceBtnStyle.Layout), + layout.Rigid(onceBtn.Layout), layout.Rigid(func(gtx C) D { return s.triggerChannelNumber.Layout(gtx, vtrig, th, &th.NumericUpDown, "Trigger channel") }), @@ -71,7 +71,7 @@ func (s *OscilloscopeState) Layout(gtx C, vtrig, vlen tracker.Int, wave tracker. layout.Rigid(leftSpacer), layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Buffer").Layout), layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), - layout.Rigid(wrapBtnStyle.Layout), + layout.Rigid(wrapBtn.Layout), layout.Rigid(func(gtx C) D { return s.lengthInBeatsNumber.Layout(gtx, vlen, th, &th.NumericUpDown, "Buffer length in beats") }), diff --git a/tracker/gioui/songpanel.go b/tracker/gioui/songpanel.go index a27e5fb..add8592 100644 --- a/tracker/gioui/songpanel.go +++ b/tracker/gioui/songpanel.go @@ -46,10 +46,10 @@ func NewSongPanel(model *tracker.Model) *SongPanel { SongLength: NewNumericUpDown(), Scope: NewOscilloscope(model), MenuBar: NewMenuBar(model), - PlayBar: NewPlayBar(model), + PlayBar: NewPlayBar(), - WeightingTypeBtn: &Clickable{}, - OversamplingBtn: &Clickable{}, + WeightingTypeBtn: new(Clickable), + OversamplingBtn: new(Clickable), SongSettingsExpander: &Expander{Expanded: true}, ScopeExpander: &Expander{}, @@ -75,7 +75,7 @@ func (s *SongPanel) Layout(gtx C, t *Tracker) D { return s.MenuBar.Layout(gtx, t) }), layout.Rigid(func(gtx C) D { - return s.PlayBar.Layout(gtx, t.Theme) + return s.PlayBar.Layout(gtx, t) }), layout.Rigid(func(gtx C) D { return s.layoutSongOptions(gtx, t) @@ -98,13 +98,13 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { weightingTxt = "No weight (RMS)" } - weightingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.WeightingTypeBtn, weightingTxt) + weightingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.WeightingTypeBtn, weightingTxt, "") oversamplingTxt := "Sample peak" if tr.Model.Oversampling().Value() { oversamplingTxt = "True peak" } - oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt) + oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt, "") return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { @@ -201,7 +201,7 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { }), layout.Flexed(1, func(gtx C) D { 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) + return t.Scope.Layout(gtx, tr.Model.SignalAnalyzer().TriggerChannel(), tr.Model.SignalAnalyzer().LengthInBeats(), tr.Model.SignalAnalyzer().Once(), tr.Model.SignalAnalyzer().Wrap(), tr.Model.SignalAnalyzer().Waveform(), tr.Theme, &tr.Theme.Oscilloscope) }) }), layout.Rigid(Label(tr.Theme, &tr.Theme.SongPanel.Version, version.VersionOrHash).Layout), @@ -304,14 +304,14 @@ type MenuBar struct { midiMenuItems []MenuItem panicHint string - PanicBtn *BoolClickable + PanicBtn *Clickable } func NewMenuBar(model *tracker.Model) *MenuBar { ret := &MenuBar{ Clickables: make([]Clickable, 3), Menus: make([]Menu, 3), - PanicBtn: NewBoolClickable(model.Panic()), + PanicBtn: new(Clickable), panicHint: makeHint("Panic", " (%s)", "PanicToggle"), } ret.fileMenuItems = []MenuItem{ @@ -343,15 +343,15 @@ func (t *MenuBar) Layout(gtx C, tr *Tracker) D { gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36)) gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36)) - panicBtnStyle := ToggleIcon(gtx, tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint) - if t.PanicBtn.Bool.Value() { - panicBtnStyle.IconButtonStyle.Color = tr.Theme.SongPanel.ErrorColor + panicBtn := ToggleIconBtn(tr.Panic(), tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint) + if tr.Panic().Value() { + panicBtn.Style = &tr.Theme.IconButton.Error } flex := layout.Flex{Axis: layout.Horizontal, Alignment: layout.End} fileFC := layout.Rigid(tr.layoutMenu(gtx, "File", &t.Clickables[0], &t.Menus[0], unit.Dp(200), t.fileMenuItems...)) editFC := layout.Rigid(tr.layoutMenu(gtx, "Edit", &t.Clickables[1], &t.Menus[1], unit.Dp(200), t.editMenuItems...)) midiFC := layout.Rigid(tr.layoutMenu(gtx, "MIDI", &t.Clickables[2], &t.Menus[2], unit.Dp(200), t.midiMenuItems...)) - panicFC := layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, panicBtnStyle.Layout) }) + panicFC := layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, panicBtn.Layout) }) if len(t.midiMenuItems) > 0 { return flex.Layout(gtx, fileFC, editFC, midiFC, panicFC) } @@ -359,11 +359,11 @@ func (t *MenuBar) Layout(gtx C, tr *Tracker) D { } type PlayBar struct { - RewindBtn *ActionClickable - PlayingBtn *BoolClickable - RecordBtn *BoolClickable - FollowBtn *BoolClickable - LoopBtn *BoolClickable + RewindBtn *Clickable + PlayingBtn *Clickable + RecordBtn *Clickable + FollowBtn *Clickable + LoopBtn *Clickable // Hints rewindHint string playHint, stopHint string @@ -372,13 +372,13 @@ type PlayBar struct { loopOffHint, loopOnHint string } -func NewPlayBar(model *tracker.Model) *PlayBar { +func NewPlayBar() *PlayBar { ret := &PlayBar{ - LoopBtn: NewBoolClickable(model.LoopToggle()), - RecordBtn: NewBoolClickable(model.IsRecording()), - FollowBtn: NewBoolClickable(model.Follow()), - PlayingBtn: NewBoolClickable(model.Playing()), - RewindBtn: NewActionClickable(model.PlaySongStart()), + LoopBtn: new(Clickable), + RecordBtn: new(Clickable), + FollowBtn: new(Clickable), + PlayingBtn: new(Clickable), + RewindBtn: new(Clickable), } ret.rewindHint = makeHint("Rewind", "\n(%s)", "PlaySongStartUnfollow") ret.playHint = makeHint("Play", " (%s)", "PlayCurrentPosUnfollow") @@ -392,20 +392,20 @@ func NewPlayBar(model *tracker.Model) *PlayBar { return ret } -func (pb *PlayBar) Layout(gtx C, th *Theme) D { - rewindBtnStyle := ActionIcon(gtx, th, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint) - playBtnStyle := ToggleIcon(gtx, th, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint) - recordBtnStyle := ToggleIcon(gtx, th, pb.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, pb.recordHint, pb.stopRecordHint) - noteTrackBtnStyle := ToggleIcon(gtx, th, pb.FollowBtn, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, pb.followOffHint, pb.followOnHint) - loopBtnStyle := ToggleIcon(gtx, th, pb.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, pb.loopOffHint, pb.loopOnHint) +func (pb *PlayBar) Layout(gtx C, tr *Tracker) D { + playBtn := ToggleIconBtn(tr.Playing(), tr.Theme, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint) + rewindBtn := ActionIconBtn(tr.PlaySongStart(), tr.Theme, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint) + recordBtn := ToggleIconBtn(tr.IsRecording(), tr.Theme, pb.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, pb.recordHint, pb.stopRecordHint) + followBtn := ToggleIconBtn(tr.Follow(), tr.Theme, pb.FollowBtn, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, pb.followOffHint, pb.followOnHint) + loopBtn := ToggleIconBtn(tr.LoopToggle(), tr.Theme, pb.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, pb.loopOffHint, pb.loopOnHint) return Surface{Gray: 37}.Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, - layout.Flexed(1, playBtnStyle.Layout), - layout.Rigid(rewindBtnStyle.Layout), - layout.Rigid(recordBtnStyle.Layout), - layout.Rigid(noteTrackBtnStyle.Layout), - layout.Rigid(loopBtnStyle.Layout), + layout.Flexed(1, playBtn.Layout), + layout.Rigid(rewindBtn.Layout), + layout.Rigid(recordBtn.Layout), + layout.Rigid(followBtn.Layout), + layout.Rigid(loopBtn.Layout), ) }) } diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index 1077fe3..2f22481 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -19,6 +19,12 @@ type Theme struct { Disabled ButtonStyle Menu ButtonStyle } + IconButton struct { + Enabled IconButtonStyle + Disabled IconButtonStyle + Emphasis IconButtonStyle + Error IconButtonStyle + } Oscilloscope OscilloscopeStyle NumericUpDown NumericUpDownStyle DialogTitle LabelStyle diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index c47eb22..5d6dbea 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -54,6 +54,24 @@ button: cornerradius: 0 height: *buttonheight inset: *buttoninset +iconbutton: + enabled: + color: *primarycolor + size: 24 + inset: { top: 6, bottom: 6, left: 6, right: 6 } + disabled: + color: *disabled + size: 24 + inset: { top: 6, bottom: 6, left: 6, right: 6 } + emphasis: + color: *contrastfg + background: *primarycolor + size: 24 + inset: { top: 6, bottom: 6, left: 6, right: 6 } + error: + color: *errorcolor + size: 24 + inset: { top: 6, bottom: 6, left: 6, right: 6 } oscilloscope: curvecolors: [*primarycolor, *secondarycolor] limitcolor: { r: 255, g: 255, b: 255, a: 8 } diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index 1f58467..d24d16e 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -210,7 +210,7 @@ func (t *Tracker) Layout(gtx layout.Context, w *app.Window) { paint.Fill(gtx.Ops, t.Theme.Material.Bg) event.Op(gtx.Ops, t) // area for capturing scroll events - if t.InstrumentEditor.enlargeBtn.Bool.Value() { + if t.InstrEnlarged().Value() { t.layoutTop(gtx) } else { t.VerticalSplit.Layout(gtx, diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 602a914..fd87e23 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -30,10 +30,10 @@ type UnitEditor struct { sliderList *DragList searchList *DragList Parameters []*ParameterWidget - DeleteUnitBtn *ActionClickable - CopyUnitBtn *TipClickable - ClearUnitBtn *ActionClickable - DisableUnitBtn *BoolClickable + DeleteUnitBtn *Clickable + CopyUnitBtn *Clickable + ClearUnitBtn *Clickable + DisableUnitBtn *Clickable SelectTypeBtn *Clickable commentEditor *Editor caser cases.Caser @@ -45,10 +45,10 @@ type UnitEditor struct { func NewUnitEditor(m *tracker.Model) *UnitEditor { ret := &UnitEditor{ - DeleteUnitBtn: NewActionClickable(m.DeleteUnit()), - ClearUnitBtn: NewActionClickable(m.ClearUnit()), - DisableUnitBtn: NewBoolClickable(m.UnitDisabled()), - CopyUnitBtn: new(TipClickable), + DeleteUnitBtn: new(Clickable), + ClearUnitBtn: new(Clickable), + DisableUnitBtn: new(Clickable), + CopyUnitBtn: new(Clickable), SelectTypeBtn: new(Clickable), commentEditor: NewEditor(true, true, text.Start), sliderList: NewDragList(m.Params().List(), layout.Vertical), @@ -127,15 +127,12 @@ func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D { } func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D { - for pe.CopyUnitBtn.Clickable.Clicked(gtx) { + for pe.CopyUnitBtn.Clicked(gtx) { if contents, ok := t.Units().List().CopyElements(); ok { gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) t.Alerts().Add("Unit copied to clipboard", tracker.Info) } } - copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint) - deleteUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)") - disableUnitBtnStyle := ToggleIcon(gtx, t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint) text := t.Units().SelectedType() if text == "" { text = "Choose unit type" @@ -143,15 +140,19 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D { text = pe.caser.String(text) } hintText := Label(t.Theme, &t.Theme.UnitEditor.Hint, text) + deleteUnitBtn := ActionIconBtn(t.DeleteUnit(), t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)") + copyUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint) + disableUnitBtn := ToggleIconBtn(t.UnitDisabled(), t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint) + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(deleteUnitBtnStyle.Layout), - layout.Rigid(copyUnitBtnStyle.Layout), - layout.Rigid(disableUnitBtnStyle.Layout), + layout.Rigid(deleteUnitBtn.Layout), + layout.Rigid(copyUnitBtn.Layout), + layout.Rigid(disableUnitBtn.Layout), layout.Rigid(func(gtx C) D { var dims D if t.Units().SelectedType() != "" { - clearUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit") - dims = clearUnitBtnStyle.Layout(gtx) + clearUnitBtn := ActionIconBtn(t.ClearUnit(), t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit") + dims = clearUnitBtn.Layout(gtx) } return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)} }),