Skip to content

Fix maxSize method to handle nil canvas case in ShowCompletion#112

Open
syllith wants to merge 1 commit intofyne-io:masterfrom
syllith:master
Open

Fix maxSize method to handle nil canvas case in ShowCompletion#112
syllith wants to merge 1 commit intofyne-io:masterfrom
syllith:master

Conversation

@syllith
Copy link

@syllith syllith commented Sep 12, 2025

I’ve been maintaining this patch manually for several years. Without it, if an app tab contains a completion widget, entering data and then leaving the tab idle for around five minutes triggers garbage collection, which clears cnv and causes a crash when switching back to that tab.

I’ve reapplied this fix across many updates and have yet to encounter any negative side effects. Given its stability, I’d like to propose integrating it into the official repository so others can benefit without needing to patch it locally.

@Jacalz
Copy link
Member

Jacalz commented Sep 13, 2025

Without it, if an app tab contains a completion widget, entering data and then leaving the tab idle for around five minutes triggers garbage collection, which clears cnv and causes a crash when switching back to that tab.

It sounds more like you are working around a bug somewhere else than a GC cycle garbage collecting an object. What you are describing only happens for weak pointers other than that case, the GC never reclaims objects which have a valid reference to them. Something is setting the object to nil somewhere and causing the crash.

@syllith
Copy link
Author

syllith commented Sep 13, 2025

Here's a minimal recreatable example:

package main

import (
	"strings"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
	xwidget "fyne.io/x/fyne/widget"
)

func main() {
	a := app.New()
	w := a.NewWindow("Fyne Tabs Example")

	// CompletionEntry setup
	options := []string{"Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew"}
	completion := xwidget.NewCompletionEntry(options)
	completion.PlaceHolder = "Type to search fruits..."

	// Live filtering as you type
	completion.OnChanged = func(s string) {
		var filtered []string
		lower := strings.ToLower(s)
		for _, opt := range options {
			if strings.Contains(strings.ToLower(opt), lower) {
				filtered = append(filtered, opt)
			}
		}
		completion.SetOptions(filtered)
		completion.ShowCompletion()
	}

	// Tab 1: CompletionEntry
	tab1 := container.NewVBox(
		widget.NewLabel("Completion Entry Demo"),
		completion,
	)

	// Tab 2: Placeholder
	tab2 := container.NewVBox(
		widget.NewLabel("Second Tab (empty)"),
	)

	tabs := container.NewAppTabs(
		container.NewTabItem("Search", tab1),
		container.NewTabItem("Other", tab2),
	)
	tabs.SetTabLocation(container.TabLocationLeading)

	w.SetContent(tabs)
	w.Resize(fyne.NewSize(500, 300))
	w.ShowAndRun()
}

To recreate the issue, select one of the items from the completion widget, then switch to the second tab. Allow around 5 minutes to pass, then switch back to the original tab.

It will crash with the following:

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0xa8 pc=0x7ff6b0fe6996]

goroutine 1 [running, locked to thread]:
fyne.io/x/fyne/widget.(*CompletionEntry).maxSize(0xc00014c008)
        C:/Users/syllith/go/pkg/mod/fyne.io/x/fyne@v0.0.0-20250910205345-ecc79984d005/widget/completionentry.go:106 +0x96
fyne.io/x/fyne/widget.(*CompletionEntry).Move(0xc00014c008, {0xb11df4e0?, 0x7ff6?})
        C:/Users/syllith/go/pkg/mod/fyne.io/x/fyne@v0.0.0-20250910205345-ecc79984d005/widget/completionentry.go:42 +0x79
