From e2601ea9fa1e1803f59988fd42002cccb2b16439 Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 15 Sep 2026 00:52:05 +0800 Subject: [PATCH] Keep Linux WebViews on one persistent GTK UI thread --- .../webview/goprovider/mainthread_linux.go | 47 +++++++++++++++++++ .../webview/goprovider/mainthread_other.go | 2 +- internal/webview/goprovider/provider.go | 13 ++++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 internal/webview/goprovider/mainthread_linux.go diff --git a/internal/webview/goprovider/mainthread_linux.go b/internal/webview/goprovider/mainthread_linux.go new file mode 100644 index 00000000..3e0a09da --- /dev/null +++ b/internal/webview/goprovider/mainthread_linux.go @@ -0,0 +1,47 @@ +//go:build linux && cgo + +package goprovider + +/* +#cgo pkg-config: gtk+-3.0 +#include +static void pumpGTK(void) { + // Bound each batch so Go creation/removal requests cannot be starved. + for (int i = 0; i < 64 && g_main_context_pending(NULL); ++i) + g_main_context_iteration(NULL, FALSE); +} +*/ +import "C" + +import ( + "runtime" + "sync" + "time" +) + +var gtkThreadOnce sync.Once +var gtkThreadTasks = make(chan func()) + +func RunMainThreadLoop(run func() int) int { return run() } + +func postMainThreadTask(task func()) bool { + gtkThreadOnce.Do(func() { + go func() { + runtime.LockOSThread() + // WebKit must always use its original UI thread, including after + // the last page closes. A page cannot stop this shared event loop. + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + for { + select { + case task := <-gtkThreadTasks: + task() + case <-ticker.C: + } + C.pumpGTK() + } + }() + }) + gtkThreadTasks <- task + return true +} diff --git a/internal/webview/goprovider/mainthread_other.go b/internal/webview/goprovider/mainthread_other.go index aa3ee4a1..45ad0255 100644 --- a/internal/webview/goprovider/mainthread_other.go +++ b/internal/webview/goprovider/mainthread_other.go @@ -1,4 +1,4 @@ -//go:build !darwin +//go:build !darwin && (!linux || !cgo) package goprovider diff --git a/internal/webview/goprovider/provider.go b/internal/webview/goprovider/provider.go index da6130ff..c9d06a5e 100644 --- a/internal/webview/goprovider/provider.go +++ b/internal/webview/goprovider/provider.go @@ -135,12 +135,18 @@ func (p *pageWrapper) start() error { if err := w.Bind(p.callbackName, func(payload string) error { return p.handleCallback(payload) }); err != nil { + w.Destroy() p.ready <- err + close(p.done) return } applyWindowOptions(w, p.opts) w.SetHtml(buildBootstrapHTML(p.callbackName, p.readyID)) + if runtimepkg.GOOS == "linux" { + // All GTK pages share the process-wide event loop below. + return + } w.Run() w.Destroy() close(p.done) @@ -326,7 +332,12 @@ func (p *pageWrapper) Close() error { // the quit message lands on the correct queue. terminateDone := make(chan struct{}) w.Dispatch(func() { - w.Terminate() + if runtimepkg.GOOS == "linux" { + w.Destroy() + close(p.done) + } else { + w.Terminate() + } close(terminateDone) }) <-terminateDone