feat!: rewrote the GUI and model for better testability

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.
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2023-10-24 13:35:43 +03:00
parent 6d3c65e11d
commit d92426a100
53 changed files with 5992 additions and 4507 deletions

View File

@ -4,48 +4,54 @@ 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 {
SelectedItem int
SelectedItem2 int
HoverItem int
List *layout.List
drag bool
dragID pointer.ID
tags []bool
swapped bool
focused bool
requestFocus bool
mainTag bool
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
Count int
element func(gtx C, i int) D
swap func(i, j int)
dragList *DragList
HoverColor color.NRGBA
SelectedColor color.NRGBA
CursorColor color.NRGBA
ScrollBarWidth unit.Dp
element, bg func(gtx C, i int) D
}
func FilledDragList(th *material.Theme, dragList *DragList, count int, element func(gtx C, i int) D, swap func(i, j int)) FilledDragListStyle {
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,
swap: swap,
Count: count,
HoverColor: dragListHoverColor,
SelectedColor: dragListSelectedColor,
CursorColor: cursorColor,
dragList: dragList,
element: element,
bg: bg,
HoverColor: dragListHoverColor,
SelectedColor: dragListSelectedColor,
CursorColor: cursorColor,
ScrollBarWidth: unit.Dp(10),
}
}
@ -57,14 +63,18 @@ func (d *DragList) Focused() bool {
return d.focused
}
func (s *FilledDragListStyle) Layout(gtx C) D {
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-↓")
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-→")
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)
@ -79,65 +89,45 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
}
if !s.dragList.focused {
s.dragList.SelectedItem2 = s.dragList.SelectedItem
}
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
}
delta := 0
switch {
case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameLeftArrow && s.dragList.SelectedItem > 0:
delta = -1
case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameRightArrow && s.dragList.SelectedItem < s.Count-1:
delta = 1
case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameUpArrow && s.dragList.SelectedItem > 0:
delta = -1
case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameDownArrow && s.dragList.SelectedItem < s.Count-1:
delta = 1
}
if delta != 0 {
if ke.Modifiers.Contain(key.ModShortcut) {
swap = delta
} else {
s.dragList.SelectedItem += delta
if !ke.Modifiers.Contain(key.ModShift) {
s.dragList.SelectedItem2 = s.dragList.SelectedItem
}
}
}
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)
}
bg := func(gtx C) D {
cursorBg := func(gtx C) D {
var color color.NRGBA
if s.dragList.SelectedItem == index {
if s.dragList.TrackerList.Selected() == index {
if s.dragList.focused {
color = s.CursorColor
} else {
color = s.SelectedColor
}
} else if between(s.dragList.SelectedItem, index, s.dragList.SelectedItem2) {
} 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())
return D{Size: gtx.Constraints.Min}
}
inputFg := func(gtx C) D {
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
for _, ev := range gtx.Events(&s.dragList.tags[index]) {
e, ok := ev.(pointer.Event)
if !ok {
@ -154,9 +144,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
if s.dragList.drag {
break
}
s.dragList.SelectedItem = index
s.dragList.TrackerList.SetSelected(index)
if !e.Modifiers.Contain(key.ModShift) {
s.dragList.SelectedItem2 = index
s.dragList.TrackerList.SetSelected2(index)
}
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
}
@ -167,7 +157,7 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
Types: pointer.Press | pointer.Enter | pointer.Leave,
}.Add(gtx.Ops)
area.Pop()
if index == s.dragList.SelectedItem {
if index == s.dragList.TrackerList.Selected() && isMutable {
for _, ev := range gtx.Events(&s.dragList.focused) {
e, ok := ev.(pointer.Event)
if !ok {
@ -212,29 +202,31 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}
return layout.Stack{Alignment: layout.W}.Layout(gtx,
layout.Expanded(bg),
layout.Expanded(inputFg),
layout.Stacked(func(gtx C) D {
return s.element(gtx, index)
}),
)
}
dims := s.dragList.List.Layout(gtx, s.Count, listElem)
a := intMin(s.dragList.SelectedItem, s.dragList.SelectedItem2)
b := intMax(s.dragList.SelectedItem, s.dragList.SelectedItem2)
if !s.dragList.swapped && swap != 0 && a+swap >= 0 && b+swap < s.Count {
if swap < 0 {
for i := a; i <= b; i++ {
s.swap(i, i+swap)
}
} else {
for i := b; i >= a; i-- {
s.swap(i, i+swap)
}
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.SelectedItem += swap
s.dragList.SelectedItem2 += swap
s.dragList.swapped = true
} else {
s.dragList.swapped = false
@ -242,6 +234,88 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
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)
}