diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index f8c6285..b06414a 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -43,8 +43,8 @@ type ( unitDragList *DragList unitEditor *UnitEditor wasFocused bool - presetMenuItems []MenuItem - presetMenu Menu + presetMenuItems []ActionMenuItem + presetMenu MenuState addUnit tracker.Action @@ -85,10 +85,10 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal), unitDragList: NewDragList(model.Units().List(), layout.Vertical), unitEditor: NewUnitEditor(model), - presetMenuItems: []MenuItem{}, + presetMenuItems: []ActionMenuItem{}, } 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 }) 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 { header := func(gtx C) D { - m := PopupMenu(t.Theme, &t.Theme.Menu.Text, &ie.presetMenu) - 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))}) @@ -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") 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 := Menu(t.Theme, &ie.presetMenu) + m.Style = &t.Theme.Menu.Preset m.Layout(gtx, ie.presetMenuItems...) return dims }), @@ -248,7 +246,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { } 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 diff --git a/tracker/gioui/menu.go b/tracker/gioui/menu.go index 29e2f2a..a9228e6 100644 --- a/tracker/gioui/menu.go +++ b/tracker/gioui/menu.go @@ -14,104 +14,116 @@ import ( "github.com/vsariola/sointu/tracker" ) -type Menu struct { - Visible bool - tags []bool - clicks []int - hover int - 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 +type ( + // MenuState is the part of the menu that needs to be retained between frames. + MenuState struct { + visible bool + tags []bool + hover int + list layout.List + scrollBar ScrollBar } - first := m.clicks[0] - for i := 1; i < len(m.clicks); i++ { - m.clicks[i-1] = m.clicks[i] + + // MenuStyle is the style for a menu that is stored in the theme.yml. + 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 { - contents := func(gtx C) D { - for i, item := range items { - // make sure we have a tag for every item - for len(m.Menu.tags) <= i { - m.Menu.tags = append(m.Menu.tags, false) - } - // handle pointer events for this item - for { - ev, ok := gtx.Event(pointer.Filter{ - Target: &m.Menu.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.Doer.Do() - m.Menu.Visible = false - case pointer.Enter: - m.Menu.hover = i + 1 - case pointer.Leave: - if m.Menu.hover == i+1 { - m.Menu.hover = 0 - } - } - } - } - m.Menu.list.Axis = layout.Vertical - m.Menu.scrollBar.Axis = layout.Vertical +func MenuItem(act tracker.Action, text, shortcut string, icon []byte) ActionMenuItem { + return ActionMenuItem{ + Icon: icon, + Text: text, + Shortcut: shortcut, + Action: act, + } +} + +func MenuBtn(th *Theme, ms *MenuState, cl *Clickable, title string) MenuButton { + 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.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() var macro op.MacroOp 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) } - icon := m.Theme.Icon(item.IconBytes) - iconColor := m.LabelStyle.Color + 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.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 + 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.LabelStyle.TextSize)) + p := gtx.Dp(unit.Dp(m.Style.Text.TextSize)) gtx.Constraints.Min = image.Pt(p, p) return icon.Layout(gtx, iconColor) }) @@ -122,58 +134,71 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { 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() - 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), }.Op()) recording.Add(gtx.Ops) } - if item.Doer.Enabled() { + 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.Menu.tags[i]) + event.Op(gtx.Ops, &m.State.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) + 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 { - return MenuStyle{ - Menu: menu, - ShortCutColor: th.Menu.ShortCut, - LabelStyle: *s, - HoverColor: th.Menu.Hover, - Disabled: th.Menu.Disabled, - Theme: th, +func (m *MenuWidget) update(gtx C, items ...ActionMenuItem) { + for i, item := range items { + // make sure we have a tag for every item + for len(m.State.tags) <= i { + m.State.tags = append(m.State.tags, false) + } + // handle pointer events for this item + 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 { - for clickable.Clicked(gtx) { - menu.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 +func (mb MenuButton) Layout(gtx C, items ...ActionMenuItem) D { + for mb.Clickable.Clicked(gtx) { + mb.MenuState.visible = true } + 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 } diff --git a/tracker/gioui/songpanel.go b/tracker/gioui/songpanel.go index 8b6bdad..d390e0d 100644 --- a/tracker/gioui/songpanel.go +++ b/tracker/gioui/songpanel.go @@ -302,12 +302,9 @@ func (e *Expander) layoutHeader(gtx C, th *Theme, title string, smallWidget layo type MenuBar struct { Clickables []Clickable - Menus []Menu + MenuStates []MenuState - fileMenuItems []MenuItem - editMenuItems []MenuItem - midiMenuItems []MenuItem - helpMenuItems []MenuItem + midiMenuItems []ActionMenuItem panicHint string PanicBtn *Clickable @@ -316,37 +313,14 @@ type MenuBar struct { func NewMenuBar(tr *Tracker) *MenuBar { ret := &MenuBar{ Clickables: make([]Clickable, 4), - Menus: make([]Menu, 4), + MenuStates: make([]MenuState, 4), PanicBtn: new(Clickable), 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 { - ret.midiMenuItems = append(ret.midiMenuItems, MenuItem{ - IconBytes: 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()}, + ret.midiMenuItems = append(ret.midiMenuItems, + MenuItem(tr.SelectMidiInput(input), input.String(), "", icons.ImageControlPoint), + ) } 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.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) 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...)) - 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) }) if len(t.midiMenuItems) > 0 { return flex.Layout(gtx, fileFC, editFC, midiFC, helpFC, panicFC) diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index 6c2a94e..bddeb52 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -63,10 +63,8 @@ type Theme struct { Play color.NRGBA } Menu struct { - Text LabelStyle - ShortCut color.NRGBA - Hover color.NRGBA - Disabled color.NRGBA + Main MenuStyle + Preset MenuStyle } InstrumentEditor struct { Octave LabelStyle diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index 41b2b7d..54786b9 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -135,10 +135,20 @@ noteeditor: onebeat: { r: 31, g: 37, b: 38, a: 255 } twobeat: { r: 31, g: 51, b: 53, a: 255 } menu: - text: { textsize: 16, color: *highemphasis, shadowcolor: *black } - shortcut: *mediumemphasis - hover: { r: 100, g: 140, b: 255, a: 48 } - disabled: *disabled + main: + 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: 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: octave: { textsize: 14, color: *disabled } voices: { textsize: 14, color: *disabled } diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 0f48e2d..fac97a1 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -224,9 +224,9 @@ type ParameterWidget struct { floatWidget widget.Float boolWidget widget.Bool instrBtn Clickable - instrMenu Menu + instrMenu MenuState unitBtn Clickable - unitMenu Menu + unitMenu MenuState Parameter tracker.Parameter tipArea TipArea } @@ -315,43 +315,45 @@ func (p ParameterStyle) Layout(gtx C) D { case tracker.IDParameter: gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200)) 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 { i := i name, _, _, _ := p.tracker.Instruments().Item(i) instrItems[i].Text = name - instrItems[i].IconBytes = icons.NavigationChevronRight - instrItems[i].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { + instrItems[i].Icon = icons.NavigationChevronRight + instrItems[i].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() { if id, ok := p.tracker.Instruments().FirstID(i); ok { p.w.Parameter.SetValue(id) } })) } - var unitItems []MenuItem + var unitItems []ActionMenuItem instrName := "" unitName := "" targetInstrName, units, targetUnitIndex, ok := p.tracker.UnitInfo(p.w.Parameter.Value()) if ok { instrName = targetInstrName unitName = buildUnitLabel(targetUnitIndex, units[targetUnitIndex]) - unitItems = make([]MenuItem, len(units)) + unitItems = make([]ActionMenuItem, len(units)) for j, unit := range units { id := unit.ID unitItems[j].Text = buildUnitLabel(j, unit) - unitItems[j].IconBytes = icons.NavigationChevronRight - unitItems[j].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { + unitItems[j].Icon = icons.NavigationChevronRight + unitItems[j].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() { p.w.Parameter.SetValue(id) })) } } 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, - layout.Rigid(p.tracker.layoutMenu(gtx, instrName, &p.w.instrBtn, &p.w.instrMenu, unit.Dp(200), - instrItems..., - )), - layout.Rigid(p.tracker.layoutMenu(gtx, unitName, &p.w.unitBtn, &p.w.unitMenu, unit.Dp(240), - unitItems..., - )), + layout.Rigid(func(gtx C) D { + return instrBtn.Layout(gtx, instrItems...) + }), + layout.Rigid(func(gtx C) D { + return unitBtn.Layout(gtx, unitItems...) + }), ) } return D{}