Skip to content

Commit

Permalink
Introduce a new Edge container
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Jacalz committed Apr 30, 2024
1 parent 56342a2 commit f815323
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 0 deletions.
28 changes: 28 additions & 0 deletions container/layouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
94 changes: 94 additions & 0 deletions layout/edge.go
Original file line number Diff line number Diff line change
@@ -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
}
172 changes: 172 additions & 0 deletions layout/edge_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit f815323

Please sign in to comment.