Search code examples
gofyne

Custom Fyne Adaptive Grid Layout


I'm modifying container.NewAdaptiveGrid() of fyne library in a way that we render widgets's width according the ratio slice we pass. As of now, container.NewAdaptiveGrid() renders widgets of equal width in a row. basically (total row size / now of widgets).

My code:

package main

import (
    "fmt"
    "math"

    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/container"
    "fyne.io/fyne/v2/theme"
    "fyne.io/fyne/v2/widget"
)

func New(layout fyne.Layout, objects ...fyne.CanvasObject) *fyne.Container {
    return fyne.NewContainerWithLayout(layout, objects...)
}

func NewAdaptiveGridWithRatios(ratios []float32, objects ...fyne.CanvasObject) *fyne.Container {
    return New(NewAdaptiveGridLayoutWithRatios(ratios), objects...)
}

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

type adaptiveGridLayoutWithRatios struct {
    ratios          []float32
    adapt, vertical bool
}

func NewAdaptiveGridLayoutWithRatios(ratios []float32) fyne.Layout {
    return &adaptiveGridLayoutWithRatios{ratios: ratios, adapt: true}
}

func (g *adaptiveGridLayoutWithRatios) horizontal() bool {
    if g.adapt {
        return fyne.IsHorizontal(fyne.CurrentDevice().Orientation())
    }

    return !g.vertical
}

func (g *adaptiveGridLayoutWithRatios) countRows(objects []fyne.CanvasObject) int {
    count := 0
    for _, child := range objects {
        if child.Visible() {
            count++
        }
    }

    return int(math.Ceil(float64(count) / float64(len(g.ratios))))
}

// Get the leading (top or left) edge of a grid cell.
// size is the ideal cell size and the offset is which col or row its on.
func getLeading(size float64, offset int) float32 {
    ret := (size + float64(theme.Padding())) * float64(offset)

    return float32(ret)
}

// Get the trailing (bottom or right) edge of a grid cell.
// size is the ideal cell size and the offset is which col or row its on.
func getTrailing(size float64, offset int) float32 {
    return getLeading(size, offset+1) - theme.Padding()
}

// Layout is called to pack all child objects into a specified size.
// For a GridLayout this will pack objects into a table format with the number
// of columns specified in our constructor.
func (g *adaptiveGridLayoutWithRatios) Layout(objects []fyne.CanvasObject, size fyne.Size) {
    rows := g.countRows(objects)
    cols := len(g.ratios)
    if g.horizontal() {
        cols = rows
        rows = len(g.ratios)
    }

    padWidth := float32(cols-1) * theme.Padding()
    padHeight := float32(rows-1) * theme.Padding()
    var totalRatio float32
    for _, r := range g.ratios {
        totalRatio += r
    }

    cellWidth := (float64(size.Width) - float64(padWidth)) / float64(len(g.ratios))
    cellHeight := float64(size.Height-padHeight) / float64(rows)

    if !g.horizontal() {
        cellWidth, cellHeight = cellHeight, cellWidth
        cellWidth = float64(size.Width-padWidth) / float64(rows)
        cellHeight = float64(size.Height-padHeight) / float64(len(g.ratios))
    }

    row, col := 0, 0
    i := 0
    for _, child := range objects {
        if !child.Visible() {
            continue
        }

        //ratio := g.ratios[j%len(g.ratios)]
        cellSize := fyne.NewSize(float32(cellWidth)*g.ratios[i], float32(cellHeight))

        x1 := getLeading(float64(cellSize.Width), col)
        y1 := getLeading(float64(cellSize.Height), row)
        x2 := getTrailing(float64(cellSize.Width), col)
        y2 := getTrailing(float64(cellSize.Height), row)
        fmt.Println("1s :", x1, y1)
        fmt.Println("2s :", x2, y2)
        child.Move(fyne.NewPos(x1, y1))
        child.Resize(cellSize)

        if g.horizontal() {
            if (i+1)%cols == 0 {
                row++
                col = 0
            } else {
                col++
            }
        } else {
            if (i+1)%cols == 0 {
                col++
                row = 0
            } else {
                row++
            }
        }
        i++
    }
    fmt.Println("i :", i)
}

