sointu/tracker/gioui/split.go
2025-04-26 01:48:42 +03:00

194 lines
4.9 KiB
Go

package gioui
import (
"image"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/unit"
)
type Split struct {
// Ratio keeps the current layout.
// 0 is center, -1 completely to the left, 1 completely to the right.
Ratio float32
// Bar is the width for resizing the layout
Bar unit.Dp
// Axis is the split direction: layout.Horizontal splits the view in left
// and right, layout.Vertical splits the view in top and bottom
Axis layout.Axis
// Minimum sizes of the first and second widget in the split, in dp
MinSize1, MinSize2 unit.Dp
drag bool
dragID pointer.ID
dragCoord float32
}
var defaultBarWidth = unit.Dp(10)
func (s *Split) Update(gtx layout.Context) {
for {
ev, ok := gtx.Event(pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release,
// TODO: there should be a grab; there was Grab: s.drag,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Kind {
case pointer.Press:
if s.drag {
break
}
s.dragID = e.PointerID
if s.Axis == layout.Horizontal {
s.dragCoord = e.Position.X
} else {
s.dragCoord = e.Position.Y
}
s.drag = true
// when the user start dragging, the new display ratio becomes the underlying ratio
s.Ratio = s.calculateRatio(gtx)
case pointer.Drag:
if s.dragID != e.PointerID {
break
}
if s.Axis == layout.Horizontal {
s.Ratio += (e.Position.X - s.dragCoord) / float32(gtx.Constraints.Max.X) * 2
s.dragCoord = e.Position.X
} else {
s.Ratio += (e.Position.Y - s.dragCoord) / float32(gtx.Constraints.Max.Y) * 2
s.dragCoord = e.Position.Y
}
case pointer.Release, pointer.Cancel:
if s.dragID == e.PointerID {
// when the user release the grab, the new display ratio becomes the underlying ratio
s.Ratio = s.calculateRatio(gtx)
}
s.drag = false
}
}
}
func (s *Split) Layout(gtx layout.Context, first, second layout.Widget) layout.Dimensions {
s.Update(gtx)
size1, size2, bar := s.calculateSplitSizes(gtx)
secondOffset := size1 + bar
{
// register for input
var barRect image.Rectangle
if s.Axis == layout.Horizontal {
barRect = image.Rect(size1, 0, secondOffset, gtx.Constraints.Max.Y)
} else {
barRect = image.Rect(0, size1, gtx.Constraints.Max.X, secondOffset)
}
area := clip.Rect(barRect).Push(gtx.Ops)
event.Op(gtx.Ops, s)
if s.Axis == layout.Horizontal {
pointer.CursorColResize.Add(gtx.Ops)
} else {
pointer.CursorRowResize.Add(gtx.Ops)
}
area.Pop()
}
{
gtx := gtx
if s.Axis == layout.Horizontal {
gtx.Constraints = layout.Exact(image.Pt(size1, gtx.Constraints.Max.Y))
} else {
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, size1))
}
area := clip.Rect(image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)).Push(gtx.Ops)
first(gtx)
area.Pop()
}
{
gtx := gtx
var transform op.TransformStack
if s.Axis == layout.Horizontal {
transform = op.Offset(image.Pt(secondOffset, 0)).Push(gtx.Ops)
gtx.Constraints = layout.Exact(image.Pt(size2, gtx.Constraints.Max.Y))
} else {
transform = op.Offset(image.Pt(0, secondOffset)).Push(gtx.Ops)
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, size2))
}
area := clip.Rect(image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)).Push(gtx.Ops)
second(gtx)
area.Pop()
transform.Pop()
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}
func (s *Split) calculateRatio(gtx layout.Context) float32 {
size1, size2, bar := s.calculateSplitSizes(gtx)
total := size1 + size2 + bar
if total <= 0 {
return 0
}
return 2*float32(size1+bar/2)/float32(total) - 1
}
func (s *Split) calculateSplitSizes(gtx layout.Context) (size1, size2, bar int) {
bar = gtx.Dp(s.Bar)
if bar <= 1 {
bar = gtx.Dp(defaultBarWidth)
}
total := gtx.Constraints.Max.Y
if s.Axis == layout.Horizontal {
total = gtx.Constraints.Max.X
}
if total < 0 {
total = 0
}
if total < bar {
return 0, 0, total
}
totalSize := total - bar
size1 = int((s.Ratio+1)/2*float32(total) - float32(bar)/2)
minSize1 := gtx.Dp(s.MinSize1)
minSize2 := gtx.Dp(s.MinSize2)
// we always hide the smaller split first
if s.Ratio < 0 {
size1 = limitSplitSize(size1, totalSize, minSize1, minSize2)
} else {
size1 = totalSize - limitSplitSize(totalSize-size1, totalSize, minSize2, minSize1)
}
size2 = totalSize - size1
return size1, size2, bar
}
// limitSplitSize hides the first split if it is smaller than minSize1/2 or if
// the total size is smaller than minSize1+minSize2. Otherwise, it clamps the
// size so that both split get at least minSize1 and minSize2 respectively.
func limitSplitSize(size, totalPx, minSize1, minSize2 int) int {
if size < minSize1/2 || totalPx < minSize1+minSize2 {
return 0 // the first split is completely hidden
}
return min(max(size, minSize1), totalPx-minSize2)
}