mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
360 lines
9.7 KiB
Go
360 lines
9.7 KiB
Go
package gioui
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"io"
|
|
|
|
"gioui.org/io/clipboard"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/transfer"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget/material"
|
|
"github.com/vsariola/sointu/tracker"
|
|
)
|
|
|
|
type DragList struct {
|
|
TrackerList tracker.List
|
|
HoverItem int
|
|
List *layout.List
|
|
ScrollBar *ScrollBar
|
|
drag bool
|
|
dragID pointer.ID
|
|
tags []bool
|
|
swapped bool
|
|
focused bool
|
|
requestFocus bool
|
|
}
|
|
|
|
type FilledDragListStyle struct {
|
|
dragList *DragList
|
|
HoverColor color.NRGBA
|
|
SelectedColor color.NRGBA
|
|
CursorColor color.NRGBA
|
|
ScrollBarWidth unit.Dp
|
|
element, bg func(gtx C, i int) D
|
|
}
|
|
|
|
func NewDragList(model tracker.List, axis layout.Axis) *DragList {
|
|
return &DragList{TrackerList: model, List: &layout.List{Axis: axis}, HoverItem: -1, ScrollBar: &ScrollBar{Axis: axis}}
|
|
}
|
|
|
|
func FilledDragList(th *material.Theme, dragList *DragList, element, bg func(gtx C, i int) D) FilledDragListStyle {
|
|
return FilledDragListStyle{
|
|
dragList: dragList,
|
|
element: element,
|
|
bg: bg,
|
|
HoverColor: dragListHoverColor,
|
|
SelectedColor: dragListSelectedColor,
|
|
CursorColor: cursorColor,
|
|
ScrollBarWidth: unit.Dp(10),
|
|
}
|
|
}
|
|
|
|
func (d *DragList) Focus() {
|
|
d.requestFocus = true
|
|
}
|
|
|
|
func (d *DragList) Focused() bool {
|
|
return d.focused
|
|
}
|
|
|
|
func (s FilledDragListStyle) LayoutScrollBar(gtx C) D {
|
|
return s.dragList.ScrollBar.Layout(gtx, s.ScrollBarWidth, s.dragList.TrackerList.Count(), &s.dragList.List.Position)
|
|
}
|
|
|
|
func (s FilledDragListStyle) Layout(gtx C) D {
|
|
swap := 0
|
|
|
|
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
|
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
|
event.Op(gtx.Ops, s.dragList)
|
|
|
|
if s.dragList.List.Axis == layout.Horizontal {
|
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
|
} else {
|
|
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
|
}
|
|
|
|
if s.dragList.requestFocus {
|
|
s.dragList.requestFocus = false
|
|
gtx.Execute(key.FocusCmd{Tag: s.dragList})
|
|
}
|
|
|
|
prevKey := key.NameUpArrow
|
|
nextKey := key.NameDownArrow
|
|
firstKey := key.NamePageUp
|
|
lastKey := key.NamePageDown
|
|
if s.dragList.List.Axis == layout.Horizontal {
|
|
prevKey = key.NameLeftArrow
|
|
nextKey = key.NameRightArrow
|
|
firstKey = key.NameHome
|
|
lastKey = key.NameEnd
|
|
}
|
|
|
|
for {
|
|
event, ok := gtx.Event(
|
|
key.FocusFilter{Target: s.dragList},
|
|
transfer.TargetFilter{Target: s.dragList, Type: "application/text"},
|
|
key.Filter{Focus: s.dragList, Name: prevKey, Optional: key.ModShift | key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: nextKey, Optional: key.ModShift | key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: firstKey, Optional: key.ModShift | key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: lastKey, Optional: key.ModShift | key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: "A", Required: key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: "C", Required: key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: "X", Required: key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: "V", Required: key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: key.NameDeleteBackward, Required: key.ModShortcut},
|
|
key.Filter{Focus: s.dragList, Name: key.NameDeleteForward},
|
|
)
|
|
if !ok {
|
|
break
|
|
}
|
|
switch ke := event.(type) {
|
|
case key.FocusEvent:
|
|
s.dragList.focused = ke.Focus
|
|
if !s.dragList.focused {
|
|
s.dragList.TrackerList.SetSelected2(s.dragList.TrackerList.Selected())
|
|
}
|
|
case key.Event:
|
|
if !s.dragList.focused || ke.State != key.Press {
|
|
break
|
|
}
|
|
s.dragList.command(gtx, ke)
|
|
case transfer.DataEvent:
|
|
if b, err := io.ReadAll(ke.Open()); err == nil {
|
|
s.dragList.TrackerList.PasteElements([]byte(b))
|
|
}
|
|
|
|
}
|
|
gtx.Execute(op.InvalidateCmd{})
|
|
}
|
|
|
|
_, isMutable := s.dragList.TrackerList.ListData.(tracker.MutableListData)
|
|
|
|
listElem := func(gtx C, index int) D {
|
|
for len(s.dragList.tags) <= index {
|
|
s.dragList.tags = append(s.dragList.tags, false)
|
|
}
|
|
cursorBg := func(gtx C) D {
|
|
var color color.NRGBA
|
|
if s.dragList.TrackerList.Selected() == index {
|
|
if s.dragList.focused {
|
|
color = s.CursorColor
|
|
} else {
|
|
color = s.SelectedColor
|
|
}
|
|
} else if between(s.dragList.TrackerList.Selected(), index, s.dragList.TrackerList.Selected2()) {
|
|
color = s.SelectedColor
|
|
} else if s.dragList.HoverItem == index {
|
|
color = s.HoverColor
|
|
}
|
|
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
|
|
|
for {
|
|
ev, ok := gtx.Event(pointer.Filter{
|
|
Target: &s.dragList.tags[index],
|
|
Kinds: pointer.Press | pointer.Enter | pointer.Leave,
|
|
})
|
|
if !ok {
|
|
break
|
|
}
|
|
e, ok := ev.(pointer.Event)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch e.Kind {
|
|
case pointer.Enter:
|
|
s.dragList.HoverItem = index
|
|
case pointer.Leave:
|
|
if s.dragList.HoverItem == index {
|
|
s.dragList.HoverItem = -1
|
|
}
|
|
case pointer.Press:
|
|
if s.dragList.drag {
|
|
break
|
|
}
|
|
s.dragList.TrackerList.SetSelected(index)
|
|
if !e.Modifiers.Contain(key.ModShift) {
|
|
s.dragList.TrackerList.SetSelected2(index)
|
|
}
|
|
gtx.Execute(key.FocusCmd{Tag: s.dragList})
|
|
}
|
|
}
|
|
rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
|
|
area := clip.Rect(rect).Push(gtx.Ops)
|
|
event.Op(gtx.Ops, &s.dragList.tags[index])
|
|
area.Pop()
|
|
if index == s.dragList.TrackerList.Selected() && isMutable {
|
|
for {
|
|
target := &s.dragList.focused
|
|
if s.dragList.drag {
|
|
target = nil
|
|
}
|
|
ev, ok := gtx.Event(pointer.Filter{Target: target, Kinds: pointer.Drag | pointer.Press | pointer.Release | pointer.Cancel})
|
|
if !ok {
|
|
break
|
|
}
|
|
e, ok := ev.(pointer.Event)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch e.Kind {
|
|
case pointer.Press:
|
|
s.dragList.dragID = e.PointerID
|
|
s.dragList.drag = true
|
|
case pointer.Drag:
|
|
if s.dragList.dragID != e.PointerID {
|
|
break
|
|
}
|
|
if s.dragList.List.Axis == layout.Horizontal {
|
|
if e.Position.X < 0 {
|
|
swap = -1
|
|
}
|
|
if e.Position.X > float32(gtx.Constraints.Min.X) {
|
|
swap = 1
|
|
}
|
|
} else {
|
|
if e.Position.Y < 0 {
|
|
swap = -1
|
|
}
|
|
if e.Position.Y > float32(gtx.Constraints.Min.Y) {
|
|
swap = 1
|
|
}
|
|
}
|
|
case pointer.Release, pointer.Cancel:
|
|
s.dragList.drag = false
|
|
}
|
|
}
|
|
area := clip.Rect(rect).Push(gtx.Ops)
|
|
event.Op(gtx.Ops, &s.dragList.focused)
|
|
pointer.CursorGrab.Add(gtx.Ops)
|
|
area.Pop()
|
|
}
|
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
|
}
|
|
macro := op.Record(gtx.Ops)
|
|
dims := s.element(gtx, index)
|
|
call := macro.Stop()
|
|
gtx.Constraints.Min = dims.Size
|
|
if s.bg != nil {
|
|
s.bg(gtx, index)
|
|
}
|
|
cursorBg(gtx)
|
|
call.Add(gtx.Ops)
|
|
if s.dragList.List.Axis == layout.Horizontal {
|
|
dims.Size.Y = gtx.Constraints.Max.Y
|
|
} else {
|
|
dims.Size.X = gtx.Constraints.Max.X
|
|
}
|
|
return dims
|
|
}
|
|
count := s.dragList.TrackerList.Count()
|
|
if count < 1 {
|
|
count = 1 // draw at least one empty element to get the correct size
|
|
}
|
|
dims := s.dragList.List.Layout(gtx, count, listElem)
|
|
if !s.dragList.swapped && swap != 0 {
|
|
if s.dragList.TrackerList.MoveElements(swap) {
|
|
gtx.Execute(op.InvalidateCmd{})
|
|
}
|
|
s.dragList.swapped = true
|
|
} else {
|
|
s.dragList.swapped = false
|
|
}
|
|
return dims
|
|
}
|
|
|
|
func (e *DragList) command(gtx layout.Context, k key.Event) {
|
|
if k.Modifiers.Contain(key.ModShortcut) {
|
|
switch k.Name {
|
|
case "V":
|
|
gtx.Execute(clipboard.ReadCmd{Tag: e})
|
|
return
|
|
case "C", "X":
|
|
data, ok := e.TrackerList.CopyElements()
|
|
if ok && (k.Name == "C" || e.TrackerList.DeleteElements(false)) {
|
|
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(data))})
|
|
}
|
|
return
|
|
case "A":
|
|
e.TrackerList.SetSelected(0)
|
|
e.TrackerList.SetSelected2(e.TrackerList.Count() - 1)
|
|
return
|
|
}
|
|
}
|
|
delta := 0
|
|
switch k.Name {
|
|
case key.NameDeleteBackward:
|
|
if k.Modifiers.Contain(key.ModShortcut) {
|
|
e.TrackerList.DeleteElements(true)
|
|
}
|
|
return
|
|
case key.NameDeleteForward:
|
|
e.TrackerList.DeleteElements(false)
|
|
return
|
|
case key.NameLeftArrow:
|
|
delta = -1
|
|
case key.NameRightArrow:
|
|
delta = 1
|
|
case key.NameHome:
|
|
delta = -1e6
|
|
case key.NameEnd:
|
|
delta = 1e6
|
|
case key.NameUpArrow:
|
|
delta = -1
|
|
case key.NameDownArrow:
|
|
delta = 1
|
|
case key.NamePageUp:
|
|
delta = -1e6
|
|
case key.NamePageDown:
|
|
delta = 1e6
|
|
}
|
|
if k.Modifiers.Contain(key.ModShortcut) {
|
|
e.TrackerList.MoveElements(delta)
|
|
} else {
|
|
e.TrackerList.SetSelected(e.TrackerList.Selected() + delta)
|
|
if !k.Modifiers.Contain(key.ModShift) {
|
|
e.TrackerList.SetSelected2(e.TrackerList.Selected())
|
|
}
|
|
}
|
|
e.EnsureVisible(e.TrackerList.Selected())
|
|
}
|
|
|
|
func (l *DragList) EnsureVisible(item int) {
|
|
first := l.List.Position.First
|
|
last := l.List.Position.First + l.List.Position.Count - 1
|
|
if item < first || (item == first && l.List.Position.Offset > 0) {
|
|
l.List.ScrollTo(item)
|
|
}
|
|
if item > last || (item == last && l.List.Position.OffsetLast < 0) {
|
|
o := -l.List.Position.OffsetLast + l.List.Position.Offset
|
|
l.List.ScrollTo(item - l.List.Position.Count + 1)
|
|
l.List.Position.Offset = o
|
|
}
|
|
}
|
|
|
|
func (l *DragList) CenterOn(item int) {
|
|
lenPerChildPx := l.List.Position.Length / l.TrackerList.Count()
|
|
if lenPerChildPx == 0 {
|
|
return
|
|
}
|
|
listLengthPx := l.List.Position.Count*l.List.Position.Length/l.TrackerList.Count() + l.List.Position.OffsetLast - l.List.Position.Offset
|
|
lenBeforeItem := (listLengthPx - lenPerChildPx) / 2
|
|
quot := lenBeforeItem / lenPerChildPx
|
|
rem := lenBeforeItem % lenPerChildPx
|
|
l.List.ScrollTo(item - quot - 1)
|
|
l.List.Position.Offset = lenPerChildPx - rem
|
|
}
|
|
|
|
func between(a, b, c int) bool {
|
|
return (a <= b && b <= c) || (c <= b && b <= a)
|
|
}
|