Skip to content

Commit

Permalink
Fix responsive problems
Browse files Browse the repository at this point in the history
- The padding were not respected between element in a line and stepping
  to the next line
- We now prefer move the first element in a row to the X padding, to
  avoid computation - the responsive behavior is to center elements in a
  row, so it must be OK
- We accept now that a non-responsive element is added to the container,
  it will be resized to the default 100% of the container width
- We previously use the window size to compute the reponsive element
  sizes. That's an error... The reponsivity should be set to the
  container size. That fixed now.
  • Loading branch information
metal3d committed Oct 30, 2023
1 parent ef32303 commit 369c47f
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 56 deletions.
6 changes: 3 additions & 3 deletions cmd/responsive_layout/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ func main() {
winSizeLabel(window), // 100% by default
layout.Responsive(
widget.NewButton("One !", func() {}),
1, 1.0/3.0,
1, layout.OneThird,
),
layout.Responsive(
widget.NewButton("Two !", func() {}),
1, 1.0/3.0,
1, layout.OneThird,
),
layout.Responsive(
widget.NewButton("Three !", func() {}),
1, 1.0/3.0,
1, layout.OneThird,
),
layout.Responsive(fromLayout(), 1, .5), // 100% for small, 50% for others
layout.Responsive(fromLayout(), 1, .5), // 100% for small, 50% for others
Expand Down
64 changes: 24 additions & 40 deletions layout/responsive.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,17 @@ import (
// responsiveBreakpoint is a integer representing a breakpoint size as defined in Bootstrap.
//
// See: https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints
type responsiveBreakpoint uint16
type responsiveBreakpoint = float32

const (
Full float32 = 1.0

Check failure on line 34 in layout/responsive.go

View workflow job for this annotation

GitHub Actions / checks

exported const Full should have comment (or a comment on this block) or be unexported
Half float32 = 0.5
OneThird float32 = 1.0 / 3.0
TwoThird float32 = 2.0 / 3.0
Quarter float32 = 0.25
Fifth float32 = 0.2
Sixth float32 = 1.0 / 6.0
)

const (
// SMALL is the smallest breakpoint (mobile vertical).
Expand Down Expand Up @@ -110,25 +120,16 @@ func (resp *ResponsiveLayout) Layout(objects []fyne.CanvasObject, containerSize
return
}

// Responsive is based on the window size, so we need to get it
window := fyne.CurrentApp().Driver().CanvasForObject(objects[0])
if window == nil {
return
}

// this will be updatad for each element to know where to place
// the next object.
pos := fyne.NewPos(0, 0)
pos := fyne.NewPos(theme.Padding(), 0)

// to calculate the next pos.Y when a new line is needed
maxHeight := float32(0)
var maxHeight float32

// objects in a line
line := []fyne.CanvasObject{}

// cast windowSize.Width to responsiveBreakpoint (uint16)
ww := responsiveBreakpoint(window.Size().Width)

// For each object, place it at the right position (pos) and resize it.
for _, o := range objects {
if o == nil || !o.Visible() {
Expand All @@ -138,25 +139,28 @@ func (resp *ResponsiveLayout) Layout(objects []fyne.CanvasObject, containerSize
// get tht configuration
ro, ok := o.(*responsiveWidget)
if !ok {
log.Fatal("A non responsive object has been packed inside a ResponsibleLayout. This is impossible.")
// We now allow non responsive objects to be packed inside a ResponsiveLayout. The
// size of the object will be 100% of the container.
ro = Responsive(o).(*responsiveWidget)
}
conf := ro.responsiveConfig

line = append(line, o) // add the container to the line
size := o.MinSize() // get some informations

// adapt object witdh from the configuration
if ww <= SMALL {
if containerSize.Width <= SMALL {
size.Width = conf[SMALL] * containerSize.Width
} else if ww <= MEDIUM {
} else if containerSize.Width <= MEDIUM {
size.Width = conf[MEDIUM] * containerSize.Width
} else if ww <= LARGE {
} else if containerSize.Width <= LARGE {
size.Width = conf[LARGE] * containerSize.Width
} else {
size.Width = conf[XLARGE] * containerSize.Width
}

// place and resize the element
size = size.Subtract(fyne.NewSize(theme.Padding(), 0))
o.Resize(size)
o.Move(pos)

Expand All @@ -166,16 +170,13 @@ func (resp *ResponsiveLayout) Layout(objects []fyne.CanvasObject, containerSize
maxHeight = resp.maxFloat32(maxHeight, size.Height)

// Manage end of line, the next position overflows, so go to next line.
if pos.X >= containerSize.Width-theme.Padding() {
// we now know the number of object in a line, fix padding
resp.fixPaddingOnLine(line)
if pos.X >= containerSize.Width {
line = []fyne.CanvasObject{}
pos.X = 0 // back to left
pos.Y += maxHeight // move to the next line
pos.X = theme.Padding() // back to left
pos.Y += maxHeight + theme.Padding() // move to the next line
maxHeight = 0
}
}
resp.fixPaddingOnLine(line) // fix padding for the last line
}

// MinSize return the minimum size ot the layout.
Expand All @@ -197,7 +198,7 @@ func (resp *ResponsiveLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
if o.Position().Y != currentY {
currentY = o.Position().Y
// new line, so we can add the maxHeight to h
h += maxHeight
h += maxHeight + theme.Padding()

// drop the line
maxHeight = 0
Expand All @@ -208,23 +209,6 @@ func (resp *ResponsiveLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
return fyne.NewSize(w, h)
}

// fixPaddingOnLine fix the space between the objects in a line.
func (resp *ResponsiveLayout) fixPaddingOnLine(line []fyne.CanvasObject) {
if len(line) <= 1 {
return
}
for i, o := range line {
s := o.Size()
s.Width -= theme.Padding() / float32(len(line)-1)
o.Resize(s)
if i > 0 {
p := o.Position()
p.X -= theme.Padding() * float32(i)
o.Move(p)
}
}
}

// math.Max only works with float64, so let's make our own
func (resp *ResponsiveLayout) maxFloat32(a, b float32) float32 {
return float32(math.Max(float64(a), float64(b)))
Expand Down
30 changes: 17 additions & 13 deletions layout/responsive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ func TestResponsive_Responsive(t *testing.T) {
win.Resize(fyne.NewSize(w, h))
size1 := label1.Size()
size2 := label2.Size()
assert.Equal(t, w-padding*2, size1.Width)
assert.Equal(t, w-padding*2, size2.Width)
assert.Equal(t, size1.Width, size2.Width)
assert.Equal(t, w-padding*3, size1.Width)

// Then resize to w > SMALL so the labels should be sized to 50% of the layout
w = float32(MEDIUM)
Expand Down Expand Up @@ -85,8 +85,11 @@ func TestResponsive_GoToNextLine(t *testing.T) {
assert.NotEqual(t, label1.Position().Y, label3.Position().Y)

// just to be sure...
// the label3 should be at label1.Position().Y + label1.Size().Height
assert.Equal(t, label1.Position().Y+label1.Size().Height, label3.Position().Y)
// the label3 should be at label1.Position().Y + label1.Size().Height + theme.Padding()
assert.Equal(t,
label1.Position().Y+label1.Size().Height+theme.Padding(), // expected
label3.Position().Y, // actual
)
}

// Check if sizes are correctly computed for responsive widgets when the window size
Expand All @@ -111,36 +114,37 @@ func TestResponsive_SwitchAllSizes(t *testing.T) {
w = w - 2*p
for i := 0; i < n; i++ {
size := labels[i].Size()
assert.Equal(t, w, size.Width)
assert.Equal(t, w-p, size.Width)
}

// Then resize to w > SMALL so the labels should be sized to 50% of the layout
w = float32(MEDIUM)
win.Resize(fyne.NewSize(w, h))
w = w - 2*p
w = w/2 - 2*p
for i := 0; i < n; i++ {
size := labels[i].Size()
assert.Equal(t, w/2-p, size.Width) // 1 padding between 2 widgets
assert.Equal(t, w, size.Width) // 1 padding between 2 widgets
}

// Then resize to w > MEDIUM so the labels should be sized to 33% of the layout
w = float32(LARGE)
win.Resize(fyne.NewSize(w, h))
w = w - 2*p
for i := 0; i < n-1; i++ { // note: n-1 because the last element seems to be resized
w = w / 3
w = float32(math.Floor(float64(w))) - p - 2 // note: 2px are removed to avoid rounding errors
for i := 0; i < n-1; i++ {
size := labels[i].Size()
floor := math.Floor(float64(w)/3 - float64(p)/3)
assert.Equal(t, float32(floor), size.Width) // 2 paddings between 3 widgets
assert.Equal(t, w, size.Width) // 2 paddings between 3 widgets
}

// Then resize to w > LARGE so the labels should be sized to 25% of the layout
w = float32(XLARGE)
win.Resize(fyne.NewSize(w, h))
w = w - 2*p
w = w / 4
w = w - p*2 + 2 // note: 2px are added to avoid rounding errors
for i := 0; i < n; i++ {
size := labels[i].Size()
// note: 1px is added to the size to avoid rounding errors
assert.Equal(t, w/4-p/4-1, float32(math.Floor(float64(size.Width))))
assert.Equal(t, w, size.Width) // 3 paddings between 4 widgets
}
}

Expand Down

0 comments on commit 369c47f

Please sign in to comment.