From f815323143f10384a44f525cad8ac021ebf0606a Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 30 Apr 2024 21:57:35 +0200 Subject: [PATCH] Introduce a new Edge container This new container is a replacement for container.NewBorder but without the footguns. There is a center item instead of a slcie of objects that act like a Stack container. The center item can optionally be nil also. --- container/layouts.go | 28 +++++++ layout/edge.go | 94 +++++++++++++++++++++++ layout/edge_test.go | 172 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+) create mode 100644 layout/edge.go create mode 100644 layout/edge_test.go diff --git a/container/layouts.go b/container/layouts.go index e6c53c69ea..4b37a360fd 100644 --- a/container/layouts.go +++ b/container/layouts.go @@ -49,6 +49,34 @@ func NewCenter(objects ...fyne.CanvasObject) *fyne.Container { return New(layout.NewCenterLayout(), objects...) } +// NewEdge creates a new container with the specified objects and using the edge layout. +// The top, bottom, left and right parameters specify the items that should be placed around edges, +// with the center object placed in the center. Nil can be used to an edge if it should not be filled. +// Items at the top and bottom edges use the item's MinSize height and items at the left and right +// edges use the item's MinSize width. +// +// Since: 2.5 +func NewEdge(top, bottom, left, right, center fyne.CanvasObject) *fyne.Container { + objects := make([]fyne.CanvasObject, 0, 5) + if top != nil { + objects = append(objects, top) + } + if bottom != nil { + objects = append(objects, bottom) + } + if left != nil { + objects = append(objects, left) + } + if right != nil { + objects = append(objects, right) + } + if center != nil { + objects = append(objects, center) + } + + return New(layout.NewEdge(top, bottom, left, right, center), objects...) +} + // NewGridWithColumns creates a new container with the specified objects and using the grid layout with // a specified number of columns. The number of rows will depend on how many children are in the container. // diff --git a/layout/edge.go b/layout/edge.go new file mode 100644 index 0000000000..deef558435 --- /dev/null +++ b/layout/edge.go @@ -0,0 +1,94 @@ +package layout + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// NewEdge creates a new edge layout instance with top, bottom, left, right +// and center objects set. +func NewEdge(top, bottom, left, right, center fyne.CanvasObject) fyne.Layout { + return edgeLayout{top: top, bottom: bottom, left: left, right: right, center: center} +} + +// Declare conformity with Layout interface +var _ fyne.Layout = (*borderLayout)(nil) + +type edgeLayout struct { + top, bottom, left, right, center fyne.CanvasObject +} + +// Layout is called to pack all child objects into a specified size. +// For BorderLayout this arranges the top, bottom, left and right widgets at +// the sides and any remaining widgets are maximised in the middle space. +func (b edgeLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + padding := theme.Padding() + var topSize, bottomSize, leftSize, rightSize fyne.Size + if b.top != nil && b.top.Visible() { + topHeight := b.top.MinSize().Height + b.top.Resize(fyne.NewSize(size.Width, topHeight)) + b.top.Move(fyne.NewPos(0, 0)) + topSize = fyne.NewSize(size.Width, topHeight+padding) + } + if b.bottom != nil && b.bottom.Visible() { + bottomHeight := b.bottom.MinSize().Height + b.bottom.Resize(fyne.NewSize(size.Width, bottomHeight)) + b.bottom.Move(fyne.NewPos(0, size.Height-bottomHeight)) + bottomSize = fyne.NewSize(size.Width, bottomHeight+padding) + } + if b.left != nil && b.left.Visible() { + leftWidth := b.left.MinSize().Width + b.left.Resize(fyne.NewSize(leftWidth, size.Height-topSize.Height-bottomSize.Height)) + b.left.Move(fyne.NewPos(0, topSize.Height)) + leftSize = fyne.NewSize(leftWidth+padding, size.Height-topSize.Height-bottomSize.Height) + } + if b.right != nil && b.right.Visible() { + rightWidth := b.right.MinSize().Width + b.right.Resize(fyne.NewSize(rightWidth, size.Height-topSize.Height-bottomSize.Height)) + b.right.Move(fyne.NewPos(size.Width-rightWidth, topSize.Height)) + rightSize = fyne.NewSize(rightWidth+padding, size.Height-topSize.Height-bottomSize.Height) + } + if b.center != nil && b.center.Visible() { + middleSize := fyne.NewSize(size.Width-leftSize.Width-rightSize.Width, size.Height-topSize.Height-bottomSize.Height) + middlePos := fyne.NewPos(leftSize.Width, topSize.Height) + b.center.Resize(middleSize) + b.center.Move(middlePos) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For the edge layout, this is determined by the MinSize height of the top and +// plus the MinSize width of the left and right, plus any padding needed. +// This is then added to the union of the MinSize for any remaining content. +func (b edgeLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + padding := theme.Padding() + + var minSize fyne.Size + if b.center != nil && b.center.Visible() { + minSize = b.center.MinSize() + } + + if b.left != nil && b.left.Visible() { + leftMin := b.left.MinSize() + minHeight := fyne.Max(minSize.Height, leftMin.Height) + minSize = fyne.NewSize(minSize.Width+leftMin.Width+padding, minHeight) + } + if b.right != nil && b.right.Visible() { + rightMin := b.right.MinSize() + minHeight := fyne.Max(minSize.Height, rightMin.Height) + minSize = fyne.NewSize(minSize.Width+rightMin.Width+padding, minHeight) + } + + if b.top != nil && b.top.Visible() { + topMin := b.top.MinSize() + minWidth := fyne.Max(minSize.Width, topMin.Width) + minSize = fyne.NewSize(minWidth, minSize.Height+topMin.Height+padding) + } + if b.bottom != nil && b.bottom.Visible() { + bottomMin := b.bottom.MinSize() + minWidth := fyne.Max(minSize.Width, bottomMin.Width) + minSize = fyne.NewSize(minWidth, minSize.Height+bottomMin.Height+padding) + } + + return minSize +} diff --git a/layout/edge_test.go b/layout/edge_test.go new file mode 100644 index 0000000000..bc56106acc --- /dev/null +++ b/layout/edge_test.go @@ -0,0 +1,172 @@ +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/test" + "fyne.io/fyne/v2/theme" + + "github.com/stretchr/testify/assert" +) + +func TestNewEdgeContainer(t *testing.T) { + top := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + top.SetMinSize(fyne.NewSize(10, 10)) + right := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + right.SetMinSize(fyne.NewSize(10, 10)) + middle := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + + c := container.NewEdge(top, nil, nil, right, middle) + assert.Equal(t, 3, len(c.Objects)) + + c.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), top.Position().X) + assert.Equal(t, float32(0), top.Position().Y) + assert.Equal(t, float32(90), right.Position().X) + assert.Equal(t, 10+theme.Padding(), right.Position().Y) + assert.Equal(t, float32(0), middle.Position().X) + assert.Equal(t, 10+theme.Padding(), middle.Position().Y) + assert.Equal(t, 90-theme.Padding(), middle.Size().Width) + assert.Equal(t, 90-theme.Padding(), middle.Size().Height) +} + +func TestEdgeayout_Size_Empty(t *testing.T) { + size := fyne.NewSize(100, 100) + + obj := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + container := &fyne.Container{ + Objects: []fyne.CanvasObject{obj}, + } + container.Resize(size) + + layout.NewEdge(nil, nil, nil, nil, obj).Layout(container.Objects, size) + + assert.Equal(t, obj.Size(), size) +} + +func TestEdgeLayout_Size_TopBottom(t *testing.T) { + size := fyne.NewSize(100, 100) + + obj1 := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + obj2 := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + obj3 := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + + container := &fyne.Container{ + Objects: []fyne.CanvasObject{obj1, obj2, obj3}, + } + container.Resize(size) + + layout.NewEdge(obj1, obj2, nil, nil, obj3).Layout(container.Objects, size) + + innerSize := fyne.NewSize(size.Width, size.Height-obj1.Size().Height-obj2.Size().Height-theme.Padding()*2) + assert.Equal(t, innerSize, obj3.Size()) + assert.Equal(t, fyne.NewPos(0, 0), obj1.Position()) + assert.Equal(t, fyne.NewPos(0, size.Height-obj2.Size().Height), obj2.Position()) + assert.Equal(t, fyne.NewPos(0, obj1.Size().Height+theme.Padding()), obj3.Position()) +} + +func TestEdgeLayout_Size_LeftRight(t *testing.T) { + size := fyne.NewSize(100, 100) + + obj1 := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + obj2 := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + obj3 := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) + + container := &fyne.Container{ + Objects: []fyne.CanvasObject{obj1, obj2, obj3}, + } + container.Resize(size) + + layout.NewEdge(nil, nil, obj1, obj2, obj3).Layout(container.Objects, size) + + innerSize := fyne.NewSize(size.Width-obj1.Size().Width-obj2.Size().Width-theme.Padding()*2, size.Height) + assert.Equal(t, innerSize, obj3.Size()) + assert.Equal(t, fyne.NewPos(0, 0), obj1.Position()) + assert.Equal(t, fyne.NewPos(size.Width-obj2.Size().Width, 0), obj2.Position()) + assert.Equal(t, fyne.NewPos(obj1.Size().Width+theme.Padding(), 0), obj3.Position()) +} + +func TestEdgeLayout_MinSize_Center(t *testing.T) { + text := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + minSize := text.MinSize() + + container := container.NewWithoutLayout(text) + layoutMin := layout.NewEdge(nil, nil, nil, nil, text).MinSize(container.Objects) + + assert.Equal(t, minSize, layoutMin) +} + +func TestEdgeLayout_MinSize_TopBottom(t *testing.T) { + text1 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text2 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text3 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + minSize := fyne.NewSize(text3.MinSize().Width, text1.MinSize().Height+text2.MinSize().Height+text3.MinSize().Height+theme.Padding()*2) + + container := container.NewWithoutLayout(text1, text2, text3) + layoutMin := layout.NewEdge(text1, text2, nil, nil, text3).MinSize(container.Objects) + + assert.Equal(t, minSize, layoutMin) +} + +func TestEdgeLayout_MinSize_TopBottomHidden(t *testing.T) { + text1 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text1.Hide() + text2 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text2.Hide() + text3 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + + container := container.NewWithoutLayout(text1, text2, text3) + layoutMin := layout.NewEdge(text1, text2, nil, nil, text3).MinSize(container.Objects) + + assert.Equal(t, text1.MinSize(), layoutMin) +} + +func TestEdgeLayout_MinSize_TopOnly(t *testing.T) { + text1 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + minSize := fyne.NewSize(text1.MinSize().Width, text1.MinSize().Height+theme.Padding()) + + container := container.NewWithoutLayout(text1) + layoutMin := layout.NewEdge(text1, nil, nil, nil, nil).MinSize(container.Objects) + + assert.Equal(t, minSize, layoutMin) +} + +func TestEdgeLayout_MinSize_LeftRight(t *testing.T) { + text1 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text2 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text3 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + minSize := fyne.NewSize(text1.MinSize().Width+text2.MinSize().Width+text3.MinSize().Width+theme.Padding()*2, text3.MinSize().Height) + + container := container.NewWithoutLayout(text1, text2, text3) + layoutMin := layout.NewEdge(nil, nil, text1, text2, text3).MinSize(container.Objects) + + assert.Equal(t, minSize, layoutMin) +} + +func TestEdgeLayout_MinSize_LeftRightHidden(t *testing.T) { + text1 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text1.Hide() + text2 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + text2.Hide() + text3 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + + container := container.NewWithoutLayout(text1, text2, text3) + layoutMin := layout.NewEdge(nil, nil, text1, text2, text3).MinSize(container.Objects) + + assert.Equal(t, text3.MinSize(), layoutMin) +} + +func TestEdgeLayout_MinSize_LeftOnly(t *testing.T) { + text1 := canvas.NewText("Padding", color.NRGBA{0, 0xff, 0, 0}) + minSize := fyne.NewSize(text1.MinSize().Width+theme.Padding(), text1.MinSize().Height) + + container := container.NewWithoutLayout(text1) + layoutMin := layout.NewEdge(nil, nil, text1, nil, nil).MinSize(container.Objects) + + assert.Equal(t, minSize, layoutMin) +}