fyne.io/fyne/v2/layout.vBoxLayout.Layout({0x7ff6b1185680?}, {0xc00013a020, 0x2, 0xc00004f701?}, {0xb1303ec0?, 0x7ff6?})
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/layout/boxlayout.go:102 +0x26b
fyne.io/fyne/v2.(*Container).layout(...)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container.go:185
fyne.io/fyne/v2.(*Container).Refresh(0xc000152000)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container.go:109 +0x47
fyne.io/fyne/v2/container.(*baseTabsRenderer).applyTheme(0xc00011a1e0, {0x7ff6b13027d0?, 0xc000154000})
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/tabs.go:326 +0x1c4
fyne.io/fyne/v2/container.(*baseTabsRenderer).refresh(0xc00011a1e0, {0x7ff6b13027d0?, 0xc000154000?})
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/tabs.go:491 +0x25
fyne.io/fyne/v2/container.(*appTabsRenderer).Refresh(0xc00011a1e0)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/apptabs.go:319 +0x3a
fyne.io/fyne/v2/widget.(*BaseWidget).Refresh(0x0?)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/widget/widget.go:127 +0x52
fyne.io/fyne/v2/container.selectIndex({0x7ff6b13027d0, 0xc000154000}, 0x0)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/tabs.go:207 +0x103
fyne.io/fyne/v2/container.selectItem({0x7ff6b13027d0, 0xc000154000}, 0xc000152080)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/tabs.go:218 +0x57
fyne.io/fyne/v2/container.(*AppTabs).Select(...)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/apptabs.go:163
fyne.io/fyne/v2/container.(*appTabsRenderer).buildTabButtons.func1()
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/apptabs.go:381 +0x26
fyne.io/fyne/v2/container.(*tabButton).Tapped(0x7ff6b1f09500?, 0x7ff6b11ba2c0?)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/container/tabs.go:584 +0x1b
fyne.io/fyne/v2/internal/driver/glfw.(*window).mouseClickedHandleTapDoubleTap(0xc00012c000, {0x7ff6b12febc0, 0xc00014e460}, 0xc00152a010)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/internal/driver/glfw/window.go:574 +0x182
fyne.io/fyne/v2/internal/driver/glfw.(*window).processMouseClicked(0xc00012c000, 0x1, 0x0, 0x0)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/internal/driver/glfw/window.go:535 +0x725
fyne.io/fyne/v2/internal/driver/glfw.(*window).mouseClicked(0xc00012c000, 0xc00004fa48?, 0x7ff6b0f812a0?, 0x0, 0xc00004fa28?)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/internal/driver/glfw/window_desktop.go:410 +0x5d
github.com/go-gl/glfw/v3.3/glfw.goMouseButtonCB(0xc0000021c0?, 0x0, 0x0, 0x0)
        C:/Users/syllith/go/pkg/mod/github.com/go-gl/glfw/v3.3/glfw@v0.0.0-20240506104042-037f3cc74f2a/input.go:333 +0x4e
github.com/go-gl/glfw/v3.3/glfw._Cfunc_glfwPollEvents()
        _cgo_gotypes.go:1545 +0x45
github.com/go-gl/glfw/v3.3/glfw.PollEvents()
        C:/Users/syllith/go/pkg/mod/github.com/go-gl/glfw/v3.3/glfw@v0.0.0-20240506104042-037f3cc74f2a/window.go:931 +0x13
fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).pollEvents(...)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/internal/driver/glfw/loop_desktop.go:22
fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).runGL(0xc0015f9e28?)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/internal/driver/glfw/loop.go:152 +0x1aa
fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).Run(0xc000316c60)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/internal/driver/glfw/driver.go:162 +0x72
fyne.io/fyne/v2/app.(*fyneApp).Run(0xc000316d10)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/app/app.go:77 +0x102
fyne.io/fyne/v2/internal/driver/glfw.(*window).ShowAndRun(0xc00012c000)
        C:/Users/syllith/go/pkg/mod/fyne.io/fyne/v2@v2.6.3/internal/driver/glfw/window.go:217 +0x64
main.main()
        C:/Users/syllith/Desktop/test/main.go:54 +0x56d
exit status 2

Do you see anything that might be useful for understanding why this is happening? From what I can tell, based off this code, I'm not manipulating anything I shouldn't be and don't understand why cnv is nil. Is this perhaps a bug in the main Fyne package? Or perhaps am I filtering incorrectly and it's resulting in this side effect?

@syllith

This comment has been minimized.

@Jacalz
Copy link
Member

Jacalz commented Sep 13, 2025

While I did bail out quickly from that AI generated stuff (please avoid flooding the comments with that in the future), I think you are right. I think I may have read the code incorrectly last time I looked at this. I'll look at how the Entry widget uses CanvasForObject in the code there.

@Jacalz
Copy link
Member

Jacalz commented Sep 13, 2025

However, I still do not agree with the statement that the object just suddenly becomes nil because it is garbage collected. The widget may not have a canvas before and after it is part of the list of visible objects but I don't see how it ever could just become nil randomly.

Comment on lines +100 to +102
if cnv == nil {
return fyne.NewSize(0, 0)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking into entry and select_entry, it does indeed look like you may wish to check for it being nil. I do however wonder if this function is absolutely necessary? Look at the bottom of select_entry.go in the regular fyne widgets and you'll see that we can just set the height of the dropdown to be MinSize and fyne should crop it automatically at the end of the canvas. Maybe we can solve this issue by refactoring the code to avoid this function entirely instead? :)

@syllith
Copy link
Author

syllith commented Sep 13, 2025

Yea sorry for the AI stuff, I just figured that was the most effective way of getting the gist across. I was a bit out of my depth. The GC thing was my early speculation, just because it seemed to be very time dependent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants