Skip to content

Add RowWrap layout #5612

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions layout/rowwrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package layout

import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)

type rowWrapLayout struct {
horizontalPadding float32
minSize fyne.Size
verticalPadding float32
}

// NewRowWrapLayout returns a layout that dynamically arranges objects of similar height
// in rows and wraps them dynamically.
// Objects are separated with horizontal and vertical padding.
//
// Since: 2.7
func NewRowWrapLayout() fyne.Layout {
p := theme.Padding()
return &rowWrapLayout{
horizontalPadding: p,
verticalPadding: p,
}
}

// NewRowWrapLayoutWithCustomPadding returns a new RowWrapLayout instance
// with custom horizontal and inner padding.
//
// Since: 2.7
func NewRowWrapLayoutWithCustomPadding(horizontal, vertical float32) fyne.Layout {
return &rowWrapLayout{
horizontalPadding: horizontal,
verticalPadding: vertical,
}
}

var _ fyne.Layout = (*rowWrapLayout)(nil)

// MinSize finds the smallest size that satisfies all the child objects.
// For a RowWrapLayout this is initially the width of the widest child
// and the height of the tallest child multiplied by the number of children,
// with appropriate padding between them.
// After Layout() has run it returns the actual min size.
func (l *rowWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
if len(objects) == 0 {
return fyne.NewSize(0, 0)
}
if !l.minSize.IsZero() {
return l.minSize
}
var maxW, maxH float32
var objCount int
for _, o := range objects {
if !o.Visible() {
continue
}
objCount++
s := o.MinSize()
maxW = fyne.Max(maxW, s.Width)
maxH = fyne.Max(maxH, s.Height)
}
return fyne.NewSize(maxW, l.minHeight(maxH, objCount))
}

func (l *rowWrapLayout) minHeight(rowHeight float32, rowCount int) float32 {
return rowHeight*float32(rowCount) + l.verticalPadding*float32(rowCount-1)
}

// Layout is called to pack all child objects into a specified size.
// For RowWrapLayout this will arrange all objects into rows of equal size
// and wrap objects into additional rows as needed.
func (l *rowWrapLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.Size) {
if len(objects) == 0 {
return
}
var maxH float32
for _, o := range objects {
if !o.Visible() {
continue
}
maxH = fyne.Max(maxH, o.MinSize().Height)
}
var minSize fyne.Size
pos := fyne.NewPos(0, 0)
rows := 1
isFirst := true
for _, o := range objects {
if !o.Visible() {
continue
}
size := o.MinSize()
o.Resize(size)
if !isFirst && pos.X+size.Width+l.horizontalPadding >= containerSize.Width {
y := float32(rows) * (maxH + l.verticalPadding)
pos = fyne.NewPos(0, y)
rows++
}
isFirst = false
minSize.Width = fyne.Max(minSize.Width, pos.X+size.Width)
minSize.Height = l.minHeight(maxH, rows)
o.Move(pos)
pos = pos.Add(fyne.NewPos(size.Width+l.horizontalPadding, 0))
}
l.minSize = minSize
}
229 changes: 229 additions & 0 deletions layout/rowwrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package layout_test

import (
"image/color"
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"github.com/stretchr/testify/assert"
)

func TestRowWrapLayout_MinSize(t *testing.T) {
p := theme.Padding()
t.Run("should return min size of single object when container has only one", func(t *testing.T) {
// given
a := makeObject(10, 10)
container := container.NewWithoutLayout(a)
layout := layout.NewRowWrapLayout()

// when/then
got := layout.MinSize(container.Objects)

// then
want := a.MinSize()
assert.Equal(t, want, got)
})
t.Run("should return size 0 when container is empty", func(t *testing.T) {
// given
container := container.NewWithoutLayout()
layout := layout.NewRowWrapLayout()

// when/then
got := layout.MinSize(container.Objects)

// then
want := fyne.NewSize(0, 0)
assert.Equal(t, want, got)
})
t.Run("should estimate min size when layout not yet known", func(t *testing.T) {
// given
a := makeObject(10, 10)
b := makeObject(20, 10)
container := container.NewWithoutLayout(a, b)
layout := layout.NewRowWrapLayout()

// when/then
got := layout.MinSize(container.Objects)

// then
want := fyne.NewSize(20, 10+p+10)
assert.Equal(t, want, got)
})
t.Run("should use custom padding when estimating min size", func(t *testing.T) {
// given
a := makeObject(10, 10)
b := makeObject(20, 10)
container := container.NewWithoutLayout(a, b)
layout := layout.NewRowWrapLayoutWithCustomPadding(5, 7)

// when/then
got := layout.MinSize(container.Objects)

// then
want := fyne.NewSize(20, 10+7+10)
assert.Equal(t, want, got)
})
t.Run("should ignore invisible objects when estimating min size", func(t *testing.T) {
// given
a := makeObject(10, 10)
b := makeObject(20, 10)
b.Hide()
container := container.NewWithoutLayout(a, b)
layout := layout.NewRowWrapLayout()

// when/then
got := layout.MinSize(container.Objects)

// then
want := fyne.NewSize(10, 10)
assert.Equal(t, want, got)
})

t.Run("should return actual size of arranged objects after layout was calculated", func(t *testing.T) {
// given
a := makeObject(10, 10)
b := makeObject(20, 10)
c := makeObject(20, 10)
container := container.New(layout.NewRowWrapLayout(), a, b, c)
container.Resize(fyne.NewSize(55, 50))

// when/then
got := container.MinSize()

// then
p := theme.Padding()
want := fyne.NewSize(10+p+20, 10+p+10)
assert.Equal(t, want, got)
})
}

