Skip to content

Commit

Permalink
Make it a real container and optimize computation
Browse files Browse the repository at this point in the history
  • Loading branch information
metal3d committed Oct 30, 2023
1 parent 6770176 commit b01b4a2
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 62 deletions.
5 changes: 3 additions & 2 deletions cmd/responsive_layout/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
xcontainer "fyne.io/x/fyne/container"
"fyne.io/x/fyne/layout"
)

Expand All @@ -23,7 +24,7 @@ func main() {
dialog.NewInformation("Hello", "Hello World", window).Show()
})

resp := layout.NewResponsiveLayout(
resp := xcontainer.NewResponsive(
presentation(), // 100% by default
winSizeLabel(window), // 100% by default
layout.Responsive(
Expand Down Expand Up @@ -118,7 +119,7 @@ func formLayout() fyne.CanvasObject {
entryw := float32(.75)
labelx := layout.OneThird
entryx := layout.TwoThird
return layout.NewResponsiveLayout(
return xcontainer.NewResponsive(
title,
layout.Responsive(label, 1, 1, labelw, labelx),
layout.Responsive(entry, 1, 1, entryw, entryx),
Expand Down
51 changes: 51 additions & 0 deletions container/responsive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package container

import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/x/fyne/layout"
)

// NewResponsive returns a container with a responsive layout. The objects
// can be copmmon containers or responsive objects using the Responsive()
// function.
//
// Example:
//
// NewResponsive(
// widget.NewLabel("Hello World"), // 100% by default
// Responsive(widget.NewLabel("Hello World"), 1, .5), // 100% for small, 50% for others
// Responsive(widget.NewLabel("Hello World"), 1, .5), // 100% for small, 50% for others
// )
func NewResponsive(objects ...fyne.CanvasObject) *fyne.Container {
container := container.New(layout.NewResponsiveLayout())
container.Objects = objects
return container
}

// Responsive returns a responsive object configured with breakpoint sizes.
// If no size is provided, the object will be 100% of the layout.
// The number of sizes can be up to 4, for small, medium, large and extra large.
// If more than 4 sizes are provided, the extra sizes are ignored.
//
// Example:
//
// Responsive(widget.NewLabel("Hello World"), 1, .5)
//
// It's commonly use in a responsive container, like this:
//
// NewResponsive(
// Responsive(widget.NewLabel("Hello World"), 1, .5),
// Responsive(widget.NewLabel("Hello World"), 1, .5),
// )
//
// Or with the Add() method:
//
// ctn := NewResponsive()
// ctn.Add(Responsive(widget.NewLabel("Hello World"), 1, .5))
// ctn.Add(Responsive(widget.NewLabel("Hello World"), 1, .5))
//
// This function is a shortcut for layout.Responsive()
func Responsive(object fyne.CanvasObject, sizes ...float32) fyne.CanvasObject {
return layout.Responsive(object, sizes...)
}
27 changes: 27 additions & 0 deletions container/responsive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package container

import (
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/widget"
"github.com/stretchr/testify/assert"
)

// This is a simple test to check if the responsive layout is correctly configured.
// The others tests are in the layout package.

// Check if a simple widget is responsive to fill 100% of the layout.
func TestResponsive_Responsive(t *testing.T) {
responsive := NewResponsive(
widget.NewLabel("Hello World"), // 100% by default
Responsive(widget.NewLabel("Hello World"), 1, .5), // 100% for small, 50% for others
Responsive(widget.NewLabel("Hello World"), 1, .5), // 100% for small, 50% for others
)
win := test.NewWindow(responsive)
win.Resize(fyne.NewSize(320, 480))
size1 := responsive.Objects[1].Size()
size2 := responsive.Objects[2].Size()
assert.Equal(t, size1.Width, size2.Width)
}
89 changes: 51 additions & 38 deletions layout/responsive.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"math"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
Expand All @@ -19,6 +18,10 @@ import (
// By default, a standard fyne.CanvasObject will always be width to 1 * containerSize and place in vertical.
// If you want to change the behavior, you can use Responsive() function that registers the layout configuration.
//
// To avoid using fractions, some constants are defined like Half, OneThird, OneQuarter, OneFifth and OneSixth.
//
// Responsive() function takes a fyne.CanvasObject and a list of ratios. The number of ratios could be 1, 2, 3 or 4.
//
// Example:
// layout := NewResponsiveLayout(
// Responsive(label, 1, .5, .25, .5), // small, medium, large, xlarge ratio
Expand Down Expand Up @@ -114,6 +117,8 @@ func newResponsiveConf(ratios ...float32) responsiveConfig {
return responsive
}

var _ fyne.Layout = (*ResponsiveLayout)(nil)

// ResponsiveLayout is the layout that will adapt objects with the responsive rules. See NewResponsiveLayout
// for details.
type ResponsiveLayout struct{}
Expand All @@ -140,27 +145,29 @@ func (resp *ResponsiveLayout) Layout(objects []fyne.CanvasObject, containerSize
continue
}

// place the object
currentObject.Move(currentPos)

// calculate the size of the object based on the container size
size := resp.calcSize(currentObject, containerSize)
if shouldCount {
// calculate the number of object that can be placed in the same line
// starting from the current object
appliedPadding = resp.numberInLine(objects[currentIndex:], containerSize)
appliedPadding = resp.computeElementInLine(objects[currentIndex:], containerSize)
shouldCount = false
}
// adapt object witdh from the number of padding set in this line
size.Width += theme.Padding() / float32(appliedPadding-1)

// and resize it
currentObject.Resize(size)
// resize the object width using the number of applied paddings
// and place it at the right position
currentObject.Resize(
currentObject.Size().Add(fyne.NewSize(
theme.Padding()/float32(appliedPadding-1),
0,
)),
)
currentObject.Move(currentPos)

// next element X position is the current X + the current width + padding
currentPos = currentPos.Add(fyne.NewPos(size.Width+theme.Padding(), 0))
currentPos = currentPos.Add(fyne.NewPos(
currentObject.Size().Width+theme.Padding(), 0,
))

lineHeight = resp.maxFloat32(lineHeight, size.Height)
lineHeight = resp.maxFloat32(lineHeight, currentObject.Size().Height)
// Manage end of line, the next position overflows, so go to next line.
if currentPos.X >= containerSize.Width {
currentPos.X = 0 // back to left
Expand Down Expand Up @@ -201,7 +208,7 @@ func (resp *ResponsiveLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
return fyne.NewSize(w, h)
}

// calcSize calculate the size of the object based on the container size.
// calcSize calculate the size to apply to the object based on the container size.
func (resp *ResponsiveLayout) calcSize(o fyne.CanvasObject, containerSize fyne.Size) fyne.Size {
ro, ok := o.(*responsiveWidget)
if !ok {
Expand All @@ -228,7 +235,6 @@ func (resp *ResponsiveLayout) calcSize(o fyne.CanvasObject, containerSize fyne.S
objectSize.Width = factor * containerSize.Width
// set the size (without padding adaptation)
objectSize = objectSize.Subtract(fyne.NewSize(theme.Padding(), 0))
o.Resize(objectSize)
return objectSize
}

Expand All @@ -237,14 +243,15 @@ func (resp *ResponsiveLayout) maxFloat32(a, b float32) float32 {
return float32(math.Max(float64(a), float64(b)))
}

// count the number of object that can be placed in the same line. The objects should contain only the leading objects.
func (resp *ResponsiveLayout) numberInLine(objects []fyne.CanvasObject, containerSize fyne.Size) int {
var ww float32
// computeElementInLine resize the objects in the same line and return the number of object contained in the line.
func (resp *ResponsiveLayout) computeElementInLine(objects []fyne.CanvasObject, containerSize fyne.Size) int {
var lineWidth float32
count := 1
for _, o := range objects {
size := resp.calcSize(o, containerSize)
ww += size.Width + theme.Padding()
if ww > containerSize.Width {
o.Resize(size)
lineWidth += size.Width + theme.Padding()
if lineWidth > containerSize.Width {
break
}
count++
Expand All @@ -261,30 +268,37 @@ func (resp *ResponsiveLayout) numberInLine(objects []fyne.CanvasObject, containe
// Responsive(label, 1, .5, .25), // 100% for small, 50% for medium, 25% for large
// Responsive(button, 1, .5, .25), // ...
// label2, // this will be placed and resized with default behaviors
// // => 1, 1, 1
// // => 1, 1, 1, 1
// )
func NewResponsiveLayout(o ...fyne.CanvasObject) *fyne.Container {
func NewResponsiveLayout() fyne.Layout {
r := &ResponsiveLayout{}

objects := []fyne.CanvasObject{}
for _, unknowObject := range o {
if _, ok := unknowObject.(*responsiveWidget); !ok {
unknowObject = Responsive(unknowObject)
}
objects = append(objects, unknowObject)
}
//objects := []fyne.CanvasObject{}
//for _, unknowObject := range o {
// if _, ok := unknowObject.(*responsiveWidget); !ok {
// unknowObject = Responsive(unknowObject)
// }
// objects = append(objects, unknowObject)
//}

return container.New(r, objects...)
return r
}

var _ fyne.Widget = (*responsiveWidget)(nil)

type responsiveWidget struct {
widget.BaseWidget

render fyne.CanvasObject
responsiveConfig responsiveConfig
}

var _ fyne.Widget = (*responsiveWidget)(nil)
func (ro *responsiveWidget) CreateRenderer() fyne.WidgetRenderer {
if ro.render == nil {
return nil
}
return widget.NewSimpleRenderer(ro.render)
}

// Responsive register the object with a responsive configuration.
// The optional ratios must
Expand All @@ -295,14 +309,13 @@ var _ fyne.Widget = (*responsiveWidget)(nil)
// They are set to previous value if a value is not passed, or 1.0 if there is no previous value.
// The returned object is not modified.
func Responsive(object fyne.CanvasObject, breakpointRatio ...float32) fyne.CanvasObject {
if len(breakpointRatio) > 4 {
fyne.LogError(
"Too many arguments in Responsive()",
fmt.Errorf("The function can take at most 4 arguments, you provided %d", len(breakpointRatio)),

Check failure on line 315 in layout/responsive.go

View workflow job for this annotation

GitHub Actions / checks

error strings should not be capitalized (ST1005)
)
}
ro := &responsiveWidget{render: object, responsiveConfig: newResponsiveConf(breakpointRatio...)}
ro.ExtendBaseWidget(ro)
return ro
}

func (ro *responsiveWidget) CreateRenderer() fyne.WidgetRenderer {
if ro.render == nil {
return nil
}
return widget.NewSimpleRenderer(ro.render)
}
44 changes: 22 additions & 22 deletions layout/responsive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
Expand All @@ -17,13 +18,15 @@ func TestResponsive_SimpleLayout(t *testing.T) {

// build
label := widget.NewLabel("Hello World")
layout := NewResponsiveLayout(label)
layout := NewResponsiveLayout()

win := test.NewWindow(layout)
ctn := container.New(layout)
ctn.Add(label)
win := test.NewWindow(ctn)
defer win.Close()
win.Resize(fyne.NewSize(w, h))

size := layout.Size()
size := ctn.Size()

assert.Equal(t, w-padding*2, size.Width)

Expand All @@ -38,8 +41,11 @@ func TestResponsive_Responsive(t *testing.T) {
label1 := Responsive(widget.NewLabel("Hello World"), 1, .5)
label2 := Responsive(widget.NewLabel("Hello World"), 1, .5)

ctn := container.New(NewResponsiveLayout())
ctn.Add(label1)
ctn.Add(label2)
win := test.NewWindow(
NewResponsiveLayout(label1, label2),
ctn,
)
win.SetPadded(true)
defer win.Close()
Expand Down Expand Up @@ -75,8 +81,12 @@ func TestResponsive_GoToNextLine(t *testing.T) {
label2 := Responsive(widget.NewLabel("Hello World"), .5)
label3 := Responsive(widget.NewLabel("Hello World"), .5)

layout := NewResponsiveLayout(label1, label2, label3)
win := test.NewWindow(layout)
layout := NewResponsiveLayout()
ctn := container.New(layout)
ctn.Add(label1)
ctn.Add(label2)
ctn.Add(label3)
win := test.NewWindow(ctn)
defer win.Close()
w = float32(MEDIUM)
win.Resize(fyne.NewSize(w, h))
Expand Down Expand Up @@ -104,8 +114,12 @@ func TestResponsive_SwitchAllSizes(t *testing.T) {
for i := 0; i < n; i++ {
labels[i] = Responsive(widget.NewLabel("Hello World"), 1, 1/float32(2), 1/float32(3), 1/float32(4))
}
layout := NewResponsiveLayout(labels...)
win := test.NewWindow(layout)
layout := NewResponsiveLayout()
ctn := container.New(layout)
for i := 0; i < n; i++ {
ctn.Add(labels[i])
}
win := test.NewWindow(ctn)
defer win.Close()

h := float32(1200)
Expand Down Expand Up @@ -143,17 +157,3 @@ func TestResponsive_SwitchAllSizes(t *testing.T) {
assert.Equal(t, size.Width, (w-p*5)/4)
}
}

// Test if a widget is responsive to fill 100% of the layout
// when we don't provides rsponsive ratios.
func TestResponsive_NoArgs(t *testing.T) {
label := widget.NewLabel("Hello World")
resp := NewResponsiveLayout(Responsive(label))
for _, child := range resp.Objects {
ro, ok := child.(*responsiveWidget)
assert.Equal(t, true, ok)
for _, s := range []responsiveBreakpoint{SMALL, MEDIUM, LARGE, XLARGE} {
assert.Equal(t, float32(1), ro.responsiveConfig[s])
}
}
}

0 comments on commit b01b4a2

Please sign in to comment.