Go fyne, How to resize the Text Canvas?

Viewed 108

I am writing a Viewdata Client in Go using the using fyne cross platform toolkit that has a display of 40 cols by 24 rows. With Viewdata block graphics each character needs to be sitting next to its neighbours with no gaps so I am using a custom grid layout which is based on the fyne grid layout but with padding removed.

There are many attributes associated with each character, primarily these relate to foreground and backgound colour, the font is fixed. To achieve this, in each cell, I am using a Max container which itself contains a Text canvas placed on top of a Rectangle canvas. The issue I have is that whilst the rectangle (background) scales correctly when the window is resized, the Text does not and can only be changed by changing the text size property programatically.

I can get around this to a degree if I make the application use a fixed size window and select an appropriate text size for that particular window size. However, on some displays this still causes slight gaps between the block graphic characters.

My question is, how can I make the text scale in the same way that the rectange does or is there a better approach.

The code example below shows the issue, the bacgound colour gets resized but the text does not.

package main
    
import (
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/canvas"
    "fyne.io/fyne/v2/container"
    "image/color"
)
        
func main() {
    
    a := app.New()
    w := a.NewWindow("Example Program")
    c := container.New(NewGridLayout(40))

    // this is just simple code to populate the grid for this example
    for col := 0; col < 40; col++ {
        for row := 0; row < 24; row++ {

            // this represents our character cell
            maxContainer := container.New(NewMaxLayout())

            //create a background and add it to the character cell
            background := new(canvas.Rectangle)
            background.FillColor =  color.NRGBA{R: 255, G: 255, B: 0, A: 255}
            maxContainer.Objects = append(maxContainer.Objects, background)

            // create a text object for the character
            txt := canvas.NewText(" ", color.Black)

            // TODO the actual character would typically be a block graphic and should
            //  its neighbours when resized in the same way as the rectangle does.
            txt.Text = "@" // example graphic
            txt.TextSize = 18

            // add them to the grid
            maxContainer.Objects = append(maxContainer.Objects, txt)
            c.Objects = append(c.Objects, maxContainer)
        }
    }

    w.SetContent(c)
    w.ShowAndRun()

}

The custom Grid Layout.

package main

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

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

type gridLayout struct {
    Cols            int
    vertical, adapt bool
}

// NewAdaptiveGridLayout returns a new grid layout which uses columns when horizontal but rows when vertical.
func NewAdaptiveGridLayout(rowcols int) fyne.Layout {
    return &gridLayout{Cols: rowcols, adapt: true}
}

// NewGridLayout returns a grid layout arranged in a specified number of columns.
// The number of rows will depend on how many children are in the container that uses this layout.
func NewGridLayout(cols int) fyne.Layout {
    return NewGridLayoutWithColumns(cols)
}

// NewGridLayoutWithColumns returns a new grid layout that specifies a column count and wrap to new rows when needed.
func NewGridLayoutWithColumns(cols int) fyne.Layout {
    return &gridLayout{Cols: cols}
}

// NewGridLayoutWithRows returns a new grid layout that specifies a row count that creates new rows as required.
func NewGridLayoutWithRows(rows int) fyne.Layout {
    return &gridLayout{Cols: rows, vertical: true}
}

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

    return !g.vertical
}

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

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

// 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(offset)
    return float32(math.Round(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)
}

// 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 *gridLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
    rows := g.countRows(objects)

    cellWidth := float64(size.Width) / float64(g.Cols)
    cellHeight := float64(size.Height) / float64(rows)

    if !g.horizontal() {
        cellWidth = float64(size.Width) / float64(rows)
        cellHeight = float64(size.Height) / float64(g.Cols)
    }

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

        x1 := getLeading(cellWidth, col)
        y1 := getLeading(cellHeight, row)
        x2 := getTrailing(cellWidth, col)
        y2 := getTrailing(cellHeight, row)

        child.Move(fyne.NewPos(x1, y1))
        child.Resize(fyne.NewSize(x2-x1, y2-y1))

        if g.horizontal() {
            if (i+1)%g.Cols == 0 {
                row++
                col = 0
            } else {
                col++
            }
        } else {
            if (i+1)%g.Cols == 0 {
                col++
                row = 0
            } else {
                row++
            }
        }
        i++
    }
}

// MinSize finds the smallest size that satisfies all the child objects.
// For a GridLayout this is the size of the largest child object multiplied by
// the required number of columns and rows.
func (g *gridLayout) 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(g.Cols), minSize.Height*float32(rows))
        return minContentSize.Add(fyne.NewSize(0, 0))
    }

    minContentSize := fyne.NewSize(minSize.Width*float32(rows), minSize.Height*float32(g.Cols))
    return minContentSize.Add(fyne.NewSize(0, 0))
}

Edit: Having been pointed to a fyne example, I have added a custom Max layout to the project that tries to resize the text, this only works if I reduce the text size by a factor of 0.7 anything greater and the window (and text) continues to expand indefinitely or the machine freezes.

Here is the custom Max layout.

package main

// Package layout defines the various layouts available to Fyne apps

import "C"
import (
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/canvas"
)

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

type maxLayout struct {
}

// NewMaxLayout creates a new MaxLayout instance
func NewMaxLayout() fyne.Layout {
    return &maxLayout{}
}

// Layout is called to pack all child objects into a specified size.
// For MaxLayout this sets all children to the full size passed.
func (m *maxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {

    topLeft := fyne.NewPos(0, 0)

    for _, child := range objects {

        // something special needed if the child is a text canvas
        if _, ok := child.(*canvas.Text); ok {

            // get the text canvas
            txt := child.(*canvas.Text)

            // set the text size based on the new size
            textSize := size.Height * 0.8

            // calculate the new height and width given the actual text
            textMin := fyne.MeasureText(txt.Text, textSize, fyne.TextStyle{Monospace: true})

            // resize the child (txt canvas)
            txt.Resize(fyne.NewSize(size.Width, textMin.Height))
            txt.TextSize = textSize

        } else {
            child.Resize(size)
        }
        child.Move(topLeft)

    }
}

// MinSize finds the smallest size that satisfies all the child objects.
// For MaxLayout this is determined simply as the MinSize of the largest child.
func (m *maxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
    minSize := fyne.NewSize(0, 0)
    for _, child := range objects {
        if !child.Visible() {
            continue
        }

        minSize = minSize.Max(child.MinSize())
    }

    return minSize
}
1 Answers

Scaling text content that way would need some custom code. But it’s not too complicated to adjust font size based on space.

Essentially you would have to calculate a new font size based on the input fyne.Size into your Layout function. Once calculated you can set it as myText.TextSize = newFontSize; myText.Refresh(). This will work instead of the Resize which merely sets the space available.

You can see an example in a Fyne demo repo https://github.com/fyne-io/examples/blob/06b751e39187df843fd23e1215f55fb41845f5a5/bugs/button.go#L36

Related