refactor(tracker/gioui): Menu binds to Model during Layout

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-24 14:05:47 +03:00
parent b79de95f91
commit 58f6cceb9a
6 changed files with 221 additions and 183 deletions

View File

@ -43,8 +43,8 @@ type (
unitDragList *DragList unitDragList *DragList
unitEditor *UnitEditor unitEditor *UnitEditor
wasFocused bool wasFocused bool
presetMenuItems []MenuItem presetMenuItems []ActionMenuItem
presetMenu Menu presetMenu MenuState
addUnit tracker.Action addUnit tracker.Action
@ -85,10 +85,10 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal), instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal),
unitDragList: NewDragList(model.Units().List(), layout.Vertical), unitDragList: NewDragList(model.Units().List(), layout.Vertical),
unitEditor: NewUnitEditor(model), unitEditor: NewUnitEditor(model),
presetMenuItems: []MenuItem{}, presetMenuItems: []ActionMenuItem{},
} }
model.IterateInstrumentPresets(func(index int, name string) bool { model.IterateInstrumentPresets(func(index int, name string) bool {
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)}) ret.presetMenuItems = append(ret.presetMenuItems, ActionMenuItem{Text: name, Icon: icons.ImageAudiotrack, Action: model.LoadPreset(index)})
return true return true
}) })
ret.addUnit = model.AddUnit(false) ret.addUnit = model.AddUnit(false)
@ -185,8 +185,6 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
header := func(gtx C) D { header := func(gtx C) D {
m := PopupMenu(t.Theme, &t.Theme.Menu.Text, &ie.presetMenu)
for ie.copyInstrumentBtn.Clicked(gtx) { for ie.copyInstrumentBtn.Clicked(gtx) {
if contents, ok := t.Instruments().List().CopyElements(); ok { if contents, ok := t.Instruments().List().CopyElements(); ok {
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
@ -235,8 +233,8 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
presetBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.presetMenuBtn, icons.NavigationMenu, "Load preset") presetBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.presetMenuBtn, icons.NavigationMenu, "Load preset")
dims := presetBtn.Layout(gtx) dims := presetBtn.Layout(gtx)
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops) op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500)) m := Menu(t.Theme, &ie.presetMenu)
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180)) m.Style = &t.Theme.Menu.Preset
m.Layout(gtx, ie.presetMenuItems...) m.Layout(gtx, ie.presetMenuItems...)
return dims return dims
}), }),
@ -248,7 +246,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
} }
for ie.presetMenuBtn.Clicked(gtx) { for ie.presetMenuBtn.Clicked(gtx) {
ie.presetMenu.Visible = true ie.presetMenu.visible = true
} }
if t.CommentExpanded().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

View File

