sointu/tracker/gioui/scroll_table.go
2024-10-15 13:24:14 +03:00

318 lines
9.2 KiB
Go

package gioui
import (
"bytes"
"image"
"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/unit"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
)
type ScrollTable struct {
ColTitleList *DragList
RowTitleList *DragList
Table tracker.Table
focused bool
requestFocus bool
cursorMoved bool
eventFilters []event.Filter
drag bool
dragID pointer.ID
}
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 {
ret := &ScrollTable{
Table: table,
ColTitleList: NewDragList(vertList, layout.Horizontal),
RowTitleList: NewDragList(horizList, layout.Vertical),
}
ret.eventFilters = []event.Filter{
key.FocusFilter{Target: ret},
transfer.TargetFilter{Target: ret, Type: "application/text"},
pointer.Filter{Target: ret, Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel},
key.Filter{Focus: ret, Name: key.NameLeftArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NameUpArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NameRightArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NameDownArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NamePageUp, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NamePageDown, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NameHome, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NameEnd, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NameDeleteBackward},
key.Filter{Focus: ret, Name: key.NameDeleteForward},
}
for k, a := range keyBindingMap {
switch a {
case "Copy", "Paste", "Cut", "Increase", "Decrease":
ret.eventFilters = append(ret.eventFilters, key.Filter{Focus: ret, Name: k.Name, Required: k.Modifiers})
}
}
return ret
}
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 {
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
event.Op(gtx.Ops, s.ScrollTable)
p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight))
s.handleEvents(gtx, p)
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()
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)
s.RowTitleStyle.LayoutScrollBar(gtx)
s.ColTitleStyle.LayoutScrollBar(gtx)
return D{Size: dims}
})
}
func (s *ScrollTableStyle) handleEvents(gtx layout.Context, p image.Point) {
for {
e, ok := gtx.Event(s.ScrollTable.eventFilters...)
if !ok {
break
}
switch e := e.(type) {
case key.FocusEvent:
s.ScrollTable.focused = e.Focus
case pointer.Event:
switch e.Kind {
case pointer.Press:
if s.ScrollTable.drag {
break
}
s.ScrollTable.dragID = e.PointerID
s.ScrollTable.drag = true
fallthrough
case pointer.Drag:
if s.ScrollTable.dragID != e.PointerID {
break
}
if int(e.Position.X) < p.X || int(e.Position.Y) < p.Y {
break
}
e.Position.X -= float32(p.X)
e.Position.Y -= float32(p.Y)
if e.Kind == pointer.Press {
gtx.Execute(key.FocusCmd{Tag: s.ScrollTable})
}
dx := (e.Position.X + float32(s.ScrollTable.ColTitleList.List.Position.Offset)) / float32(gtx.Dp(s.CellWidth))
dy := (e.Position.Y + float32(s.ScrollTable.RowTitleList.List.Position.Offset)) / float32(gtx.Dp(s.CellHeight))
x := dx + float32(s.ScrollTable.ColTitleList.List.Position.First)
y := dy + float32(s.ScrollTable.RowTitleList.List.Position.First)
s.ScrollTable.Table.SetCursor2(tracker.Point{X: int(x), Y: int(y)})
if e.Kind == pointer.Press && !e.Modifiers.Contain(key.ModShift) {
s.ScrollTable.Table.SetCursorFloat(x, y)
}
s.ScrollTable.cursorMoved = true
case pointer.Release:
fallthrough
case pointer.Cancel:
s.ScrollTable.drag = false
}
case key.Event:
if e.State == key.Press {
s.ScrollTable.command(gtx, e)
}
case transfer.DataEvent:
if b, err := io.ReadAll(e.Open()); err == nil {
s.ScrollTable.Table.Paste(b)
}
}
}
for {
e, ok := gtx.Event(
key.Filter{Focus: s.ScrollTable.RowTitleList, Name: "→"},
)
if !ok {
break
}
if e, ok := e.(key.Event); ok && e.State == key.Press {
s.ScrollTable.Focus()
}
}
for {
e, ok := gtx.Event(
key.Filter{Focus: s.ScrollTable.ColTitleList, Name: "↓"},
)
if !ok {
break
}
if e, ok := e.(key.Event); ok && e.State == key.Press {
s.ScrollTable.Focus()
}
}
}
func (s ScrollTableStyle) layoutTable(gtx C) {
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
if s.ScrollTable.requestFocus {
s.ScrollTable.requestFocus = false
gtx.Execute(key.FocusCmd{Tag: s.ScrollTable})
}
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 := 0; x < colP.Count; x++ {
for y := 0; y < rowP.Count; y++ {
o := op.Offset(image.Pt(cellWidth*x, cellHeight*y)).Push(gtx.Ops)
s.element(gtx, x+colP.First, y+rowP.First)
o.Pop()
}
}
}
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
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
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 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)
default:
a := keyBindingMap[e]
switch a {
case "Copy", "Cut":
contents, ok := s.Table.Copy()
if !ok {
return
}
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
if a == "Cut" {
s.Table.Clear()
}
return
case "Paste":
gtx.Execute(clipboard.ReadCmd{Tag: s})
return
case "Increase":
s.Table.Add(1)
return
case "Decrease":
s.Table.Add(-1)
return
}
}
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
}