3

I'm trying to display the alphabet in a listwithdata widget. I am using a custom clickable label as the widget to display in the list.
For some reason everything displays fine when the widget loads. But when I start scrolling the letters starts being displayed in a completely random order and I cant figure out why.
Here is a fully working code to reproduce the bug.

package main

import (
    "fmt"

    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/data/binding"
    "fyne.io/fyne/v2/widget"
)

func makeAlphabet() []string {
    var alphabet []string
    for ch := 'A'; ch <= 'Z'; ch++ {
        alphabet = append(alphabet, string(ch))
    }
    return alphabet
}

type TapLabel struct {
    *widget.Label //composition

    //function pointers to set to get events
    OnTapped func(string)
}

func (mc *TapLabel) Tapped(pe *fyne.PointEvent) {
    if mc.OnTapped != nil {
        mc.OnTapped(mc.Text)
    }
}

func NewTapLabel(text string, tappedLeft func(string)) *TapLabel {
    return &TapLabel{widget.NewLabel(text), tappedLeft}
}

func alphabetToBrands(letter string) {
    fmt.Println(letter)
}

func main() {
    app := app.New()
    window := app.NewWindow("tac_hub")
    window.Resize(fyne.NewSize(200,200))

    rawData := makeAlphabet()
    data := binding.BindStringList(&rawData)
    list := widget.NewListWithData(
        data,
        func() fyne.CanvasObject {
            return NewTapLabel("template", alphabetToBrands)
        },
        func(i binding.DataItem, o fyne.CanvasObject) {
            o.(*TapLabel).Bind(i.(binding.String))
        },
    )

    window.SetContent(list)
    window.ShowAndRun()
}

The click action works correctly and gives me the correct letter (not the one that is displayed but the one that should be displayed).
I'm guessing I must be doing something wrong somewhere but I can't figure out what.

If anyone can help it would be appreciated!

alex_bits
  • 642
  • 1
  • 4
  • 14

3 Answers3

2

Solution with custom widget:

This is based on Fyne Extending Widgets tutorial. In particular you call ExtendBaseWidget:

ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.

You still embed widget.Label, but not as a pointer (using a pointer also works, but the documentation uses the non-pointer example), so that the necessary methods are promoted into your widget:

func NewTapLabel(text string, tappedLeft func(string)) *TapLabel {
    label := &TapLabel{}
    label.ExtendBaseWidget(label)
    label.SetText(text)
    label.OnTapped = tappedLeft
    return label
}

And then you change the update function to set the actual bound string to the label, instead of just binding the data again as in your original example:

        func(i binding.DataItem, o fyne.CanvasObject) {
            s, _ := i.(binding.String).Get()
            o.(*TapLabel).SetText(s)
        },

As of why the bug occurs if you omit calling ExtendBaseWidget, I suspect it's because it was missing the base widget implementation, which this method sets.


Solution without custom widget:

    list := widget.NewListWithData(
        data,
        func() fyne.CanvasObject {
            return &widget.Label{Text: "template"}
        },
        func(i binding.DataItem, o fyne.CanvasObject) {
            o.(*widget.Label).Bind(i.(binding.String))
        },
    )
    list.OnSelected = func(id widget.ListItemID) {
        fmt.Println(rawData[id])
    }

Dropping your custom TapLabel in favor of &widget.Label{Text: "template"}, then to print the tapped item you use list.OnSelected and close around the data slice.

blackgreen
  • 34,072
  • 23
  • 111
  • 129
  • Thank you! This solves my current problem. Looks like I was trying to reinvent the wheel! However it does not solve the underlying problem which is why it doesn't work with a custom widget. – alex_bits Nov 10 '21 at 11:00
  • 1
    @alex_bits see the edit above for a solution with your own custom widget – blackgreen Nov 10 '21 at 14:05
  • Awsome answer. And it works exactly as you say. I feel like the customization sections of the doc, be it widget, container or layout customization, could do with a lot more explanation and examples. Thank you very much for the time spent on this! – alex_bits Nov 10 '21 at 14:39
  • 1
    It is true that more docs could be written. The book contains a lot more information that will over time be adapted to the web - though perhaps not all :) https://www.packtpub.com/product/building-cross-platform-gui-applications-with-fyne/9781800563162 – andy.xyz Nov 11 '21 at 10:00
  • 1
    @andy.xyz thanks for chiming in. I'm interested in knowing exactly why the custom widget in the OP's original code did not work properly. I single-stepped into the code but couldn't pinpoint the issue. Could you shed some light? – blackgreen Nov 11 '21 at 10:04
  • @andy.xyz funny you should say that... I got a copy of the book yesterday before seeing your message! I'm not on the custom widgets chapter yet but I can attest that the custom layouts section is definitely a lot more detailed than the docs and was very clear to me. Made 3 custom layouts so far for everyday use :D It would be nice to know why my extended widget didn't work though! ;) – alex_bits Nov 12 '21 at 21:02
0

Looks like the argument here o.(*TapLabel).Bind(i.(binding.String)) changes as the scroll action happens. Add a log to observe the i.(binding.String) value in that function as you scroll

Take a look at this: list example

RRK
  • 29
  • 4
  • the value of `i` is always the correct one based on the visible elements in the scroll area. the issue seems to be in the rendering – blackgreen Nov 10 '21 at 11:08
0

As far as I can see the constructor for the custom widget did not call ExtendBaseWidget which is required for rendering to work correctly. I think that answers @blackgreen question too.

You should not embed other widgets using their constructor as noted in the accepted answer.

andy.xyz
  • 2,567
  • 1
  • 13
  • 18