@ -14,55 +14,159 @@ import (
"github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker"
) )
type Menu struct { type (
Visible bool // MenuState is the part of the menu that needs to be retained between frames.
MenuState struct {
visible bool
tags []bool tags []bool
clicks []int
hover int hover int
list layout.List list layout.List
scrollBar ScrollBar scrollBar ScrollBar
} }
type MenuStyle struct { // MenuStyle is the style for a menu that is stored in the theme.yml.
Menu *Menu MenuStyle struct {
Title string Text LabelStyle
ShortCutColor color.NRGBA Shortcut LabelStyle
HoverColor color.NRGBA
Theme *Theme
LabelStyle LabelStyle
Disabled color.NRGBA Disabled color.NRGBA
} Hover color.NRGBA
Width unit.Dp
Height unit.Dp
}
type MenuItem struct { // ActionMenuItem is a menu item that has an icon, text, shortcut and an action.
IconBytes []byte ActionMenuItem struct {
Icon []byte
Text string Text string
ShortcutText string Shortcut string
Doer tracker.Action Action tracker.Action
}
// MenuWidget has a Layout method to display a menu
MenuWidget struct {
Theme *Theme
State *MenuState
Style *MenuStyle
}
// MenuButton displayes a button with text that opens a menu when clicked.
MenuButton struct {
Theme *Theme
Title string
Style *ButtonStyle
Clickable *Clickable
MenuState *MenuState
Width unit.Dp
}
)
func Menu(th *Theme, state *MenuState) MenuWidget {
return MenuWidget{
Theme: th,
State: state,
Style: &th.Menu.Main,
}
} }
func (m *Menu) Clicked() (int, bool) { func MenuItem(act tracker.Action, text, shortcut string, icon []byte) ActionMenuItem {
if len(m.clicks) == 0 { return ActionMenuItem{
return 0, false Icon: icon,
Text: text,
Shortcut: shortcut,
Action: act,
} }
first := m.clicks[0]
for i := 1; i < len(m.clicks); i++ {
m.clicks[i-1] = m.clicks[i]
}
m.clicks = m.clicks[:len(m.clicks)-1]
return first, true
} }
func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { func MenuBtn(th *Theme, ms *MenuState, cl *Clickable, title string) MenuButton {
contents := func(gtx C) D { return MenuButton{
Theme: th,
Title: title,
Clickable: cl,
MenuState: ms,
Style: &th.Button.Menu,
}
}
func (m *MenuWidget) Layout(gtx C, items ...ActionMenuItem) D {
// unfortunately, there was no way to include items into the MenuWidget
// without causing heap escapes, so they are passed as a parameter to the Layout
m.update(gtx, items...)
popup := Popup(m.Theme, &m.State.visible)
popup.NE = unit.Dp(0)
popup.ShadowN = unit.Dp(0)
popup.NW = unit.Dp(0)
return popup.Layout(gtx, func(gtx C) D {
gtx.Constraints.Max.X = gtx.Dp(m.Style.Width)
gtx.Constraints.Max.Y = gtx.Dp(m.Style.Height)
m.State.list.Axis = layout.Vertical
m.State.scrollBar.Axis = layout.Vertical
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
layout.Expanded(func(gtx C) D {
return m.State.list.Layout(gtx, len(items), func(gtx C, i int) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
var macro op.MacroOp
item := &items[i]
if i == m.State.hover-1 && item.Action.Enabled() {
macro = op.Record(gtx.Ops)
}
icon := m.Theme.Icon(item.Icon)
iconColor := m.Style.Text.Color
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := Label(m.Theme, &m.Style.Text, item.Text)
shortcutLabel := Label(m.Theme, &m.Style.Shortcut, item.Shortcut)
if !item.Action.Enabled() {
iconColor = m.Style.Disabled
textLabel.Color = m.Style.Disabled
shortcutLabel.Color = m.Style.Disabled
}
shortcutInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(12), Bottom: unit.Dp(2), Top: unit.Dp(2)}
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return iconInset.Layout(gtx, func(gtx C) D {
p := gtx.Dp(unit.Dp(m.Style.Text.TextSize))
gtx.Constraints.Min = image.Pt(p, p)
return icon.Layout(gtx, iconColor)
})
}),
layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }),
layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}),
)
if i == m.State.hover-1 && item.Action.Enabled() {
recording := macro.Stop()
paint.FillShape(gtx.Ops, m.Style.Hover, clip.Rect{
Max: image.Pt(dims.Size.X, dims.Size.Y),
}.Op())
recording.Add(gtx.Ops)
}
if item.Action.Enabled() {
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
area := clip.Rect(rect).Push(gtx.Ops)
event.Op(gtx.Ops, &m.State.tags[i])
area.Pop()
}
return dims
})
}),
layout.Expanded(func(gtx C) D {
return m.State.scrollBar.Layout(gtx, &m.Theme.ScrollBar, len(items), &m.State.list.Position)
}),
)
})
}
func (m *MenuWidget) update(gtx C, items ...ActionMenuItem) {
for i, item := range items { for i, item := range items {
// make sure we have a tag for every item // make sure we have a tag for every item
for len(m.Menu.tags) <= i { for len(m.State.tags) <= i {
m.Menu.tags = append(m.Menu.tags, false) m.State.tags = append(m.State.tags, false)
} }
// handle pointer events for this item // handle pointer events for this item
for { for {
ev, ok := gtx.Event(pointer.Filter{ ev, ok := gtx.Event(pointer.Filter{
Target: &m.Menu.tags[i], Target: &m.State.tags[i],
Kinds: pointer.Press | pointer.Enter | pointer.Leave, Kinds: pointer.Press | pointer.Enter | pointer.Leave,
}) })
if !ok { if !ok {
@ -74,106 +178,27 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
} }
switch e.Kind { switch e.Kind {
case pointer.Press: case pointer.Press:
item.Doer.Do() item.Action.Do()
m.Menu.Visible = false m.State.visible = false
case pointer.Enter: case pointer.Enter:
m.Menu.hover = i + 1 m.State.hover = i + 1
case pointer.Leave: case pointer.Leave:
if m.Menu.hover == i+1 { if m.State.hover == i+1 {
m.Menu.hover = 0 m.State.hover = 0
} }
} }
} }
} }
m.Menu.list.Axis = layout.Vertical
m.Menu.scrollBar.Axis = layout.Vertical
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
layout.Expanded(func(gtx C) D {
return m.Menu.list.Layout(gtx, len(items), func(gtx C, i int) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
var macro op.MacroOp
item := &items[i]
if i == m.Menu.hover-1 && item.Doer.Enabled() {
macro = op.Record(gtx.Ops)
}
icon := m.Theme.Icon(item.IconBytes)
iconColor := m.LabelStyle.Color
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := Label(m.Theme, &m.Theme.Menu.Text, item.Text)
shortcutLabel := Label(m.Theme, &m.Theme.Menu.Text, item.ShortcutText)
shortcutLabel.Color = m.ShortCutColor
if !item.Doer.Enabled() {
iconColor = m.Disabled
textLabel.Color = m.Disabled
shortcutLabel.Color = m.Disabled
}
shortcutInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(12), Bottom: unit.Dp(2), Top: unit.Dp(2)}
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return iconInset.Layout(gtx, func(gtx C) D {
p := gtx.Dp(unit.Dp(m.LabelStyle.TextSize))
gtx.Constraints.Min = image.Pt(p, p)
return icon.Layout(gtx, iconColor)
})
}),
layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }),
layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}),
)
if i == m.Menu.hover-1 && item.Doer.Enabled() {
recording := macro.Stop()
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{
Max: image.Pt(dims.Size.X, dims.Size.Y),
}.Op())
recording.Add(gtx.Ops)
}
if item.Doer.Enabled() {
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
area := clip.Rect(rect).Push(gtx.Ops)
event.Op(gtx.Ops, &m.Menu.tags[i])
area.Pop()
}
return dims
})
}),
layout.Expanded(func(gtx C) D {
return m.Menu.scrollBar.Layout(gtx, &m.Theme.ScrollBar, len(items), &m.Menu.list.Position)
}),
)
}
popup := Popup(m.Theme, &m.Menu.Visible)
popup.NE = unit.Dp(0)
popup.ShadowN = unit.Dp(0)
popup.NW = unit.Dp(0)
return popup.Layout(gtx, contents)
}
func PopupMenu(th *Theme, s *LabelStyle, menu *Menu) MenuStyle {
return MenuStyle{
Menu: menu,
ShortCutColor: th.Menu.ShortCut,
LabelStyle: *s,
HoverColor: th.Menu.Hover,
Disabled: th.Menu.Disabled,
Theme: th,
}
} }
func (tr *Tracker) layoutMenu(gtx C, title string, clickable *Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget { func (mb MenuButton) Layout(gtx C, items ...ActionMenuItem) D {
for clickable.Clicked(gtx) { for mb.Clickable.Clicked(gtx) {
menu.Visible = true mb.MenuState.visible = true
} }
m := PopupMenu(tr.Theme, &tr.Theme.Menu.Text, menu) btn := Btn(mb.Theme, mb.Style, mb.Clickable, mb.Title, "")
return func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
btn := Btn(tr.Theme, &tr.Theme.Button.Menu, clickable, title, "")
dims := btn.Layout(gtx) dims := btn.Layout(gtx)
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops) defer op.Offset(image.Pt(0, dims.Size.Y)).Push(gtx.Ops).Pop()
gtx.Constraints.Max.X = gtx.Dp(width) m := Menu(mb.Theme, mb.MenuState)
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(300))
m.Layout(gtx, items...) m.Layout(gtx, items...)
return dims return dims
}
} }

