From 46f0988742e447e2b15f6652b304a565c36c27cf Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Mon, 17 Mar 2025 17:34:12 +0100 Subject: [PATCH 1/3] Add RowWrap layout --- layout/rowwrap.go | 67 +++++++++++++++++++++ layout/rowwrap_test.go | 133 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 layout/rowwrap.go create mode 100644 layout/rowwrap_test.go diff --git a/layout/rowwrap.go b/layout/rowwrap.go new file mode 100644 index 0000000000..1d38cdfec9 --- /dev/null +++ b/layout/rowwrap.go @@ -0,0 +1,67 @@ +package layout + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +type rowWrapLayout struct { + rowCount int +} + +// NewRowWrapLayout returns a layout that dynamically arranges objects +// with the same height in rows and wraps them as necessary. +// +// Object visibility is supported. +// +// Since: 2.7 +func NewRowWrapLayout() *rowWrapLayout { + return &rowWrapLayout{} +} + +var _ fyne.Layout = (*rowWrapLayout)(nil) + +func (l *rowWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + if len(objects) == 0 { + return fyne.NewSize(0, 0) + } + rows := l.rowCount + if rows == 0 { + rows = 1 + } + rowHeight := objects[0].MinSize().Height + var w float32 + for _, o := range objects { + size := o.MinSize() + if size.Width > w { + w = size.Width + } + } + s := fyne.NewSize(w, rowHeight*float32(rows)+theme.Padding()*float32(rows-1)) + return s +} + +func (l *rowWrapLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.Size) { + if len(objects) == 0 { + return + } + padding := theme.Padding() + rowHeight := objects[0].MinSize().Height + pos := fyne.NewPos(0, 0) + rows := 1 + for _, o := range objects { + if !o.Visible() { + continue + } + size := o.MinSize() + o.Resize(size) + w := size.Width + padding + if pos.X+w > containerSize.Width { + pos = fyne.NewPos(0, float32(rows)*(rowHeight+padding)) + rows++ + } + o.Move(pos) + pos = pos.Add(fyne.NewPos(w, 0)) + } + l.rowCount = rows +} diff --git a/layout/rowwrap_test.go b/layout/rowwrap_test.go new file mode 100644 index 0000000000..e225450c08 --- /dev/null +++ b/layout/rowwrap_test.go @@ -0,0 +1,133 @@ +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_Layout(t *testing.T) { + t.Run("should arrange objects in a row and wrap overflow objects into next row", func(t *testing.T) { + // given + h := float32(10) + o1 := canvas.NewRectangle(color.Opaque) + o1.SetMinSize(fyne.NewSize(30, h)) + o2 := canvas.NewRectangle(color.Opaque) + o2.SetMinSize(fyne.NewSize(80, h)) + o3 := canvas.NewRectangle(color.Opaque) + o3.SetMinSize(fyne.NewSize(50, h)) + + containerSize := fyne.NewSize(125, 125) + container := &fyne.Container{ + Objects: []fyne.CanvasObject{o1, o2, o3}, + } + container.Resize(containerSize) + + // when + layout.NewRowWrapLayout().Layout(container.Objects, containerSize) + + // then + p := theme.Padding() + assert.Equal(t, fyne.NewPos(0, 0), o1.Position()) + assert.Equal(t, fyne.NewPos(o1.Size().Width+p, 0), o2.Position()) + assert.Equal(t, fyne.NewPos(0, o1.Size().Height+p), o3.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 + h := float32(10) + o1 := canvas.NewRectangle(color.Opaque) + o1.SetMinSize(fyne.NewSize(30, h)) + o2 := canvas.NewRectangle(color.Opaque) + o2.SetMinSize(fyne.NewSize(80, h)) + o2.Hide() + o3 := canvas.NewRectangle(color.Opaque) + o3.SetMinSize(fyne.NewSize(50, h)) + + containerSize := fyne.NewSize(125, 125) + container := &fyne.Container{ + Objects: []fyne.CanvasObject{o1, o2, o3}, + } + container.Resize(containerSize) + + // when + layout.NewRowWrapLayout().Layout(container.Objects, containerSize) + + // then + p := theme.Padding() + assert.Equal(t, fyne.NewPos(0, 0), o1.Position()) + assert.Equal(t, fyne.NewPos(o1.Size().Width+p, 0), o3.Position()) + }) +} + +func TestRowWrapLayout_MinSize(t *testing.T) { + t.Run("should return min size of single object when container has only one", func(t *testing.T) { + // given + o := canvas.NewRectangle(color.Opaque) + o.SetMinSize(fyne.NewSize(10, 10)) + container := container.NewWithoutLayout(o) + layout := layout.NewRowWrapLayout() + + // when/then + minSize := layout.MinSize(container.Objects) + + // then + assert.Equal(t, o.MinSize(), minSize) + }) + t.Run("should return size 0 when container is empty", func(t *testing.T) { + // given + container := container.NewWithoutLayout() + layout := layout.NewRowWrapLayout() + + // when/then + minSize := layout.MinSize(container.Objects) + + // then + assert.Equal(t, fyne.NewSize(0, 0), minSize) + }) + t.Run("should initially return height of first object and width of widest object", func(t *testing.T) { + // given + h := float32(10) + o1 := canvas.NewRectangle(color.Opaque) + o1.SetMinSize(fyne.NewSize(10, h)) + o2 := canvas.NewRectangle(color.Opaque) + o2.SetMinSize(fyne.NewSize(20, h)) + container := container.NewWithoutLayout(o1, o2) + layout := layout.NewRowWrapLayout() + + // when/then + minSize := layout.MinSize(container.Objects) + + // then + assert.Equal(t, fyne.NewSize(20, h), minSize) + }) + t.Run("should return height of arranged objects after layout was calculated", func(t *testing.T) { + // given + h := float32(10) + o1 := canvas.NewRectangle(color.Opaque) + o1.SetMinSize(fyne.NewSize(10, h)) + o2 := canvas.NewRectangle(color.Opaque) + o2.SetMinSize(fyne.NewSize(20, h)) + container := container.New(layout.NewRowWrapLayout(), o1, o2) + container.Resize(fyne.NewSize(15, 50)) + + // when/then + minSize := container.MinSize() + + // then + assert.Equal(t, fyne.NewSize(o2.Size().Width, (o1.Size().Height*2)+theme.Padding()), minSize) + }) +} From e6784644e70a0cce6faddc40bd176e427dc85d1b Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Mon, 17 Mar 2025 17:55:12 +0100 Subject: [PATCH 2/3] Return layout interface when creating new RowWrap --- layout/rowwrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layout/rowwrap.go b/layout/rowwrap.go index 1d38cdfec9..e818264d85 100644 --- a/layout/rowwrap.go +++ b/layout/rowwrap.go @@ -15,7 +15,7 @@ type rowWrapLayout struct { // Object visibility is supported. // // Since: 2.7 -func NewRowWrapLayout() *rowWrapLayout { +func NewRowWrapLayout() fyne.Layout { return &rowWrapLayout{} } From 38a21aaebd0ff82cdd5cd4d9408e12237f29cf99 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Wed, 9 Apr 2025 16:08:17 +0200 Subject: [PATCH 3/3] Add ability to specifiy custom padding for RowWrapLayout --- layout/rowwrap.go | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/layout/rowwrap.go b/layout/rowwrap.go index e818264d85..db269787fb 100644 --- a/layout/rowwrap.go +++ b/layout/rowwrap.go @@ -6,7 +6,9 @@ import ( ) type rowWrapLayout struct { - rowCount int + rowCount int + horizontalPadding float32 + verticalPadding float32 } // NewRowWrapLayout returns a layout that dynamically arranges objects @@ -16,11 +18,29 @@ type rowWrapLayout struct { // // Since: 2.7 func NewRowWrapLayout() fyne.Layout { - return &rowWrapLayout{} + return &rowWrapLayout{ + horizontalPadding: theme.Padding(), + verticalPadding: theme.Padding(), + } +} + +// NewRowWrapLayoutWithCustomPadding creates a new RowWrapLayout instance +// with the specified paddings. +// +// 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 the width of the widest child +// and the height of the first child multiplied by the number of children, +// with appropriate padding between them. func (l *rowWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { if len(objects) == 0 { return fyne.NewSize(0, 0) @@ -30,22 +50,23 @@ func (l *rowWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { rows = 1 } rowHeight := objects[0].MinSize().Height - var w float32 + var width float32 for _, o := range objects { size := o.MinSize() - if size.Width > w { - w = size.Width + if size.Width > width { + width = size.Width } } - s := fyne.NewSize(w, rowHeight*float32(rows)+theme.Padding()*float32(rows-1)) + s := fyne.NewSize(width, rowHeight*float32(rows)+l.verticalPadding*float32(rows-1)) return s } +// Layout is called to pack all child objects into a specified size. +// For RowWrapLayout this will arrange all objects into rows of equal size. func (l *rowWrapLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.Size) { if len(objects) == 0 { return } - padding := theme.Padding() rowHeight := objects[0].MinSize().Height pos := fyne.NewPos(0, 0) rows := 1 @@ -55,9 +76,9 @@ func (l *rowWrapLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.S } size := o.MinSize() o.Resize(size) - w := size.Width + padding + w := size.Width + l.horizontalPadding if pos.X+w > containerSize.Width { - pos = fyne.NewPos(0, float32(rows)*(rowHeight+padding)) + pos = fyne.NewPos(0, float32(rows)*(rowHeight+l.verticalPadding)) rows++ } o.Move(pos)