func (g *adaptiveGridLayoutWithRatios) MinSize(objects []fyne.CanvasObject) fyne.Size {
    minSize := fyne.NewSize(0, 0)
    return minSize
}

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("My Windows")
    myWindow.Resize(fyne.NewSize(600, 200))

    button1 := widget.NewButton("Button 1", func() {
        // Handle button click for button 1
    })

    button2 := widget.NewButton("Button 2", func() {
        // Handle button click for button 2
    })
    button1.Importance = widget.WarningImportance
    button2.Importance = widget.DangerImportance
    title := widget.NewLabelWithStyle("Custom", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})

    myWindow.SetContent(container.NewVBox(title,
        NewAdaptiveGridWithRatios([]float32{0.3, 0.7}, button1, button2)))

    myWindow.ShowAndRun()
}

I expected buttons to be side by side where relative width of the buttons are in ration 3:7. But I'm getting 2 horizontal line one below another. I'm modifying : https://github.com/fyne-io/fyne/blob/8c2509518b2df442a6b748d9b07754739592e6d7/layout/gridlayout.go to make my custom one.

enter image description here


Solution

  • This works :

    package main
    
    import (
        "fmt"
        "math"
    
        "fyne.io/fyne/v2"
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/theme"
        "fyne.io/fyne/v2/widget"
    )
    
    func New(layout fyne.Layout, objects ...fyne.CanvasObject) *fyne.Container {
        return fyne.NewContainerWithLayout(layout, objects...)
    }
    
    func NewAdaptiveGridWithRatios(ratios []float32, objects ...fyne.CanvasObject) *fyne.Container {
        return New(NewAdaptiveGridLayoutWithRatios(ratios), objects...)
    }
    
    // Declare conformity with Layout interface
    var _ fyne.Layout = (*adaptiveGridLayoutWithRatios)(nil)
    
    type adaptiveGridLayoutWithRatios struct {
        ratios          []float32
        adapt, vertical bool
    }
    
    func NewAdaptiveGridLayoutWithRatios(ratios []float32) fyne.Layout {
        return &adaptiveGridLayoutWithRatios{ratios: ratios, adapt: true}
    }
    
    func (g *adaptiveGridLayoutWithRatios) horizontal() bool {
        if g.adapt {
            return fyne.IsHorizontal(fyne.CurrentDevice().Orientation())
        }
    
        return !g.vertical
    }
    
    func (g *adaptiveGridLayoutWithRatios) countRows(objects []fyne.CanvasObject) int {
        count := 0
        for _, child := range objects {
            if child.Visible() {
                count++
            }
        }
    
        return int(math.Ceil(float64(count) / float64(len(g.ratios))))
    }
    
    // Layout is called to pack all child objects into a specified size.
    // For a GridLayout this will pack objects into a table format with the number
    // of columns specified in our constructor.
    func (g *adaptiveGridLayoutWithRatios) Layout(objects []fyne.CanvasObject, size fyne.Size) {
    
        rows := g.countRows(objects)
        cols := len(g.ratios)
    
        padWidth := float32(cols-1) * theme.Padding()
        padHeight := float32(rows-1) * theme.Padding()
        tGap := float64(padWidth)
        tcellWidth := float64(size.Width) - tGap
        cellHeight := float64(size.Height-padHeight) / float64(rows)
    
        fmt.Println(cols, rows)
        fmt.Println(cellHeight, tcellWidth+tGap, tGap)
        fmt.Println("tcellWidth, cellHeight", tcellWidth, cellHeight)
        if !g.horizontal() {
            padWidth, padHeight = padHeight, padWidth
            tcellWidth = float64(size.Width-padWidth) - tGap
            cellHeight = float64(size.Height-padHeight) / float64(cols)
        }
    
        row, col := 0, 0
        i := 0
        var x1, x2, y1, y2 float32 = 0.0, 0.0, 0.0, 0.0
        fmt.Println("padWidth, padHeight, tcellWidth, cellHeight, float32(theme.Padding()):", padWidth, padHeight, tcellWidth, cellHeight, float32(theme.Padding()))
        for _, child := range objects {
            if !child.Visible() {
                continue
            }
    
            if i == 0 {
                x1 = 0
                y1 = 0
            } else {
                x1 = x2 + float32(theme.Padding())*float32(1)
                y1 = y2 - float32(cellHeight)
            } // float32(tGap/float64(col))
            //  (size + float64(theme.Padding())) * float64(offset)  float32(theme.Padding())*float32(1)
            x2 = x1 + float32(tcellWidth*float64(g.ratios[i]))
            y2 = float32(cellHeight)
    
            fmt.Println("x1,y1 :", x1, y1)
            fmt.Println("x2, y2 :", x2, y2)
            fmt.Println("eff width", tcellWidth*float64(g.ratios[i]))
    
            fmt.Println("------")
            child.Move(fyne.NewPos(x1, y1))
            child.Resize(fyne.NewSize((x2 - x1), y2-y1))
    
            if g.horizontal() {
                if (i+1)%cols == 0 {
                    row++
                    col = 0
                } else {
                    col++
                }
            } else {
                if (i+1)%cols == 0 {
                    col++
                    row = 0
                } else {
                    row++
                }
            }
            i++
        }
        fmt.Println("i :", i)
    }
    
    func (g *adaptiveGridLayoutWithRatios) MinSize(objects []fyne.CanvasObject) fyne.Size {
        rows := g.countRows(objects)
        minSize := fyne.NewSize(0, 0)
        for _, child := range objects {
            if !child.Visible() {
                continue
            }
    
            minSize = minSize.Max(child.MinSize())
        }
    
        if g.horizontal() {
            minContentSize := fyne.NewSize(minSize.Width*float32(len(g.ratios)), minSize.Height*float32(rows))
            return minContentSize.Add(fyne.NewSize(theme.Padding()*fyne.Max(float32(len(g.ratios)-1), 0), theme.Padding()*fyne.Max(float32(rows-1), 0)))
        }
    
        minContentSize := fyne.NewSize(minSize.Width*float32(rows), minSize.Height*float32(len(g.ratios)))
        return minContentSize.Add(fyne.NewSize(theme.Padding()*fyne.Max(float32(rows-1), 0), theme.Padding()*fyne.Max(float32(len(g.ratios)-1), 0)))
    }
    
    func main() {
        myApp := app.New()
        myWindow := myApp.NewWindow("My Windows Custom UI")
        myWindow.Resize(fyne.NewSize(600, 200))
    
        var buttons [16]*widget.Button
    
        for i := 0; i < 16; i++ {
            button := widget.NewButton(fmt.Sprintf("Btn %d", i+1), func() {
                // Handle button click for this button
            })
    
            // Set the button importance based on the button index
            if i%2 == 0 {
                button.Importance = widget.WarningImportance
            } else {
                button.Importance = widget.DangerImportance
            }
    
            buttons[i] = button
        }
    
        pgBar := widget.NewLabelWithStyle("Progress :", fyne.TextAlignCenter, fyne.TextStyle{Italic: true})
        progressBar := widget.NewProgressBar()
        progressBar.SetValue(0.95)
    
        myWindow.SetContent(container.NewVBox(
            NewAdaptiveGridWithRatios([]float32{0.1, 0.4, 0.4, 0.1}, buttons[0], buttons[1], buttons[2], buttons[3]),
            NewAdaptiveGridWithRatios([]float32{0.2, 0.3, 0.1, 0.4}, buttons[4], buttons[5], buttons[6], buttons[7]),
            NewAdaptiveGridWithRatios([]float32{0.6, 0.1, 0.2, 0.1}, buttons[8], buttons[9], buttons[10], buttons[11]),
            NewAdaptiveGridWithRatios([]float32{0.1, 0.4, 0.4, 0.1}, buttons[12], buttons[13], buttons[14], buttons[15]),
            NewAdaptiveGridWithRatios([]float32{0.1, 0.9}, pgBar, progressBar),
        ))
    
        myWindow.ShowAndRun()
    }
    

    I've added multiple buttons and other widgets in different ratios. enter image description here