wails/pr_4570.diff
copilot-swe-agent[bot] 35a3e11680 Rebase branch onto v3-alpha: clean up master-only files and restore v3-alpha content
This cleanup commit resolves the issue where the branch was accidentally
based on master instead of v3-alpha. It removes all master-specific files,
restores any v3-alpha files to their correct state, and ensures only the
3 intended bugfix changes differ from v3-alpha:
- v3/pkg/application/webview_window_windows.go
- v3/pkg/w32/constants.go
- v3/pkg/w32/user32.go
2026-02-22 06:20:11 +00:00

3455 lines
92 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

diff --git a/v2/internal/frontend/desktop/linux/clipboard.go b/v2/internal/frontend/desktop/linux/clipboard.go
index a2a46dacc34..5ceada902fe 100644
--- a/v2/internal/frontend/desktop/linux/clipboard.go
+++ b/v2/internal/frontend/desktop/linux/clipboard.go
@@ -1,5 +1,5 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
diff --git a/v2/internal/frontend/desktop/linux/clipboard_webkit6.go b/v2/internal/frontend/desktop/linux/clipboard_webkit6.go
new file mode 100644
index 00000000000..41034a4ef94
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/clipboard_webkit6.go
@@ -0,0 +1,64 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0
+
+#include "gtk/gtk.h"
+#include "webkit/webkit.h"
+
+static gchar* GetClipboardText() {
+ GdkClipboard *clip = gdk_display_get_primary_clipboard(gdk_display_get_default());
+ GdkContentProvider *provider = gdk_clipboard_get_content(clip);
+
+ GValue value = G_VALUE_INIT;
+ g_value_init(&value, G_TYPE_STRING);
+
+ if(!gdk_content_provider_get_value(provider, &value, NULL)) {
+ g_value_unset(&value);
+ return g_strdup("");
+ }
+
+ gchar *result = g_value_dup_string(&value);
+ g_value_unset(&value);
+
+ return result ? result : g_strdup("");
+}
+
+static void SetClipboardText(gchar* text) {
+ GdkDisplay *display = gdk_display_get_default();
+
+ GdkClipboard *clip = gdk_display_get_primary_clipboard(display);
+ gdk_clipboard_set_text(clip, text);
+
+ clip = gdk_display_get_clipboard(display);
+ gdk_clipboard_set_text(clip, text);
+}
+*/
+import "C"
+import "sync"
+
+func (f *Frontend) ClipboardGetText() (string, error) {
+ var text string
+ var wg sync.WaitGroup
+ wg.Add(1)
+ invokeOnMainThread(func() {
+ ctxt := C.GetClipboardText()
+ defer C.g_free(C.gpointer(ctxt))
+ text = C.GoString(ctxt)
+ wg.Done()
+ })
+ wg.Wait()
+ return text, nil
+}
+
+func (f *Frontend) ClipboardSetText(text string) error {
+ invokeOnMainThread(func() {
+ ctxt := (*C.gchar)(C.CString(text))
+ defer C.g_free(C.gpointer(ctxt))
+ C.SetClipboardText(ctxt)
+ })
+ return nil
+}
diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go
index c009a18cad2..6a8721e3a93 100644
--- a/v2/internal/frontend/desktop/linux/frontend.go
+++ b/v2/internal/frontend/desktop/linux/frontend.go
@@ -1,10 +1,10 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
diff --git a/v2/internal/frontend/desktop/linux/frontend_webkit6.go b/v2/internal/frontend/desktop/linux/frontend_webkit6.go
new file mode 100644
index 00000000000..35bebcbaa49
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/frontend_webkit6.go
@@ -0,0 +1,580 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0
+
+#include "gtk/gtk.h"
+#include "webkit/webkit.h"
+
+// CREDIT: https://github.com/rainycape/magick
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+
+static void fix_signal(int signum)
+{
+ struct sigaction st;
+
+ if (sigaction(signum, NULL, &st) < 0) {
+ goto fix_signal_error;
+ }
+ st.sa_flags |= SA_ONSTACK;
+ if (sigaction(signum, &st, NULL) < 0) {
+ goto fix_signal_error;
+ }
+ return;
+fix_signal_error:
+ fprintf(stderr, "error fixing handler for signal %d, please "
+ "report this issue to "
+ "https://github.com/wailsapp/wails: %s\n",
+ signum, strerror(errno));
+}
+
+static void install_signal_handlers()
+{
+#if defined(SIGCHLD)
+ fix_signal(SIGCHLD);
+#endif
+#if defined(SIGHUP)
+ fix_signal(SIGHUP);
+#endif
+#if defined(SIGINT)
+ fix_signal(SIGINT);
+#endif
+#if defined(SIGQUIT)
+ fix_signal(SIGQUIT);
+#endif
+#if defined(SIGABRT)
+ fix_signal(SIGABRT);
+#endif
+#if defined(SIGFPE)
+ fix_signal(SIGFPE);
+#endif
+#if defined(SIGTERM)
+ fix_signal(SIGTERM);
+#endif
+#if defined(SIGBUS)
+ fix_signal(SIGBUS);
+#endif
+#if defined(SIGSEGV)
+ fix_signal(SIGSEGV);
+#endif
+#if defined(SIGXCPU)
+ fix_signal(SIGXCPU);
+#endif
+#if defined(SIGXFSZ)
+ fix_signal(SIGXFSZ);
+#endif
+}
+
+*/
+import "C"
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "net/url"
+ "os"
+ "runtime"
+ "strings"
+ "sync"
+ "text/template"
+ "unsafe"
+
+ "github.com/wailsapp/wails/v2/pkg/assetserver"
+ "github.com/wailsapp/wails/v2/pkg/assetserver/webview"
+
+ "github.com/wailsapp/wails/v2/internal/binding"
+ "github.com/wailsapp/wails/v2/internal/frontend"
+ "github.com/wailsapp/wails/v2/internal/frontend/originvalidator"
+ wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime"
+ "github.com/wailsapp/wails/v2/internal/logger"
+ "github.com/wailsapp/wails/v2/pkg/options"
+)
+
+var initOnce = sync.Once{}
+
+const startURL = "wails://wails/"
+
+var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
+
+type Frontend struct {
+
+ // Context
+ ctx context.Context
+
+ frontendOptions *options.App
+ logger *logger.Logger
+ debug bool
+ devtoolsEnabled bool
+
+ // Assets
+ assets *assetserver.AssetServer
+ startURL *url.URL
+
+ // main window handle
+ mainWindow *Window
+ bindings *binding.Bindings
+ dispatcher frontend.Dispatcher
+
+ originValidator *originvalidator.OriginValidator
+}
+
+var mainLoop *C.GMainLoop
+
+func (f *Frontend) RunMainLoop() {
+ mainLoop = C.g_main_loop_new(nil, C.gboolean(1))
+ C.g_main_loop_run(mainLoop)
+}
+
+func (f *Frontend) WindowClose() {
+ f.mainWindow.Destroy()
+}
+
+func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
+ initOnce.Do(func() {
+ runtime.LockOSThread()
+
+ // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings
+ if os.Getenv("GDK_BACKEND") == "" && (os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") {
+ _ = os.Setenv("GDK_BACKEND", "x11")
+ }
+
+ if ok := C.gtk_init_check(); ok != 1 {
+ panic(errors.New("failed to init GTK"))
+ }
+ })
+
+ result := &Frontend{
+ frontendOptions: appoptions,
+ logger: myLogger,
+ bindings: appBindings,
+ dispatcher: dispatcher,
+ ctx: ctx,
+ }
+ result.startURL, _ = url.Parse(startURL)
+ result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
+
+ if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
+ result.startURL = _starturl
+ result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
+ } else {
+ if port, _ := ctx.Value("assetserverport").(string); port != "" {
+ result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port)
+ result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
+ }
+
+ var bindings string
+ var err error
+ if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated {
+ bindings, err = appBindings.ToJSON()
+ if err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ appBindings.DB().UpdateObfuscatedCallMap()
+ }
+ assets, err := assetserver.NewAssetServerMainPage(bindings, appoptions, ctx.Value("assetdir") != nil, myLogger, wailsruntime.RuntimeAssetsBundle)
+ if err != nil {
+ log.Fatal(err)
+ }
+ result.assets = assets
+
+ go result.startRequestProcessor()
+ }
+
+ go result.startMessageProcessor()
+ go result.startBindingsMessageProcessor()
+
+ var _debug = ctx.Value("debug")
+ var _devtoolsEnabled = ctx.Value("devtoolsEnabled")
+
+ if _debug != nil {
+ result.debug = _debug.(bool)
+ }
+ if _devtoolsEnabled != nil {
+ result.devtoolsEnabled = _devtoolsEnabled.(bool)
+ }
+
+ result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled)
+
+ C.install_signal_handlers()
+
+ if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" {
+ prgname := C.CString(appoptions.Linux.ProgramName)
+ C.g_set_prgname(prgname)
+ C.free(unsafe.Pointer(prgname))
+ }
+
+ go result.startSecondInstanceProcessor()
+
+ return result
+}
+
+func (f *Frontend) startMessageProcessor() {
+ for message := range messageBuffer {
+ f.processMessage(message)
+ }
+}
+
+func (f *Frontend) startBindingsMessageProcessor() {
+ for msg := range bindingsMessageBuffer {
+ origin, err := f.originValidator.GetOriginFromURL(msg.source)
+ if err != nil {
+ f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err))
+ continue
+ }
+
+ allowed := f.originValidator.IsOriginAllowed(origin)
+ if !allowed {
+ f.logger.Error("Blocked request from unauthorized origin: %s", origin)
+ continue
+ }
+
+ f.processMessage(msg.message)
+ }
+}
+
+func (f *Frontend) WindowReload() {
+ f.ExecJS("runtime.WindowReload();")
+}
+
+func (f *Frontend) WindowSetSystemDefaultTheme() {
+ return
+}
+
+func (f *Frontend) WindowSetLightTheme() {
+ return
+}
+
+func (f *Frontend) WindowSetDarkTheme() {
+ return
+}
+
+func (f *Frontend) Run(ctx context.Context) error {
+ f.ctx = ctx
+
+ go func() {
+ if f.frontendOptions.OnStartup != nil {
+ f.frontendOptions.OnStartup(f.ctx)
+ }
+ }()
+
+ if f.frontendOptions.SingleInstanceLock != nil {
+ SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
+ }
+
+ f.mainWindow.Run(f.startURL.String())
+
+ return nil
+}
+
+func (f *Frontend) WindowCenter() {
+ f.mainWindow.Center()
+}
+
+func (f *Frontend) WindowSetAlwaysOnTop(b bool) {
+ f.mainWindow.SetKeepAbove(b)
+}
+
+func (f *Frontend) WindowSetPosition(x, y int) {
+ f.mainWindow.SetPosition(x, y)
+}
+func (f *Frontend) WindowGetPosition() (int, int) {
+ return f.mainWindow.GetPosition()
+}
+
+func (f *Frontend) WindowSetSize(width, height int) {
+ f.mainWindow.SetSize(width, height)
+}
+
+func (f *Frontend) WindowGetSize() (int, int) {
+ return f.mainWindow.Size()
+}
+
+func (f *Frontend) WindowSetTitle(title string) {
+ f.mainWindow.SetTitle(title)
+}
+
+func (f *Frontend) WindowFullscreen() {
+ if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
+ f.ExecJS("window.wails.flags.enableResize = false;")
+ }
+ f.mainWindow.Fullscreen()
+}
+
+func (f *Frontend) WindowUnfullscreen() {
+ if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
+ f.ExecJS("window.wails.flags.enableResize = true;")
+ }
+ f.mainWindow.UnFullscreen()
+}
+
+func (f *Frontend) WindowReloadApp() {
+ f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL))
+}
+
+func (f *Frontend) WindowShow() {
+ f.mainWindow.Show()
+}
+
+func (f *Frontend) WindowHide() {
+ f.mainWindow.Hide()
+}
+
+func (f *Frontend) Show() {
+ f.mainWindow.Show()
+}
+
+func (f *Frontend) Hide() {
+ f.mainWindow.Hide()
+}
+func (f *Frontend) WindowMaximise() {
+ f.mainWindow.Maximise()
+}
+func (f *Frontend) WindowToggleMaximise() {
+ f.mainWindow.ToggleMaximise()
+}
+func (f *Frontend) WindowUnmaximise() {
+ f.mainWindow.UnMaximise()
+}
+func (f *Frontend) WindowMinimise() {
+ f.mainWindow.Minimise()
+}
+func (f *Frontend) WindowUnminimise() {
+ f.mainWindow.UnMinimise()
+}
+
+func (f *Frontend) WindowSetMinSize(width int, height int) {
+ f.mainWindow.SetMinSize(width, height)
+}
+func (f *Frontend) WindowSetMaxSize(width int, height int) {
+ f.mainWindow.SetMaxSize(width, height)
+}
+
+func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) {
+ if col == nil {
+ return
+ }
+ f.mainWindow.SetBackgroundColour(col.R, col.G, col.B, col.A)
+}
+
+func (f *Frontend) ScreenGetAll() ([]Screen, error) {
+ return GetAllScreens(f.mainWindow.asGTKWindow())
+}
+
+func (f *Frontend) WindowIsMaximised() bool {
+ return f.mainWindow.IsMaximised()
+}
+
+func (f *Frontend) WindowIsMinimised() bool {
+ return f.mainWindow.IsMinimised()
+}
+
+func (f *Frontend) WindowIsNormal() bool {
+ return f.mainWindow.IsNormal()
+}
+
+func (f *Frontend) WindowIsFullscreen() bool {
+ return f.mainWindow.IsFullScreen()
+}
+
+func (f *Frontend) Quit() {
+ if f.frontendOptions.OnBeforeClose != nil {
+ go func() {
+ if !f.frontendOptions.OnBeforeClose(f.ctx) {
+ f.mainWindow.Quit()
+ }
+ }()
+ return
+ }
+ f.mainWindow.Quit()
+}
+
+func (f *Frontend) WindowPrint() {
+ f.ExecJS("window.print();")
+}
+
+type EventNotify struct {
+ Name string `json:"name"`
+ Data []interface{} `json:"data"`
+}
+
+func (f *Frontend) Notify(name string, data ...interface{}) {
+ notification := EventNotify{
+ Name: name,
+ Data: data,
+ }
+ payload, err := json.Marshal(notification)
+ if err != nil {
+ f.logger.Error(err.Error())
+ return
+ }
+ f.mainWindow.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`)
+}
+
+var edgeMap = map[string]uintptr{
+ "n-resize": C.GDK_SURFACE_EDGE_NORTH,
+ "ne-resize": C.GDK_SURFACE_EDGE_NORTH_EAST,
+ "e-resize": C.GDK_SURFACE_EDGE_EAST,
+ "se-resize": C.GDK_SURFACE_EDGE_SOUTH_EAST,
+ "s-resize": C.GDK_SURFACE_EDGE_SOUTH,
+ "sw-resize": C.GDK_SURFACE_EDGE_SOUTH_WEST,
+ "w-resize": C.GDK_SURFACE_EDGE_WEST,
+ "nw-resize": C.GDK_SURFACE_EDGE_NORTH_WEST,
+}
+
+func (f *Frontend) processMessage(message string) {
+ if message == "DomReady" {
+ if f.frontendOptions.OnDomReady != nil {
+ f.frontendOptions.OnDomReady(f.ctx)
+ }
+ return
+ }
+
+ if message == "drag" {
+ if !f.mainWindow.IsFullScreen() {
+ f.startDrag()
+ }
+ return
+ }
+
+ if message == "wails:showInspector" {
+ f.mainWindow.ShowInspector()
+ return
+ }
+
+ if strings.HasPrefix(message, "resize:") {
+ if !f.mainWindow.IsFullScreen() {
+ sl := strings.Split(message, ":")
+ if len(sl) != 2 {
+ f.logger.Info("Unknown message returned from dispatcher: %+v", message)
+ return
+ }
+ edge := edgeMap[sl[1]]
+ err := f.startResize(edge)
+ if err != nil {
+ f.logger.Error(err.Error())
+ }
+ }
+ return
+ }
+
+ if message == "runtime:ready" {
+ cmd := fmt.Sprintf(
+ "window.wails.setCSSDragProperties('%s', '%s');\n"+
+ "window.wails.setCSSDropProperties('%s', '%s');\n"+
+ "window.wails.flags.deferDragToMouseMove = true;",
+ f.frontendOptions.CSSDragProperty,
+ f.frontendOptions.CSSDragValue,
+ f.frontendOptions.DragAndDrop.CSSDropProperty,
+ f.frontendOptions.DragAndDrop.CSSDropValue,
+ )
+
+ f.ExecJS(cmd)
+
+ if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
+ f.ExecJS("window.wails.flags.enableResize = true;")
+ }
+
+ if f.frontendOptions.DragAndDrop.EnableFileDrop {
+ f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;")
+ }
+
+ return
+ }
+
+ go func() {
+ result, err := f.dispatcher.ProcessMessage(message, f)
+ if err != nil {
+ f.logger.Error(err.Error())
+ f.Callback(result)
+ return
+ }
+ if result == "" {
+ return
+ }
+
+ switch result[0] {
+ case 'c':
+ // Callback from a method call
+ f.Callback(result[1:])
+ default:
+ f.logger.Info("Unknown message returned from dispatcher: %+v", result)
+ }
+ }()
+}
+
+func (f *Frontend) Callback(message string) {
+ escaped, err := json.Marshal(message)
+ if err != nil {
+ panic(err)
+ }
+ f.ExecJS(`window.wails.Callback(` + string(escaped) + `);`)
+}
+
+func (f *Frontend) startDrag() {
+ f.mainWindow.StartDrag()
+}
+
+func (f *Frontend) startResize(edge uintptr) error {
+ f.mainWindow.StartResize(edge)
+ return nil
+}
+
+func (f *Frontend) ExecJS(js string) {
+ f.mainWindow.ExecJS(js)
+}
+
+type bindingsMessage struct {
+ message string
+ source string
+}
+
+var messageBuffer = make(chan string, 100)
+var bindingsMessageBuffer = make(chan *bindingsMessage, 100)
+
+//export processMessage
+func processMessage(message *C.char) {
+ goMessage := C.GoString(message)
+ messageBuffer <- goMessage
+}
+
+//export processBindingMessage
+func processBindingMessage(message *C.char, source *C.char) {
+ goMessage := C.GoString(message)
+ goSource := C.GoString(source)
+ bindingsMessageBuffer <- &bindingsMessage{
+ message: goMessage,
+ source: goSource,
+ }
+}
+
+var requestBuffer = make(chan webview.Request, 100)
+
+func (f *Frontend) startRequestProcessor() {
+ for request := range requestBuffer {
+ f.assets.ServeWebViewRequest(request)
+ }
+}
+
+//export processURLRequest
+func processURLRequest(request unsafe.Pointer) {
+ requestBuffer <- webview.NewRequest(request)
+}
+
+func (f *Frontend) startSecondInstanceProcessor() {
+ for secondInstanceData := range secondInstanceBuffer {
+ if f.frontendOptions.SingleInstanceLock != nil &&
+ f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil {
+ f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData)
+ }
+ }
+}
diff --git a/v2/internal/frontend/desktop/linux/gtk.go b/v2/internal/frontend/desktop/linux/gtk.go
index 67a38c7a045..0f80125d354 100644
--- a/v2/internal/frontend/desktop/linux/gtk.go
+++ b/v2/internal/frontend/desktop/linux/gtk.go
@@ -1,10 +1,10 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
diff --git a/v2/internal/frontend/desktop/linux/gtk_webkit6.go b/v2/internal/frontend/desktop/linux/gtk_webkit6.go
new file mode 100644
index 00000000000..97ad8b6da1b
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/gtk_webkit6.go
@@ -0,0 +1,65 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+import "C"
+import (
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+//export handleMenuRadioItemClick
+func handleMenuRadioItemClick(rName *C.char, prev *C.char, curr *C.char) {
+ radioActionName := C.GoString(rName)
+ prevId := C.GoString(prev)
+ itemId := C.GoString(curr)
+
+ actionName := radioActionName + "::" + itemId
+ it, ok := gActionIdToMenuItem.Load(actionName)
+ if !ok {
+ return
+ }
+
+ item := it.(*menu.MenuItem)
+
+ prevActionId := radioActionName + "::" + prevId
+ prevIt, ok := gActionIdToMenuItem.Load(prevActionId)
+ if !ok {
+ return
+ }
+
+ prevItem := prevIt.(*menu.MenuItem)
+
+ prevItem.Checked = false
+ item.Checked = true
+
+ go item.Click(&menu.CallbackData{MenuItem: item})
+}
+
+//export handleMenuCheckItemClick
+func handleMenuCheckItemClick(aName *C.char, checked C.int) {
+ actionName := C.GoString(aName)
+ it, ok := gActionIdToMenuItem.Load(actionName)
+ if !ok {
+ return
+ }
+
+ item := it.(*menu.MenuItem)
+
+ item.Checked = int(checked) == 1
+
+ go item.Click(&menu.CallbackData{MenuItem: item})
+}
+
+//export handleMenuItemClick
+func handleMenuItemClick(aName *C.char) {
+ actionName := C.GoString(aName)
+ it, ok := gActionIdToMenuItem.Load(actionName)
+ if !ok {
+ return
+ }
+
+ item := it.(*menu.MenuItem)
+
+ go item.Click(&menu.CallbackData{MenuItem: item})
+}
diff --git a/v2/internal/frontend/desktop/linux/invoke.go b/v2/internal/frontend/desktop/linux/invoke.go
index 16d5e73d298..83aa509a143 100644
--- a/v2/internal/frontend/desktop/linux/invoke.go
+++ b/v2/internal/frontend/desktop/linux/invoke.go
@@ -4,7 +4,8 @@
package linux
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo !webkit_6 pkg-config: gtk+-3.0
+#cgo webkit_6 pkg-config: gtk4
#include <stdio.h>
#include "gtk/gtk.h"
diff --git a/v2/internal/frontend/desktop/linux/keys.go b/v2/internal/frontend/desktop/linux/keys.go
index e5a127dbdfc..74762090264 100644
--- a/v2/internal/frontend/desktop/linux/keys.go
+++ b/v2/internal/frontend/desktop/linux/keys.go
@@ -1,10 +1,10 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
diff --git a/v2/internal/frontend/desktop/linux/keys_webkit6.go b/v2/internal/frontend/desktop/linux/keys_webkit6.go
new file mode 100644
index 00000000000..5cde3aacc11
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/keys_webkit6.go
@@ -0,0 +1,107 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0
+
+#include "gtk/gtk.h"
+
+*/
+import "C"
+import (
+ "github.com/wailsapp/wails/v2/pkg/menu/keys"
+)
+
+var namedKeysToGTK = map[string]C.guint{
+ "backspace": C.guint(0xff08),
+ "tab": C.guint(0xff09),
+ "return": C.guint(0xff0d),
+ "enter": C.guint(0xff0d),
+ "escape": C.guint(0xff1b),
+ "left": C.guint(0xff51),
+ "right": C.guint(0xff53),
+ "up": C.guint(0xff52),
+ "down": C.guint(0xff54),
+ "space": C.guint(0xff80),
+ "delete": C.guint(0xff9f),
+ "home": C.guint(0xff95),
+ "end": C.guint(0xff9c),
+ "page up": C.guint(0xff9a),
+ "page down": C.guint(0xff9b),
+ "f1": C.guint(0xffbe),
+ "f2": C.guint(0xffbf),
+ "f3": C.guint(0xffc0),
+ "f4": C.guint(0xffc1),
+ "f5": C.guint(0xffc2),
+ "f6": C.guint(0xffc3),
+ "f7": C.guint(0xffc4),
+ "f8": C.guint(0xffc5),
+ "f9": C.guint(0xffc6),
+ "f10": C.guint(0xffc7),
+ "f11": C.guint(0xffc8),
+ "f12": C.guint(0xffc9),
+ "f13": C.guint(0xffca),
+ "f14": C.guint(0xffcb),
+ "f15": C.guint(0xffcc),
+ "f16": C.guint(0xffcd),
+ "f17": C.guint(0xffce),
+ "f18": C.guint(0xffcf),
+ "f19": C.guint(0xffd0),
+ "f20": C.guint(0xffd1),
+ "f21": C.guint(0xffd2),
+ "f22": C.guint(0xffd3),
+ "f23": C.guint(0xffd4),
+ "f24": C.guint(0xffd5),
+ "f25": C.guint(0xffd6),
+ "f26": C.guint(0xffd7),
+ "f27": C.guint(0xffd8),
+ "f28": C.guint(0xffd9),
+ "f29": C.guint(0xffda),
+ "f30": C.guint(0xffdb),
+ "f31": C.guint(0xffdc),
+ "f32": C.guint(0xffdd),
+ "f33": C.guint(0xffde),
+ "f34": C.guint(0xffdf),
+ "f35": C.guint(0xffe0),
+ "numlock": C.guint(0xff7f),
+}
+
+func acceleratorToGTK(accelerator *keys.Accelerator) (C.guint, C.GdkModifierType) {
+ key := parseKey(accelerator.Key)
+ mods := parseModifiers(accelerator.Modifiers)
+ return key, mods
+}
+
+func parseKey(key string) C.guint {
+ var result C.guint
+ result, found := namedKeysToGTK[key]
+ if found {
+ return result
+ }
+ // Check for unknown namedkeys
+ // Check if we only have a single character
+ if len(key) != 1 {
+ return C.guint(0)
+ }
+ keyval := rune(key[0])
+ return C.gdk_unicode_to_keyval(C.guint(keyval))
+}
+
+func parseModifiers(modifiers []keys.Modifier) C.GdkModifierType {
+
+ var result C.GdkModifierType
+
+ for _, modifier := range modifiers {
+ switch modifier {
+ case keys.ShiftKey:
+ result |= C.GDK_SHIFT_MASK
+ case keys.ControlKey, keys.CmdOrCtrlKey:
+ result |= C.GDK_CONTROL_MASK
+ case keys.OptionOrAltKey:
+ result |= C.GDK_ALT_MASK
+ }
+ }
+ return result
+}
diff --git a/v2/internal/frontend/desktop/linux/menu.go b/v2/internal/frontend/desktop/linux/menu.go
index a61d190bdba..81775428d6e 100644
--- a/v2/internal/frontend/desktop/linux/menu.go
+++ b/v2/internal/frontend/desktop/linux/menu.go
@@ -1,10 +1,10 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
diff --git a/v2/internal/frontend/desktop/linux/menu_webkit6.go b/v2/internal/frontend/desktop/linux/menu_webkit6.go
new file mode 100644
index 00000000000..9df04ce0edb
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/menu_webkit6.go
@@ -0,0 +1,256 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0
+
+#include "gtk/gtk.h"
+#include <string.h>
+
+static GActionMap *toActionMap(GtkWindow *window) { return (G_ACTION_MAP(window)); }
+static GAction *toGAction(GSimpleAction *action) { return (G_ACTION(action)); }
+static GMenuModel *toGMenuModel(GMenu *menu) { return (G_MENU_MODEL(menu)); }
+
+extern void handleMenuItemClick(char* aid);
+extern void handleMenuCheckItemClick(char* aid, int checked);
+extern void handleMenuRadioItemClick(char* radioId, char* prev, char* curr);
+
+static void onAction(GAction *action, GVariant *param) {
+ GVariantType *stateType = g_action_get_state_type(action);
+
+ if(stateType != NULL) {
+ GVariant *state = g_action_get_state(action);
+ gchar *stateStr = g_variant_type_dup_string(stateType);
+
+ if(strcmp(stateStr, "s") == 0) {
+ g_simple_action_set_state(G_SIMPLE_ACTION(action), param);
+
+ handleMenuRadioItemClick(
+ g_action_get_name(action),
+ g_variant_get_string(state, NULL),
+ g_variant_get_string(param, NULL));
+
+ } else if(strcmp(stateStr, "b") == 0) {
+ gboolean checked = !g_variant_get_boolean(state);
+ GVariant *newState = g_variant_new_boolean(checked);
+
+ g_simple_action_set_state(G_SIMPLE_ACTION(action), newState);
+
+ handleMenuCheckItemClick(g_action_get_name(action), checked);
+ }
+
+ if(state != NULL) {
+ g_variant_unref(state);
+ }
+
+ if(stateStr != NULL) {
+ g_free(stateStr);
+ }
+ } else {
+ handleMenuItemClick(g_action_get_name(action));
+ }
+}
+
+gulong connectClick(GSimpleAction *action) {
+ return g_signal_connect(action, "activate", G_CALLBACK(onAction), NULL);
+}
+
+void setAccels(GtkApplication *app, char *actionName, char *accels) {
+ gtk_application_set_accels_for_action(app, actionName, (const char *[]) { accels, NULL });
+}
+*/
+import "C"
+import (
+ "strings"
+ "sync"
+ "unsafe"
+
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+var menuIdCounter int
+var menuItemToId map[*menu.MenuItem]int
+var menuIdToItem map[int]*menu.MenuItem
+var gtkMenuCache map[*menu.MenuItem]*C.GMenu
+var gActionIdToMenuItem sync.Map
+
+func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) {
+ f.mainWindow.SetApplicationMenu(menu)
+}
+
+func (f *Frontend) MenuUpdateApplicationMenu() {
+ f.mainWindow.SetApplicationMenu(f.mainWindow.applicationMenu)
+}
+
+func (w *Window) SetApplicationMenu(inmenu *menu.Menu) {
+ if inmenu == nil {
+ return
+ }
+
+ // Clean up previous menu
+ for _, gmenu := range gtkMenuCache {
+ if gmenu != nil {
+ C.g_object_unref(C.gpointer(gmenu))
+ }
+ }
+
+ if w.menubar != nil {
+ C.gtk_widget_unparent(w.menubar)
+ w.menubar = nil
+ }
+
+ menuItemToId = make(map[*menu.MenuItem]int)
+ menuIdToItem = make(map[int]*menu.MenuItem)
+ gtkMenuCache = make(map[*menu.MenuItem]*C.GMenu)
+
+ processMenu(w, inmenu)
+}
+
+func processMenu(window *Window, menu *menu.Menu) {
+ gmenu := C.g_menu_new()
+ defer C.g_object_unref(C.gpointer(gmenu))
+
+ for _, menuItem := range menu.Items {
+ itemLabel := C.CString(menuItem.Label)
+ defer C.free(unsafe.Pointer(itemLabel))
+
+ submenu := processSubmenu(window, menuItem)
+ defer C.g_object_unref(C.gpointer(submenu))
+
+ C.g_menu_append_submenu(gmenu, itemLabel, C.toGMenuModel(submenu))
+ }
+
+ window.menubar = C.gtk_popover_menu_bar_new_from_model(C.toGMenuModel(gmenu))
+}
+
+func processSubmenu(window *Window, menuItem *menu.MenuItem) *C.GMenu {
+ existingMenu := gtkMenuCache[menuItem]
+
+ if existingMenu != nil {
+ return existingMenu
+ }
+
+ submenu := C.g_menu_new()
+
+ for _, subItem := range menuItem.SubMenu.Items {
+ menuID := menuIdCounter
+ menuIdToItem[menuID] = subItem
+ menuItemToId[subItem] = menuID
+ menuIdCounter++
+
+ processMenuItem(window, submenu, subItem)
+ }
+
+ gtkMenuCache[menuItem] = submenu
+
+ return submenu
+}
+
+var currentRadioActionId string
+
+func processMenuItem(window *Window, parent *C.GMenu, menuItem *menu.MenuItem) {
+ if menuItem.Hidden {
+ return
+ }
+
+ if menuItem.Type != menu.RadioType {
+ currentRadioActionId = ""
+ }
+
+ var action *C.GSimpleAction
+
+ itemId := strings.ReplaceAll(strings.ToLower(menuItem.Label), " ", "-")
+ actionName := itemId
+
+ switch menuItem.Type {
+ case menu.SubmenuType:
+ itemLabel := C.CString(menuItem.Label)
+ defer C.free(unsafe.Pointer(itemLabel))
+
+ submenu := processSubmenu(window, menuItem)
+ defer C.g_object_unref(C.gpointer(submenu))
+
+ C.g_menu_append_submenu(parent, itemLabel, C.toGMenuModel(submenu))
+ return
+
+ case menu.SeparatorType:
+ return
+
+ case menu.CheckboxType:
+ name := C.CString(actionName)
+ defer C.free(unsafe.Pointer(name))
+
+ action = C.g_simple_action_new_stateful(name, nil, C.g_variant_new_boolean(gtkBool(menuItem.Checked)))
+
+ case menu.RadioType:
+ if currentRadioActionId == "" {
+ currentRadioActionId = itemId
+ }
+
+ if menuItem.Checked {
+ name := C.CString(currentRadioActionId)
+ defer C.free(unsafe.Pointer(name))
+
+ vType := C.CString("s")
+ defer C.free(unsafe.Pointer(vType))
+
+ paramType := C.g_variant_type_new(vType)
+ defer C.g_variant_type_free(paramType)
+
+ vContent := C.CString(itemId)
+ defer C.free(unsafe.Pointer(vContent))
+
+ action = C.g_simple_action_new_stateful(name, paramType, C.g_variant_new_string(vContent))
+
+ C.g_action_map_add_action(C.toActionMap(window.asGTKWindow()), C.toGAction(action))
+ }
+
+ // Use currentRadioActionId as the Action Name and itemId as the Target
+ actionName = currentRadioActionId + "::" + itemId
+
+ default:
+ name := C.CString(actionName)
+ defer C.free(unsafe.Pointer(name))
+
+ action = C.g_simple_action_new(name, nil)
+ }
+
+ if currentRadioActionId == "" {
+ C.g_action_map_add_action(C.toActionMap(window.asGTKWindow()), C.toGAction(action))
+ }
+
+ if action != nil {
+ defer C.g_object_unref(C.gpointer(action))
+
+ if menuItem.Disabled {
+ C.g_simple_action_set_enabled(action, gtkBool(false))
+ }
+
+ if menuItem.Click != nil {
+ C.connectClick(action)
+ }
+ }
+
+ gActionIdToMenuItem.Store(actionName, menuItem)
+
+ detActionName := C.CString("win." + actionName)
+ defer C.free(unsafe.Pointer(detActionName))
+
+ itemName := C.CString(menuItem.Label)
+ defer C.free(unsafe.Pointer(itemName))
+
+ item := C.g_menu_item_new(itemName, detActionName)
+ defer C.g_object_unref(C.gpointer(item))
+ C.g_menu_append_item(parent, item)
+
+ if menuItem.Accelerator != nil {
+ key, mods := acceleratorToGTK(menuItem.Accelerator)
+
+ accelName := C.gtk_accelerator_name(key, mods)
+ defer C.free(unsafe.Pointer(accelName))
+
+ C.setAccels(window.gtkApp, detActionName, accelName)
+ }
+}
diff --git a/v2/internal/frontend/desktop/linux/screen.go b/v2/internal/frontend/desktop/linux/screen.go
index 0a05074256c..1d7b5b44a04 100644
--- a/v2/internal/frontend/desktop/linux/screen.go
+++ b/v2/internal/frontend/desktop/linux/screen.go
@@ -1,15 +1,16 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#cgo CFLAGS: -w
#include <stdio.h>
+
#include "webkit2/webkit2.h"
#include "gtk/gtk.h"
#include "gdk/gdk.h"
diff --git a/v2/internal/frontend/desktop/linux/screen_webkit6.go b/v2/internal/frontend/desktop/linux/screen_webkit6.go
new file mode 100644
index 00000000000..ca5086f15db
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/screen_webkit6.go
@@ -0,0 +1,99 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+/*
+#cgo pkg-config: gtk4
+#cgo webkit_6 pkg-config: webkitgtk-6.0
+
+#cgo CFLAGS: -w
+#include <stdio.h>
+
+#include "webkit/webkit.h"
+#include "gtk/gtk.h"
+#include "gdk/gdk.h"
+
+typedef struct Screen {
+ int isCurrent;
+ int isPrimary;
+ int height;
+ int width;
+ int scale;
+} Screen;
+
+GListModel* GetMonitors(GtkWindow *window){
+ GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
+ return gdk_display_get_monitors(display);
+}
+
+Screen GetNThMonitor(int monitor_num, GListModel *monitors, GtkWindow *window){
+ GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window));
+ GdkSurface *surface = gtk_native_get_surface(native);
+
+ GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
+
+ GdkMonitor *monitor = g_list_model_get_item(monitors, monitor_num);
+ GdkMonitor *currentMonitor = gdk_display_get_monitor_at_surface(display, surface);
+
+ Screen screen;
+ GdkRectangle geometry;
+
+ gdk_monitor_get_geometry(monitor, &geometry);
+
+ screen.isCurrent = currentMonitor == monitor;
+ // screen.isPrimary = gdk_monitor_is_primary(monitor); //// TODO: is_primary no longer exists on monitor
+ screen.height = geometry.height;
+ screen.width = geometry.width;
+ screen.scale = gdk_monitor_get_scale_factor(monitor);
+
+ return screen;
+}
+*/
+import "C"
+import (
+ "sync"
+
+ "github.com/pkg/errors"
+ "github.com/wailsapp/wails/v2/internal/frontend"
+)
+
+type Screen = frontend.Screen
+
+func GetAllScreens(window *C.GtkWindow) ([]Screen, error) {
+ if window == nil {
+ return nil, errors.New("window is nil, cannot perform screen operations")
+ }
+ var wg sync.WaitGroup
+ var screens []Screen
+ wg.Add(1)
+ invokeOnMainThread(func() {
+ monitors := C.GetMonitors(window)
+ numMonitors := C.g_list_model_get_n_items(monitors)
+
+ for i := 0; i < int(numMonitors); i++ {
+ cMonitor := C.GetNThMonitor(C.int(i), monitors, window)
+
+ screen := Screen{
+ IsCurrent: cMonitor.isCurrent == 1,
+ IsPrimary: cMonitor.isPrimary == 1,
+ Width: int(cMonitor.width),
+ Height: int(cMonitor.height),
+
+ Size: frontend.ScreenSize{
+ Width: int(cMonitor.width),
+ Height: int(cMonitor.height),
+ },
+ PhysicalSize: frontend.ScreenSize{
+ Width: int(cMonitor.width * cMonitor.scale),
+ Height: int(cMonitor.height * cMonitor.scale),
+ },
+ }
+ screens = append(screens, screen)
+ }
+
+ wg.Done()
+ })
+ wg.Wait()
+ return screens, nil
+}
diff --git a/v2/internal/frontend/desktop/linux/webkit2.go b/v2/internal/frontend/desktop/linux/webkit2.go
index 06e0c782484..436e395b437 100644
--- a/v2/internal/frontend/desktop/linux/webkit2.go
+++ b/v2/internal/frontend/desktop/linux/webkit2.go
@@ -1,4 +1,5 @@
-//go:build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
diff --git a/v2/internal/frontend/desktop/linux/webkit6.go b/v2/internal/frontend/desktop/linux/webkit6.go
new file mode 100644
index 00000000000..66e119c7164
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/webkit6.go
@@ -0,0 +1,33 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+/*
+#cgo pkg-config: webkitgtk-6.0
+
+#include "webkit/webkit.h"
+*/
+import "C"
+import (
+ "fmt"
+
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/linux"
+
+ "github.com/wailsapp/wails/v2/pkg/assetserver/webview"
+)
+
+func validateWebKit2Version(options *options.App) {
+ if C.webkit_get_major_version() == 2 && C.webkit_get_minor_version() >= webview.Webkit2MinMinorVersion {
+ return
+ }
+
+ msg := linux.DefaultMessages()
+ if options.Linux != nil && options.Linux.Messages != nil {
+ msg = options.Linux.Messages
+ }
+
+ v := fmt.Sprintf("2.%d.0", webview.Webkit2MinMinorVersion)
+ showModalDialogAndExit("WebKit2GTK", fmt.Sprintf(msg.WebKit2GTKMinRequired, v))
+}
diff --git a/v2/internal/frontend/desktop/linux/window.c b/v2/internal/frontend/desktop/linux/window.c
index 5441db022cc..dfb06f8cecc 100644
--- a/v2/internal/frontend/desktop/linux/window.c
+++ b/v2/internal/frontend/desktop/linux/window.c
@@ -1,3 +1,6 @@
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
+
#include <JavaScriptCore/JavaScript.h>
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>
diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go
index 0bf5ac51d1f..51576caeed6 100644
--- a/v2/internal/frontend/desktop/linux/window.go
+++ b/v2/internal/frontend/desktop/linux/window.go
@@ -1,10 +1,10 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package linux
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
diff --git a/v2/internal/frontend/desktop/linux/window.h b/v2/internal/frontend/desktop/linux/window.h
index 04410959a9a..5cfb7e4e8c6 100644
--- a/v2/internal/frontend/desktop/linux/window.h
+++ b/v2/internal/frontend/desktop/linux/window.h
@@ -1,3 +1,6 @@
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
+
#ifndef window_h
#define window_h
diff --git a/v2/internal/frontend/desktop/linux/window_webkit6.c b/v2/internal/frontend/desktop/linux/window_webkit6.c
new file mode 100644
index 00000000000..7ac8928fadf
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/window_webkit6.c
@@ -0,0 +1,948 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+#include <jsc/jsc.h>
+#include <gtk/gtk.h>
+#include <webkit/webkit.h>
+#include <stdio.h>
+#include <limits.h>
+#include <stdint.h>
+#include <string.h>
+#include <locale.h>
+#include "window_webkit6.h"
+
+// These are the x,y,time & button of the last mouse down event
+// It's used for window dragging
+static float xroot = 0.0f;
+static float yroot = 0.0f;
+static int dragTime = -1;
+static guint mouseButton = 0;
+static int wmIsWayland = -1;
+static int decoratorWidth = -1;
+static int decoratorHeight = -1;
+
+// casts
+void ExecuteOnMainThread(void *f, gpointer jscallback)
+{
+ g_idle_add((GSourceFunc)f, (gpointer)jscallback);
+}
+
+GtkWidget *GTKWIDGET(void *pointer)
+{
+ return GTK_WIDGET(pointer);
+}
+
+GtkWindow *GTKWINDOW(void *pointer)
+{
+ return GTK_WINDOW(pointer);
+}
+
+GtkBox *GTKBOX(void *pointer)
+{
+ return GTK_BOX(pointer);
+}
+
+extern void processMessage(char *);
+
+static void sendMessageToBackend(WebKitUserContentManager *contentManager,
+ JSCValue *value,
+ void *data)
+{
+ char *message = jsc_value_to_string(value);
+
+ processMessage(message);
+ g_free(message);
+}
+
+static bool isNULLRectangle(GdkRectangle input)
+{
+ return input.x == -1 && input.y == -1 && input.width == -1 && input.height == -1;
+}
+
+static gboolean onWayland()
+{
+ switch (wmIsWayland)
+ {
+ case -1:
+ char *gdkBackend = getenv("XDG_SESSION_TYPE");
+ if(gdkBackend != NULL && strcmp(gdkBackend, "wayland") == 0)
+ {
+ wmIsWayland = 1;
+ return TRUE;
+ }
+
+ wmIsWayland = 0;
+ return FALSE;
+ case 1:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static GdkMonitor *getCurrentMonitor(GtkWindow *window)
+{
+ // Get the monitor that the window is currently on
+ GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window));
+
+ if(native == NULL) {
+ return NULL;
+ }
+
+ GdkSurface *surface = gtk_native_get_surface(native);
+
+ GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
+
+ GdkMonitor *currentMonitor = gdk_display_get_monitor_at_surface(display, surface);
+
+ return currentMonitor;
+}
+
+static GdkRectangle getCurrentMonitorGeometry(GtkWindow *window)
+{
+ GdkMonitor *monitor = getCurrentMonitor(window);
+ GdkRectangle result;
+ if (monitor == NULL)
+ {
+ result.x = result.y = result.height = result.width = -1;
+ return result;
+ }
+
+ // Get the geometry of the monitor
+ gdk_monitor_get_geometry(monitor, &result);
+ return result;
+}
+
+static int getCurrentMonitorScaleFactor(GtkWindow *window)
+{
+ GdkMonitor *monitor = getCurrentMonitor(window);
+
+ if(monitor == NULL)
+ {
+ return 1;
+ }
+
+ return gdk_monitor_get_scale_factor(monitor);
+}
+
+// window
+
+gulong SetupInvokeSignal(void *contentManager)
+{
+ return g_signal_connect((WebKitUserContentManager *)contentManager, "script-message-received::external", G_CALLBACK(sendMessageToBackend), NULL);
+}
+
+//// TODO:
+void SetWindowIcon(GtkWindow *window, const guchar *buf, gsize len)
+{
+ // GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
+ // if (!loader)
+ // {
+ // return;
+ // }
+ // if (gdk_pixbuf_loader_write(loader, buf, len, NULL) && gdk_pixbuf_loader_close(loader, NULL))
+ // {
+ // GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
+ // if (pixbuf)
+ // {
+ // gtk_window_set_icon(window, pixbuf);
+ // }
+ // }
+ // g_object_unref(loader);
+}
+
+void SetWindowTransparency(GtkWidget *widget, guchar alpha)
+{
+ // This opacity includes the menu as well as the webview's contents
+ gtk_widget_set_opacity(widget, alpha / 255.0);
+}
+
+static GtkCssProvider *windowCssProvider = NULL;
+
+void SetBackgroundColour(void *data)
+{
+ // set webview's background color
+ RGBAOptions *options = (RGBAOptions *)data;
+
+ GdkRGBA colour = {options->r / 255.0, options->g / 255.0, options->b / 255.0, options->a / 255.0};
+ if (options->windowIsTranslucent != NULL && options->windowIsTranslucent == TRUE)
+ {
+ colour.alpha = 0.0;
+ }
+ webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(options->webview), &colour);
+
+ // set window's background color
+ // Get the name of the current locale
+ char *old_locale, *saved_locale;
+ old_locale = setlocale(LC_ALL, NULL);
+
+ // Copy the name so it wont be clobbered by setlocale.
+ saved_locale = strdup(old_locale);
+ if (saved_locale == NULL)
+ return;
+
+ //Now change the locale to english for so printf always converts floats with a dot decimal separator
+ setlocale(LC_ALL, "en_US.UTF-8");
+ gchar *str = g_strdup_printf("#webview-box {background-color: rgba(%d, %d, %d, %1.1f);}", options->r, options->g, options->b, options->a / 255.0);
+
+ //Restore the original locale.
+ setlocale(LC_ALL, saved_locale);
+ free(saved_locale);
+
+ if (windowCssProvider == NULL)
+ {
+ windowCssProvider = gtk_css_provider_new();
+ gtk_style_context_add_provider(
+ gtk_widget_get_style_context(GTK_WIDGET(options->webviewBox)),
+ GTK_STYLE_PROVIDER(windowCssProvider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+ g_object_unref(windowCssProvider);
+ }
+
+ // TODO: gtk_css_provider_load_from_data is deprecated since 4.12
+ // but the user's system might not offer a compatible version.
+ //
+ // see: https://docs.gtk.org/gtk4/method.CssProvider.load_from_data.html
+ gtk_css_provider_load_from_data(windowCssProvider, str, -1);
+
+ g_free(str);
+}
+
+static gboolean setTitle(gpointer data)
+{
+ SetTitleArgs *args = (SetTitleArgs *)data;
+ gtk_window_set_title(args->window, args->title);
+ free((void *)args->title);
+ free((void *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+void SetTitle(GtkWindow *window, char *title)
+{
+ SetTitleArgs *args = malloc(sizeof(SetTitleArgs));
+ args->window = window;
+ args->title = title;
+ ExecuteOnMainThread(setTitle, (gpointer)args);
+}
+
+//// TODO: gtk_window_move has been removed
+// see: https://docs.gtk.org/gtk4/migrating-3to4.html#adapt-to-gtkwindow-api-changes
+static gboolean setPosition(gpointer data)
+{
+ // SetPositionArgs *args = (SetPositionArgs *)data;
+ // gtk_window_move((GtkWindow *)args->window, args->x, args->y);
+ // free(args);
+
+ return G_SOURCE_REMOVE;
+}
+
+//// TODO: gtk_window_move has been removed
+// see: https://docs.gtk.org/gtk4/migrating-3to4.html#adapt-to-gtkwindow-api-changes
+void SetPosition(void *window, int x, int y)
+{
+ // GdkRectangle monitorDimensions = getCurrentMonitorGeometry(window);
+ // if (isNULLRectangle(monitorDimensions))
+ // {
+ // return;
+ // }
+ // SetPositionArgs *args = malloc(sizeof(SetPositionArgs));
+ // args->window = window;
+ // args->x = monitorDimensions.x + x;
+ // args->y = monitorDimensions.y + y;
+ // ExecuteOnMainThread(setPosition, (gpointer)args);
+}
+
+//// TODO: gtk_window_set_geometry_hints has been removed
+void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_width, int max_height)
+{
+ // GdkGeometry size;
+ // size.min_width = size.min_height = size.max_width = size.max_height = 0;
+
+ // GdkRectangle monitorSize = getCurrentMonitorGeometry(window);
+ // if (isNULLRectangle(monitorSize))
+ // {
+ // return;
+ // }
+
+ // int flags = GDK_HINT_MAX_SIZE | GDK_HINT_MIN_SIZE;
+
+ // size.max_height = (max_height == 0 ? monitorSize.height : max_height);
+ // size.max_width = (max_width == 0 ? monitorSize.width : max_width);
+ // size.min_height = min_height;
+ // size.min_width = min_width;
+
+ // // On Wayland window manager get the decorators and calculate the differences from the windows' size.
+ // if(onWayland())
+ // {
+ // if(decoratorWidth == -1 && decoratorHeight == -1)
+ // {
+ // int windowWidth, windowHeight;
+ // gtk_window_get_size(window, &windowWidth, &windowHeight);
+
+ // GtkAllocation windowAllocation;
+ // gtk_widget_get_allocation(GTK_WIDGET(window), &windowAllocation);
+
+ // decoratorWidth = (windowAllocation.width-windowWidth);
+ // decoratorHeight = (windowAllocation.height-windowHeight);
+ // }
+
+ // // Add the decorator difference to the window so fullscreen and maximise can fill the window.
+ // size.max_height = decoratorHeight+size.max_height;
+ // size.max_width = decoratorWidth+size.max_width;
+ // }
+
+ // gtk_window_set_geometry_hints(window, NULL, &size, flags);
+}
+
+// function to disable the context menu but propagate the event
+static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_test_result, gpointer data)
+{
+ // return true to disable the context menu
+ return TRUE;
+}
+
+void DisableContextMenu(void *webview)
+{
+ // Disable the context menu but propagate the event
+ g_signal_connect(WEBKIT_WEB_VIEW(webview), "context-menu", G_CALLBACK(disableContextMenu), NULL);
+}
+
+static void buttonPress(GtkGestureClick* gesture, gint n_press, gdouble gesture_x, gdouble gesture_y, gpointer data)
+{
+ GdkEvent *event = gtk_event_controller_get_current_event(gesture);
+
+ if (event == NULL)
+ {
+ xroot = yroot = 0.0f;
+ dragTime = -1;
+ return;
+ }
+
+ guint button = gtk_gesture_single_get_button(gesture);
+ mouseButton = button;
+
+ if (button == 3)
+ {
+ return;
+ }
+
+ if (gdk_event_get_event_type(event) == GDK_BUTTON_PRESS && button == 1)
+ {
+ double x, y;
+ gboolean success = gdk_event_get_position(event, &x, &y);
+
+ if(success) {
+ xroot = x;
+ yroot = y;
+ }
+
+ dragTime = gdk_event_get_time(event);
+ }
+}
+
+static void buttonRelease(GtkGestureClick* gesture, gint n_press, gdouble gesture_x, gdouble gesture_y, gpointer data)
+{
+ GdkEvent *event = gtk_event_controller_get_current_event(gesture);
+
+ if (event == NULL ||
+ (gdk_event_get_event_type(event) == GDK_BUTTON_RELEASE && gtk_gesture_single_get_button(gesture) == 1))
+ {
+ xroot = yroot = 0.0f;
+ dragTime = -1;
+ }
+}
+
+void ConnectButtons(void *webview)
+{
+ GtkGesture *press = gtk_gesture_click_new();
+ GtkGesture *release = gtk_gesture_click_new();
+
+ gtk_widget_add_controller(GTK_WIDGET(webview), press);
+ gtk_widget_add_controller(GTK_WIDGET(webview), release);
+
+ g_signal_connect(press, "pressed", G_CALLBACK(buttonPress), NULL);
+ g_signal_connect(release, "released", G_CALLBACK(buttonRelease), NULL);
+}
+
+int IsFullscreen(GtkWidget *widget)
+{
+ GtkWindow *gtkwindow = gtk_widget_get_root(widget);
+ return gtk_window_is_fullscreen(gtkwindow);
+}
+
+int IsMaximised(GtkWidget *widget)
+{
+ GtkWindow *gtkwindow = gtk_widget_get_root(widget);
+ return gtk_window_is_maximized(gtkwindow);
+}
+
+int IsMinimised(GtkWidget *widget)
+{
+ // The minimized state must be retrieved from the Gdk Toplevel
+ // See: https://docs.gtk.org/gtk4/migrating-3to4.html#the-iconified-window-state-has-been-renamed-to-minimized
+ GtkNative *native = gtk_widget_get_native(widget);
+ GdkSurface *surface = gtk_native_get_surface(native);
+
+ GdkToplevelState state = gdk_toplevel_get_state(GDK_TOPLEVEL(surface));
+
+ return state & GDK_TOPLEVEL_STATE_MINIMIZED;
+}
+
+//// TODO: gtk_window_move has been removed
+// see: https://docs.gtk.org/gtk4/migrating-3to4.html#adapt-to-gtkwindow-api-changes
+gboolean Center(gpointer data)
+{
+ // GtkWindow *window = (GtkWindow *)data;
+
+ // // Get the geometry of the monitor
+ // GdkRectangle m = getCurrentMonitorGeometry(window);
+ // if (isNULLRectangle(m))
+ // {
+ // return G_SOURCE_REMOVE;
+ // }
+
+ // // Get the window width/height
+ // int windowWidth, windowHeight;
+ // gtk_window_get_size(window, &windowWidth, &windowHeight);
+
+ // int newX = ((m.width - windowWidth) / 2) + m.x;
+ // int newY = ((m.height - windowHeight) / 2) + m.y;
+
+ // // Place the window at the center of the monitor
+ // gtk_window_move(window, newX, newY);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean Show(gpointer data)
+{
+ gtk_widget_show((GtkWidget *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean Hide(gpointer data)
+{
+ gtk_widget_hide((GtkWidget *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean Maximise(gpointer data)
+{
+ gtk_window_maximize((GtkWindow *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean UnMaximise(gpointer data)
+{
+ gtk_window_unmaximize((GtkWindow *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean Minimise(gpointer data)
+{
+ gtk_window_minimize((GtkWindow *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean UnMinimise(gpointer data)
+{
+ gtk_window_present((GtkWindow *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean Fullscreen(gpointer data)
+{
+ GtkWindow *window = (GtkWindow *)data;
+
+ // Get the geometry of the monitor.
+ GdkRectangle m = getCurrentMonitorGeometry(window);
+ if (isNULLRectangle(m))
+ {
+ return G_SOURCE_REMOVE;
+ }
+ int scale = getCurrentMonitorScaleFactor(window);
+ SetMinMaxSize(window, 0, 0, m.width * scale, m.height * scale);
+
+ gtk_window_fullscreen(window);
+
+ return G_SOURCE_REMOVE;
+}
+
+gboolean UnFullscreen(gpointer data)
+{
+ gtk_window_unfullscreen((GtkWindow *)data);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void webviewLoadChanged(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer data)
+{
+ if (load_event == WEBKIT_LOAD_FINISHED)
+ {
+ processMessage("DomReady");
+ }
+}
+
+extern void processURLRequest(void *request);
+
+void window_hide(GtkWindow* window, gpointer data) {
+ gtk_widget_set_visible(GTK_WIDGET(window), false);
+}
+
+// This is called when the close button on the window is pressed
+// gboolean close_button_pressed(GtkWidget *widget, GdkEvent *event, void *data)
+gboolean close_button_pressed(GtkWindow* window, gpointer data)
+{
+ processMessage("Q");
+ // since we handle the close in processMessage tell GTK to not invoke additional handlers - see:
+ // https://docs.gtk.org/gtk3/signal.Widget.delete-event.html
+ return TRUE;
+}
+
+// Drag and drop might encounter issues
+// See: https://discourse.gnome.org/t/file-drag-and-drop-in-gtkmm4/10548/5
+static gboolean onDragDrop(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data) {
+ GdkFileList *file_list = g_value_get_boxed(value);
+
+ GSList *list = gdk_file_list_get_files(file_list);
+
+ int limit = 250 * g_slist_length(list);
+ char *paths = calloc(limit, 1);
+ bool first = true;
+
+ for(GSList *l = list; l != NULL; l = l->next) {
+ GFile* file = l->data;
+
+ char* path = g_file_get_path(file);
+ g_print("%s\n", path);
+
+ if(strlen(paths) + strlen(path) + 2 >= limit)
+ {
+ g_print("path '%s' exceeds limit %d\n", path, limit);
+ free(path);
+ free(paths);
+ return TRUE; // Return early to guard against overflow
+ }
+
+ if(!first)
+ {
+ strncat(paths, "\n", 1);
+ }
+
+ first = false;
+
+ strncat(paths, path, strlen(path));
+ free(path);
+ }
+
+ size_t resLen = strlen(paths)+(sizeof(int)*2)+6;
+ char *res = calloc(resLen, 1);
+
+ snprintf(res, resLen, "DD:%d:%d:%s", (int) x, (int) y, paths);
+
+ processMessage(res);
+ free(paths);
+ free(res);
+
+ return TRUE;
+}
+
+static void onDelete(GtkWidget* self) {}
+
+// WebView
+GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop)
+{
+ GtkWidget *webview = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW, "user-content-manager", (WebKitUserContentManager *) contentManager, NULL));
+
+ gtk_widget_set_vexpand(webview, true);
+
+ WebKitWebContext *context = webkit_web_context_get_default();
+ webkit_web_context_register_uri_scheme(context, "wails", (WebKitURISchemeRequestCallback)processURLRequest, NULL, NULL);
+ g_signal_connect(G_OBJECT(webview), "load-changed", G_CALLBACK(webviewLoadChanged), NULL);
+
+ // if(disableWebViewDragAndDrop)
+ // {
+ // gtk_drag_dest_unset(webview);
+ // }
+
+ if(enableDragAndDrop)
+ {
+ GtkDropTarget *target = gtk_drop_target_new(G_TYPE_INVALID, GDK_ACTION_COPY);
+
+ gtk_drop_target_set_gtypes(target, (GType[1]) { GDK_TYPE_FILE_LIST, }, 1);
+
+ g_signal_connect(target, "drop", G_CALLBACK(onDragDrop), NULL);
+
+ gtk_widget_add_controller(webview, GTK_EVENT_CONTROLLER(target));
+ }
+
+ if (hideWindowOnClose)
+ {
+ g_signal_connect(window, "close-request", G_CALLBACK(window_hide), NULL);
+ }
+ else
+ {
+ g_signal_connect(window, "close-request", G_CALLBACK(close_button_pressed), NULL);
+ }
+
+ WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview));
+ webkit_settings_set_user_agent_with_application_details(settings, "wails.io", "");
+
+ switch (gpuPolicy)
+ {
+ case 0:
+ webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
+ break;
+ case 2:
+ webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
+ break;
+ default:
+ webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
+ }
+
+ return webview;
+}
+
+void DevtoolsEnabled(void *webview, int enabled, bool showInspector)
+{
+ WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview));
+ gboolean genabled = enabled == 1 ? true : false;
+ webkit_settings_set_enable_developer_extras(settings, genabled);
+
+ if (genabled && showInspector)
+ {
+ ShowInspector(webview);
+ }
+}
+
+void LoadIndex(void *webview, char *url)
+{
+ webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url);
+}
+
+static gboolean startDrag(gpointer data)
+{
+ DragOptions *options = (DragOptions *)data;
+
+ // Ignore non-toplevel widgets
+ GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(options->webview));
+ if (!GTK_IS_WINDOW(root))
+ {
+ free(data);
+ return G_SOURCE_REMOVE;
+ }
+
+ gdk_toplevel_begin_move(options->mainwindow, NULL, mouseButton, xroot, yroot, dragTime);
+
+ free(data);
+
+ return G_SOURCE_REMOVE;
+}
+
+void StartDrag(void *webview, GtkWindow *mainwindow)
+{
+ DragOptions *data = malloc(sizeof(DragOptions));
+ data->webview = webview;
+ data->mainwindow = mainwindow;
+ ExecuteOnMainThread(startDrag, (gpointer)data);
+}
+
+static gboolean startResize(gpointer data)
+{
+ ResizeOptions *options = (ResizeOptions *)data;
+
+ // Ignore non-toplevel widgets
+ GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(options->webview));
+ if (!GTK_IS_WINDOW(root))
+ {
+ free(data);
+ return G_SOURCE_REMOVE;
+ }
+
+ gdk_toplevel_begin_resize(options->mainwindow, options->edge, NULL, mouseButton, xroot, yroot, dragTime);
+ free(data);
+
+ return G_SOURCE_REMOVE;
+}
+
+void StartResize(void *webview, GtkWindow *mainwindow, GdkSurfaceEdge edge)
+{
+ ResizeOptions *data = malloc(sizeof(ResizeOptions));
+ data->webview = webview;
+ data->mainwindow = mainwindow;
+ data->edge = edge;
+ ExecuteOnMainThread(startResize, (gpointer)data);
+}
+
+void ExecuteJS(void *data)
+{
+ struct JSCallback *js = data;
+ webkit_web_view_evaluate_javascript(js->webview, js->script, -1, NULL, NULL, NULL, NULL, NULL);
+
+ free(js->script);
+}
+
+extern void processMessageDialogResult(char *);
+
+void messageResult(GtkDialog* dialog, gint response_id, gpointer user_data) {
+ if(response_id == GTK_RESPONSE_YES) {
+ processMessageDialogResult("Yes");
+ } else if(response_id == GTK_RESPONSE_NO) {
+ processMessageDialogResult("No");
+ } else if(response_id == GTK_RESPONSE_OK) {
+ processMessageDialogResult("OK");
+ } else if(response_id == GTK_RESPONSE_CANCEL) {
+ processMessageDialogResult("Cancel");
+ } else {
+ processMessageDialogResult("");
+ }
+
+ gtk_window_destroy(GTK_WINDOW(dialog));
+}
+
+void MessageDialog(void *data)
+{
+ GtkButtonsType buttons;
+ GtkMessageType messageType;
+ MessageDialogOptions *options = (MessageDialogOptions *)data;
+ if (options->messageType == 0)
+ {
+ messageType = GTK_MESSAGE_INFO;
+ buttons = GTK_BUTTONS_OK;
+ }
+ else if (options->messageType == 1)
+ {
+ messageType = GTK_MESSAGE_ERROR;
+ buttons = GTK_BUTTONS_OK;
+ }
+ else if (options->messageType == 2)
+ {
+ messageType = GTK_MESSAGE_QUESTION;
+ buttons = GTK_BUTTONS_YES_NO;
+ }
+ else
+ {
+ messageType = GTK_MESSAGE_WARNING;
+ buttons = GTK_BUTTONS_OK;
+ }
+
+ // TODO: gtk_message_dialog_new is deprecated since 4.10
+ // but the user's system might not offer a compatible version.
+ //
+ // see: https://docs.gtk.org/gtk4/ctor.MessageDialog.new.html
+ GtkWidget *dialog;
+ dialog = gtk_message_dialog_new(GTK_WINDOW(options->window),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ messageType,
+ buttons,
+ options->message, NULL);
+
+ g_object_ref_sink(dialog);
+
+ gtk_window_set_title(GTK_WINDOW(dialog), options->title);
+ gtk_window_set_modal(GTK_WINDOW(dialog), true);
+
+ g_signal_connect(dialog, "response", G_CALLBACK(messageResult), NULL);
+
+ gtk_widget_show(dialog);
+
+ free(options->title);
+ free(options->message);
+}
+
+void extern processOpenFileResult(void *);
+
+GtkFileFilter **AllocFileFilterArray(size_t ln)
+{
+ return (GtkFileFilter **)malloc(ln * sizeof(GtkFileFilter *));
+}
+
+void freeFileFilterArray(GtkFileFilter **filters)
+{
+ free(filters);
+}
+
+void openFileResult(GtkDialog *dialog, int response) {
+ GtkFileChooser *fc = GTK_FILE_CHOOSER(dialog);
+
+ // Max 1024 files to select
+ char **result = calloc(1024, sizeof(char *));
+ int resultIndex = 0;
+
+ if(response == GTK_RESPONSE_ACCEPT) {
+ GListModel *files = gtk_file_chooser_get_files(fc);
+
+ GObject *item = g_list_model_get_object(files, resultIndex);
+
+ while(item) {
+ GFile *file = G_FILE(item);
+ char *path = g_file_get_path(file);
+
+ result[resultIndex] = path;
+ resultIndex++;
+
+ g_object_unref(file);
+
+ if(resultIndex == 1024) {
+ break;
+ }
+
+ item = g_list_model_get_object(files, resultIndex);
+ }
+
+ processOpenFileResult(result);
+
+ for(int i = 0; i < resultIndex; i++) {
+ g_free(result[i]);
+ }
+
+ g_object_unref(files);
+ } else {
+ processOpenFileResult(result);
+ }
+ free(result);
+
+ gtk_window_destroy(GTK_WINDOW(dialog));
+}
+
+void Opendialog(void *data)
+{
+ struct OpenFileDialogOptions *options = data;
+ char *label = "_Open";
+ if (options->action == GTK_FILE_CHOOSER_ACTION_SAVE)
+ {
+ label = "_Save";
+ }
+
+ // TODO: gtk_file_chooser_dialog_new is deprecated since 4.10
+ // but the user's system might not offer a compatible version.
+ //
+ // see: https://docs.gtk.org/gtk4/class.FileChooserDialog.html
+ GtkWidget *dialog = gtk_file_chooser_dialog_new(options->title, options->window, options->action,
+ "_Cancel", GTK_RESPONSE_CANCEL,
+ label, GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ g_object_ref_sink(dialog);
+
+ // TODO: GtkFileChooser is deprecated since 4.10
+ // but the user's system might not offer a compatible version.
+ //
+ // see: https://docs.gtk.org/gtk4/iface.FileChooser.html
+ GtkFileChooser *fc = GTK_FILE_CHOOSER(dialog);
+
+ // filters
+ if (options->filters != 0)
+ {
+ int index = 0;
+ GtkFileFilter *thisFilter;
+ while (options->filters[index] != NULL)
+ {
+ thisFilter = options->filters[index];
+ gtk_file_chooser_add_filter(fc, thisFilter);
+ index++;
+ }
+ }
+
+ if (options->multipleFiles == 1)
+ {
+ gtk_file_chooser_set_select_multiple(fc, TRUE);
+ }
+
+ if (options->createDirectories == 1)
+ {
+ gtk_file_chooser_set_create_folders(fc, TRUE);
+ }
+
+ if (options->defaultDirectory != NULL)
+ {
+ // TODO: gtk_file_chooser_set_current_folder is deprecated since 4.10
+ // but the user's system might not offer a compatible version.
+ //
+ // see: https://docs.gtk.org/gtk4/method.FileChooser.set_current_folder.html
+ gtk_file_chooser_set_current_folder(fc, options->defaultDirectory, NULL);
+ free(options->defaultDirectory);
+ }
+
+ if (options->action == GTK_FILE_CHOOSER_ACTION_SAVE)
+ {
+ if (options->defaultFilename != NULL)
+ {
+ gtk_file_chooser_set_current_name(fc, options->defaultFilename);
+ free(options->defaultFilename);
+ }
+ }
+
+ g_signal_connect(dialog, "response", G_CALLBACK(openFileResult), NULL);
+
+ gtk_widget_show(dialog);
+
+ // Release filters
+ if (options->filters != NULL)
+ {
+ int index = 0;
+ GtkFileFilter *thisFilter;
+ while (options->filters[index] != 0)
+ {
+ thisFilter = options->filters[index];
+ g_object_unref(thisFilter);
+ index++;
+ }
+ freeFileFilterArray(options->filters);
+ }
+
+ free(options->title);
+}
+
+GtkFileFilter *newFileFilter()
+{
+ GtkFileFilter *result = gtk_file_filter_new();
+ g_object_ref(result);
+ return result;
+}
+
+void ShowInspector(void *webview) {
+ WebKitWebInspector *inspector = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(webview));
+ webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector));
+}
+
+void sendShowInspectorMessage(GAction *action, GVariant *param) {
+ processMessage("wails:showInspector");
+}
+
+// When the user presses Ctrl+Shift+F12, call ShowInspector
+void InstallF12Hotkey(GtkApplication *app, GtkWindow *window)
+{
+ GSimpleAction *action = g_simple_action_new("show-inspector", NULL);
+ g_signal_connect(action, "activate", G_CALLBACK(sendShowInspectorMessage), NULL);
+ g_action_map_add_action(G_ACTION_MAP(window), G_ACTION(action));
+
+ gtk_application_set_accels_for_action(
+ app,
+ "win.show-inspector",
+ (const char *[]) { "<Control><Shift>F12", NULL });
+}
+
+extern void onActivate();
+
+static void activate(GtkApplication *app, gpointer user_data) {
+ onActivate();
+}
+
+GtkApplication* createApp(char *appId) {
+ GtkApplication *app = gtk_application_new(appId, G_APPLICATION_DEFAULT_FLAGS);
+ g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
+ return app;
+}
+
+void runApp(GtkApplication *app) {
+ g_application_run(G_APPLICATION(app), 0, NULL);
+ g_object_unref(app);
+}
diff --git a/v2/internal/frontend/desktop/linux/window_webkit6.go b/v2/internal/frontend/desktop/linux/window_webkit6.go
new file mode 100644
index 00000000000..637ec665ad2
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/window_webkit6.go
@@ -0,0 +1,514 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package linux
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0
+
+#include <jsc/jsc.h>
+#include <gtk/gtk.h>
+#include <webkit/webkit.h>
+#include <stdio.h>
+#include <limits.h>
+#include <stdint.h>
+#include "window_webkit6.h"
+
+*/
+import "C"
+import (
+ "log"
+ "strings"
+ "sync"
+ "unsafe"
+
+ "github.com/wailsapp/wails/v2/internal/frontend"
+ "github.com/wailsapp/wails/v2/pkg/menu"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/linux"
+)
+
+func gtkBool(input bool) C.gboolean {
+ if input {
+ return C.gboolean(1)
+ }
+ return C.gboolean(0)
+}
+
+type Window struct {
+ appoptions *options.App
+ debug bool
+ devtoolsEnabled bool
+ gtkApp *C.GtkApplication
+ gtkWindow unsafe.Pointer
+ contentManager unsafe.Pointer
+ webview unsafe.Pointer
+ applicationMenu *menu.Menu
+ menubar *C.GtkWidget
+ webviewBox *C.GtkWidget
+ vbox *C.GtkWidget
+ minWidth, minHeight, maxWidth, maxHeight int
+}
+
+func bool2Cint(value bool) C.int {
+ if value {
+ return C.int(1)
+ }
+ return C.int(0)
+}
+
+var activateWg sync.WaitGroup
+
+//export onActivate
+func onActivate() {
+ activateWg.Done()
+}
+
+func NewWindow(appoptions *options.App, debug bool, devtoolsEnabled bool) *Window {
+ validateWebKit2Version(appoptions)
+
+ if appoptions.Linux == nil || appoptions.Linux.ProgramName == "" {
+ log.Fatal("App option Linux.ProgramName required for gtk4. ex: org.author.app-name")
+ }
+
+ result := &Window{
+ appoptions: appoptions,
+ debug: debug,
+ devtoolsEnabled: devtoolsEnabled,
+ minHeight: appoptions.MinHeight,
+ minWidth: appoptions.MinWidth,
+ maxHeight: appoptions.MaxHeight,
+ maxWidth: appoptions.MaxWidth,
+ }
+
+ activateWg.Add(1)
+
+ appId := C.CString(appoptions.Linux.ProgramName)
+ defer C.free(unsafe.Pointer(appId))
+ gtkApp := C.createApp(appId)
+ result.gtkApp = gtkApp
+
+ go func(gtkApp *C.GtkApplication) {
+ C.runApp(gtkApp)
+ }(gtkApp)
+
+ activateWg.Wait()
+
+ gtkWindow := C.gtk_application_window_new(gtkApp)
+ C.g_object_ref_sink(C.gpointer(gtkWindow))
+ result.gtkWindow = unsafe.Pointer(gtkWindow)
+
+ webviewName := C.CString("webview-box")
+ defer C.free(unsafe.Pointer(webviewName))
+ result.webviewBox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)
+ C.gtk_widget_set_name(result.webviewBox, webviewName)
+
+ result.vbox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)
+ C.gtk_window_set_child(result.asGTKWindow(), result.vbox)
+
+ result.contentManager = unsafe.Pointer(C.webkit_user_content_manager_new())
+ external := C.CString("external")
+ defer C.free(unsafe.Pointer(external))
+ C.webkit_user_content_manager_register_script_message_handler(result.cWebKitUserContentManager(), external, nil)
+ C.SetupInvokeSignal(result.contentManager)
+
+ var webviewGpuPolicy int
+ if appoptions.Linux != nil {
+ webviewGpuPolicy = int(appoptions.Linux.WebviewGpuPolicy)
+ } else {
+ // workaround for https://github.com/wailsapp/wails/issues/2977
+ webviewGpuPolicy = int(linux.WebviewGpuPolicyNever)
+ }
+
+ webview := C.SetupWebview(
+ result.contentManager,
+ result.asGTKWindow(),
+ bool2Cint(appoptions.HideWindowOnClose),
+ C.int(webviewGpuPolicy),
+ bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.DisableWebViewDrop),
+ bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.EnableFileDrop),
+ )
+ result.webview = unsafe.Pointer(webview)
+ buttonPressedName := C.CString("button-press-event")
+ defer C.free(unsafe.Pointer(buttonPressedName))
+ C.ConnectButtons(unsafe.Pointer(webview))
+
+ if devtoolsEnabled {
+ C.DevtoolsEnabled(unsafe.Pointer(webview), C.int(1), C.bool(debug && appoptions.Debug.OpenInspectorOnStartup))
+ // Install Ctrl-Shift-F12 hotkey to call ShowInspector
+ C.InstallF12Hotkey(result.gtkApp, result.asGTKWindow())
+ }
+
+ if !(debug || appoptions.EnableDefaultContextMenu) {
+ C.DisableContextMenu(unsafe.Pointer(webview))
+ }
+
+ // Set background colour
+ RGBA := appoptions.BackgroundColour
+ result.SetBackgroundColour(RGBA.R, RGBA.G, RGBA.B, RGBA.A)
+
+ // Setup window
+ result.SetKeepAbove(appoptions.AlwaysOnTop)
+ result.SetResizable(!appoptions.DisableResize)
+ result.SetDefaultSize(appoptions.Width, appoptions.Height)
+ result.SetDecorated(!appoptions.Frameless)
+ result.SetTitle(appoptions.Title)
+ result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight)
+ result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight)
+ if appoptions.Linux != nil {
+ if appoptions.Linux.Icon != nil {
+ result.SetWindowIcon(appoptions.Linux.Icon)
+ }
+ if appoptions.Linux.WindowIsTranslucent {
+ C.SetWindowTransparency(gtkWindow, C.uchar(RGBA.A))
+ }
+ }
+
+ // Menu
+ result.SetApplicationMenu(appoptions.Menu)
+
+ return result
+}
+
+func (w *Window) asGTKWidget() *C.GtkWidget {
+ return C.GTKWIDGET(w.gtkWindow)
+}
+
+func (w *Window) asGTKWindow() *C.GtkWindow {
+ return C.GTKWINDOW(w.gtkWindow)
+}
+
+func (w *Window) asGTKBox() *C.GtkBox {
+ return C.GTKBOX(w.gtkWindow)
+}
+
+func (w *Window) cWebKitUserContentManager() *C.WebKitUserContentManager {
+ return (*C.WebKitUserContentManager)(w.contentManager)
+}
+
+func (w *Window) Fullscreen() {
+ C.ExecuteOnMainThread(C.Fullscreen, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) UnFullscreen() {
+ if !w.IsFullScreen() {
+ return
+ }
+ C.ExecuteOnMainThread(C.UnFullscreen, C.gpointer(w.asGTKWindow()))
+ w.SetMinSize(w.minWidth, w.minHeight)
+ w.SetMaxSize(w.maxWidth, w.maxHeight)
+}
+
+func (w *Window) Destroy() {
+ C.gtk_window_destroy(w.asGTKWindow())
+ C.g_object_unref(C.gpointer(w.gtkWindow))
+}
+
+func (w *Window) Close() {
+ C.gtk_window_close(w.asGTKWindow())
+}
+
+func (w *Window) Center() {
+ C.ExecuteOnMainThread(C.Center, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) SetPosition(x int, y int) {
+ invokeOnMainThread(func() {
+ C.SetPosition(unsafe.Pointer(w.asGTKWindow()), C.int(x), C.int(y))
+ })
+}
+
+func (w *Window) Size() (int, int) {
+ var width, height C.int
+ var wg sync.WaitGroup
+ wg.Add(1)
+ invokeOnMainThread(func() {
+ C.gtk_window_get_default_size(w.asGTKWindow(), &width, &height)
+ wg.Done()
+ })
+ wg.Wait()
+ return int(width), int(height)
+}
+
+func (w *Window) GetPosition() (int, int) {
+ //// TODO: gtk_window_get_position was removed in gtk4
+ // var width, height C.int
+ // var wg sync.WaitGroup
+ // wg.Add(1)
+ // invokeOnMainThread(func() {
+ // C.gtk_window_get_position(w.asGTKWindow(), &width, &height)
+ // wg.Done()
+ // })
+ // wg.Wait()
+ // return int(width), int(height)
+ return 0, 0
+}
+
+func (w *Window) SetMaxSize(maxWidth int, maxHeight int) {
+ w.maxHeight = maxHeight
+ w.maxWidth = maxWidth
+ invokeOnMainThread(func() {
+ C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight))
+ })
+}
+
+func (w *Window) SetMinSize(minWidth int, minHeight int) {
+ w.minHeight = minHeight
+ w.minWidth = minWidth
+ invokeOnMainThread(func() {
+ C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight))
+ })
+}
+
+func (w *Window) Show() {
+ C.ExecuteOnMainThread(C.Show, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) Hide() {
+ C.ExecuteOnMainThread(C.Hide, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) Maximise() {
+ C.ExecuteOnMainThread(C.Maximise, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) UnMaximise() {
+ C.ExecuteOnMainThread(C.UnMaximise, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) Minimise() {
+ C.ExecuteOnMainThread(C.Minimise, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) UnMinimise() {
+ C.ExecuteOnMainThread(C.UnMinimise, C.gpointer(w.asGTKWindow()))
+}
+
+func (w *Window) IsFullScreen() bool {
+ result := C.IsFullscreen(w.asGTKWidget())
+ if result != 0 {
+ return true
+ }
+ return false
+}
+
+func (w *Window) IsMaximised() bool {
+ result := C.IsMaximised(w.asGTKWidget())
+ return result > 0
+}
+
+func (w *Window) IsMinimised() bool {
+ result := C.IsMinimised(w.asGTKWidget())
+ return result > 0
+}
+
+func (w *Window) IsNormal() bool {
+ return !w.IsMaximised() && !w.IsMinimised() && !w.IsFullScreen()
+}
+
+func (w *Window) SetBackgroundColour(r uint8, g uint8, b uint8, a uint8) {
+ windowIsTranslucent := false
+ if w.appoptions.Linux != nil && w.appoptions.Linux.WindowIsTranslucent {
+ windowIsTranslucent = true
+ }
+ data := C.RGBAOptions{
+ r: C.uchar(r),
+ g: C.uchar(g),
+ b: C.uchar(b),
+ a: C.uchar(a),
+ webview: w.webview,
+ webviewBox: unsafe.Pointer(w.webviewBox),
+ windowIsTranslucent: gtkBool(windowIsTranslucent),
+ }
+ invokeOnMainThread(func() { C.SetBackgroundColour(unsafe.Pointer(&data)) })
+}
+
+func (w *Window) SetWindowIcon(icon []byte) {
+ if len(icon) == 0 {
+ return
+ }
+ C.SetWindowIcon(w.asGTKWindow(), (*C.guchar)(&icon[0]), (C.gsize)(len(icon)))
+}
+
+func (w *Window) Run(url string) {
+ if w.menubar != nil {
+ C.gtk_box_prepend(C.GTKBOX(unsafe.Pointer(w.vbox)), w.menubar)
+ }
+
+ C.gtk_box_prepend(C.GTKBOX(unsafe.Pointer(w.webviewBox)), C.GTKWIDGET(w.webview))
+ C.gtk_box_append(C.GTKBOX(unsafe.Pointer(w.vbox)), w.webviewBox)
+
+ _url := C.CString(url)
+ defer C.free(unsafe.Pointer(_url))
+ C.LoadIndex(w.webview, _url)
+ if w.appoptions.StartHidden {
+ w.Hide()
+ }
+
+ C.gtk_window_present(w.asGTKWindow())
+
+ w.Center()
+ switch w.appoptions.WindowStartState {
+ case options.Fullscreen:
+ w.Fullscreen()
+ case options.Minimised:
+ w.Minimise()
+ case options.Maximised:
+ w.Maximise()
+ }
+}
+
+func (w *Window) SetKeepAbove(top bool) {
+ //// TODO: gtk_window_set_keep_above was removed in gtk4
+ // C.gtk_window_set_keep_above(w.asGTKWindow(), gtkBool(top))
+}
+
+func (w *Window) SetResizable(resizable bool) {
+ C.gtk_window_set_resizable(w.asGTKWindow(), gtkBool(resizable))
+}
+
+func (w *Window) SetDefaultSize(width int, height int) {
+ C.gtk_window_set_default_size(w.asGTKWindow(), C.int(width), C.int(height))
+}
+
+func (w *Window) SetSize(width int, height int) {
+ C.gtk_window_set_default_size(w.asGTKWindow(), C.gint(width), C.gint(height))
+}
+
+func (w *Window) SetDecorated(frameless bool) {
+ C.gtk_window_set_decorated(w.asGTKWindow(), gtkBool(frameless))
+}
+
+func (w *Window) SetTitle(title string) {
+ // No need to free 'title' C-string here since it's freed in C-side
+ C.SetTitle(w.asGTKWindow(), C.CString(title))
+}
+
+func (w *Window) ExecJS(js string) {
+ jscallback := C.JSCallback{
+ webview: w.webview,
+ script: C.CString(js),
+ }
+ invokeOnMainThread(func() { C.ExecuteJS(unsafe.Pointer(&jscallback)) })
+}
+
+func (w *Window) StartDrag() {
+ C.StartDrag(w.webview, w.asGTKWindow())
+}
+
+func (w *Window) StartResize(edge uintptr) {
+ C.StartResize(w.webview, w.asGTKWindow(), C.GdkSurfaceEdge(edge))
+}
+
+func (w *Window) Quit() {
+ if mainLoop == nil {
+ return
+ }
+
+ C.g_main_loop_quit(mainLoop)
+ C.g_main_loop_unref(mainLoop)
+
+ mainLoop = nil
+}
+
+func (w *Window) OpenFileDialog(dialogOptions frontend.OpenDialogOptions, multipleFiles int, action C.GtkFileChooserAction) {
+
+ data := C.OpenFileDialogOptions{
+ window: w.asGTKWindow(),
+ title: C.CString(dialogOptions.Title),
+ multipleFiles: C.int(multipleFiles),
+ action: action,
+ }
+
+ if len(dialogOptions.Filters) > 0 {
+ // Create filter array
+ mem := NewCalloc()
+ arraySize := len(dialogOptions.Filters) + 1
+ data.filters = C.AllocFileFilterArray((C.size_t)(arraySize))
+ filters := unsafe.Slice((**C.struct__GtkFileFilter)(unsafe.Pointer(data.filters)), arraySize)
+ for index, filter := range dialogOptions.Filters {
+ thisFilter := C.gtk_file_filter_new()
+ C.g_object_ref(C.gpointer(thisFilter))
+ if filter.DisplayName != "" {
+ cName := mem.String(filter.DisplayName)
+ C.gtk_file_filter_set_name(thisFilter, cName)
+ }
+ if filter.Pattern != "" {
+ for _, thisPattern := range strings.Split(filter.Pattern, ";") {
+ cThisPattern := mem.String(thisPattern)
+ C.gtk_file_filter_add_pattern(thisFilter, cThisPattern)
+ }
+ }
+ // Add filter to array
+ filters[index] = thisFilter
+ }
+ mem.Free()
+ filters[arraySize-1] = nil
+ }
+
+ if dialogOptions.CanCreateDirectories {
+ data.createDirectories = C.int(1)
+ }
+
+ if dialogOptions.ShowHiddenFiles {
+ data.showHiddenFiles = C.int(1)
+ }
+
+ if dialogOptions.DefaultFilename != "" {
+ data.defaultFilename = C.CString(dialogOptions.DefaultFilename)
+ }
+
+ if dialogOptions.DefaultDirectory != "" {
+ data.defaultDirectory = C.CString(dialogOptions.DefaultDirectory)
+ }
+
+ invokeOnMainThread(func() { C.Opendialog(unsafe.Pointer(&data)) })
+}
+
+func (w *Window) MessageDialog(dialogOptions frontend.MessageDialogOptions) {
+
+ data := C.MessageDialogOptions{
+ window: w.gtkWindow,
+ title: C.CString(dialogOptions.Title),
+ message: C.CString(dialogOptions.Message),
+ }
+ switch dialogOptions.Type {
+ case frontend.InfoDialog:
+ data.messageType = C.int(0)
+ case frontend.ErrorDialog:
+ data.messageType = C.int(1)
+ case frontend.QuestionDialog:
+ data.messageType = C.int(2)
+ case frontend.WarningDialog:
+ data.messageType = C.int(3)
+ }
+ invokeOnMainThread(func() { C.MessageDialog(unsafe.Pointer(&data)) })
+}
+
+func (w *Window) ToggleMaximise() {
+ if w.IsMaximised() {
+ w.UnMaximise()
+ } else {
+ w.Maximise()
+ }
+}
+
+func (w *Window) ShowInspector() {
+ invokeOnMainThread(func() { C.ShowInspector(w.webview) })
+}
+
+// showModalDialogAndExit shows a modal dialog and exits the app.
+func showModalDialogAndExit(title, message string) {
+ go func() {
+ data := C.MessageDialogOptions{
+ title: C.CString(title),
+ message: C.CString(message),
+ messageType: C.int(1),
+ }
+
+ C.MessageDialog(unsafe.Pointer(&data))
+ }()
+
+ <-messageDialogResult
+ log.Fatal(message)
+}
diff --git a/v2/internal/frontend/desktop/linux/window_webkit6.h b/v2/internal/frontend/desktop/linux/window_webkit6.h
new file mode 100644
index 00000000000..948bafc9365
--- /dev/null
+++ b/v2/internal/frontend/desktop/linux/window_webkit6.h
@@ -0,0 +1,134 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+#ifndef window_h
+#define window_h
+
+#include <jsc/jsc.h>
+#include <gtk/gtk.h>
+#include <webkit/webkit.h>
+#include <stdio.h>
+#include <limits.h>
+#include <stdint.h>
+
+typedef struct DragOptions
+{
+ void *webview;
+ GtkWindow *mainwindow;
+} DragOptions;
+
+typedef struct ResizeOptions
+{
+ void *webview;
+ GtkWindow *mainwindow;
+ GdkSurfaceEdge edge;
+} ResizeOptions;
+
+typedef struct JSCallback
+{
+ void *webview;
+ char *script;
+} JSCallback;
+
+typedef struct MessageDialogOptions
+{
+ void *window;
+ char *title;
+ char *message;
+ int messageType;
+} MessageDialogOptions;
+
+typedef struct OpenFileDialogOptions
+{
+ GtkWindow *window;
+ char *title;
+ char *defaultFilename;
+ char *defaultDirectory;
+ int createDirectories;
+ int multipleFiles;
+ int showHiddenFiles;
+ GtkFileChooserAction action;
+ GtkFileFilter **filters;
+} OpenFileDialogOptions;
+
+typedef struct RGBAOptions
+{
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+ uint8_t a;
+ void *webview;
+ void *webviewBox;
+ gboolean windowIsTranslucent;
+} RGBAOptions;
+
+typedef struct SetTitleArgs
+{
+ GtkWindow *window;
+ char *title;
+} SetTitleArgs;
+
+typedef struct SetPositionArgs
+{
+ int x;
+ int y;
+ void *window;
+} SetPositionArgs;
+
+void ExecuteOnMainThread(void *f, gpointer jscallback);
+
+GtkWidget *GTKWIDGET(void *pointer);
+GtkWindow *GTKWINDOW(void *pointer);
+// GtkContainer *GTKCONTAINER(void *pointer);
+GtkBox *GTKBOX(void *pointer);
+
+// window
+gulong SetupInvokeSignal(void *contentManager);
+
+void SetWindowIcon(GtkWindow *window, const guchar *buf, gsize len);
+void SetWindowTransparency(GtkWidget *widget, guchar alpha);
+void SetBackgroundColour(void *data);
+void SetTitle(GtkWindow *window, char *title);
+void SetPosition(void *window, int x, int y);
+void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_width, int max_height);
+void DisableContextMenu(void *webview);
+void ConnectButtons(void *webview);
+
+int IsFullscreen(GtkWidget *widget);
+int IsMaximised(GtkWidget *widget);
+int IsMinimised(GtkWidget *widget);
+
+gboolean Center(gpointer data);
+gboolean Show(gpointer data);
+gboolean Hide(gpointer data);
+gboolean Maximise(gpointer data);
+gboolean UnMaximise(gpointer data);
+gboolean Minimise(gpointer data);
+gboolean UnMinimise(gpointer data);
+gboolean Fullscreen(gpointer data);
+gboolean UnFullscreen(gpointer data);
+
+// WebView
+GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop);
+void LoadIndex(void *webview, char *url);
+void DevtoolsEnabled(void *webview, int enabled, bool showInspector);
+void ExecuteJS(void *data);
+
+// Drag
+void StartDrag(void *webview, GtkWindow *mainwindow);
+void StartResize(void *webview, GtkWindow *mainwindow, GdkSurfaceEdge edge);
+
+// Dialog
+void MessageDialog(void *data);
+GtkFileFilter **AllocFileFilterArray(size_t ln);
+void Opendialog(void *data);
+
+// Inspector
+void sendShowInspectorMessage(GAction *action, GVariant *param);
+void ShowInspector(void *webview);
+void InstallF12Hotkey(GtkApplication *app, GtkWindow *window);
+
+GtkApplication* createApp(char *appId);
+void runApp(GtkApplication *app);
+
+#endif /* window_h */
diff --git a/v2/pkg/assetserver/webview/request_linux.go b/v2/pkg/assetserver/webview/request_linux.go
index c6785fb1cb4..8ae1cb01fc5 100644
--- a/v2/pkg/assetserver/webview/request_linux.go
+++ b/v2/pkg/assetserver/webview/request_linux.go
@@ -1,5 +1,5 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package webview
diff --git a/v2/pkg/assetserver/webview/request_linux_webkit6.go b/v2/pkg/assetserver/webview/request_linux_webkit6.go
new file mode 100644
index 00000000000..55ee82dab54
--- /dev/null
+++ b/v2/pkg/assetserver/webview/request_linux_webkit6.go
@@ -0,0 +1,83 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package webview
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0 gio-unix-2.0
+
+#include "gtk/gtk.h"
+#include "webkit/webkit.h"
+*/
+import "C"
+
+import (
+ "io"
+ "net/http"
+ "unsafe"
+)
+
+// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest`
+func NewRequest(webKitURISchemeRequest unsafe.Pointer) Request {
+ webkitReq := (*C.WebKitURISchemeRequest)(webKitURISchemeRequest)
+ C.g_object_ref(C.gpointer(webkitReq))
+
+ req := &request{req: webkitReq}
+ return newRequestFinalizer(req)
+}
+
+var _ Request = &request{}
+
+type request struct {
+ req *C.WebKitURISchemeRequest
+
+ header http.Header
+ body io.ReadCloser
+ rw *responseWriter
+}
+
+func (r *request) URL() (string, error) {
+ return C.GoString(C.webkit_uri_scheme_request_get_uri(r.req)), nil
+}
+
+func (r *request) Method() (string, error) {
+ return webkit_uri_scheme_request_get_http_method(r.req), nil
+}
+
+func (r *request) Header() (http.Header, error) {
+ if r.header != nil {
+ return r.header, nil
+ }
+
+ r.header = webkit_uri_scheme_request_get_http_headers(r.req)
+ return r.header, nil
+}
+
+func (r *request) Body() (io.ReadCloser, error) {
+ if r.body != nil {
+ return r.body, nil
+ }
+
+ r.body = webkit_uri_scheme_request_get_http_body(r.req)
+
+ return r.body, nil
+}
+
+func (r *request) Response() ResponseWriter {
+ if r.rw != nil {
+ return r.rw
+ }
+
+ r.rw = &responseWriter{req: r.req}
+ return r.rw
+}
+
+func (r *request) Close() error {
+ var err error
+ if r.body != nil {
+ err = r.body.Close()
+ }
+ r.Response().Finish()
+ C.g_object_unref(C.gpointer(r.req))
+ return err
+}
diff --git a/v2/pkg/assetserver/webview/responsewriter_linux.go b/v2/pkg/assetserver/webview/responsewriter_linux.go
index 59646ce29b3..b166d83748c 100644
--- a/v2/pkg/assetserver/webview/responsewriter_linux.go
+++ b/v2/pkg/assetserver/webview/responsewriter_linux.go
@@ -1,5 +1,5 @@
-//go:build linux
-// +build linux
+//go:build linux && !webkit_6
+// +build linux,!webkit_6
package webview
diff --git a/v2/pkg/assetserver/webview/responsewriter_linux_webkit6.go b/v2/pkg/assetserver/webview/responsewriter_linux_webkit6.go
new file mode 100644
index 00000000000..20d86476900
--- /dev/null
+++ b/v2/pkg/assetserver/webview/responsewriter_linux_webkit6.go
@@ -0,0 +1,130 @@
+//go:build linux && webkit_6
+// +build linux,webkit_6
+
+package webview
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0 gio-unix-2.0
+
+#include "gtk/gtk.h"
+#include "webkit/webkit.h"
+#include "gio/gunixinputstream.h"
+
+*/
+import "C"
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strconv"
+ "syscall"
+ "unsafe"
+)
+
+type responseWriter struct {
+ req *C.WebKitURISchemeRequest
+
+ header http.Header
+ wroteHeader bool
+ finished bool
+
+ w io.WriteCloser
+ wErr error
+}
+
+func (rw *responseWriter) Header() http.Header {
+ if rw.header == nil {
+ rw.header = http.Header{}
+ }
+ return rw.header
+}
+
+func (rw *responseWriter) Write(buf []byte) (int, error) {
+ if rw.finished {
+ return 0, errResponseFinished
+ }
+
+ rw.WriteHeader(http.StatusOK)
+ if rw.wErr != nil {
+ return 0, rw.wErr
+ }
+ return rw.w.Write(buf)
+}
+
+func (rw *responseWriter) WriteHeader(code int) {
+ if rw.wroteHeader || rw.finished {
+ return
+ }
+ rw.wroteHeader = true
+
+ contentLength := int64(-1)
+ if sLen := rw.Header().Get(HeaderContentLength); sLen != "" {
+ if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 {
+ contentLength = pLen
+ }
+ }
+
+ // We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the
+ // read FD is given to the InputStream and will be closed there.
+ // Furthermore we especially don't want to have the FD_CLOEXEC
+ rFD, w, err := pipe()
+ if err != nil {
+ rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err))
+ return
+ }
+ rw.w = w
+
+ stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1))
+ defer C.g_object_unref(C.gpointer(stream))
+
+ if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil {
+ rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err))
+ return
+ }
+}
+
+func (rw *responseWriter) Finish() error {
+ if !rw.wroteHeader {
+ rw.WriteHeader(http.StatusNotImplemented)
+ }
+
+ if rw.finished {
+ return nil
+ }
+ rw.finished = true
+ if rw.w != nil {
+ rw.w.Close()
+ }
+ return nil
+}
+
+func (rw *responseWriter) finishWithError(code int, err error) {
+ if rw.w != nil {
+ rw.w.Close()
+ rw.w = &nopCloser{io.Discard}
+ }
+ rw.wErr = err
+
+ msg := C.CString(err.Error())
+ gerr := C.g_error_new_literal(C.g_quark_from_string(msg), C.int(code), msg)
+ C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
+ C.g_error_free(gerr)
+ C.free(unsafe.Pointer(msg))
+}
+
+type nopCloser struct {
+ io.Writer
+}
+
+func (nopCloser) Close() error { return nil }
+
+func pipe() (r int, w *os.File, err error) {
+ var p [2]int
+ e := syscall.Pipe2(p[0:], 0)
+ if e != nil {
+ return 0, nil, fmt.Errorf("pipe2: %s", e)
+ }
+
+ return p[0], os.NewFile(uintptr(p[1]), "|1"), nil
+}
diff --git a/v2/pkg/assetserver/webview/webkit2_36+.go b/v2/pkg/assetserver/webview/webkit2_36+.go
index 1f0db3c8950..e551ddbe44f 100644
--- a/v2/pkg/assetserver/webview/webkit2_36+.go
+++ b/v2/pkg/assetserver/webview/webkit2_36+.go
@@ -1,9 +1,9 @@
-//go:build linux && (webkit2_36 || webkit2_40 || webkit2_41 )
+//go:build linux && (webkit2_36 || webkit2_40 || webkit2_41)
package webview
/*
-#cgo linux pkg-config: gtk+-3.0
+#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 libsoup-2.4
#cgo webkit2_41 pkg-config: webkit2gtk-4.1 libsoup-3.0
diff --git a/v2/pkg/assetserver/webview/webkit2_legacy.go b/v2/pkg/assetserver/webview/webkit2_legacy.go
index 1d1cf7c2b69..018985303e0 100644
--- a/v2/pkg/assetserver/webview/webkit2_legacy.go
+++ b/v2/pkg/assetserver/webview/webkit2_legacy.go
@@ -1,4 +1,4 @@
-//go:build linux && !(webkit2_36 || webkit2_40 || webkit2_41)
+//go:build linux && !(webkit2_36 || webkit2_40 || webkit2_41 || webkit_6)
package webview
diff --git a/v2/pkg/assetserver/webview/webkit6.go b/v2/pkg/assetserver/webview/webkit6.go
new file mode 100644
index 00000000000..f8ad8adbdef
--- /dev/null
+++ b/v2/pkg/assetserver/webview/webkit6.go
@@ -0,0 +1,141 @@
+//go:build linux && webkit_6
+
+package webview
+
+/*
+#cgo pkg-config: gtk4 webkitgtk-6.0 libsoup-3.0 gio-unix-2.0
+
+#include "gtk/gtk.h"
+#include "webkit/webkit.h"
+#include "libsoup/soup.h"
+#include "gio/gunixinputstream.h"
+*/
+import "C"
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "unsafe"
+)
+
+const Webkit2MinMinorVersion = 48
+
+func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string {
+ method := C.GoString(C.webkit_uri_scheme_request_get_http_method(req))
+ return strings.ToUpper(method)
+}
+
+func webkit_uri_scheme_request_get_http_headers(req *C.WebKitURISchemeRequest) http.Header {
+ hdrs := C.webkit_uri_scheme_request_get_http_headers(req)
+
+ var iter C.SoupMessageHeadersIter
+ C.soup_message_headers_iter_init(&iter, hdrs)
+
+ var name *C.char
+ var value *C.char
+
+ h := http.Header{}
+ for C.soup_message_headers_iter_next(&iter, &name, &value) != 0 {
+ h.Add(C.GoString(name), C.GoString(value))
+ }
+
+ return h
+}
+
+func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error {
+ resp := C.webkit_uri_scheme_response_new(stream, C.gint64(streamLength))
+ defer C.g_object_unref(C.gpointer(resp))
+
+ cReason := C.CString(http.StatusText(code))
+ C.webkit_uri_scheme_response_set_status(resp, C.guint(code), cReason)
+ C.free(unsafe.Pointer(cReason))
+
+ cMimeType := C.CString(header.Get(HeaderContentType))
+ C.webkit_uri_scheme_response_set_content_type(resp, cMimeType)
+ C.free(unsafe.Pointer(cMimeType))
+
+ hdrs := C.soup_message_headers_new(C.SOUP_MESSAGE_HEADERS_RESPONSE)
+ for name, values := range header {
+ cName := C.CString(name)
+ for _, value := range values {
+ cValue := C.CString(value)
+ C.soup_message_headers_append(hdrs, cName, cValue)
+ C.free(unsafe.Pointer(cValue))
+ }
+ C.free(unsafe.Pointer(cName))
+ }
+
+ C.webkit_uri_scheme_response_set_http_headers(resp, hdrs)
+
+ C.webkit_uri_scheme_request_finish_with_response(req, resp)
+ return nil
+}
+
+func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser {
+ stream := C.webkit_uri_scheme_request_get_http_body(req)
+ if stream == nil {
+ return http.NoBody
+ }
+ return &webkitRequestBody{stream: stream}
+}
+
+type webkitRequestBody struct {
+ stream *C.GInputStream
+ closed bool
+}
+
+// Read implements io.Reader
+func (r *webkitRequestBody) Read(p []byte) (int, error) {
+ if r.closed {
+ return 0, io.ErrClosedPipe
+ }
+
+ var content unsafe.Pointer
+ var contentLen int
+ if p != nil {
+ content = unsafe.Pointer(&p[0])
+ contentLen = len(p)
+ }
+
+ if contentLen == 0 {
+ return 0, nil
+ }
+
+ var gErr *C.GError
+ n := C.g_input_stream_read(r.stream, content, C.gsize(contentLen), nil, &gErr)
+ if n == -1 {
+ return 0, formatGError("stream read failed", gErr)
+ } else if n == 0 {
+ return 0, io.EOF
+ }
+ return int(n), nil
+}
+
+func (r *webkitRequestBody) Close() error {
+ if r.closed {
+ return nil
+ }
+ r.closed = true
+
+ // https://docs.gtk.org/gio/method.InputStream.close.html
+ // Streams will be automatically closed when the last reference is dropped, but you might want to call this function
+ // to make sure resources are released as early as possible.
+ var err error
+ var gErr *C.GError
+ if C.g_input_stream_close(r.stream, nil, &gErr) == 0 {
+ err = formatGError("stream close failed", gErr)
+ }
+ C.g_object_unref(C.gpointer(r.stream))
+ r.stream = nil
+ return err
+}
+
+func formatGError(msg string, gErr *C.GError, args ...any) error {
+ if gErr != nil && gErr.message != nil {
+ msg += ": " + C.GoString(gErr.message)
+ C.g_error_free(gErr)
+ }
+ return fmt.Errorf(msg, args...)
+}