View File

@ -302,12 +302,9 @@ func (e *Expander) layoutHeader(gtx C, th *Theme, title string, smallWidget layo
type MenuBar struct { type MenuBar struct {
Clickables []Clickable Clickables []Clickable
Menus []Menu MenuStates []MenuState
fileMenuItems []MenuItem midiMenuItems []ActionMenuItem
editMenuItems []MenuItem
midiMenuItems []MenuItem
helpMenuItems []MenuItem
panicHint string panicHint string
PanicBtn *Clickable PanicBtn *Clickable
@ -316,37 +313,14 @@ type MenuBar struct {
func NewMenuBar(tr *Tracker) *MenuBar { func NewMenuBar(tr *Tracker) *MenuBar {
ret := &MenuBar{ ret := &MenuBar{
Clickables: make([]Clickable, 4), Clickables: make([]Clickable, 4),
Menus: make([]Menu, 4), MenuStates: make([]MenuState, 4),
PanicBtn: new(Clickable), PanicBtn: new(Clickable),
panicHint: makeHint("Panic", " (%s)", "PanicToggle"), panicHint: makeHint("Panic", " (%s)", "PanicToggle"),
} }
ret.fileMenuItems = []MenuItem{
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: keyActionMap["NewSong"], Doer: tr.NewSong()},
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: keyActionMap["OpenSong"], Doer: tr.OpenSong()},
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: keyActionMap["SaveSong"], Doer: tr.SaveSong()},
{IconBytes: icons.ContentSave, Text: "Save Song As...", ShortcutText: keyActionMap["SaveSongAs"], Doer: tr.SaveSongAs()},
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", ShortcutText: keyActionMap["ExportWav"], Doer: tr.Export()},
}
if canQuit {
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", ShortcutText: keyActionMap["Quit"], Doer: tr.RequestQuit()})
}
ret.editMenuItems = []MenuItem{
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: keyActionMap["Undo"], Doer: tr.Undo()},
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: keyActionMap["Redo"], Doer: tr.Redo()},
{IconBytes: icons.ImageCrop, Text: "Remove unused data", ShortcutText: keyActionMap["RemoveUnused"], Doer: tr.RemoveUnused()},
}
for input := range tr.MIDI.InputDevices { for input := range tr.MIDI.InputDevices {
ret.midiMenuItems = append(ret.midiMenuItems, MenuItem{ ret.midiMenuItems = append(ret.midiMenuItems,
IconBytes: icons.ImageControlPoint, MenuItem(tr.SelectMidiInput(input), input.String(), "", icons.ImageControlPoint),
Text: input.String(), )
Doer: tr.SelectMidiInput(input),
})
}
ret.helpMenuItems = []MenuItem{
{IconBytes: icons.AVLibraryBooks, Text: "Manual", ShortcutText: keyActionMap["ShowManual"], Doer: tr.ShowManual()},
{IconBytes: icons.ActionHelp, Text: "Ask help", ShortcutText: keyActionMap["AskHelp"], Doer: tr.AskHelp()},
{IconBytes: icons.ActionBugReport, Text: "Report bug", ShortcutText: keyActionMap["ReportBug"], Doer: tr.ReportBug()},
{IconBytes: icons.ActionCopyright, Text: "License", ShortcutText: keyActionMap["ShowLicense"], Doer: tr.ShowLicense()},
} }
return ret return ret
} }
@ -355,15 +329,46 @@ func (t *MenuBar) Layout(gtx C, tr *Tracker) D {
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36)) gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36)) gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
flex := layout.Flex{Axis: layout.Horizontal, Alignment: layout.End}
fileBtn := MenuBtn(tr.Theme, &t.MenuStates[0], &t.Clickables[0], "File")
fileFC := layout.Rigid(func(gtx C) D {
items := [...]ActionMenuItem{
MenuItem(tr.NewSong(), "New Song", keyActionMap["NewSong"], icons.ContentClear),
MenuItem(tr.OpenSong(), "Open Song", keyActionMap["OpenSong"], icons.FileFolder),
MenuItem(tr.SaveSong(), "Save Song", keyActionMap["SaveSong"], icons.ContentSave),
MenuItem(tr.SaveSongAs(), "Save Song As...", keyActionMap["SaveSongAs"], icons.ContentSave),
MenuItem(tr.Export(), "Export Wav...", keyActionMap["ExportWav"], icons.ImageAudiotrack),
MenuItem(tr.RequestQuit(), "Quit", keyActionMap["Quit"], icons.ActionExitToApp),
}
if !canQuit {
return fileBtn.Layout(gtx, items[:len(items)-1]...)
}
return fileBtn.Layout(gtx, items[:]...)
})
editBtn := MenuBtn(tr.Theme, &t.MenuStates[1], &t.Clickables[1], "Edit")
editFC := layout.Rigid(func(gtx C) D {
return editBtn.Layout(gtx,
MenuItem(tr.Undo(), "Undo", keyActionMap["Undo"], icons.ContentUndo),
MenuItem(tr.Redo(), "Redo", keyActionMap["Redo"], icons.ContentRedo),
MenuItem(tr.RemoveUnused(), "Remove unused data", keyActionMap["RemoveUnused"], icons.ImageCrop),
)
})
midiBtn := MenuBtn(tr.Theme, &t.MenuStates[2], &t.Clickables[2], "MIDI")
midiFC := layout.Rigid(func(gtx C) D {
return midiBtn.Layout(gtx, t.midiMenuItems...)
})
helpBtn := MenuBtn(tr.Theme, &t.MenuStates[3], &t.Clickables[3], "?")
helpFC := layout.Rigid(func(gtx C) D {
return helpBtn.Layout(gtx,
MenuItem(tr.ShowManual(), "Manual", keyActionMap["ShowManual"], icons.AVLibraryBooks),
MenuItem(tr.AskHelp(), "Ask help", keyActionMap["AskHelp"], icons.ActionHelp),
MenuItem(tr.ReportBug(), "Report bug", keyActionMap["ReportBug"], icons.ActionBugReport),
MenuItem(tr.ShowLicense(), "License", keyActionMap["ShowLicense"], icons.ActionCopyright))
})
panicBtn := ToggleIconBtn(tr.Panic(), tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint) panicBtn := ToggleIconBtn(tr.Panic(), tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint)
if tr.Panic().Value() { if tr.Panic().Value() {
panicBtn.Style = &tr.Theme.IconButton.Error 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...))
helpFC := layout.Rigid(tr.layoutMenu(gtx, "?", &t.Clickables[3], &t.Menus[3], unit.Dp(200), t.helpMenuItems...))
panicFC := layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, panicBtn.Layout) }) panicFC := layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, panicBtn.Layout) })
if len(t.midiMenuItems) > 0 { if len(t.midiMenuItems) > 0 {
return flex.Layout(gtx, fileFC, editFC, midiFC, helpFC, panicFC) return flex.Layout(gtx, fileFC, editFC, midiFC, helpFC, panicFC)

View File

@ -63,10 +63,8 @@ type Theme struct {
Play color.NRGBA Play color.NRGBA
} }
Menu struct { Menu struct {
Text LabelStyle Main MenuStyle
ShortCut color.NRGBA Preset MenuStyle
Hover color.NRGBA
Disabled color.NRGBA
} }
InstrumentEditor struct { InstrumentEditor struct {
Octave LabelStyle Octave LabelStyle

View File

@ -135,10 +135,20 @@ noteeditor:
onebeat: { r: 31, g: 37, b: 38, a: 255 } onebeat: { r: 31, g: 37, b: 38, a: 255 }
twobeat: { r: 31, g: 51, b: 53, a: 255 } twobeat: { r: 31, g: 51, b: 53, a: 255 }
menu: menu:
main:
text: { textsize: 16, color: *highemphasis, shadowcolor: *black } text: { textsize: 16, color: *highemphasis, shadowcolor: *black }
shortcut: *mediumemphasis shortcut: { textsize: 16, color: *mediumemphasis, shadowcolor: *black }
hover: { r: 100, g: 140, b: 255, a: 48 } hover: { r: 100, g: 140, b: 255, a: 48 }
disabled: *disabled disabled: *disabled
width: 200
height: 300
preset:
text: { textsize: 16, color: *highemphasis, shadowcolor: *black }
shortcut: { textsize: 16, color: *mediumemphasis, shadowcolor: *black }
hover: { r: 100, g: 140, b: 255, a: 48 }
disabled: *disabled
width: 180
height: 300
instrumenteditor: instrumenteditor:
octave: { textsize: 14, color: *disabled } octave: { textsize: 14, color: *disabled }
voices: { textsize: 14, color: *disabled } voices: { textsize: 14, color: *disabled }

View File

@ -224,9 +224,9 @@ type ParameterWidget struct {
floatWidget widget.Float floatWidget widget.Float
boolWidget widget.Bool boolWidget widget.Bool
instrBtn Clickable instrBtn Clickable
instrMenu Menu instrMenu MenuState
unitBtn Clickable unitBtn Clickable
unitMenu Menu unitMenu MenuState
Parameter tracker.Parameter Parameter tracker.Parameter
tipArea TipArea tipArea TipArea
} }
@ -315,43 +315,45 @@ func (p ParameterStyle) Layout(gtx C) D {
case tracker.IDParameter: case tracker.IDParameter:
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200)) gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
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([]ActionMenuItem, p.tracker.Instruments().Count())
for i := range instrItems { for i := range instrItems {
i := i 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].Icon = icons.NavigationChevronRight
instrItems[i].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { instrItems[i].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
if id, ok := p.tracker.Instruments().FirstID(i); ok { if id, ok := p.tracker.Instruments().FirstID(i); ok {
p.w.Parameter.SetValue(id) p.w.Parameter.SetValue(id)
} }
})) }))
} }
var unitItems []MenuItem var unitItems []ActionMenuItem
instrName := "<instr>" instrName := "<instr>"
unitName := "<unit>" unitName := "<unit>"
targetInstrName, units, targetUnitIndex, ok := p.tracker.UnitInfo(p.w.Parameter.Value()) targetInstrName, units, targetUnitIndex, ok := p.tracker.UnitInfo(p.w.Parameter.Value())
if ok { if ok {
instrName = targetInstrName instrName = targetInstrName
unitName = buildUnitLabel(targetUnitIndex, units[targetUnitIndex]) unitName = buildUnitLabel(targetUnitIndex, units[targetUnitIndex])
unitItems = make([]MenuItem, len(units)) unitItems = make([]ActionMenuItem, len(units))
for j, unit := range units { for j, unit := range units {
id := unit.ID id := unit.ID
unitItems[j].Text = buildUnitLabel(j, unit) unitItems[j].Text = buildUnitLabel(j, unit)
unitItems[j].IconBytes = icons.NavigationChevronRight unitItems[j].Icon = icons.NavigationChevronRight
unitItems[j].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { unitItems[j].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
p.w.Parameter.SetValue(id) p.w.Parameter.SetValue(id)
})) }))
} }
} }
defer pointer.PassOp{}.Push(gtx.Ops).Pop() defer pointer.PassOp{}.Push(gtx.Ops).Pop()
instrBtn := MenuBtn(p.tracker.Theme, &p.w.instrMenu, &p.w.instrBtn, instrName)
unitBtn := MenuBtn(p.tracker.Theme, &p.w.unitMenu, &p.w.unitBtn, unitName)
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(p.tracker.layoutMenu(gtx, instrName, &p.w.instrBtn, &p.w.instrMenu, unit.Dp(200), layout.Rigid(func(gtx C) D {
instrItems..., return instrBtn.Layout(gtx, instrItems...)
)), }),
layout.Rigid(p.tracker.layoutMenu(gtx, unitName, &p.w.unitBtn, &p.w.unitMenu, unit.Dp(240), layout.Rigid(func(gtx C) D {
unitItems..., return unitBtn.Layout(gtx, unitItems...)
)), }),
) )
} }
return D{} return D{}