func TestRowWrapLayout_Layout(t *testing.T) {
p := theme.Padding()
t.Run("should arrange single object", func(t *testing.T) {
// given
a := makeObject(30, 10)
containerSize := fyne.NewSize(120, 30)
container := &fyne.Container{
Objects: []fyne.CanvasObject{a},
}
container.Resize(containerSize)

// when
layout.NewRowWrapLayout().Layout(container.Objects, containerSize)

// then
assert.Equal(t, fyne.NewPos(0, 0), a.Position())
})

t.Run("should arrange objects in single row when they fit", func(t *testing.T) {
// given
a := makeObject(30, 10)
b := makeObject(80, 10)
containerSize := fyne.NewSize(120, 30)
container := &fyne.Container{
Objects: []fyne.CanvasObject{a, b},
}
container.Resize(containerSize)

// when
layout.NewRowWrapLayout().Layout(container.Objects, containerSize)

// then
assert.Equal(t, fyne.NewPos(0, 0), a.Position())
assert.Equal(t, fyne.NewPos(30+p, 0), b.Position())
})
t.Run("should wrap overflowing object into new row with multiple objects in a row", func(t *testing.T) {
// given
a := makeObject(30, 10)
b := makeObject(80, 10)
c := makeObject(50, 10)
containerSize := fyne.NewSize(125, 125)
container := &fyne.Container{
Objects: []fyne.CanvasObject{a, b, c},
}
container.Resize(containerSize)

// when
layout.NewRowWrapLayout().Layout(container.Objects, containerSize)

// then
assert.Equal(t, fyne.NewPos(0, 0), a.Position())
assert.Equal(t, fyne.NewPos(30+p, 0), b.Position())
assert.Equal(t, fyne.NewPos(0, 10+p), c.Position())
})
t.Run("should wrap overflowing object into new row with one object on a row", func(t *testing.T) {
// given
a := makeObject(80, 10)
b := makeObject(30, 10)
containerSize := fyne.NewSize(40, 30)
container := &fyne.Container{
Objects: []fyne.CanvasObject{a, b},
}
container.Resize(containerSize)

// when
layout.NewRowWrapLayout().Layout(container.Objects, containerSize)

// then
assert.Equal(t, fyne.NewPos(0, 0), a.Position())
assert.Equal(t, fyne.NewPos(0, 10+p), b.Position())
})
t.Run("should do nothing when container is empty", func(t *testing.T) {
containerSize := fyne.NewSize(125, 125)
container := &fyne.Container{}
container.Resize(containerSize)

// when
layout.NewRowWrapLayout().Layout(container.Objects, containerSize)
})
t.Run("should ignore hidden objects", func(t *testing.T) {
// given
a := makeObject(30, 10)
b := makeObject(80, 10)
b.Hide()
c := makeObject(50, 10)

containerSize := fyne.NewSize(125, 125)
container := &fyne.Container{
Objects: []fyne.CanvasObject{a, b, c},
}
container.Resize(containerSize)

// when
layout.NewRowWrapLayout().Layout(container.Objects, containerSize)

// then
assert.Equal(t, fyne.NewPos(0, 0), a.Position())
assert.Equal(t, fyne.NewPos(30+p, 0), c.Position())
})

t.Run("should arrange objects with custom padding", func(t *testing.T) {
// given
a := makeObject(30, 10)
b := makeObject(80, 10)
c := makeObject(50, 10)
containerSize := fyne.NewSize(125, 125)
container := &fyne.Container{
Objects: []fyne.CanvasObject{a, b, c},
}
container.Resize(containerSize)

// when
layout.NewRowWrapLayoutWithCustomPadding(5, 7).Layout(container.Objects, containerSize)

// then
assert.Equal(t, fyne.NewPos(0, 0), a.Position())
assert.Equal(t, fyne.NewPos(30+5, 0), b.Position())
assert.Equal(t, fyne.NewPos(0, 10+7), c.Position())
})
}

func makeObject(w, h float32) fyne.CanvasObject {
a := canvas.NewRectangle(color.Opaque)
a.SetMinSize(fyne.NewSize(w, h))
return a
}
Loading