This commit is contained in:
qm210 2024-11-22 14:46:54 +00:00 committed by GitHub
commit d7df2707f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 202 additions and 41 deletions

View File

@ -19,6 +19,7 @@ import (
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/vsariola/sointu/tracker"
)
@ -44,6 +45,14 @@ type (
TipArea component.TipArea
Bool tracker.Bool
}
MenuClickable struct {
Clickable Clickable
menu Menu
Selected tracker.OptionalInt
TipArea component.TipArea
Tooltip component.Tooltip
}
)
func NewActionClickable(a tracker.Action) *ActionClickable {
@ -136,7 +145,10 @@ func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) Butt
ret := Button(th, &b.Clickable, text)
ret.Background = transparent
ret.Inset = layout.UniformInset(unit.Dp(6))
if b.Bool.Value() {
if !b.Bool.Enabled() {
ret.Color = disabledTextColor
ret.Background = transparent
} else if b.Bool.Value() {
ret.Color = th.Palette.ContrastFg
ret.Background = th.Palette.Fg
} else {

View File

@ -10,6 +10,9 @@ var iconCache = map[*byte]*widget.Icon{}
// widgetForIcon returns a widget for IconVG data, but caching the results
func widgetForIcon(icon []byte) *widget.Icon {
if icon == nil {
return nil
}
if widget, ok := iconCache[&icon[0]]; ok {
return widget
}

View File

@ -46,5 +46,17 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
}
func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget {
return LabelStyle{Text: str, Color: color, ShadeColor: black, Font: labelDefaultFont, FontSize: labelDefaultFontSize, Alignment: layout.W, Shaper: shaper}.Layout
return SizedLabel(str, color, shaper, labelDefaultFontSize)
}
func SizedLabel(str string, color color.NRGBA, shaper *text.Shaper, fontSize unit.Sp) layout.Widget {
return LabelStyle{
Text: str,
Color: color,
ShadeColor: black,
Font: labelDefaultFont,
FontSize: fontSize,
Alignment: layout.W,
Shaper: shaper,
}.Layout
}

16
tracker/gioui/layout.go Normal file
View File

@ -0,0 +1,16 @@
package gioui
import "gioui.org/layout"
// general helpers for layout that do not belong to any specific widget
func EmptyWidget() layout.Spacer {
return layout.Spacer{}
}
func OnlyIf(condition bool, widget layout.Widget) layout.Widget {
if condition {
return widget
}
return EmptyWidget().Layout
}

View File

@ -12,8 +12,6 @@ import (
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
)
@ -103,12 +101,12 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
}
icon := widgetForIcon(item.IconBytes)
iconColor := m.IconColor
if !item.Doer.Allowed() {
iconColor = mediumEmphasisTextColor
}
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper}
if !item.Doer.Allowed() {
// note: might be a bug in gioui, but for iconColor = mediumEmphasisTextColor
// this does not render the icon at all. other colors seem to work fine.
iconColor = disabledTextColor
textLabel.Color = mediumEmphasisTextColor
}
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper}
@ -116,13 +114,18 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
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.IconSize))
p := gtx.Dp(m.IconSize)
gtx.Constraints.Min = image.Pt(p, p)
if icon == nil {
return D{Size: gtx.Constraints.Min}
}
return icon.Layout(gtx, iconColor)
})
}),
layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }),
layout.Flexed(1, func(gtx C) D {
return D{Size: image.Pt(gtx.Constraints.Max.X, 1)}
}),
layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}),
@ -168,14 +171,14 @@ func PopupMenu(menu *Menu, shaper *text.Shaper) MenuStyle {
}
}
func (tr *Tracker) layoutMenu(gtx C, title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
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(menu, tr.Theme.Shaper)
return func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
titleBtn := material.Button(tr.Theme, clickable, title)
titleBtn := Button(tr.Theme, clickable, title)
titleBtn.Color = white
titleBtn.Background = transparent
titleBtn.CornerRadius = unit.Dp(0)

View File

@ -158,11 +158,8 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
in := layout.UniformInset(unit.Dp(1))
voiceUpDown := func(gtx C) D {
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
return in.Layout(gtx, numStyle.Layout)
}
voiceUpDown := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
voiceUpDown.Padding = unit.Dp(1)
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI")
@ -176,10 +173,10 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
layout.Rigid(effectBtnStyle.Layout),
layout.Rigid(uniqueBtnStyle.Layout),
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
layout.Rigid(voiceUpDown),
layout.Rigid(voiceUpDown.Layout),
layout.Rigid(splitTrackBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(midiInBtnStyle.Layout),
layout.Rigid(OnlyIf(t.HasAnyMidiInput(), midiInBtnStyle.Layout)),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(deleteTrackBtnStyle.Layout),
layout.Rigid(newTrackBtnStyle.Layout))
@ -287,15 +284,15 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
c = cursorColor
}
if hasTrackMidiIn {
c = cursorForTrackMidiInColor
c = trackMidiInCurrentColor
}
te.paintColumnCell(gtx, x, t, c)
te.paintColumnCell(gtx, x, t, c, hasTrackMidiIn)
}
// draw the corresponding "fake cursors" for instrument-track-groups (for polyphony)
if hasTrackMidiIn {
if hasTrackMidiIn && y == cursor.Y {
for _, trackIndex := range t.Model.TracksWithSameInstrumentAsCurrent() {
if x == trackIndex && y == cursor.Y {
te.paintColumnCell(gtx, x, t, cursorNeighborForTrackMidiInColor)
if x == trackIndex {
te.paintColumnCell(gtx, x, t, trackMidiInAdditionalColor, hasTrackMidiIn)
}
}
}
@ -334,10 +331,10 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
return table.Layout(gtx)
}
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA) {
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA, ignoreEffect bool) {
cw := gtx.Constraints.Min.X
cx := 0
if t.Model.Notes().Effect(x) {
if t.Model.Notes().Effect(x) && !ignoreEffect {
cw /= 2
if t.Model.Notes().LowNibble() {
cx += cw

View File

@ -48,6 +48,7 @@ type NumericUpDownStyle struct {
Tooltip component.Tooltip
Width unit.Dp
Height unit.Dp
Padding unit.Dp
shaper text.Shaper
}
@ -74,11 +75,19 @@ func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) Nume
Tooltip: Tooltip(th, tooltip),
Width: unit.Dp(70),
Height: unit.Dp(20),
Padding: unit.Dp(0),
shaper: *th.Shaper,
}
}
func (s *NumericUpDownStyle) Layout(gtx C) D {
if s.Padding <= 0 {
return s.layoutWithTooltip(gtx)
}
return layout.UniformInset(s.Padding).Layout(gtx, s.layoutWithTooltip)
}
func (s *NumericUpDownStyle) layoutWithTooltip(gtx C) D {
if s.Tooltip.Text.Text != "" {
return s.NumberInput.tipArea.Layout(gtx, s.Tooltip, s.actualLayout)
}

View File

@ -7,14 +7,13 @@ import (
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/version"
"golang.org/x/exp/shiny/materialdesign/icons"
)
type SongPanel struct {
MenuBar []widget.Clickable
MenuBar []Clickable
Menus []Menu
BPM *NumberInput
RowsPerPattern *NumberInput
@ -57,7 +56,7 @@ type SongPanel struct {
func NewSongPanel(model *tracker.Model) *SongPanel {
ret := &SongPanel{
MenuBar: make([]widget.Clickable, 3),
MenuBar: make([]Clickable, 3),
Menus: make([]Menu, 3),
BPM: NewNumberInput(model.BPM().Int()),
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),

View File

@ -60,8 +60,10 @@ var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}
var cursorForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 48}
var cursorNeighborForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 24}
var trackMidiInCurrentColor = color.NRGBA{R: 255, G: 100, B: 140, A: 48}
var trackMidiInAdditionalColor = withScaledAlpha(trackMidiInCurrentColor, 0.7)
var trackMidiVelInColor = withScaledAlpha(trackMidiInCurrentColor, 0.3)
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
@ -75,3 +77,13 @@ var dialogBgColor = color.NRGBA{R: 0, G: 0, B: 0, A: 224}
var paramIsSendTargetColor = color.NRGBA{R: 120, G: 120, B: 210, A: 255}
var paramValueInvalidColor = color.NRGBA{R: 120, G: 120, B: 120, A: 190}
func withScaledAlpha(c color.NRGBA, factor float32) color.NRGBA {
A := factor * float32(c.A)
return color.NRGBA{
R: c.R,
G: c.G,
B: c.B,
A: uint8(A),
}
}

View File

@ -101,9 +101,7 @@ func NewTracker(model *tracker.Model) *Tracker {
func (t *Tracker) Main() {
titleFooter := ""
w := new(app.Window)
w.Option(app.Title("Sointu Tracker"))
w.Option(app.Size(unit.Dp(800), unit.Dp(600)))
w := NewWindow()
t.InstrumentEditor.Focus()
recoveryTicker := time.NewTicker(time.Second * 30)
t.Explorer = explorer.NewExplorer(w)
@ -127,9 +125,7 @@ func (t *Tracker) Main() {
}
if !t.Quitted() {
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
w = new(app.Window)
w.Option(app.Title("Sointu Tracker"))
w.Option(app.Size(unit.Dp(800), unit.Dp(600)))
w = NewWindow()
t.Explorer = explorer.NewExplorer(w)
go eventLoop(w, events, acks)
}
@ -165,6 +161,13 @@ func (t *Tracker) Main() {
t.quitWG.Done()
}
func NewWindow() *app.Window {
w := new(app.Window)
w.Option(app.Title("Sointu Tracker"))
w.Option(app.Size(unit.Dp(800), unit.Dp(600)))
return w
}
func eventLoop(w *app.Window, events chan<- event.Event, acks <-chan struct{}) {
// Iterate window events, sending each to the old event loop and waiting for
// a signal that processing is complete before iterating again.
@ -344,3 +347,10 @@ func (t *Tracker) removeFromMidiNotePlaying(note byte) {
}
}
}
func (t *Tracker) HasAnyMidiInput() bool {
for _ = range t.Model.MIDI.InputDevices {
return true
}
return false
}

View File

@ -32,7 +32,7 @@ type UnitEditor struct {
CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
SelectTypeBtn *Clickable
commentEditor *Editor
caser cases.Caser
@ -47,7 +47,7 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
SelectTypeBtn: new(Clickable),
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
@ -236,9 +236,9 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) {
type ParameterWidget struct {
floatWidget widget.Float
boolWidget widget.Bool
instrBtn widget.Clickable
instrBtn Clickable
instrMenu Menu
unitBtn widget.Clickable
unitBtn Clickable
unitMenu Menu
Parameter tracker.Parameter
tipArea component.TipArea
@ -332,7 +332,6 @@ func (p ParameterStyle) Layout(gtx C) D {
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
instrItems := make([]MenuItem, 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

42
tracker/optional_int.go Normal file
View File

@ -0,0 +1,42 @@
package tracker
type (
// OptionalInt tries to follow the same convention as e.g. Int{...} or Bool{...}
// Do not confuse with types.OptionalInteger, which you might use as a model,
// but don't necessarily have to.
OptionalInt struct {
optionalIntData
}
optionalIntData interface {
Unpack() (int, bool)
Value() int
Range() intRange
setValue(int)
unsetValue()
change(kind string) func()
}
TrackMidiVelIn Model
)
func (v OptionalInt) Set(value int, present bool) (ok bool) {
if !present {
v.unsetValue()
return true
}
// TODO: can we deduplicate this by referencing Int{...}.Set(value) ?
r := v.Range()
if v.Equals(value, present) || value < r.Min || value > r.Max {
return false
}
defer v.change("Set")()
v.setValue(value)
return true
}
func (v OptionalInt) Equals(value int, present bool) bool {
oldValue, oldPresent := v.Unpack()
return value == oldValue && present == oldPresent
}

47
tracker/types/optional.go Normal file
View File

@ -0,0 +1,47 @@
package types
type (
// OptionalInteger is the simple struct, not to be confused with tracker.OptionalInt.
// It implements the tracker.optionalIntData interface, without needing to know so.
OptionalInteger struct {
value int
exists bool
}
)
func NewOptionalInteger(value int, exists bool) OptionalInteger {
return OptionalInteger{value, exists}
}
func NewOptionalIntegerOf(value int) OptionalInteger {
return OptionalInteger{
value: value,
exists: true,
}
}
func NewEmptyOptionalInteger() OptionalInteger {
// could also just use OptionalInteger{}
return OptionalInteger{
exists: false,
}
}
func (i OptionalInteger) Unpack() (int, bool) {
return i.value, i.exists
}
func (i OptionalInteger) Value() int {
if !i.exists {
panic("Access value of empty OptionalInteger")
}
return i.value
}
func (i OptionalInteger) Empty() bool {
return !i.exists
}
func (i OptionalInteger) Equals(value int) bool {
return i.exists && i.value == value
}