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.
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-21 23:49:07 +03:00
parent 0ea20ea5bf
commit db2ccf977d
11 changed files with 453 additions and 411 deletions

View File

@ -22,41 +22,273 @@ import (
) )
type ( type (
TipClickable struct { Clickable struct {
Clickable Clickable click gesture.Click
TipArea component.TipArea 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 { 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
}
IconButtonStyle struct {
Background color.NRGBA
// Color is the icon color.
Color color.NRGBA
// Size is the icon size.
Size unit.Dp
Inset layout.Inset
}
// 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 Action tracker.Action
TipClickable DisabledStyle *ButtonStyle
Button
} }
TipIconButtonStyle struct { // ToggleButton is a text button that toggles a boolean value when clicked.
TipArea *component.TipArea ToggleButton struct {
IconButtonStyle IconButtonStyle
Tooltip component.Tooltip
}
BoolClickable struct {
Clickable Clickable
TipArea component.TipArea
Bool tracker.Bool 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 { func Btn(th *Theme, st *ButtonStyle, c *Clickable, txt string, tip string) Button {
return &ActionClickable{ return Button{
Action: a, Theme: th,
Style: st,
Clickable: c,
Text: txt,
Tip: tip,
} }
} }
func NewBoolClickable(b tracker.Bool) *BoolClickable { func ActionBtn(act tracker.Action, th *Theme, c *Clickable, txt string, tip string) ActionButton {
return &BoolClickable{ return ActionButton{
Bool: b, 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 { func Tooltip(th *Theme, tip string) component.Tooltip {
tooltip := component.PlatformTooltip(&th.Material, tip) tooltip := component.PlatformTooltip(&th.Material, tip)
tooltip.Bg = th.Tooltip.Bg tooltip.Bg = th.Tooltip.Bg
@ -64,88 +296,6 @@ func Tooltip(th *Theme, tip string) component.Tooltip {
return 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. // Click executes a simple programmatic click.
func (b *Clickable) Click() { func (b *Clickable) Click() {
b.requestClicks++ b.requestClicks++
@ -252,135 +402,6 @@ func (b *Clickable) update(_ event.Tag, gtx layout.Context) (widget.Click, bool)
return widget.Click{}, false 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) { func drawInk(gtx layout.Context, c widget.Press) {
// duration is the number of seconds for the // duration is the number of seconds for the
// completed animation: expand while fading in, then // completed animation: expand while fading in, then

View File

@ -23,19 +23,19 @@ import (
type ( type (
InstrumentEditor struct { InstrumentEditor struct {
newInstrumentBtn *ActionClickable newInstrumentBtn *Clickable
enlargeBtn *BoolClickable enlargeBtn *Clickable
deleteInstrumentBtn *ActionClickable deleteInstrumentBtn *Clickable
linkInstrTrackBtn *BoolClickable linkInstrTrackBtn *Clickable
splitInstrumentBtn *ActionClickable splitInstrumentBtn *Clickable
copyInstrumentBtn *TipClickable copyInstrumentBtn *Clickable
saveInstrumentBtn *TipClickable saveInstrumentBtn *Clickable
loadInstrumentBtn *TipClickable loadInstrumentBtn *Clickable
addUnitBtn *ActionClickable addUnitBtn *Clickable
presetMenuBtn *TipClickable presetMenuBtn *Clickable
commentExpandBtn *BoolClickable commentExpandBtn *Clickable
soloBtn *BoolClickable soloBtn *Clickable
muteBtn *BoolClickable muteBtn *Clickable
commentEditor *Editor commentEditor *Editor
commentString tracker.String commentString tracker.String
nameEditor *Editor nameEditor *Editor
@ -68,18 +68,19 @@ type (
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
ret := &InstrumentEditor{ ret := &InstrumentEditor{
newInstrumentBtn: NewActionClickable(model.AddInstrument()), newInstrumentBtn: new(Clickable),
enlargeBtn: NewBoolClickable(model.InstrEnlarged()), enlargeBtn: new(Clickable),
deleteInstrumentBtn: NewActionClickable(model.DeleteInstrument()), deleteInstrumentBtn: new(Clickable),
linkInstrTrackBtn: NewBoolClickable(model.LinkInstrTrack()), linkInstrTrackBtn: new(Clickable),
splitInstrumentBtn: NewActionClickable(model.SplitInstrument()), splitInstrumentBtn: new(Clickable),
copyInstrumentBtn: new(TipClickable), copyInstrumentBtn: new(Clickable),
saveInstrumentBtn: new(TipClickable), saveInstrumentBtn: new(Clickable),
loadInstrumentBtn: new(TipClickable), loadInstrumentBtn: new(Clickable),
commentExpandBtn: NewBoolClickable(model.CommentExpanded()), commentExpandBtn: new(Clickable),
presetMenuBtn: new(TipClickable), presetMenuBtn: new(Clickable),
soloBtn: NewBoolClickable(model.Solo()), soloBtn: new(Clickable),
muteBtn: NewBoolClickable(model.Mute()), muteBtn: new(Clickable),
addUnitBtn: new(Clickable),
commentEditor: NewEditor(false, false, text.Start), commentEditor: NewEditor(false, false, text.Start),
nameEditor: NewEditor(true, true, text.Middle), nameEditor: NewEditor(true, true, text.Middle),
searchEditor: NewEditor(true, true, text.Start), searchEditor: NewEditor(true, true, text.Start),
@ -95,7 +96,6 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
return true return true
}) })
ret.addUnit = model.AddUnit(false) ret.addUnit = model.AddUnit(false)
ret.addUnitBtn = NewActionClickable(tracker.MakeEnabledAction(ret.AddUnitThenFocus()))
ret.enlargeHint = makeHint("Enlarge", " (%s)", "InstrEnlargedToggle") ret.enlargeHint = makeHint("Enlarge", " (%s)", "InstrEnlargedToggle")
ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle") ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle")
ret.addInstrumentHint = makeHint("Add\ninstrument", "\n(%s)", "AddInstrument") 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 { func (ie *InstrumentEditor) childFocused(gtx C) bool {
return ie.unitEditor.sliderList.Focused(gtx) || 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) || 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.addUnitBtn) || gtx.Source.Focused(ie.commentExpandBtn) || gtx.Source.Focused(ie.presetMenuBtn) ||
gtx.Source.Focused(ie.deleteInstrumentBtn.Clickable) || gtx.Source.Focused(ie.copyInstrumentBtn.Clickable) gtx.Source.Focused(ie.deleteInstrumentBtn) || gtx.Source.Focused(ie.copyInstrumentBtn)
} }
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
ie.wasFocused = ie.Focused(gtx) || ie.childFocused(gtx) 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 { octave := func(gtx C) D {
in := layout.UniformInset(unit.Dp(1)) 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, ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout( 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(layout.Spacer{Width: 4}.Layout),
layout.Rigid(octave), layout.Rigid(octave),
layout.Rigid(func(gtx C) D { 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 { 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 { 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 { func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
header := func(gtx C) 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) 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 { 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))})
t.Alerts().Add("Instrument copied to clipboard", tracker.Info) 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") writer, err := t.Explorer.CreateFile(t.InstrumentName().Value() + ".yml")
if err != nil { if err != nil {
continue continue
@ -217,7 +207,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
t.SaveInstrument(writer) t.SaveInstrument(writer)
} }
for ie.loadInstrumentBtn.Clickable.Clicked(gtx) { for ie.loadInstrumentBtn.Clicked(gtx) {
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp") reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
if err != nil { if err != nil {
continue continue
@ -225,6 +215,15 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
t.LoadInstrument(reader) 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 { header := func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(layout.Spacer{Width: 6}.Layout), 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 { 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") 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.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(commentExpandBtnStyle.Layout), layout.Rigid(commentExpandedBtn.Layout),
layout.Rigid(soloBtnStyle.Layout), layout.Rigid(soloBtn.Layout),
layout.Rigid(muteBtnStyle.Layout), layout.Rigid(muteBtn.Layout),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() presetBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.presetMenuBtn, icons.NavigationMenu, "Load preset")
dims := presetMenuBtnStyle.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)) gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180)) gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
m.Layout(gtx, ie.presetMenuItems...) m.Layout(gtx, ie.presetMenuItems...)
return dims return dims
}), }),
layout.Rigid(saveInstrumentBtnStyle.Layout), layout.Rigid(saveInstrumentBtn.Layout),
layout.Rigid(loadInstrumentBtnStyle.Layout), layout.Rigid(loadInstrumentBtn.Layout),
layout.Rigid(copyInstrumentBtnStyle.Layout), layout.Rigid(copyInstrumentBtn.Layout),
layout.Rigid(deleteInstrumentBtnStyle.Layout)) layout.Rigid(deleteInstrumentBtn.Layout),
)
} }
for ie.presetMenuBtn.Clickable.Clicked(gtx) { for ie.presetMenuBtn.Clicked(gtx) {
ie.presetMenu.Visible = true 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, ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(header), layout.Rigid(header),
layout.Rigid(func(gtx C) D { 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 { 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 var units [256]tracker.UnitListItem
for i, item := range (*tracker.Units)(t.Model).Iterate { for i, item := range (*tracker.Units)(t.Model).Iterate {
if i >= 256 { if i >= 256 {
@ -482,8 +476,12 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
return dims return dims
}), }),
layout.Stacked(func(gtx C) D { 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)} 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)
}), }),
) )
}) })

View File

@ -263,7 +263,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
case t.TrackEditor.scrollTable.Focused(gtx): case t.TrackEditor.scrollTable.Focused(gtx):
t.OrderEditor.scrollTable.Focus() t.OrderEditor.scrollTable.Focus()
case t.InstrumentEditor.Focused(gtx): case t.InstrumentEditor.Focused(gtx):
if t.InstrumentEditor.enlargeBtn.Bool.Value() { if t.InstrEnlarged().Value() {
t.InstrumentEditor.unitEditor.sliderList.Focus() t.InstrumentEditor.unitEditor.sliderList.Focus()
} else { } else {
t.TrackEditor.scrollTable.Focus() t.TrackEditor.scrollTable.Focus()
@ -280,7 +280,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
case t.InstrumentEditor.Focused(gtx): case t.InstrumentEditor.Focused(gtx):
t.InstrumentEditor.unitEditor.sliderList.Focus() t.InstrumentEditor.unitEditor.sliderList.Focus()
default: default:
if t.InstrumentEditor.enlargeBtn.Bool.Value() { if t.InstrEnlarged().Value() {
t.InstrumentEditor.Focus() t.InstrumentEditor.Focus()
} else { } else {
t.OrderEditor.scrollTable.Focus() t.OrderEditor.scrollTable.Focus()

View File

@ -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) m := PopupMenu(tr.Theme, &tr.Theme.Menu.Text, menu)
return func(gtx C) D { return func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
titleBtn := Btn(tr.Theme, &tr.Theme.Button.Menu, clickable, title) btn := Btn(tr.Theme, &tr.Theme.Button.Menu, clickable, title, "")
titleBtn.CornerRadius = unit.Dp(0) dims := btn.Layout(gtx)
dims := titleBtn.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.X = gtx.Dp(width) gtx.Constraints.Max.X = gtx.Dp(width)
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(300)) gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(300))

View File

@ -52,18 +52,18 @@ func init() {
type NoteEditor struct { type NoteEditor struct {
TrackVoices *NumericUpDown TrackVoices *NumericUpDown
NewTrackBtn *ActionClickable NewTrackBtn *Clickable
DeleteTrackBtn *ActionClickable DeleteTrackBtn *Clickable
SplitTrackBtn *ActionClickable SplitTrackBtn *Clickable
AddSemitoneBtn *ActionClickable AddSemitoneBtn *Clickable
SubtractSemitoneBtn *ActionClickable SubtractSemitoneBtn *Clickable
AddOctaveBtn *ActionClickable AddOctaveBtn *Clickable
SubtractOctaveBtn *ActionClickable SubtractOctaveBtn *Clickable
NoteOffBtn *ActionClickable NoteOffBtn *Clickable
EffectBtn *BoolClickable EffectBtn *Clickable
UniqueBtn *BoolClickable UniqueBtn *Clickable
TrackMidiInBtn *BoolClickable TrackMidiInBtn *Clickable
scrollTable *ScrollTable scrollTable *ScrollTable
eventFilters []event.Filter eventFilters []event.Filter
@ -77,17 +77,17 @@ type NoteEditor struct {
func NewNoteEditor(model *tracker.Model) *NoteEditor { func NewNoteEditor(model *tracker.Model) *NoteEditor {
ret := &NoteEditor{ ret := &NoteEditor{
TrackVoices: NewNumericUpDown(), TrackVoices: NewNumericUpDown(),
NewTrackBtn: NewActionClickable(model.AddTrack()), NewTrackBtn: new(Clickable),
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()), DeleteTrackBtn: new(Clickable),
SplitTrackBtn: NewActionClickable(model.SplitTrack()), SplitTrackBtn: new(Clickable),
AddSemitoneBtn: NewActionClickable(model.AddSemitone()), AddSemitoneBtn: new(Clickable),
SubtractSemitoneBtn: NewActionClickable(model.SubtractSemitone()), SubtractSemitoneBtn: new(Clickable),
AddOctaveBtn: NewActionClickable(model.AddOctave()), AddOctaveBtn: new(Clickable),
SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()), SubtractOctaveBtn: new(Clickable),
NoteOffBtn: NewActionClickable(model.EditNoteOff()), NoteOffBtn: new(Clickable),
EffectBtn: NewBoolClickable(model.Effect()), EffectBtn: new(Clickable),
UniqueBtn: NewBoolClickable(model.UniquePatterns()), UniqueBtn: new(Clickable),
TrackMidiInBtn: NewBoolClickable(model.TrackMidiIn()), TrackMidiInBtn: new(Clickable),
scrollTable: NewScrollTable( scrollTable: NewScrollTable(
model.Notes().Table(), model.Notes().Table(),
model.Tracks().List(), 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 { 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 { 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") addSemitoneBtn := ActionBtn(t.AddSemitone(), t.Theme, te.AddSemitoneBtn, "+1", "Add semitone")
subtractSemitoneBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.SubtractSemitoneBtn, "-1") subtractSemitoneBtn := ActionBtn(t.SubtractSemitone(), t.Theme, te.SubtractSemitoneBtn, "-1", "Subtract semitone")
addOctaveBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.AddOctaveBtn, "+12") addOctaveBtn := ActionBtn(t.AddOctave(), t.Theme, te.AddOctaveBtn, "+12", "Add octave")
subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.SubtractOctaveBtn, "-12") subtractOctaveBtn := ActionBtn(t.SubtractOctave(), t.Theme, te.SubtractOctaveBtn, "-12", "Subtract octave")
noteOffBtnStyle := ActionButton(gtx, t.Theme, &t.Theme.Button.Text, te.NoteOffBtn, "Note Off") noteOffBtn := ActionBtn(t.EditNoteOff(), t.Theme, te.NoteOffBtn, "Note Off", "")
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint) deleteTrackBtn := ActionIconBtn(t.DeleteTrack(), t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint) splitTrackBtn := ActionIconBtn(t.SplitTrack(), t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint) newTrackBtn := ActionIconBtn(t.AddTrack(), t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
in := layout.UniformInset(unit.Dp(1)) in := layout.UniformInset(unit.Dp(1))
voiceUpDown := func(gtx C) D { voiceUpDown := func(gtx C) D {
return in.Layout(gtx, 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") return te.TrackVoices.Layout(gtx, t.Model.TrackVoices(), t.Theme, &t.Theme.NumericUpDown, "Track voices")
}) })
} }
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex") effectBtn := ToggleBtn(t.Effect(), t.Theme, te.EffectBtn, "Hex", "Input notes as hex values")
uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip) uniqueBtn := ToggleIconBtn(t.UniquePatterns(), t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI") 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, 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(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
layout.Rigid(addSemitoneBtnStyle.Layout), layout.Rigid(addSemitoneBtn.Layout),
layout.Rigid(subtractSemitoneBtnStyle.Layout), layout.Rigid(subtractSemitoneBtn.Layout),
layout.Rigid(addOctaveBtnStyle.Layout), layout.Rigid(addOctaveBtn.Layout),
layout.Rigid(subtractOctaveBtnStyle.Layout), layout.Rigid(subtractOctaveBtn.Layout),
layout.Rigid(noteOffBtnStyle.Layout), layout.Rigid(noteOffBtn.Layout),
layout.Rigid(effectBtnStyle.Layout), layout.Rigid(effectBtn.Layout),
layout.Rigid(uniqueBtnStyle.Layout), layout.Rigid(uniqueBtn.Layout),
layout.Rigid(layout.Spacer{Width: 10}.Layout), layout.Rigid(layout.Spacer{Width: 10}.Layout),
layout.Rigid(Label(t.Theme, &t.Theme.NoteEditor.Header, "Voices").Layout), layout.Rigid(Label(t.Theme, &t.Theme.NoteEditor.Header, "Voices").Layout),
layout.Rigid(layout.Spacer{Width: 4}.Layout), layout.Rigid(layout.Spacer{Width: 4}.Layout),
layout.Rigid(voiceUpDown), 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.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(midiInBtnStyle.Layout), layout.Rigid(deleteTrackBtn.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), layout.Rigid(newTrackBtn.Layout))
layout.Rigid(deleteTrackBtnStyle.Layout),
layout.Rigid(newTrackBtnStyle.Layout))
}) })
} }
@ -280,7 +279,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
cursor := te.scrollTable.Table.Cursor() cursor := te.scrollTable.Table.Cursor()
drawSelection := cursor != te.scrollTable.Table.Cursor2() drawSelection := cursor != te.scrollTable.Table.Cursor2()
selection := te.scrollTable.Table.Range() selection := te.scrollTable.Table.Range()
hasTrackMidiIn := te.TrackMidiInBtn.Bool.Value() hasTrackMidiIn := t.Model.TrackMidiIn().Value()
patternNoOp := colorOp(gtx, t.Theme.NoteEditor.PatternNo.Color) patternNoOp := colorOp(gtx, t.Theme.NoteEditor.PatternNo.Color)
uniqueOp := colorOp(gtx, t.Theme.NoteEditor.Unique.Color) uniqueOp := colorOp(gtx, t.Theme.NoteEditor.Unique.Color)

View File

@ -17,8 +17,8 @@ import (
type ( type (
OscilloscopeState struct { OscilloscopeState struct {
onceBtn *BoolClickable onceBtn *Clickable
wrapBtn *BoolClickable wrapBtn *Clickable
lengthInBeatsNumber *NumericUpDown lengthInBeatsNumber *NumericUpDown
triggerChannelNumber *NumericUpDown triggerChannelNumber *NumericUpDown
xScale int xScale int
@ -38,20 +38,20 @@ type (
func NewOscilloscope(model *tracker.Model) *OscilloscopeState { func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
return &OscilloscopeState{ return &OscilloscopeState{
onceBtn: NewBoolClickable(model.SignalAnalyzer().Once()), onceBtn: new(Clickable),
wrapBtn: NewBoolClickable(model.SignalAnalyzer().Wrap()), wrapBtn: new(Clickable),
lengthInBeatsNumber: NewNumericUpDown(), lengthInBeatsNumber: NewNumericUpDown(),
triggerChannelNumber: NewNumericUpDown(), triggerChannelNumber: NewNumericUpDown(),
} }
} }
func (s *OscilloscopeState) Layout(gtx C, vtrig, vlen tracker.Int, wave tracker.RingBuffer[[2]float32], th *Theme, st *OscilloscopeStyle) D { func (s *OscilloscopeState) Layout(gtx C, vtrig, vlen tracker.Int, once, wrap tracker.Bool, wave tracker.RingBuffer[[2]float32], th *Theme, st *OscilloscopeStyle) D {
wrapBtnStyle := ToggleButton(gtx, th, s.wrapBtn, "Wrap")
onceBtnStyle := ToggleButton(gtx, th, s.onceBtn, "Once")
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.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, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D { return s.layoutWave(gtx, wave, th) }), layout.Flexed(1, func(gtx C) D { return s.layoutWave(gtx, wave, th) }),
layout.Rigid(func(gtx C) D { 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(leftSpacer),
layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Trigger").Layout), layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Trigger").Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), 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 { layout.Rigid(func(gtx C) D {
return s.triggerChannelNumber.Layout(gtx, vtrig, th, &th.NumericUpDown, "Trigger channel") 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(leftSpacer),
layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Buffer").Layout), layout.Rigid(Label(th, &th.SongPanel.RowHeader, "Buffer").Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), 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 { layout.Rigid(func(gtx C) D {
return s.lengthInBeatsNumber.Layout(gtx, vlen, th, &th.NumericUpDown, "Buffer length in beats") return s.lengthInBeatsNumber.Layout(gtx, vlen, th, &th.NumericUpDown, "Buffer length in beats")
}), }),

View File

@ -46,10 +46,10 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
SongLength: NewNumericUpDown(), SongLength: NewNumericUpDown(),
Scope: NewOscilloscope(model), Scope: NewOscilloscope(model),
MenuBar: NewMenuBar(model), MenuBar: NewMenuBar(model),
PlayBar: NewPlayBar(model), PlayBar: NewPlayBar(),
WeightingTypeBtn: &Clickable{}, WeightingTypeBtn: new(Clickable),
OversamplingBtn: &Clickable{}, OversamplingBtn: new(Clickable),
SongSettingsExpander: &Expander{Expanded: true}, SongSettingsExpander: &Expander{Expanded: true},
ScopeExpander: &Expander{}, ScopeExpander: &Expander{},
@ -75,7 +75,7 @@ func (s *SongPanel) Layout(gtx C, t *Tracker) D {
return s.MenuBar.Layout(gtx, t) return s.MenuBar.Layout(gtx, t)
}), }),
layout.Rigid(func(gtx C) D { 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 { layout.Rigid(func(gtx C) D {
return s.layoutSongOptions(gtx, t) return s.layoutSongOptions(gtx, t)
@ -98,13 +98,13 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
weightingTxt = "No weight (RMS)" 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" oversamplingTxt := "Sample peak"
if tr.Model.Oversampling().Value() { if tr.Model.Oversampling().Value() {
oversamplingTxt = "True peak" 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, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D { 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 { 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.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), layout.Rigid(Label(tr.Theme, &tr.Theme.SongPanel.Version, version.VersionOrHash).Layout),
@ -304,14 +304,14 @@ type MenuBar struct {
midiMenuItems []MenuItem midiMenuItems []MenuItem
panicHint string panicHint string
PanicBtn *BoolClickable PanicBtn *Clickable
} }
func NewMenuBar(model *tracker.Model) *MenuBar { func NewMenuBar(model *tracker.Model) *MenuBar {
ret := &MenuBar{ ret := &MenuBar{
Clickables: make([]Clickable, 3), Clickables: make([]Clickable, 3),
Menus: make([]Menu, 3), Menus: make([]Menu, 3),
PanicBtn: NewBoolClickable(model.Panic()), PanicBtn: new(Clickable),
panicHint: makeHint("Panic", " (%s)", "PanicToggle"), panicHint: makeHint("Panic", " (%s)", "PanicToggle"),
} }
ret.fileMenuItems = []MenuItem{ 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.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))
panicBtnStyle := ToggleIcon(gtx, 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 t.PanicBtn.Bool.Value() { if tr.Panic().Value() {
panicBtnStyle.IconButtonStyle.Color = tr.Theme.SongPanel.ErrorColor panicBtn.Style = &tr.Theme.IconButton.Error
} }
flex := layout.Flex{Axis: layout.Horizontal, Alignment: layout.End} 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...)) 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...)) 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...)) 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 { if len(t.midiMenuItems) > 0 {
return flex.Layout(gtx, fileFC, editFC, midiFC, panicFC) return flex.Layout(gtx, fileFC, editFC, midiFC, panicFC)
} }
@ -359,11 +359,11 @@ func (t *MenuBar) Layout(gtx C, tr *Tracker) D {
} }
type PlayBar struct { type PlayBar struct {
RewindBtn *ActionClickable RewindBtn *Clickable
PlayingBtn *BoolClickable PlayingBtn *Clickable
RecordBtn *BoolClickable RecordBtn *Clickable
FollowBtn *BoolClickable FollowBtn *Clickable
LoopBtn *BoolClickable LoopBtn *Clickable
// Hints // Hints
rewindHint string rewindHint string
playHint, stopHint string playHint, stopHint string
@ -372,13 +372,13 @@ type PlayBar struct {
loopOffHint, loopOnHint string loopOffHint, loopOnHint string
} }
func NewPlayBar(model *tracker.Model) *PlayBar { func NewPlayBar() *PlayBar {
ret := &PlayBar{ ret := &PlayBar{
LoopBtn: NewBoolClickable(model.LoopToggle()), LoopBtn: new(Clickable),
RecordBtn: NewBoolClickable(model.IsRecording()), RecordBtn: new(Clickable),
FollowBtn: NewBoolClickable(model.Follow()), FollowBtn: new(Clickable),
PlayingBtn: NewBoolClickable(model.Playing()), PlayingBtn: new(Clickable),
RewindBtn: NewActionClickable(model.PlaySongStart()), RewindBtn: new(Clickable),
} }
ret.rewindHint = makeHint("Rewind", "\n(%s)", "PlaySongStartUnfollow") ret.rewindHint = makeHint("Rewind", "\n(%s)", "PlaySongStartUnfollow")
ret.playHint = makeHint("Play", " (%s)", "PlayCurrentPosUnfollow") ret.playHint = makeHint("Play", " (%s)", "PlayCurrentPosUnfollow")
@ -392,20 +392,20 @@ func NewPlayBar(model *tracker.Model) *PlayBar {
return ret return ret
} }
func (pb *PlayBar) Layout(gtx C, th *Theme) D { func (pb *PlayBar) Layout(gtx C, tr *Tracker) D {
rewindBtnStyle := ActionIcon(gtx, th, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint) playBtn := ToggleIconBtn(tr.Playing(), tr.Theme, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint)
playBtnStyle := ToggleIcon(gtx, th, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint) rewindBtn := ActionIconBtn(tr.PlaySongStart(), tr.Theme, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint)
recordBtnStyle := ToggleIcon(gtx, th, pb.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, pb.recordHint, pb.stopRecordHint) recordBtn := ToggleIconBtn(tr.IsRecording(), tr.Theme, pb.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, pb.recordHint, pb.stopRecordHint)
noteTrackBtnStyle := ToggleIcon(gtx, th, pb.FollowBtn, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, pb.followOffHint, pb.followOnHint) followBtn := ToggleIconBtn(tr.Follow(), tr.Theme, pb.FollowBtn, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, pb.followOffHint, pb.followOnHint)
loopBtnStyle := ToggleIcon(gtx, th, pb.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, pb.loopOffHint, pb.loopOnHint) 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 Surface{Gray: 37}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, playBtnStyle.Layout), layout.Flexed(1, playBtn.Layout),
layout.Rigid(rewindBtnStyle.Layout), layout.Rigid(rewindBtn.Layout),
layout.Rigid(recordBtnStyle.Layout), layout.Rigid(recordBtn.Layout),
layout.Rigid(noteTrackBtnStyle.Layout), layout.Rigid(followBtn.Layout),
layout.Rigid(loopBtnStyle.Layout), layout.Rigid(loopBtn.Layout),
) )
}) })
} }

View File

@ -19,6 +19,12 @@ type Theme struct {
Disabled ButtonStyle Disabled ButtonStyle
Menu ButtonStyle Menu ButtonStyle
} }
IconButton struct {
Enabled IconButtonStyle
Disabled IconButtonStyle
Emphasis IconButtonStyle
Error IconButtonStyle
}
Oscilloscope OscilloscopeStyle Oscilloscope OscilloscopeStyle
NumericUpDown NumericUpDownStyle NumericUpDown NumericUpDownStyle
DialogTitle LabelStyle DialogTitle LabelStyle

View File

@ -54,6 +54,24 @@ button:
cornerradius: 0 cornerradius: 0
height: *buttonheight height: *buttonheight
inset: *buttoninset 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: oscilloscope:
curvecolors: [*primarycolor, *secondarycolor] curvecolors: [*primarycolor, *secondarycolor]
limitcolor: { r: 255, g: 255, b: 255, a: 8 } limitcolor: { r: 255, g: 255, b: 255, a: 8 }

View File

@ -210,7 +210,7 @@ func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
paint.Fill(gtx.Ops, t.Theme.Material.Bg) paint.Fill(gtx.Ops, t.Theme.Material.Bg)
event.Op(gtx.Ops, t) // area for capturing scroll events event.Op(gtx.Ops, t) // area for capturing scroll events
if t.InstrumentEditor.enlargeBtn.Bool.Value() { if t.InstrEnlarged().Value() {
t.layoutTop(gtx) t.layoutTop(gtx)
} else { } else {
t.VerticalSplit.Layout(gtx, t.VerticalSplit.Layout(gtx,

View File

@ -30,10 +30,10 @@ type UnitEditor struct {
sliderList *DragList sliderList *DragList
searchList *DragList searchList *DragList
Parameters []*ParameterWidget Parameters []*ParameterWidget
DeleteUnitBtn *ActionClickable DeleteUnitBtn *Clickable
CopyUnitBtn *TipClickable CopyUnitBtn *Clickable
ClearUnitBtn *ActionClickable ClearUnitBtn *Clickable
DisableUnitBtn *BoolClickable DisableUnitBtn *Clickable
SelectTypeBtn *Clickable SelectTypeBtn *Clickable
commentEditor *Editor commentEditor *Editor
caser cases.Caser caser cases.Caser
@ -45,10 +45,10 @@ type UnitEditor struct {
func NewUnitEditor(m *tracker.Model) *UnitEditor { func NewUnitEditor(m *tracker.Model) *UnitEditor {
ret := &UnitEditor{ ret := &UnitEditor{
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()), DeleteUnitBtn: new(Clickable),
ClearUnitBtn: NewActionClickable(m.ClearUnit()), ClearUnitBtn: new(Clickable),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled()), DisableUnitBtn: new(Clickable),
CopyUnitBtn: new(TipClickable), CopyUnitBtn: new(Clickable),
SelectTypeBtn: new(Clickable), SelectTypeBtn: new(Clickable),
commentEditor: NewEditor(true, true, text.Start), commentEditor: NewEditor(true, true, text.Start),
sliderList: NewDragList(m.Params().List(), layout.Vertical), 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 { 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 { if contents, ok := t.Units().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))})
t.Alerts().Add("Unit copied to clipboard", tracker.Info) 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() text := t.Units().SelectedType()
if text == "" { if text == "" {
text = "Choose unit type" text = "Choose unit type"
@ -143,15 +140,19 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
text = pe.caser.String(text) text = pe.caser.String(text)
} }
hintText := Label(t.Theme, &t.Theme.UnitEditor.Hint, 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, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(deleteUnitBtnStyle.Layout), layout.Rigid(deleteUnitBtn.Layout),
layout.Rigid(copyUnitBtnStyle.Layout), layout.Rigid(copyUnitBtn.Layout),
layout.Rigid(disableUnitBtnStyle.Layout), layout.Rigid(disableUnitBtn.Layout),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
var dims D var dims D
if t.Units().SelectedType() != "" { if t.Units().SelectedType() != "" {
clearUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit") clearUnitBtn := ActionIconBtn(t.ClearUnit(), t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
dims = clearUnitBtnStyle.Layout(gtx) dims = clearUnitBtn.Layout(gtx)
} }
return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)} return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)}
}), }),