diff --git a/tracker/instruments.go b/tracker/instruments.go index 53197e0..2b44012 100644 --- a/tracker/instruments.go +++ b/tracker/instruments.go @@ -33,11 +33,6 @@ func (t *Tracker) layoutInstruments(gtx C) D { pointer.InputOp{Tag: &instrumentPointerTag, Types: pointer.Press, }.Add(gtx.Ops) - if t.CurrentInstrument > 7 { - t.InstrumentDragList.List.Position.First = t.CurrentInstrument - 7 - } else { - t.InstrumentDragList.List.Position.First = 0 - } for t.NewInstrumentBtn.Clicked() { t.AddInstrument() } @@ -53,7 +48,14 @@ func (t *Tracker) layoutInstruments(gtx C) D { layout.Rigid(func(gtx C) D { return layout.Flex{}.Layout( gtx, - layout.Flexed(1, t.layoutInstrumentNames), + layout.Flexed(1, func(gtx C) D { + return layout.Stack{}.Layout(gtx, + layout.Stacked(t.layoutInstrumentNames), + layout.Expanded(func(gtx C) D { + return t.InstrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.song.Patch.Instruments), &t.InstrumentDragList.List.Position) + }), + ) + }), layout.Rigid(func(gtx C) D { return layout.E.Layout(gtx, btnStyle.Layout) }), @@ -180,7 +182,6 @@ func (t *Tracker) layoutInstrumentEditor(gtx C) D { } addUnitBtnStyle := material.IconButton(t.Theme, t.AddUnitBtn, widgetForIcon(icons.ContentAdd)) addUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(4)) - margin := layout.UniformInset(unit.Dp(2)) for len(t.StackUse) < len(t.song.Patch.Instruments[t.CurrentInstrument].Units) { t.StackUse = append(t.StackUse, 0) @@ -212,10 +213,12 @@ func (t *Tracker) layoutInstrumentEditor(gtx C) D { } } stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12)} - + rightMargin := layout.Inset{Right: unit.Dp(10)} return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, layout.Flexed(1, unitNameLabel.Layout), - layout.Rigid(stackLabel.Layout), + layout.Rigid(func(gtx C) D { + return rightMargin.Layout(gtx, stackLabel.Layout) + }), ) } @@ -239,7 +242,11 @@ func (t *Tracker) layoutInstrumentEditor(gtx C) D { } return dims }), + layout.Expanded(func(gtx C) D { + return t.UnitScrollBar.Layout(gtx, unit.Dp(10), len(t.song.Patch.Instruments[t.CurrentInstrument].Units), &t.UnitDragList.List.Position) + }), layout.Stacked(func(gtx C) D { + margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)} return margin.Layout(gtx, addUnitBtnStyle.Layout) })) }), diff --git a/tracker/scrollbar.go b/tracker/scrollbar.go new file mode 100644 index 0000000..4627f69 --- /dev/null +++ b/tracker/scrollbar.go @@ -0,0 +1,128 @@ +package tracker + +import ( + "image" + + "gioui.org/f32" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" +) + +type ScrollBar struct { + Axis layout.Axis + dragStart float32 + hovering bool + dragging bool +} + +func (s *ScrollBar) Layout(gtx C, width unit.Value, numItems int, pos *layout.Position) D { + defer op.Save(gtx.Ops).Load() + clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops) + gradientSize := gtx.Px(unit.Dp(4)) + var totalPixelsEstimate, scrollBarRelLength float32 + switch s.Axis { + case layout.Vertical: + if pos.First > 0 || pos.Offset > 0 { + paint.LinearGradientOp{Color1: black, Color2: transparent, Stop2: f32.Pt(0, float32(gradientSize))}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + } + if pos.BeforeEnd { + paint.LinearGradientOp{Color1: black, Color2: transparent, Stop1: f32.Pt(0, float32(gtx.Constraints.Min.Y)), Stop2: f32.Pt(0, float32(gtx.Constraints.Min.Y-gradientSize))}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + } + totalPixelsEstimate = float32(gtx.Constraints.Min.Y+pos.Offset-pos.OffsetLast) * float32(numItems) / float32(pos.Count) + scrollBarRelLength = float32(gtx.Constraints.Min.Y) / float32(totalPixelsEstimate) + + case layout.Horizontal: + if pos.First > 0 || pos.Offset > 0 { + paint.LinearGradientOp{Color1: black, Color2: transparent, Stop2: f32.Pt(float32(gradientSize), 0)}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + } + if pos.BeforeEnd { + paint.LinearGradientOp{Color1: black, Color2: transparent, Stop1: f32.Pt(float32(gtx.Constraints.Min.X), 0), Stop2: f32.Pt(float32(gtx.Constraints.Min.X-gradientSize), 0)}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + } + totalPixelsEstimate = float32(gtx.Constraints.Min.X+pos.Offset-pos.OffsetLast) * float32(numItems) / float32(pos.Count) + scrollBarRelLength = float32(gtx.Constraints.Min.X) / float32(totalPixelsEstimate) + } + + scrollBarRelStart := (float32(pos.First)*totalPixelsEstimate/float32(numItems) + float32(pos.Offset)) / totalPixelsEstimate + scrWidth := gtx.Px(width) + + stack := op.Save(gtx.Ops) + switch s.Axis { + case layout.Vertical: + if scrollBarRelLength < 1 && (s.dragging || s.hovering) { + y1 := int(scrollBarRelStart * float32(gtx.Constraints.Min.Y)) + y2 := int((scrollBarRelStart + scrollBarRelLength) * float32(gtx.Constraints.Min.Y)) + paint.FillShape(gtx.Ops, scrollBarColor, clip.Rect{Min: image.Pt(gtx.Constraints.Min.X-scrWidth, y1), Max: image.Pt(gtx.Constraints.Min.X, y2)}.Op()) + } + rect := image.Rect(gtx.Constraints.Min.X-scrWidth, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) + pointer.Rect(rect).Add(gtx.Ops) + case layout.Horizontal: + if scrollBarRelLength < 1 && (s.dragging || s.hovering) { + x1 := int(scrollBarRelStart * float32(gtx.Constraints.Min.X)) + x2 := int((scrollBarRelStart + scrollBarRelLength) * float32(gtx.Constraints.Min.X)) + paint.FillShape(gtx.Ops, scrollBarColor, clip.Rect{Min: image.Pt(x1, gtx.Constraints.Min.Y-scrWidth), Max: image.Pt(x2, gtx.Constraints.Min.Y)}.Op()) + } + rect := image.Rect(0, gtx.Constraints.Min.Y-scrWidth, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) + pointer.Rect(rect).Add(gtx.Ops) + } + pointer.InputOp{Tag: &s.dragStart, + Types: pointer.Drag | pointer.Press | pointer.Cancel | pointer.Release, + }.Add(gtx.Ops) + stack.Load() + + for _, ev := range gtx.Events(&s.dragStart) { + e, ok := ev.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Press: + if s.Axis == layout.Horizontal { + s.dragStart = e.Position.X + s.dragging = true + } else { + s.dragStart = e.Position.Y + s.dragging = true + } + case pointer.Drag: + if s.Axis == layout.Horizontal { + pos.Offset += int(e.Position.X - s.dragStart + 0.5) + s.dragStart = e.Position.X + } else { + pos.Offset += int(e.Position.Y - s.dragStart + 0.5) + s.dragStart = e.Position.Y + } + case pointer.Release, pointer.Cancel: + s.dragging = false + } + } + + pointer.PassOp{Pass: true}.Add(gtx.Ops) + rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) + pointer.Rect(rect).Add(gtx.Ops) + pointer.InputOp{Tag: s, + Types: pointer.Enter | pointer.Leave, + }.Add(gtx.Ops) + + for _, ev := range gtx.Events(s) { + e, ok := ev.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Enter: + s.hovering = true + case pointer.Leave: + s.hovering = false + } + } + + return D{Size: gtx.Constraints.Min} +} diff --git a/tracker/theme.go b/tracker/theme.go index 84ad9d9..795a1bc 100644 --- a/tracker/theme.go +++ b/tracker/theme.go @@ -69,3 +69,5 @@ var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16} var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255} var menuHoverColor = color.NRGBA{R: 30, G: 31, B: 38, A: 255} + +var scrollBarColor = color.NRGBA{R: 255, G: 255, B: 255, A: 32} diff --git a/tracker/tracker.go b/tracker/tracker.go index 2ded5ed..fdddb1b 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -63,14 +63,18 @@ type Tracker struct { CopyInstrumentBtn *widget.Clickable ParameterSliders []*widget.Float ParameterList *layout.List + ParameterScrollBar *ScrollBar UnitDragList *DragList + UnitScrollBar *ScrollBar DeleteUnitBtn *widget.Clickable ClearUnitBtn *widget.Clickable ChooseUnitTypeList *layout.List + ChooseUnitScrollBar *ScrollBar ChooseUnitTypeBtns []*widget.Clickable AddUnitBtn *widget.Clickable ParameterLabelBtns []*widget.Clickable InstrumentDragList *DragList + InstrumentScrollBar *ScrollBar TrackHexCheckBox *widget.Bool TrackShowHex []bool VuMeter VuMeter @@ -702,15 +706,19 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr Menus: make([]Menu, 2), MenuBar: make([]widget.Clickable, 2), UnitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}}, + UnitScrollBar: &ScrollBar{Axis: layout.Vertical}, refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already undoStack: []sointu.Song{}, redoStack: []sointu.Song{}, InstrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}}, + InstrumentScrollBar: &ScrollBar{Axis: layout.Horizontal}, ParameterList: &layout.List{Axis: layout.Vertical}, + ParameterScrollBar: &ScrollBar{Axis: layout.Vertical}, TopHorizontalSplit: new(Split), BottomHorizontalSplit: new(Split), VerticalSplit: new(Split), ChooseUnitTypeList: &layout.List{Axis: layout.Vertical}, + ChooseUnitScrollBar: &ScrollBar{Axis: layout.Vertical}, KeyPlaying: make(map[string]func()), } t.UnitDragList.HoverItem = -1 diff --git a/tracker/uniteditor.go b/tracker/uniteditor.go index dd31198..5d63447 100644 --- a/tracker/uniteditor.go +++ b/tracker/uniteditor.go @@ -150,7 +150,14 @@ func (t *Tracker) layoutUnitSliders(gtx C) D { if u.Type == "oscillator" && u.Parameters["type"] == sointu.Sample { l++ } - return t.ParameterList.Layout(gtx, l, listElements) + return layout.Stack{}.Layout(gtx, + layout.Stacked(func(gtx C) D { + return t.ParameterList.Layout(gtx, l, listElements) + }), + layout.Stacked(func(gtx C) D { + gtx.Constraints.Min = gtx.Constraints.Max + return t.ParameterScrollBar.Layout(gtx, unit.Dp(10), l, &t.ParameterList.Position) + })) } func (t *Tracker) layoutUnitFooter() layout.Widget { @@ -173,17 +180,19 @@ func (t *Tracker) layoutUnitFooter() layout.Widget { } if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type == "" { return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(deleteUnitBtnStyle.Layout), layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), - layout.Rigid(deleteUnitBtnStyle.Layout)) + ) } clearUnitBtnStyle := material.IconButton(t.Theme, t.ClearUnitBtn, widgetForIcon(icons.ContentClear)) clearUnitBtnStyle.Color = primaryColor clearUnitBtnStyle.Background = transparent clearUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6)) return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), + layout.Rigid(deleteUnitBtnStyle.Layout), layout.Rigid(clearUnitBtnStyle.Layout), - layout.Rigid(deleteUnitBtnStyle.Layout)) + layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), + ) } } @@ -213,7 +222,13 @@ func (t *Tracker) layoutUnitTypeChooser(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(hintText), layout.Flexed(1, func(gtx C) D { - return t.ChooseUnitTypeList.Layout(gtx, len(allUnits), listElem) + return layout.Stack{}.Layout(gtx, + layout.Stacked(func(gtx C) D { + return t.ChooseUnitTypeList.Layout(gtx, len(allUnits), listElem) + }), + layout.Expanded(func(gtx C) D { + return t.ChooseUnitScrollBar.Layout(gtx, unit.Dp(10), len(allUnits), &t.ChooseUnitTypeList.Position) + })) })) }) }