-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
ErikKalkoken
wants to merge
7
commits into
fyne-io:develop
Choose a base branch
from
ErikKalkoken:add-rowwrap-layout
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add RowWrap layout #5612
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c76160a
Add RowWrap layout
ErikKalkoken ed312ab
Return layout interface when creating new RowWrap
ErikKalkoken 7ad569a
Add ability to specifiy custom padding for RowWrapLayout
ErikKalkoken 0b9db05
Implement review notes
ErikKalkoken 8e26f6e
Improve implementation & tests
ErikKalkoken 5a6a059
Update layout/rowwrap.go
ErikKalkoken 48e3034
Implement reviewer notes #2
ErikKalkoken File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
andydotxyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)) | ||
dweymouth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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 | ||
Jacalz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.