Sometimes there’s a need for writing a custom widget or layout.
To implement rendering of children, we can use:
type SplitVisual struct{}
func (s SplitVisual) Layout(gtx layout.Context, left, right layout.Widget) layout.Dimensions {
leftsize := gtx.Constraints.Min.X / 2
rightsize := gtx.Constraints.Min.X - leftsize
{
gtx := gtx
gtx.Constraints = layout.Exact(image.Pt(leftsize, gtx.Constraints.Max.Y))
left(gtx)
}
{
gtx := gtx
gtx.Constraints = layout.Exact(image.Pt(rightsize, gtx.Constraints.Max.Y))
trans := op.Offset(image.Pt(leftsize, 0)).Push(gtx.Ops)
right(gtx)
trans.Pop()
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}
Then we can use the widget like:
func exampleSplitVisual(gtx layout.Context, th *material.Theme) layout.Dimensions {
return SplitVisual{}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return FillWithLabel(gtx, th, "Left", red)
}, func(gtx layout.Context) layout.Dimensions {
return FillWithLabel(gtx, th, "Right", blue)
})
}
func FillWithLabel(gtx layout.Context, th *material.Theme, text string, backgroundColor color.NRGBA) layout.Dimensions {
ColorBox(gtx, gtx.Constraints.Max, backgroundColor)
return layout.Center.Layout(gtx, material.H3(th, text).Layout)
}
Ratio
Let’s make the ratio adjustable. We should try to make zero values useful, in this case 0
could mean that it’s split in the center.
type SplitRatio struct {
// Ratio keeps the current layout.
// 0 is center, -1 completely to the left, 1 completely to the right.
Ratio float32
}
func (s SplitRatio) Layout(gtx layout.Context, left, right layout.Widget) layout.Dimensions {
proportion := (s.Ratio + 1) / 2
leftsize := int(proportion * float32(gtx.Constraints.Max.X))
rightoffset := leftsize
rightsize := gtx.Constraints.Max.X - rightoffset
{
gtx := gtx
gtx.Constraints = layout.Exact(image.Pt(leftsize, gtx.Constraints.Max.Y))
left(gtx)
}
{
trans := op.Offset(image.Pt(rightoffset, 0)).Push(gtx.Ops)
gtx := gtx
gtx.Constraints = layout.Exact(image.Pt(rightsize, gtx.Constraints.Max.Y))
right(gtx)
trans.Pop()
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}
The usage code would look like:
func exampleSplitRatio(gtx layout.Context, th *material.Theme) layout.Dimensions {
return SplitRatio{Ratio: -0.3}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return FillWithLabel(gtx, th, "Left", red)
}, func(gtx layout.Context) layout.Dimensions {
return FillWithLabel(gtx, th, "Right", blue)
})
}
Interactive
To make it more useful we could make the split draggable.
Because we also need to have an area designated for moving the split, let’s add a bar into the center:
bar := gtx.Dp(s.Bar)
if bar <= 1 {
bar = gtx.Dp(defaultBarWidth)
}
proportion := (s.Ratio + 1) / 2
leftsize := int(proportion*float32(gtx.Constraints.Max.X) - float32(bar))
rightoffset := leftsize + bar
rightsize := gtx.Constraints.Max.X - rightoffset
Now we need to store our interactive state:
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
drag bool
dragID pointer.ID
dragX float32
}
And then we need to handle input events:
barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Max.X)
area := clip.Rect(barRect).Push(gtx.Ops)
// register for input
event.Op(gtx.Ops, s)
pointer.CursorColResize.Add(gtx.Ops)
for {
ev, ok := gtx.Event(pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
})
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
s.dragX = e.Position.X
s.drag = true
case pointer.Drag:
if s.dragID != e.PointerID {
break
}
deltaX := e.Position.X - s.dragX
s.dragX = e.Position.X
deltaRatio := deltaX * 2 / float32(gtx.Constraints.Max.X)
s.Ratio += deltaRatio
if e.Priority < pointer.Grabbed {
gtx.Execute(pointer.GrabCmd{
Tag: s,
ID: s.dragID,
})
}
case pointer.Release:
fallthrough
case pointer.Cancel:
s.drag = false
}
}
area.Pop()
Result
Putting the whole widget together:
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
drag bool
dragID pointer.ID
dragX float32
}
const defaultBarWidth = unit.Dp(10)
func (s *Split) Layout(gtx layout.Context, left, right layout.Widget) layout.Dimensions {
bar := gtx.Dp(s.Bar)
if bar <= 1 {
bar = gtx.Dp(defaultBarWidth)
}
proportion := (s.Ratio + 1) / 2
leftsize := int(proportion*float32(gtx.Constraints.Max.X) - float32(bar))
rightoffset := leftsize + bar
rightsize := gtx.Constraints.Max.X - rightoffset
{ // handle input
barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Max.X)
area := clip.Rect(barRect).Push(gtx.Ops)
// register for input
event.Op(gtx.Ops, s)
pointer.CursorColResize.Add(gtx.Ops)
for {
ev, ok := gtx.Event(pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
})
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
s.dragX = e.Position.X
s.drag = true
case pointer.Drag:
if s.dragID != e.PointerID {
break
}
deltaX := e.Position.X - s.dragX
s.dragX = e.Position.X
deltaRatio := deltaX * 2 / float32(gtx.Constraints.Max.X)
s.Ratio += deltaRatio
if e.Priority < pointer.Grabbed {
gtx.Execute(pointer.GrabCmd{
Tag: s,
ID: s.dragID,
})
}
case pointer.Release:
fallthrough
case pointer.Cancel:
s.drag = false
}
}
area.Pop()
}
{
gtx := gtx
gtx.Constraints = layout.Exact(image.Pt(leftsize, gtx.Constraints.Max.Y))
left(gtx)
}
{
off := op.Offset(image.Pt(rightoffset, 0)).Push(gtx.Ops)
gtx := gtx
gtx.Constraints = layout.Exact(image.Pt(rightsize, gtx.Constraints.Max.Y))
right(gtx)
off.Pop()
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}
And an example:
var split Split
func exampleSplit(gtx layout.Context, th *material.Theme) layout.Dimensions {
return split.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return FillWithLabel(gtx, th, "Left", red)
}, func(gtx layout.Context) layout.Dimensions {
return FillWithLabel(gtx, th, "Right", blue)
})
}