mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data. This rewrite closes #72, #106 and #120.
336 lines
8.8 KiB
Go
336 lines
8.8 KiB
Go
package gioui
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
|
|
"gioui.org/io/clipboard"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"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
|
|
mainTag 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()
|
|
keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓|Shift-↑|Shift-↓|⇞|⇟|Ctrl-⇞|Ctrl-⇟|Ctrl-A|Ctrl-C|Ctrl-X|Ctrl-V|⌦|Ctrl-⌫")
|
|
if s.dragList.List.Axis == layout.Horizontal {
|
|
keys = key.Set("←|→|Ctrl-←|Ctrl-→|Shift-←|Shift-→|Home|End|Ctrl-Home|Ctrl-End|Ctrl-A|Ctrl-C|Ctrl-X|Ctrl-V|⌦|Ctrl-⌫")
|
|
}
|
|
key.InputOp{Tag: &s.dragList.mainTag, Keys: keys}.Add(gtx.Ops)
|
|
|
|
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
|
|
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
|
|
}
|
|
|
|
for _, ke := range gtx.Events(&s.dragList.mainTag) {
|
|
switch ke := ke.(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 clipboard.Event:
|
|
s.dragList.TrackerList.PasteElements([]byte(ke.Text))
|
|
}
|
|
op.InvalidateOp{}.Add(gtx.Ops)
|
|
}
|
|
|
|
_, 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 := range gtx.Events(&s.dragList.tags[index]) {
|
|
e, ok := ev.(pointer.Event)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch e.Type {
|
|
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)
|
|
}
|
|
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
|
|
}
|
|
}
|
|
rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
|
|
area := clip.Rect(rect).Push(gtx.Ops)
|
|
pointer.InputOp{Tag: &s.dragList.tags[index],
|
|
Types: pointer.Press | pointer.Enter | pointer.Leave,
|
|
}.Add(gtx.Ops)
|
|
area.Pop()
|
|
if index == s.dragList.TrackerList.Selected() && isMutable {
|
|
for _, ev := range gtx.Events(&s.dragList.focused) {
|
|
e, ok := ev.(pointer.Event)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch e.Type {
|
|
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:
|
|
fallthrough
|
|
case pointer.Cancel:
|
|
s.dragList.drag = false
|
|
}
|
|
}
|
|
area := clip.Rect(rect).Push(gtx.Ops)
|
|
pointer.InputOp{Tag: &s.dragList.focused,
|
|
Types: pointer.Drag | pointer.Press | pointer.Release,
|
|
Grab: s.dragList.drag,
|
|
}.Add(gtx.Ops)
|
|
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) {
|
|
op.InvalidateOp{}.Add(gtx.Ops)
|
|
}
|
|
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":
|
|
clipboard.ReadOp{Tag: &e.mainTag}.Add(gtx.Ops)
|
|
return
|
|
case "C", "X":
|
|
data, ok := e.TrackerList.CopyElements()
|
|
if ok && (k.Name == "C" || e.TrackerList.DeleteElements(false)) {
|
|
clipboard.WriteOp{Text: string(data)}.Add(gtx.Ops)
|
|
}
|
|
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)
|
|
}
|
|
|
|
func intMax(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func intMin(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|