fyne
fyne copied to clipboard
App freezes when rapidly modifying content of container
Checklist
- [X] I have searched the issue tracker for open issues that relate to the same problem, before opening a new one.
- [X] This issue only relates to a single bug. I will open new issues for any other problems.
Describe the bug
I've for quiet some time been trying to chase down a application freeze when users are switching between preferences in my app it sometimes randomly just freezes.
I've managed to create a program that replicates the behaviour that makes the main window freeze and become unresponsive.
How to reproduce
Run attached program code. window should freeze within 3-4 seconds
Screenshots
Example code
package main
import (
"encoding/json"
"fmt"
"image/color"
"log"
"math/rand/v2"
"strconv"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
symbol "github.com/roffe/ecusymbol"
"github.com/roffe/txlogger/pkg/layout"
)
var Map = make(map[string]string)
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
Map["T5 Dash"] = `[{"Name":"Rpm","Number":86,"SramOffset":4194,"Address":0,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Medeltrot","Number":80,"SramOffset":4150,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Ign_angle","Number":168,"SramOffset":4228,"Address":0,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Lufttemp","Number":75,"SramOffset":4145,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"P_medel","Number":320,"SramOffset":10751,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Max_tryck","Number":312,"SramOffset":10747,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Regl_tryck","Number":315,"SramOffset":10748,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":0.01},{"Name":"PWM_ut10","Number":318,"SramOffset":10754,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"P_fak","Number":313,"SramOffset":11046,"Address":0,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"I_fak","Number":314,"SramOffset":11044,"Address":0,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"D_fak","Number":311,"SramOffset":11042,"Address":0,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"AD_EGR","Number":9,"SramOffset":4118,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Kyl_temp","Number":72,"SramOffset":4141,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Bil_hast","Number":60,"SramOffset":4123,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Knock_offset1234","Number":131,"SramOffset":4236,"Address":0,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Batt_volt","Number":61,"SramOffset":4122,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Insptid_ms10","Number":64,"SramOffset":4190,"Address":0,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1},{"Name":"Lambdaint","Number":73,"SramOffset":4143,"Address":0,"Length":1,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1}]`
Map["T7 Dash"] = `[{"Name":"ActualIn.n_Engine","Number":3462,"SramOffset":0,"Address":15788900,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":1},{"Name":"Out.X_AccPedal","Number":3672,"SramOffset":0,"Address":15789336,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"%"},{"Name":"In.v_Vehicle","Number":3409,"SramOffset":0,"Address":15788816,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"Km/h"},{"Name":"ActualIn.T_Engine","Number":3469,"SramOffset":0,"Address":15788916,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":1},{"Name":"ActualIn.T_AirInlet","Number":3470,"SramOffset":0,"Address":15788918,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":1},{"Name":"IgnProt.fi_Offset","Number":3045,"SramOffset":0,"Address":15787464,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"Degrees"},{"Name":"Out.fi_Ignition","Number":3686,"SramOffset":0,"Address":15789366,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"° BTDC"},{"Name":"Out.PWM_BoostCntrl","Number":3645,"SramOffset":0,"Address":15789300,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"%"},{"Name":"ActualIn.p_AirInlet","Number":3472,"SramOffset":0,"Address":15788922,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.001},{"Name":"In.p_AirBefThrottle","Number":3395,"SramOffset":0,"Address":15788788,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.001,"Unit":"Bar"},{"Name":"ECMStat.p_Diff","Number":3759,"SramOffset":0,"Address":15789466,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.001,"Unit":"Bar"},{"Name":"MAF.m_AirInlet","Number":452,"SramOffset":0,"Address":15775882,"Length":2,"Mask":0,"Type":32,"ExtendedType":0,"Correctionfactor":1,"Unit":"Mg/c"},{"Name":"m_Request","Number":59,"SramOffset":0,"Address":15775190,"Length":2,"Mask":0,"Type":0,"ExtendedType":0,"Correctionfactor":1,"Unit":"Mg/c"},{"Name":"ECMStat.ST_ActiveAirDem","Number":3754,"SramOffset":0,"Address":15789448,"Length":1,"Mask":0,"Type":36,"ExtendedType":0,"Correctionfactor":1},{"Name":"DisplProt.LambdaScanner","Number":3316,"SramOffset":0,"Address":15788686,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.01},{"Name":"Lambda.LambdaInt","Number":2606,"SramOffset":0,"Address":15787098,"Length":2,"Mask":0,"Type":33,"ExtendedType":0,"Correctionfactor":0.01}]`
Map["T8 Dash"] = `[{"Name":"ActualIn.n_Engine","Number":4181,"SramOffset":0,"Address":1066236,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":1},{"Name":"Out.X_AccPos","Number":4772,"SramOffset":0,"Address":1066522,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.1},{"Name":"In.v_Vehicle","Number":4024,"SramOffset":0,"Address":1065980,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"Km/h"},{"Name":"ActualIn.T_Engine","Number":4155,"SramOffset":0,"Address":1066180,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":1},{"Name":"ActualIn.T_AirInlet","Number":4171,"SramOffset":0,"Address":1066214,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":1},{"Name":"IgnMastProt.fi_Offset","Number":3235,"SramOffset":0,"Address":1065164,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.1},{"Name":"Out.fi_Ignition","Number":4870,"SramOffset":0,"Address":1066662,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"° BTDC"},{"Name":"Out.PWM_BoostCntrl","Number":4843,"SramOffset":0,"Address":1066622,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.1,"Unit":"%"},{"Name":"In.p_AirInlet","Number":4004,"SramOffset":0,"Address":1065938,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.001},{"Name":"ActualIn.p_AirBefThrottle","Number":4159,"SramOffset":0,"Address":1066188,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.001},{"Name":"MAF.m_AirInlet","Number":383,"SramOffset":0,"Address":1056888,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":1,"Unit":"Mg/c"},{"Name":"AirMassMast.m_Request","Number":260,"SramOffset":0,"Address":1056830,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":1},{"Name":"ECMStat.ST_ActiveAirDem","Number":4977,"SramOffset":0,"Address":1067070,"Length":1,"Mask":0,"Type":4,"ExtendedType":0,"Correctionfactor":1},{"Name":"Lambda.LambdaInt","Number":2729,"SramOffset":0,"Address":1064772,"Length":2,"Mask":0,"Type":1,"ExtendedType":0,"Correctionfactor":0.01}]`
}
func main() {
a := app.NewWithID("com.roffe.freezebug")
w := a.NewWindow("freezebug")
updatefunc := func(s []*symbol.Symbol) {
}
lst := NewSymbolListWidget(w, updatefunc)
cnt := container.NewStack(lst)
w.SetContent(cnt)
w.Resize(fyne.NewSize(800, 600))
go func() {
for {
time.Sleep(300 * time.Millisecond)
syms := loadpref()
lst.LoadSymbols(syms...)
cnt.Refresh()
}
}()
w.ShowAndRun()
}
func loadpref() []*symbol.Symbol {
var data string
switch rand.IntN(3) {
case 0:
log.Println("T5 Dash")
data = Map["T5 Dash"]
case 1:
log.Println("T7 Dash")
data = Map["T7 Dash"]
case 2:
log.Println("T8 Dash")
data = Map["T8 Dash"]
default:
log.Println("oops")
}
var symbols []*symbol.Symbol
if err := json.Unmarshal([]byte(data), &symbols); err != nil {
panic(err)
}
return symbols
}
type SymbolListWidget struct {
widget.BaseWidget
symbols []*symbol.Symbol
entryMap map[string]*SymbolWidgetEntry
entries []*SymbolWidgetEntry
container *fyne.Container
border *fyne.Container
scroll *container.Scroll
mu sync.Mutex
updateBars bool
onUpdate func([]*symbol.Symbol)
w fyne.Window
}
func NewSymbolListWidget(w fyne.Window, updateFunc func([]*symbol.Symbol), symbols ...*symbol.Symbol) *SymbolListWidget {
sl := &SymbolListWidget{
entryMap: make(map[string]*SymbolWidgetEntry),
onUpdate: updateFunc,
w: w,
}
sl.ExtendBaseWidget(sl)
sl.render()
sl.LoadSymbols(symbols...)
return sl
}
func (s *SymbolListWidget) render() {
s.container = container.NewVBox()
s.scroll = container.NewVScroll(s.container)
name := widget.NewLabel("Name")
name.TextStyle = fyne.TextStyle{Bold: true}
value := widget.NewLabel("Value")
value.TextStyle = fyne.TextStyle{Bold: true}
num := widget.NewLabel("#")
num.TextStyle = fyne.TextStyle{Bold: true}
typ := widget.NewLabel("Type")
typ.TextStyle = fyne.TextStyle{Bold: true}
factor := widget.NewLabel("Factor")
factor.TextStyle = fyne.TextStyle{Bold: true}
s.border = container.NewBorder(
container.New(&layout.RatioContainer{Widths: sz},
widget.NewLabel(""),
name,
value,
num,
typ,
factor,
),
nil,
nil,
nil,
s.scroll,
)
}
func (s *SymbolListWidget) UpdateBars(enabled bool) {
s.updateBars = enabled
}
func (s *SymbolListWidget) SetValue(name string, value float64) {
val, found := s.entryMap[name]
if found {
val.value = value
if value < val.min {
val.min = value
} else if value > val.max {
val.max = value
}
if s.updateBars {
factor := float32((value - val.min) / (val.max - val.min))
col := GetColorInterpolation(val.min, val.max, value)
col.A = 30
val.valueBar.FillColor = col
val.valueBar.Resize(fyne.NewSize(factor*100, 26))
}
switch val.symbol.Correctionfactor {
case 1:
val.symbolValue.SetText(strconv.FormatFloat(value, 'f', 0, 64))
return
case 0.1:
val.symbolValue.SetText(strconv.FormatFloat(value, 'f', 1, 64))
return
case 0.01:
val.symbolValue.SetText(strconv.FormatFloat(value, 'f', 2, 64))
return
case 0.001:
val.symbolValue.SetText(strconv.FormatFloat(value, 'f', 3, 64))
return
default:
val.symbolValue.SetText(strconv.FormatFloat(value, 'f', 2, 64))
return
}
}
}
func (s *SymbolListWidget) Disable() {
for _, e := range s.entries {
e.symbolCorrectionfactor.Disable()
e.deleteBTN.Disable()
}
}
func (s *SymbolListWidget) Enable() {
for _, e := range s.entries {
e.symbolCorrectionfactor.Enable()
e.deleteBTN.Enable()
}
}
func (s *SymbolListWidget) Add(symbols ...*symbol.Symbol) {
s.mu.Lock()
defer s.mu.Unlock()
for _, sym := range symbols {
if _, found := s.entryMap[sym.Name]; found {
continue
}
deleteFunc := func(sw *SymbolWidgetEntry) {
for i, e := range s.entries {
if e == sw {
s.mu.Lock()
defer s.mu.Unlock()
s.symbols = append(s.symbols[:i], s.symbols[i+1:]...)
s.entries = append(s.entries[:i], s.entries[i+1:]...)
delete(s.entryMap, sw.symbol.Name)
s.container.Remove(sw)
s.scroll.Refresh()
s.onUpdate(s.symbols)
break
}
}
}
entry := NewSymbolWidgetEntry(s.w, sym, deleteFunc)
s.symbols = append(s.symbols, sym)
s.entries = append(s.entries, entry)
s.container.Objects = append(s.container.Objects, entry)
s.entryMap[sym.Name] = entry
}
s.onUpdate(s.symbols)
}
func (s *SymbolListWidget) Clear() {
for _, e := range s.entries {
e.symbolValue.SetText("---")
}
}
func (s *SymbolListWidget) clear() {
s.mu.Lock()
defer s.mu.Unlock()
//s.container.Refresh()
//s.border.Refresh()
s.container.RemoveAll()
s.symbols = []*symbol.Symbol{}
s.entries = []*SymbolWidgetEntry{}
s.entryMap = make(map[string]*SymbolWidgetEntry)
s.onUpdate(s.symbols)
}
func (s *SymbolListWidget) LoadSymbols(symbols ...*symbol.Symbol) {
s.clear()
s.Add(symbols...)
}
func (s *SymbolListWidget) Count() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.symbols)
}
func (s *SymbolListWidget) Symbols() []*symbol.Symbol {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]*symbol.Symbol, len(s.symbols))
copy(out, s.symbols)
return out
}
func (s *SymbolListWidget) CreateRenderer() fyne.WidgetRenderer {
swr := &SymbolListWidgetRenderer{
sl: s,
}
return swr
}
type SymbolListWidgetRenderer struct {
sl *SymbolListWidget
}
func (sr *SymbolListWidgetRenderer) Layout(size fyne.Size) {
sr.sl.border.Resize(size)
}
func (sr *SymbolListWidgetRenderer) MinSize() fyne.Size {
var width float32
var height float32
for _, en := range sr.sl.entries {
sz := en.MinSize()
if sz.Width > width {
width = sz.Width
}
height += sz.Height
}
return fyne.NewSize(width, min(height, 200))
}
func (sr *SymbolListWidgetRenderer) Refresh() {
for _, e := range sr.sl.entries {
e.Refresh()
}
}
func (sr *SymbolListWidgetRenderer) Destroy() {
}
func (sr *SymbolListWidgetRenderer) Objects() []fyne.CanvasObject {
return []fyne.CanvasObject{sr.sl.border}
}
type SymbolWidgetEntry struct {
widget.BaseWidget
symbol *symbol.Symbol
copyName *widget.Button
symbolName *widget.Label
symbolValue *widget.Label
symbolNumber *widget.Label
symbolType *widget.Label
symbolCorrectionfactor *widget.Entry
deleteBTN *widget.Button
valueBar *canvas.Rectangle
container *fyne.Container
deleteFunc func(*SymbolWidgetEntry)
//valueSet bool
value float64
min, max float64
}
func NewSymbolWidgetEntry(w fyne.Window, sym *symbol.Symbol, deleteFunc func(*SymbolWidgetEntry)) *SymbolWidgetEntry {
sw := &SymbolWidgetEntry{
symbol: sym,
deleteFunc: deleteFunc,
}
sw.ExtendBaseWidget(sw)
sw.copyName = widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
w.Clipboard().SetContent(sym.Name)
})
sw.symbolName = widget.NewLabel(sw.symbol.Name)
sw.symbolValue = widget.NewLabel("---")
sw.symbolNumber = widget.NewLabel(strconv.Itoa(sw.symbol.Number))
sw.symbolType = widget.NewLabel(fmt.Sprintf("%02X", sw.symbol.Type))
sw.symbolCorrectionfactor = widget.NewEntry()
sw.symbolCorrectionfactor.OnChanged = func(s string) {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return
}
sw.symbol.Correctionfactor = f
}
sw.SetCorrectionFactor(sym.Correctionfactor)
sw.deleteBTN = widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
if sw.deleteFunc != nil {
sw.deleteFunc(sw)
}
})
sw.valueBar = canvas.NewRectangle(color.RGBA{255, 0, 0, 255})
sw.container = container.NewWithoutLayout(
sw.copyName,
sw.valueBar,
sw.symbolName,
sw.symbolValue,
sw.symbolNumber,
sw.symbolType,
sw.symbolCorrectionfactor,
sw.deleteBTN,
)
return sw
}
func (sw *SymbolWidgetEntry) SetCorrectionFactor(f float64) {
sw.symbol.Correctionfactor = f
switch f {
case 1:
sw.symbolCorrectionfactor.SetText(strconv.FormatFloat(f, 'f', 0, 64))
case 0.1:
sw.symbolCorrectionfactor.SetText(strconv.FormatFloat(f, 'f', 1, 64))
case 0.01:
sw.symbolCorrectionfactor.SetText(strconv.FormatFloat(f, 'f', 2, 64))
case 0.001:
sw.symbolCorrectionfactor.SetText(strconv.FormatFloat(f, 'f', 3, 64))
default:
sw.symbolCorrectionfactor.SetText(strconv.FormatFloat(f, 'f', 4, 64))
}
}
func (sw *SymbolWidgetEntry) CreateRenderer() fyne.WidgetRenderer {
swr := &SymbolWidgetEntryRenderer{
sw: sw,
}
return swr
}
type SymbolWidgetEntryRenderer struct {
sw *SymbolWidgetEntry
}
var sz = []float32{
.04, // copy
.32, // name
.18, // value
.12, // number
.14, // correctionfactor
.08, // type
.04, // deletebtn
}
func sumFloat32(a []float32) float32 {
var sum float32
for _, v := range a {
sum += v
}
return sum
}
func (sr *SymbolWidgetEntryRenderer) Layout(size fyne.Size) {
sw := sr.sw
padd := size.Width * ((1.0 - sumFloat32(sz)) / float32(len(sz)))
sw.copyName.Resize(fyne.NewSize(size.Width*sz[0], size.Height))
sw.symbolName.Resize(fyne.NewSize(size.Width*sz[1], size.Height))
sw.symbolValue.Resize(fyne.NewSize(size.Width*sz[2], size.Height))
sw.symbolNumber.Resize(fyne.NewSize(size.Width*sz[3], size.Height))
sw.symbolType.Resize(fyne.NewSize(size.Width*sz[4], size.Height))
sw.symbolCorrectionfactor.Resize(fyne.NewSize(size.Width*sz[5], size.Height))
sw.deleteBTN.Resize(fyne.NewSize(size.Width*sz[6], size.Height))
var x float32
sw.copyName.Move(fyne.NewPos(x, 0))
x += sw.copyName.Size().Width + padd
sw.symbolName.Move(fyne.NewPos(x, 0))
x += sw.symbolName.Size().Width + padd
sw.symbolValue.Move(fyne.NewPos(x, 0))
sw.valueBar.Move(fyne.NewPos(x, 6))
x += sw.symbolValue.Size().Width + padd
sw.symbolNumber.Move(fyne.NewPos(x, 0))
x += sw.symbolNumber.Size().Width + padd
sw.symbolType.Move(fyne.NewPos(x, 0))
x += sw.symbolType.Size().Width + padd
sw.symbolCorrectionfactor.Move(fyne.NewPos(x, 0))
x += sw.symbolCorrectionfactor.Size().Width + padd
sw.deleteBTN.Move(fyne.NewPos(x, 0))
}
func (sr *SymbolWidgetEntryRenderer) MinSize() fyne.Size {
sw := sr.sw
var width float32
var height float32 = sw.symbolName.MinSize().Height
width += sw.copyName.MinSize().Width
width += sw.symbolName.MinSize().Width
width += sw.symbolValue.MinSize().Width
width += sw.symbolNumber.MinSize().Width
width += sw.symbolCorrectionfactor.MinSize().Width
width += sw.deleteBTN.MinSize().Width
return fyne.NewSize(width, height)
}
func (sr *SymbolWidgetEntryRenderer) Refresh() {
sr.sw.symbolName.Refresh()
sr.sw.symbolValue.Refresh()
sr.sw.symbolNumber.Refresh()
sr.sw.symbolType.Refresh()
sr.sw.symbolCorrectionfactor.Refresh()
}
func (sr *SymbolWidgetEntryRenderer) Destroy() {
}
func (sr *SymbolWidgetEntryRenderer) Objects() []fyne.CanvasObject {
return []fyne.CanvasObject{sr.sw.container}
}
func GetColorInterpolation(min, max, value float64) color.RGBA {
//log.Println("getColorInterpolation", min, max, value)
// Normalize the value to a 0-1 range
t := (value - min) / (max - min)
divider := .5
var r, g, b float64
if t < divider { // Green to Yellow interpolation
r = lerp(0, 1, t/divider)
g = 1
} else { // Yellow to Red interpolation
r = 1
g = lerp(1, 0, (t-divider)/(1-divider))
}
b = 0
// Convert from 0-1 range to 0-255 for color.RGBA
return color.RGBA{
R: uint8(r * 255),
G: uint8(g * 255),
B: uint8(b * 255),
A: 255,
}
}
func lerp(a, b, t float64) float64 {
return a + (b-a)*t
}
Fyne version
fyne.io/fyne/v2 v2.4.6-0.20240418153625-66b892df8f5e
Go compiler version
go version go1.22.0 windows/amd64
Operating system and version
Windows 11
Additional Information
No response