mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-18 21:14:31 -04:00
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:
parent
6d3c65e11d
commit
d92426a100
258
tracker/gioui/scroll_table.go
Normal file
258
tracker/gioui/scroll_table.go
Normal file
@ -0,0 +1,258 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type ScrollTable struct {
|
||||
ColTitleList *DragList
|
||||
RowTitleList *DragList
|
||||
Table tracker.Table
|
||||
focused bool
|
||||
requestFocus bool
|
||||
tag bool
|
||||
colTag bool
|
||||
rowTag bool
|
||||
cursorMoved bool
|
||||
}
|
||||
|
||||
type ScrollTableStyle struct {
|
||||
RowTitleStyle FilledDragListStyle
|
||||
ColTitleStyle FilledDragListStyle
|
||||
ScrollTable *ScrollTable
|
||||
ScrollBarWidth unit.Dp
|
||||
RowTitleWidth unit.Dp
|
||||
ColumnTitleHeight unit.Dp
|
||||
CellWidth unit.Dp
|
||||
CellHeight unit.Dp
|
||||
element func(gtx C, x, y int) D
|
||||
}
|
||||
|
||||
func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *ScrollTable {
|
||||
return &ScrollTable{
|
||||
Table: table,
|
||||
ColTitleList: NewDragList(vertList, layout.Horizontal),
|
||||
RowTitleList: NewDragList(horizList, layout.Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
func FilledScrollTable(th *material.Theme, scrollTable *ScrollTable, element func(gtx C, x, y int) D, colTitle, rowTitle, colTitleBg, rowTitleBg func(gtx C, i int) D) ScrollTableStyle {
|
||||
return ScrollTableStyle{
|
||||
RowTitleStyle: FilledDragList(th, scrollTable.RowTitleList, rowTitle, rowTitleBg),
|
||||
ColTitleStyle: FilledDragList(th, scrollTable.ColTitleList, colTitle, colTitleBg),
|
||||
ScrollTable: scrollTable,
|
||||
element: element,
|
||||
ScrollBarWidth: unit.Dp(14),
|
||||
RowTitleWidth: unit.Dp(30),
|
||||
ColumnTitleHeight: unit.Dp(16),
|
||||
CellWidth: unit.Dp(16),
|
||||
CellHeight: unit.Dp(16),
|
||||
}
|
||||
}
|
||||
|
||||
func (st *ScrollTable) CursorMoved() bool {
|
||||
ret := st.cursorMoved
|
||||
st.cursorMoved = false
|
||||
return ret
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focus() {
|
||||
st.requestFocus = true
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focused() bool {
|
||||
return st.focused
|
||||
}
|
||||
|
||||
func (st *ScrollTable) EnsureCursorVisible() {
|
||||
st.ColTitleList.EnsureVisible(st.Table.Cursor().X)
|
||||
st.RowTitleList.EnsureVisible(st.Table.Cursor().Y)
|
||||
}
|
||||
|
||||
func (st *ScrollTable) ChildFocused() bool {
|
||||
return st.ColTitleList.Focused() || st.RowTitleList.Focused()
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) Layout(gtx C) D {
|
||||
p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight))
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
s.ScrollTable.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Position.X >= float32(p.X) && e.Position.Y >= float32(p.Y) {
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: &s.ScrollTable.tag}.Add(gtx.Ops)
|
||||
}
|
||||
dx := (int(e.Position.X) + s.ScrollTable.ColTitleList.List.Position.Offset - p.X) / gtx.Dp(s.CellWidth)
|
||||
dy := (int(e.Position.Y) + s.ScrollTable.RowTitleList.List.Position.Offset - p.Y) / gtx.Dp(s.CellHeight)
|
||||
x := dx + s.ScrollTable.ColTitleList.List.Position.First
|
||||
y := dy + s.ScrollTable.RowTitleList.List.Position.First
|
||||
s.ScrollTable.Table.SetCursor(
|
||||
tracker.Point{X: x, Y: y},
|
||||
)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.ScrollTable.Table.SetCursor2(s.ScrollTable.Table.Cursor())
|
||||
}
|
||||
s.ScrollTable.cursorMoved = true
|
||||
}
|
||||
case key.Event:
|
||||
if e.State == key.Press {
|
||||
s.ScrollTable.command(gtx, e)
|
||||
}
|
||||
case clipboard.Event:
|
||||
s.ScrollTable.Table.Paste([]byte(e.Text))
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.rowTag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.colTag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
return Surface{Gray: 24, Focus: s.ScrollTable.Focused() || s.ScrollTable.ChildFocused()}.Layout(gtx, func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{
|
||||
Tag: &s.ScrollTable.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
dims := gtx.Constraints.Max
|
||||
s.layoutColTitles(gtx, p)
|
||||
s.layoutRowTitles(gtx, p)
|
||||
defer op.Offset(p).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
|
||||
s.layoutTable(gtx, p)
|
||||
s.RowTitleStyle.LayoutScrollBar(gtx)
|
||||
s.ColTitleStyle.LayoutScrollBar(gtx)
|
||||
return D{Size: dims}
|
||||
})
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) layoutTable(gtx C, p image.Point) {
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
|
||||
|
||||
if s.ScrollTable.requestFocus {
|
||||
s.ScrollTable.requestFocus = false
|
||||
key.FocusOp{Tag: &s.ScrollTable.tag}.Add(gtx.Ops)
|
||||
}
|
||||
key.InputOp{Tag: &s.ScrollTable.tag, Keys: "←|→|↑|↓|Shift-←|Shift-→|Shift-↑|Shift-↓|Ctrl-←|Ctrl-→|Ctrl-↑|Ctrl-↓|Ctrl-Shift-←|Ctrl-Shift-→|Ctrl-Shift-↑|Ctrl-Shift-↓|Alt-←|Alt-→|Alt-↑|Alt-↓|Alt-Shift-←|Alt-Shift-→|Alt-Shift-↑|Alt-Shift-↓|⇱|⇲|Shift-⇱|Shift-⇲|⌫|⌦|⇞|⇟|Shift-⇞|Shift-⇟|Ctrl-C|Ctrl-V|Ctrl-X|Shift-,|Shift-."}.Add(gtx.Ops)
|
||||
cellWidth := gtx.Dp(s.CellWidth)
|
||||
cellHeight := gtx.Dp(s.CellHeight)
|
||||
|
||||
gtx.Constraints = layout.Exact(image.Pt(cellWidth, cellHeight))
|
||||
|
||||
colP := s.ColTitleStyle.dragList.List.Position
|
||||
rowP := s.RowTitleStyle.dragList.List.Position
|
||||
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
|
||||
for x := colP.First; x < colP.First+colP.Count; x++ {
|
||||
offs := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
for y := rowP.First; y < rowP.First+rowP.Count; y++ {
|
||||
s.element(gtx, x, y)
|
||||
op.Offset(image.Pt(0, cellHeight)).Add(gtx.Ops)
|
||||
}
|
||||
offs.Pop()
|
||||
op.Offset(image.Pt(cellWidth, 0)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutRowTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(0, p.Y)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = p.X
|
||||
gtx.Constraints.Max.Y -= p.Y
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &s.ScrollTable.rowTag, Keys: "→"}.Add(gtx.Ops)
|
||||
s.RowTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutColTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(p.X, 0)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.Y = p.Y
|
||||
gtx.Constraints.Max.X -= p.X
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &s.ScrollTable.colTag, Keys: "↓"}.Add(gtx.Ops)
|
||||
s.ColTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTable) command(gtx C, e key.Event) {
|
||||
stepX := 1
|
||||
stepY := 1
|
||||
if e.Modifiers.Contain(key.ModAlt) {
|
||||
stepX = intMax(s.ColTitleList.List.Position.Count-3, 8)
|
||||
stepY = intMax(s.RowTitleList.List.Position.Count-3, 8)
|
||||
} else if e.Modifiers.Contain(key.ModCtrl) {
|
||||
stepX = 1e6
|
||||
stepY = 1e6
|
||||
}
|
||||
switch e.Name {
|
||||
case "X", "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, ok := s.Table.Copy()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
if e.Name == "X" {
|
||||
s.Table.Clear()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
clipboard.ReadOp{Tag: &s.tag}.Add(gtx.Ops)
|
||||
}
|
||||
return
|
||||
case key.NameDeleteBackward, key.NameDeleteForward:
|
||||
s.Table.Clear()
|
||||
return
|
||||
case key.NameUpArrow:
|
||||
if !s.Table.MoveCursor(0, -stepY) && stepY == 1 {
|
||||
s.ColTitleList.Focus()
|
||||
}
|
||||
case key.NameDownArrow:
|
||||
s.Table.MoveCursor(0, stepY)
|
||||
case key.NameLeftArrow:
|
||||
if !s.Table.MoveCursor(-stepX, 0) && stepX == 1 {
|
||||
s.RowTitleList.Focus()
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
s.Table.MoveCursor(stepX, 0)
|
||||
case key.NamePageUp:
|
||||
s.Table.MoveCursor(0, -intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NamePageDown:
|
||||
s.Table.MoveCursor(0, intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NameHome:
|
||||
s.Table.SetCursorX(0)
|
||||
case key.NameEnd:
|
||||
s.Table.SetCursorX(s.Table.Width() - 1)
|
||||
case ".":
|
||||
s.Table.Add(1)
|
||||
case ",":
|
||||
s.Table.Add(-1)
|
||||
}
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.Table.SetCursor2(s.Table.Cursor())
|
||||
}
|
||||
s.ColTitleList.EnsureVisible(s.Table.Cursor().X)
|
||||
s.RowTitleList.EnsureVisible(s.Table.Cursor().Y)
|
||||
s.cursorMoved = true
|
||||
}
|
Reference in New Issue
Block a user