From 4ed9695d5732f80ba33a3096e60ee8edefdbde63 Mon Sep 17 00:00:00 2001 From: Egon Elbre Date: Tue, 17 Feb 2026 22:12:34 +0200 Subject: [PATCH] layout: add List.Gap for spacing out items Signed-off-by: Egon Elbre --- layout/list.go | 27 ++++++++++---- layout/list_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/layout/list.go b/layout/list.go index b62dc83f..2116bec4 100644 --- a/layout/list.go +++ b/layout/list.go @@ -31,6 +31,8 @@ type List struct { Alignment Alignment // ScrollAnyAxis allows any scroll axis to scroll the list, not just the main axis. ScrollAnyAxis bool + // Gap is the space in pixels between children. + Gap int cs Constraints scroll gesture.Scroll @@ -130,7 +132,7 @@ func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions { } if numLaidOut > 0 { - l.Position.Length = laidOutTotalLength * len / numLaidOut + l.Position.Length = laidOutTotalLength*len/numLaidOut + l.Gap*(len-1) } else { l.Position.Length = 0 } @@ -223,11 +225,11 @@ func (l *List) nextDir() iterationDir { if len(l.children) > 0 { if l.Position.First > 0 { firstChild := l.children[0] - firstSize = l.Axis.Convert(firstChild.size).X + firstSize = l.Axis.Convert(firstChild.size).X + l.Gap } if last < l.len { lastChild := l.children[len(l.children)-1] - lastSize = l.Axis.Convert(lastChild.size).X + lastSize = l.Axis.Convert(lastChild.size).X + l.Gap } } switch { @@ -245,6 +247,9 @@ func (l *List) nextDir() iterationDir { func (l *List) end(dims Dimensions, call op.CallOp) { child := scrollChild{dims.Size, call} mainSize := l.Axis.Convert(child.size).X + if len(l.children) > 0 { + l.maxSize += l.Gap + } l.maxSize += mainSize switch l.dir { case iterateForward: @@ -254,7 +259,7 @@ func (l *List) end(dims Dimensions, call op.CallOp) { copy(l.children[1:], l.children) l.children[0] = child l.Position.First-- - l.Position.Offset += mainSize + l.Position.Offset += mainSize + l.Gap default: panic("call Next before End") } @@ -279,7 +284,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { break } l.Position.First++ - l.Position.Offset -= mainSize + l.Position.Offset -= mainSize + l.Gap first = child children = children[1:] } @@ -291,6 +296,9 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { if c := sz.Y; c > maxCross { maxCross = c } + if i > 0 { + size += l.Gap + } size += sz.X if size >= mainMax { if i < len(children)-1 { @@ -326,14 +334,19 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { // Lay out leading invisible child. if first != (scrollChild{}) { sz := l.Axis.Convert(first.size) - pos -= sz.X + pos -= sz.X + l.Gap layout(first) + pos += l.Gap } - for _, child := range children { + for i, child := range children { + if i > 0 { + pos += l.Gap + } layout(child) } // Lay out trailing invisible child. if last != (scrollChild{}) { + pos += l.Gap layout(last) } atStart := l.Position.First == 0 && l.Position.Offset <= 0 diff --git a/layout/list_test.go b/layout/list_test.go index 4a2fe389..2fd428ae 100644 --- a/layout/list_test.go +++ b/layout/list_test.go @@ -184,6 +184,93 @@ func TestListPosition(t *testing.T) { } } +func TestListGap(t *testing.T) { + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(100, 20), + }, + } + + // Two 10px children with 5px gap: total 25px. + l := List{Gap: 5} + dims := l.Layout(gtx, 2, func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + }) + if got, exp := dims.Size.X, 25; got != exp { + t.Errorf("two children with gap: got width %d, expected %d", got, exp) + } + + // Three 10px children with 5px gap: total 40px. + l = List{Gap: 5} + dims = l.Layout(gtx, 3, func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + }) + if got, exp := dims.Size.X, 40; got != exp { + t.Errorf("three children with gap: got width %d, expected %d", got, exp) + } + + // Single child: no gap. + l = List{Gap: 5} + dims = l.Layout(gtx, 1, func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + }) + if got, exp := dims.Size.X, 10; got != exp { + t.Errorf("single child with gap: got width %d, expected %d", got, exp) + } + + // Zero children: no gap. + l = List{Gap: 5} + dims = l.Layout(gtx, 0, nil) + if got, exp := dims.Size.X, 0; got != exp { + t.Errorf("no children with gap: got width %d, expected %d", got, exp) + } +} + +func TestListGapVertical(t *testing.T) { + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(20, 100), + }, + } + + l := List{Axis: Vertical, Gap: 10} + dims := l.Layout(gtx, 3, func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 15)} + }) + // 3*15 + 2*10 = 65. + if got, exp := dims.Size.Y, 65; got != exp { + t.Errorf("vertical list with gap: got height %d, expected %d", got, exp) + } +} + +func TestListGapPosition(t *testing.T) { + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(30, 20), + }, + } + + // Viewport 30px, 5 children of 10px with 5px gap. + // Children fill: 10, 10+5+10=25, 25+5+10=40 >= 30, so 3 visible (last partially). + l := List{Gap: 5} + l.Layout(gtx, 5, func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + }) + if got, exp := l.Position.Count, 3; got != exp { + t.Errorf("visible count with gap: got %d, expected %d", got, exp) + } + if got, exp := l.Position.First, 0; got != exp { + t.Errorf("first with gap: got %d, expected %d", got, exp) + } + // OffsetLast = mainMax - size = 30 - 40 = -10. + if got, exp := l.Position.OffsetLast, -10; got != exp { + t.Errorf("offset last with gap: got %d, expected %d", got, exp) + } +} + func TestExtraChildren(t *testing.T) { var l List l.Position.First = 1