From c76160a2703596ad95c644946473abbc3700b684 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Mon, 17 Mar 2025 17:34:12 +0100 Subject: [PATCH 1/7] 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 ed312ab1cfab53dbb4d741e427c510b1e72b1774 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Mon, 17 Mar 2025 17:55:12 +0100 Subject: [PATCH 2/7] 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 7ad569a19a2ae1836d659a72bee33546ac575fd1 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Wed, 9 Apr 2025 16:08:17 +0200 Subject: [PATCH 3/7] 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) From 0b9db05de558f1b056650b240a9fefd96a2fb737 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Wed, 28 May 2025 21:28:48 +0200 Subject: [PATCH 4/7] Implement review notes --- layout/rowwrap.go | 42 ++++++++++++++++++++++++++---------------- layout/rowwrap_test.go | 22 +++++++++++++--------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/layout/rowwrap.go b/layout/rowwrap.go index db269787fb..0da8802679 100644 --- a/layout/rowwrap.go +++ b/layout/rowwrap.go @@ -11,16 +11,16 @@ type rowWrapLayout struct { verticalPadding float32 } -// NewRowWrapLayout returns a layout that dynamically arranges objects -// with the same height in rows and wraps them as necessary. -// -// Object visibility is supported. +// NewRowWrapLayout returns a layout that dynamically arranges objects of similar height +// in rows and wraps them as necessary. +// The height of the rows is determined by the tallest object and the same for all rows. // // Since: 2.7 func NewRowWrapLayout() fyne.Layout { + p := theme.Padding() return &rowWrapLayout{ - horizontalPadding: theme.Padding(), - verticalPadding: theme.Padding(), + horizontalPadding: p, + verticalPadding: p, } } @@ -39,7 +39,7 @@ 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, +// and the height of the tallest 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 { @@ -49,16 +49,16 @@ func (l *rowWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { if rows == 0 { rows = 1 } - rowHeight := objects[0].MinSize().Height - var width float32 + var w, h float32 for _, o := range objects { - size := o.MinSize() - if size.Width > width { - width = size.Width + if !o.Visible() { + continue } + s := o.MinSize() + w = fyne.Max(w, s.Width) + h = fyne.Max(h, s.Height) } - s := fyne.NewSize(width, rowHeight*float32(rows)+l.verticalPadding*float32(rows-1)) - return s + return fyne.NewSize(w, h*float32(rows)+l.verticalPadding*float32(rows-1)) } // Layout is called to pack all child objects into a specified size. @@ -67,7 +67,14 @@ func (l *rowWrapLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.S if len(objects) == 0 { return } - rowHeight := objects[0].MinSize().Height + var h float32 + for _, o := range objects { + if !o.Visible() { + continue + } + h = fyne.Max(h, o.MinSize().Height) + } + var minSize fyne.Size pos := fyne.NewPos(0, 0) rows := 1 for _, o := range objects { @@ -78,11 +85,14 @@ func (l *rowWrapLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.S o.Resize(size) w := size.Width + l.horizontalPadding if pos.X+w > containerSize.Width { - pos = fyne.NewPos(0, float32(rows)*(rowHeight+l.verticalPadding)) + y := float32(rows) * (h + l.verticalPadding) + pos = fyne.NewPos(0, y) + minSize.Height = fyne.Max(minSize.Height, y) rows++ } o.Move(pos) pos = pos.Add(fyne.NewPos(w, 0)) + minSize.Width = fyne.Max(minSize.Width, w) } l.rowCount = rows } diff --git a/layout/rowwrap_test.go b/layout/rowwrap_test.go index e225450c08..3a869916d9 100644 --- a/layout/rowwrap_test.go +++ b/layout/rowwrap_test.go @@ -82,10 +82,11 @@ func TestRowWrapLayout_MinSize(t *testing.T) { layout := layout.NewRowWrapLayout() // when/then - minSize := layout.MinSize(container.Objects) + got := layout.MinSize(container.Objects) // then - assert.Equal(t, o.MinSize(), minSize) + want := o.MinSize() + assert.Equal(t, want, got) }) t.Run("should return size 0 when container is empty", func(t *testing.T) { // given @@ -93,10 +94,11 @@ func TestRowWrapLayout_MinSize(t *testing.T) { layout := layout.NewRowWrapLayout() // when/then - minSize := layout.MinSize(container.Objects) + got := layout.MinSize(container.Objects) // then - assert.Equal(t, fyne.NewSize(0, 0), minSize) + want := fyne.NewSize(0, 0) + assert.Equal(t, want, got) }) t.Run("should initially return height of first object and width of widest object", func(t *testing.T) { // given @@ -109,12 +111,13 @@ func TestRowWrapLayout_MinSize(t *testing.T) { layout := layout.NewRowWrapLayout() // when/then - minSize := layout.MinSize(container.Objects) + got := layout.MinSize(container.Objects) // then - assert.Equal(t, fyne.NewSize(20, h), minSize) + want := fyne.NewSize(20, h) + assert.Equal(t, want, got) }) - t.Run("should return height of arranged objects after layout was calculated", func(t *testing.T) { + t.Run("should return actual size of arranged objects after layout was calculated", func(t *testing.T) { // given h := float32(10) o1 := canvas.NewRectangle(color.Opaque) @@ -125,9 +128,10 @@ func TestRowWrapLayout_MinSize(t *testing.T) { container.Resize(fyne.NewSize(15, 50)) // when/then - minSize := container.MinSize() + got := container.MinSize() // then - assert.Equal(t, fyne.NewSize(o2.Size().Width, (o1.Size().Height*2)+theme.Padding()), minSize) + want := fyne.NewSize(o2.Size().Width, (o1.Size().Height*2)+theme.Padding()) + assert.Equal(t, want, got) }) } From 8e26f6eca3c682d4f9f44081720a20d7fdc83566 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Thu, 12 Jun 2025 16:03:11 +0200 Subject: [PATCH 5/7] Improve implementation & tests --- layout/rowwrap.go | 55 +++++---- layout/rowwrap_test.go | 246 ++++++++++++++++++++++++++++------------- 2 files changed, 201 insertions(+), 100 deletions(-) diff --git a/layout/rowwrap.go b/layout/rowwrap.go index 0da8802679..a46b4897bf 100644 --- a/layout/rowwrap.go +++ b/layout/rowwrap.go @@ -6,14 +6,14 @@ import ( ) type rowWrapLayout struct { - rowCount int horizontalPadding float32 + minSize fyne.Size verticalPadding float32 } // NewRowWrapLayout returns a layout that dynamically arranges objects of similar height -// in rows and wraps them as necessary. -// The height of the rows is determined by the tallest object and the same for all rows. +// in rows and wraps them dynamically. +// Objects are separated with horizontal and vertical inner padding. // // Since: 2.7 func NewRowWrapLayout() fyne.Layout { @@ -24,8 +24,8 @@ func NewRowWrapLayout() fyne.Layout { } } -// NewRowWrapLayoutWithCustomPadding creates a new RowWrapLayout instance -// with the specified paddings. +// NewRowWrapLayoutWithCustomPadding returns a new RowWrapLayout instance +// with custom horizontal and vertical inner padding. // // Since: 2.7 func NewRowWrapLayoutWithCustomPadding(horizontal, vertical float32) fyne.Layout { @@ -38,61 +38,70 @@ func NewRowWrapLayoutWithCustomPadding(horizontal, vertical float32) fyne.Layout 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 +// 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) } - rows := l.rowCount - if rows == 0 { - rows = 1 + if !l.minSize.IsZero() { + return l.minSize } - var w, h float32 + var maxW, maxH float32 + var objCount int for _, o := range objects { if !o.Visible() { continue } + objCount++ s := o.MinSize() - w = fyne.Max(w, s.Width) - h = fyne.Max(h, s.Height) + maxW = fyne.Max(maxW, s.Width) + maxH = fyne.Max(maxH, s.Height) } - return fyne.NewSize(w, h*float32(rows)+l.verticalPadding*float32(rows-1)) + return fyne.NewSize(maxW, l.minHeight(maxH, objCount)) +} + +func (l *rowWrapLayout) minHeight(rowHeight float32, rowCount int) float32 { + height := rowHeight*float32(rowCount) + l.verticalPadding*float32(rowCount-1) + return height } // Layout is called to pack all child objects into a specified size. -// For RowWrapLayout this will arrange all objects into rows of equal 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 h float32 + var maxH float32 for _, o := range objects { if !o.Visible() { continue } - h = fyne.Max(h, o.MinSize().Height) + 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) - w := size.Width + l.horizontalPadding - if pos.X+w > containerSize.Width { - y := float32(rows) * (h + l.verticalPadding) + if !isFirst && pos.X+size.Width+l.horizontalPadding >= containerSize.Width { + y := float32(rows) * (maxH + l.verticalPadding) pos = fyne.NewPos(0, y) - minSize.Height = fyne.Max(minSize.Height, 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(w, 0)) - minSize.Width = fyne.Max(minSize.Width, w) + pos = pos.Add(fyne.NewPos(size.Width+l.horizontalPadding, 0)) } - l.rowCount = rows + l.minSize = minSize } diff --git a/layout/rowwrap_test.go b/layout/rowwrap_test.go index 3a869916d9..55a315a2f4 100644 --- a/layout/rowwrap_test.go +++ b/layout/rowwrap_test.go @@ -12,126 +12,218 @@ import ( "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) { +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 - 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) + a := makeObject(10, 10) + container := container.NewWithoutLayout(a) + layout := layout.NewRowWrapLayout() - // when - layout.NewRowWrapLayout().Layout(container.Objects, containerSize) + // when/then + got := layout.MinSize(container.Objects) // 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) + want := a.MinSize() + assert.Equal(t, want, got) }) - t.Run("should ignore hidden objects", func(t *testing.T) { + t.Run("should return size 0 when container is empty", 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) + container := container.NewWithoutLayout() + layout := layout.NewRowWrapLayout() - // when - layout.NewRowWrapLayout().Layout(container.Objects, containerSize) + // when/then + got := layout.MinSize(container.Objects) // 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()) + want := fyne.NewSize(0, 0) + assert.Equal(t, want, got) }) -} - -func TestRowWrapLayout_MinSize(t *testing.T) { - t.Run("should return min size of single object when container has only one", func(t *testing.T) { + t.Run("should estimate min size when layout not yet known", func(t *testing.T) { // given - o := canvas.NewRectangle(color.Opaque) - o.SetMinSize(fyne.NewSize(10, 10)) - container := container.NewWithoutLayout(o) + 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 := o.MinSize() + want := fyne.NewSize(20, 10+p+10) assert.Equal(t, want, got) }) - t.Run("should return size 0 when container is empty", func(t *testing.T) { + t.Run("should use custom padding when estimating min size", func(t *testing.T) { // given - container := container.NewWithoutLayout() - layout := layout.NewRowWrapLayout() + 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(0, 0) + want := fyne.NewSize(20, 10+7+10) assert.Equal(t, want, got) }) - t.Run("should initially return height of first object and width of widest object", func(t *testing.T) { + t.Run("should ignore invisible objects when estimating min size", 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) + 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(20, h) + 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 - 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)) + 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 - want := fyne.NewSize(o2.Size().Width, (o1.Size().Height*2)+theme.Padding()) + 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 +} From 5a6a0593d11928246e002c0341c87e73920b4c4e Mon Sep 17 00:00:00 2001 From: Erik Kalkoken Date: Mon, 16 Jun 2025 20:25:48 +0200 Subject: [PATCH 6/7] Update layout/rowwrap.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jacob Alzén --- layout/rowwrap.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/layout/rowwrap.go b/layout/rowwrap.go index a46b4897bf..78ecd1dbdf 100644 --- a/layout/rowwrap.go +++ b/layout/rowwrap.go @@ -64,8 +64,7 @@ func (l *rowWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { } func (l *rowWrapLayout) minHeight(rowHeight float32, rowCount int) float32 { - height := rowHeight*float32(rowCount) + l.verticalPadding*float32(rowCount-1) - return height + return rowHeight*float32(rowCount) + l.verticalPadding*float32(rowCount-1) } // Layout is called to pack all child objects into a specified size. From 48e3034f66002071337650b608eb9f0ba7c0e0f1 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Mon, 16 Jun 2025 20:36:54 +0200 Subject: [PATCH 7/7] Implement reviewer notes #2 --- layout/rowwrap.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layout/rowwrap.go b/layout/rowwrap.go index 78ecd1dbdf..b66c3bdc68 100644 --- a/layout/rowwrap.go +++ b/layout/rowwrap.go @@ -13,7 +13,7 @@ type rowWrapLayout struct { // NewRowWrapLayout returns a layout that dynamically arranges objects of similar height // in rows and wraps them dynamically. -// Objects are separated with horizontal and vertical inner padding. +// Objects are separated with horizontal and vertical padding. // // Since: 2.7 func NewRowWrapLayout() fyne.Layout { @@ -25,7 +25,7 @@ func NewRowWrapLayout() fyne.Layout { } // NewRowWrapLayoutWithCustomPadding returns a new RowWrapLayout instance -// with custom horizontal and vertical inner padding. +// with custom horizontal and inner padding. // // Since: 2.7 func NewRowWrapLayoutWithCustomPadding(horizontal, vertical float32) fyne.Layout {