Skip to content

Commit

Permalink
feat: Allow setting custom Layout paddings
Browse files Browse the repository at this point in the history
Introduce a `LayoutOption` concept, and allow setting custom paddings
for a Layout. The `BaseLayout` struct implements the default paddings
retrieval, to reduce the amount of changes required by project
maintainers to comply with the new interface methods.

This change uses the Option pattern in case other customizations can be
added to Layouts in the future, without changing the public API.

Main motivation for this change has been Supersonic needing to copy the
entire Vbox/Hbox implementation, just to add custom paddings [1]. Also
related to feature request fyne-io#1031.

[1] https://github.com/dweymouth/supersonic/blob/02d4082f3dae4d1d54a4384489785a5984a7a7de/ui/layouts/hboxcustompadding.go
  • Loading branch information
adamantike committed Apr 27, 2024
1 parent e05c0a5 commit 748a8c5
Show file tree
Hide file tree
Showing 21 changed files with 228 additions and 95 deletions.
18 changes: 11 additions & 7 deletions cmd/fyne_demo/tutorials/welcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func welcomeScreen(_ fyne.Window) fyne.CanvasObject {
underlay := canvas.NewImageFromResource(data.FyneLogo)
bg := canvas.NewRectangle(bgColor)
underlayer := underLayout{}
slideBG := container.New(underlayer, underlay)
slideBG := container.New(&underlayer, underlay)
footerBG := canvas.NewRectangle(shadowColor)

listen := make(chan fyne.Settings)
Expand All @@ -81,10 +81,10 @@ func welcomeScreen(_ fyne.Window) fyne.CanvasObject {

bgClip := container.NewScroll(slideBG)
bgClip.Direction = container.ScrollNone
return container.NewStack(container.New(unpad{top: true}, bgClip, bg),
return container.NewStack(container.New(&unpad{top: true}, bgClip, bg),
container.NewBorder(nil,
container.NewStack(footerBG, footer), nil, nil,
container.New(unpad{top: true, bottom: true}, scroll)))
container.New(&unpad{top: true, bottom: true}, scroll)))
}

