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,104 +14,116 @@ 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.
tags []bool MenuState struct {
clicks []int visible bool
hover int tags []bool
list layout.List hover int
scrollBar ScrollBar list layout.List
} scrollBar ScrollBar
type MenuStyle struct {
Menu *Menu
Title string
ShortCutColor color.NRGBA
HoverColor color.NRGBA
Theme *Theme
LabelStyle LabelStyle
Disabled color.NRGBA
}
type MenuItem struct {
IconBytes []byte
Text string
ShortcutText string
Doer tracker.Action
}
func (m *Menu) Clicked() (int, bool) {
if len(m.clicks) == 0 {
return 0, false
} }
first := m.clicks[0]
for i := 1; i < len(m.clicks); i++ { // MenuStyle is the style for a menu that is stored in the theme.yml.
m.clicks[i-1] = m.clicks[i] MenuStyle struct {
Text LabelStyle
Shortcut LabelStyle
Disabled color.NRGBA
Hover color.NRGBA
Width unit.Dp
Height unit.Dp
}
// ActionMenuItem is a menu item that has an icon, text, shortcut and an action.
ActionMenuItem struct {
Icon []byte
Text string
Shortcut string
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,
} }
m.clicks = m.clicks[:len(m.clicks)-1]
return first, true
} }
func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { func MenuItem(act tracker.Action, text, shortcut string, icon []byte) ActionMenuItem {
contents := func(gtx C) D { return ActionMenuItem{
for i, item := range items { Icon: icon,
// make sure we have a tag for every item Text: text,
for len(m.Menu.tags) <= i { Shortcut: shortcut,
m.Menu.tags = append(m.Menu.tags, false) Action: act,
} }
// handle pointer events for this item }
for {
ev, ok := gtx.Event(pointer.Filter{ func MenuBtn(th *Theme, ms *MenuState, cl *Clickable, title string) MenuButton {
Target: &m.Menu.tags[i], return MenuButton{
Kinds: pointer.Press | pointer.Enter | pointer.Leave, Theme: th,
}) Title: title,
if !ok { Clickable: cl,
break MenuState: ms,
} Style: &th.Button.Menu,
e, ok := ev.(pointer.Event) }
if !ok { }
continue
} func (m *MenuWidget) Layout(gtx C, items ...ActionMenuItem) D {
switch e.Kind { // unfortunately, there was no way to include items into the MenuWidget
case pointer.Press: // without causing heap escapes, so they are passed as a parameter to the Layout
item.Doer.Do() m.update(gtx, items...)
m.Menu.Visible = false popup := Popup(m.Theme, &m.State.visible)
case pointer.Enter: popup.NE = unit.Dp(0)
m.Menu.hover = i + 1 popup.ShadowN = unit.Dp(0)
case pointer.Leave: popup.NW = unit.Dp(0)
if m.Menu.hover == i+1 { return popup.Layout(gtx, func(gtx C) D {
m.Menu.hover = 0 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
}
m.Menu.list.Axis = layout.Vertical
m.Menu.scrollBar.Axis = layout.Vertical
return layout.Stack{Alignment: layout.SE}.Layout(gtx, return layout.Stack{Alignment: layout.SE}.Layout(gtx,
layout.Expanded(func(gtx C) D { layout.Expanded(func(gtx C) D {
return m.Menu.list.Layout(gtx, len(items), func(gtx C, i int) D { return m.State.list.Layout(gtx, len(items), func(gtx C, i int) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
var macro op.MacroOp var macro op.MacroOp
item := &items[i] item := &items[i]
if i == m.Menu.hover-1 && item.Doer.Enabled() { if i == m.State.hover-1 && item.Action.Enabled() {
macro = op.Record(gtx.Ops) macro = op.Record(gtx.Ops)
} }
icon := m.Theme.Icon(item.IconBytes) icon := m.Theme.Icon(item.Icon)
iconColor := m.LabelStyle.Color iconColor := m.Style.Text.Color
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)} iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := Label(m.Theme, &m.Theme.Menu.Text, item.Text) textLabel := Label(m.Theme, &m.Style.Text, item.Text)
shortcutLabel := Label(m.Theme, &m.Theme.Menu.Text, item.ShortcutText) shortcutLabel := Label(m.Theme, &m.Style.Shortcut, item.Shortcut)
shortcutLabel.Color = m.ShortCutColor if !item.Action.Enabled() {
if !item.Doer.Enabled() { iconColor = m.Style.Disabled
iconColor = m.Disabled textLabel.Color = m.Style.Disabled
textLabel.Color = m.Disabled shortcutLabel.Color = m.Style.Disabled
shortcutLabel.Color = m.Disabled
} }
shortcutInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(12), Bottom: unit.Dp(2), Top: unit.Dp(2)} 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, 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.LabelStyle.TextSize)) p := gtx.Dp(unit.Dp(m.Style.Text.TextSize))
gtx.Constraints.Min = image.Pt(p, p) gtx.Constraints.Min = image.Pt(p, p)
return icon.Layout(gtx, iconColor) return icon.Layout(gtx, iconColor)
}) })
@ -122,58 +134,71 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout) return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}), }),
) )
if i == m.Menu.hover-1 && item.Doer.Enabled() { if i == m.State.hover-1 && item.Action.Enabled() {
recording := macro.Stop() recording := macro.Stop()
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{ paint.FillShape(gtx.Ops, m.Style.Hover, clip.Rect{
Max: image.Pt(dims.Size.X, dims.Size.Y), Max: image.Pt(dims.Size.X, dims.Size.Y),
}.Op()) }.Op())
recording.Add(gtx.Ops) recording.Add(gtx.Ops)
} }
if item.Doer.Enabled() { if item.Action.Enabled() {
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y) rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
area := clip.Rect(rect).Push(gtx.Ops) area := clip.Rect(rect).Push(gtx.Ops)
event.Op(gtx.Ops, &m.Menu.tags[i]) event.Op(gtx.Ops, &m.State.tags[i])
area.Pop() area.Pop()
} }
return dims return dims
}) })
}), }),
layout.Expanded(func(gtx C) D { layout.Expanded(func(gtx C) D {
return m.Menu.scrollBar.Layout(gtx, &m.Theme.ScrollBar, len(items), &m.Menu.list.Position) return m.State.scrollBar.Layout(gtx, &m.Theme.ScrollBar, len(items), &m.State.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 { func (m *MenuWidget) update(gtx C, items ...ActionMenuItem) {
return MenuStyle{ for i, item := range items {
Menu: menu, // make sure we have a tag for every item
ShortCutColor: th.Menu.ShortCut, for len(m.State.tags) <= i {
LabelStyle: *s, m.State.tags = append(m.State.tags, false)
HoverColor: th.Menu.Hover, }
Disabled: th.Menu.Disabled, // handle pointer events for this item
Theme: th, for {
ev, ok := gtx.Event(pointer.Filter{
Target: &m.State.tags[i],
Kinds: pointer.Press | pointer.Enter | pointer.Leave,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Kind {
case pointer.Press:
item.Action.Do()
m.State.visible = false
case pointer.Enter:
m.State.hover = i + 1
case pointer.Leave:
if m.State.hover == i+1 {
m.State.hover = 0
}
}
}
} }
} }
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)
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)
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))
m.Layout(gtx, items...)
return dims
} }
btn := Btn(mb.Theme, mb.Style, mb.Clickable, mb.Title, "")
dims := btn.Layout(gtx)
defer op.Offset(image.Pt(0, dims.Size.Y)).Push(gtx.Ops).Pop()
m := Menu(mb.Theme, mb.MenuState)
m.Layout(gtx, items...)
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:
text: { textsize: 16, color: *highemphasis, shadowcolor: *black } main:
shortcut: *mediumemphasis text: { textsize: 16, color: *highemphasis, shadowcolor: *black }
hover: { r: 100, g: 140, b: 255, a: 48 } shortcut: { textsize: 16, color: *mediumemphasis, shadowcolor: *black }
disabled: *disabled hover: { r: 100, g: 140, b: 255, a: 48 }
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{}