func withAlpha(c color.Color, alpha uint8) color.Color {
Expand All @@ -93,6 +93,8 @@ func withAlpha(c color.Color, alpha uint8) color.Color {
}

type underLayout struct {
layout.BaseLayout

offset float32
}

Expand All @@ -107,21 +109,23 @@ func (u underLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
}

type unpad struct {
layout.BaseLayout

top, bottom bool
}

func (u unpad) Layout(objs []fyne.CanvasObject, s fyne.Size) {
pad := theme.Padding()
topPadding, bottomPadding, _, _ := u.GetPaddings()
var pos fyne.Position
if u.top {
pos = fyne.NewPos(0, -pad)
pos = fyne.NewPos(0, -topPadding)
}
size := s
if u.top {
size = size.AddWidthHeight(0, pad)
size = size.AddWidthHeight(0, topPadding)
}
if u.bottom {
size = size.AddWidthHeight(0, pad)
size = size.AddWidthHeight(0, bottomPadding)
}
for _, o := range objs {
o.Move(pos)
Expand Down
2 changes: 2 additions & 0 deletions container/multiplewindows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package container
import (
"fyne.io/fyne/v2"
intWidget "fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)

Expand Down Expand Up @@ -92,6 +93,7 @@ func (m *MultipleWindows) setupChild(w *InnerWindow) {
}

type multiWinLayout struct {
layout.BaseLayout
}

func (m *multiWinLayout) Layout(objects []fyne.CanvasObject, _ fyne.Size) {
Expand Down
6 changes: 6 additions & 0 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ func (c *customLayout) MinSize(objs []CanvasObject) Size {
return NewSize(10, float32(10*len(objs)))
}

func (c *customLayout) GetPaddings() (float32, float32, float32, float32) {
return 0, 0, 0, 0
}

func (c *customLayout) SetPaddingFn(paddingFn func() (float32, float32, float32, float32)) {}

type dummyObject struct {
size Size
pos Position
Expand Down
2 changes: 2 additions & 0 deletions dialog/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ func (renderer *themedBackgroundRenderer) Refresh() {
// ===============================================================

type dialogLayout struct {
layout.BaseLayout

d *dialog
}

Expand Down
5 changes: 4 additions & 1 deletion dialog/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/storage/repository"
"fyne.io/fyne/v2/theme"
Expand Down Expand Up @@ -911,10 +912,12 @@ func storageURI(a fyne.App) fyne.URI {
// iconPaddingLayout adds padding to the left of a widget.Icon().
// NOTE: It assumes that the slice only contains one item.
type iconPaddingLayout struct {
layout.BaseLayout
}

func (i *iconPaddingLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
padding := theme.Padding() * 2
_, _, leftPadding, _ := i.GetPaddings()
padding := leftPadding * 2
objects[0].Move(fyne.NewPos(padding, 0))
objects[0].Resize(size.SubtractWidthHeight(padding, 0))
}
Expand Down
6 changes: 6 additions & 0 deletions internal/driver/glfw/canvas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,12 @@ func (l *recordingLayout) MinSize([]fyne.CanvasObject) fyne.Size {
return fyne.NewSize(6, 9)
}

func (l *recordingLayout) GetPaddings() (float32, float32, float32, float32) {
return 0, 0, 0, 0
}

func (l *recordingLayout) SetPaddingFn(paddingFn func() (float32, float32, float32, float32)) {}

func (l *recordingLayout) popLayoutEvent() (e any) {
e, l.layoutEvents = pop(l.layoutEvents)
return
Expand Down
6 changes: 6 additions & 0 deletions layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ type Layout interface {
// MinSize calculates the smallest size that will fit the listed
// CanvasObjects using this Layout algorithm.
MinSize(objects []CanvasObject) Size
// GetPaddings returns the top, bottom, left and right paddings used
// by this layout.
GetPaddings() (float32, float32, float32, float32)
// SetPaddingFn sets the function that will be called to get the
// padding values for this layout.
SetPaddingFn(fn func() (float32, float32, float32, float32))
}
19 changes: 19 additions & 0 deletions layout/baselayout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package layout

import "fyne.io/fyne/v2/theme"

type BaseLayout struct {
paddingFn func() (float32, float32, float32, float32)
}

func (b *BaseLayout) SetPaddingFn(fn func() (float32, float32, float32, float32)) {
b.paddingFn = fn
}

func (b *BaseLayout) GetPaddings() (float32, float32, float32, float32) {
if b.paddingFn == nil {
padding := theme.Padding()
return padding, padding, padding, padding
}
return b.paddingFn()
}
32 changes: 19 additions & 13 deletions layout/borderlayout.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,58 @@ package layout

import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)

// Declare conformity with Layout interface
var _ fyne.Layout = (*borderLayout)(nil)

type borderLayout struct {
BaseLayout

top, bottom, left, right fyne.CanvasObject
}

// NewBorderLayout creates a new BorderLayout instance with top, bottom, left
// and right objects set. All other items in the container will fill the centre
// space
func NewBorderLayout(top, bottom, left, right fyne.CanvasObject) fyne.Layout {
return &borderLayout{top, bottom, left, right}
func NewBorderLayout(top, bottom, left, right fyne.CanvasObject, options ...LayoutOption) fyne.Layout {
l := &borderLayout{top: top, bottom: bottom, left: left, right: right}
for _, option := range options {
option(l)
}
return l
}

// 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 *borderLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
padding := theme.Padding()
topPadding, bottomPadding, leftPadding, rightPadding := b.GetPaddings()

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)
topSize = fyne.NewSize(size.Width, topHeight+topPadding)
}
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)
bottomSize = fyne.NewSize(size.Width, bottomHeight+bottomPadding)
}
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)
leftSize = fyne.NewSize(leftWidth+leftPadding, 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)
rightSize = fyne.NewSize(rightWidth+rightPadding, size.Height-topSize.Height-bottomSize.Height)
}

middleSize := fyne.NewSize(size.Width-leftSize.Width-rightSize.Width, size.Height-topSize.Height-bottomSize.Height)
Expand Down Expand Up @@ -80,28 +86,28 @@ func (b *borderLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
}
}

padding := theme.Padding()
topPadding, bottomPadding, leftPadding, rightPadding := b.GetPaddings()

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)
minSize = fyne.NewSize(minSize.Width+leftMin.Width+leftPadding, 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)
minSize = fyne.NewSize(minSize.Width+rightMin.Width+rightPadding, 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)
minSize = fyne.NewSize(minWidth, minSize.Height+topMin.Height+topPadding)
}
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)
minSize = fyne.NewSize(minWidth, minSize.Height+bottomMin.Height+bottomPadding)
}

return minSize
Expand Down
45 changes: 28 additions & 17 deletions layout/boxlayout.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,38 @@ package layout

import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)

// NewVBoxLayout returns a vertical box layout for stacking a number of child
// canvas objects or widgets top to bottom. The objects are always displayed
// at their vertical MinSize. Use a different layout if the objects are intended
// to be larger then their vertical MinSize.
func NewVBoxLayout() fyne.Layout {
return vBoxLayout{}
func NewVBoxLayout(options ...LayoutOption) fyne.Layout {
l := &vBoxLayout{}
for _, option := range options {
option(l)
}
return l
}

// NewHBoxLayout returns a horizontal box layout for stacking a number of child
// canvas objects or widgets left to right. The objects are always displayed
// at their horizontal MinSize. Use a different layout if the objects are intended
// to be larger then their horizontal MinSize.
func NewHBoxLayout() fyne.Layout {
return hBoxLayout{}
func NewHBoxLayout(options ...LayoutOption) fyne.Layout {
l := &hBoxLayout{}
for _, option := range options {
option(l)
}
return l
}

// Declare conformity with Layout interface
var _ fyne.Layout = (*vBoxLayout)(nil)

type vBoxLayout struct{}
type vBoxLayout struct {
BaseLayout
}

// Layout is called to pack all child objects into a specified size.
// This will pack objects into a single column where each item
Expand All @@ -50,10 +59,10 @@ func (v vBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
total += child.MinSize().Height
}

padding := theme.Padding()
topPadding, _, _, _ := v.GetPaddings()

// Amount of space not taken up by visible objects and inter-object padding
extra := size.Height - total - (padding * float32(visibleObjects-1))
extra := size.Height - total - (topPadding * float32(visibleObjects-1))

// Spacers split extra space equally
spacerSize := float32(0)
Expand All @@ -74,7 +83,7 @@ func (v vBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
child.Move(fyne.NewPos(x, y))

height := child.MinSize().Height
y += padding + height
y += topPadding + height
child.Resize(fyne.NewSize(size.Width, height))
}
}
Expand All @@ -85,7 +94,7 @@ func (v vBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
func (v vBoxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
minSize := fyne.NewSize(0, 0)
addPadding := false
padding := theme.Padding()
topPadding, _, _, _ := v.GetPaddings()
for _, child := range objects {
if !child.Visible() || isVerticalSpacer(child) {
continue
Expand All @@ -95,7 +104,7 @@ func (v vBoxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
minSize.Width = fyne.Max(childMin.Width, minSize.Width)
minSize.Height += childMin.Height
if addPadding {
minSize.Height += padding
minSize.Height += topPadding
}
addPadding = true
}
Expand All @@ -105,7 +114,9 @@ func (v vBoxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
// Declare conformity with Layout interface
var _ fyne.Layout = (*hBoxLayout)(nil)

type hBoxLayout struct{}
type hBoxLayout struct {
BaseLayout
}

// Layout is called to pack all child objects into a specified size.
// For a VBoxLayout this will pack objects into a single column where each item
Expand All @@ -131,10 +142,10 @@ func (g hBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
total += child.MinSize().Width
}

padding := theme.Padding()
_, _, leftPadding, _ := g.GetPaddings()

// Amount of space not taken up by visible objects and inter-object padding
extra := size.Width - total - (padding * float32(visibleObjects-1))
extra := size.Width - total - (leftPadding * float32(visibleObjects-1))

// Spacers split extra space equally
spacerSize := float32(0)
Expand All @@ -155,7 +166,7 @@ func (g hBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
child.Move(fyne.NewPos(x, y))

width := child.MinSize().Width
x += padding + width
x += leftPadding + width
child.Resize(fyne.NewSize(width, size.Height))
}
}
Expand All @@ -166,7 +177,7 @@ func (g hBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
func (g hBoxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
minSize := fyne.NewSize(0, 0)
addPadding := false
padding := theme.Padding()
_, _, leftPadding, _ := g.GetPaddings()
for _, child := range objects {
if !child.Visible() || isHorizontalSpacer(child) {
continue
Expand All @@ -176,7 +187,7 @@ func (g hBoxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
minSize.Height = fyne.Max(childMin.Height, minSize.Height)
minSize.Width += childMin.Width
if addPadding {
minSize.Width += padding
minSize.Width += leftPadding
}
addPadding = true
}
Expand Down
Loading

0 comments on commit 748a8c5

Please sign in to comment.