IMPLEMENTATION.md
This document tracks the implementation of WebKitGTK 6.0 (GTK4) support for Wails v3 on Linux.
Current goal (post-flip, 2026-05-16): GTK4 + WebKitGTK 6.0 is the default Linux stack for v3.0.0 GA. GTK3 + WebKit2GTK 4.1 is a legacy opt-in via -tags gtk3 for one v3 cycle and is scheduled for removal in v3.1.
Original goal (2026-02-04, superseded by Decision 1.1): Provide GTK4/WebKitGTK 6.0 support as an EXPERIMENTAL opt-in via -tags gtk4, while maintaining GTK3/WebKit2GTK 4.1 as the stable default.
Context: GTK4 + WebKitGTK 6.0 has matured through the v3 alpha cycle (alpha.74 → alpha.92). For v3.0.0 GA, shipping with the modern stack as default-of-least-resistance — rather than as an experimental opt-in — is required so distros and packagers do not need to learn a Wails-specific build tag to get a working build.
Decision: GTK4 + WebKitGTK 6.0 is the default. GTK3 + WebKit2GTK 4.1 is opt-in via -tags gtk3. The legacy path stays through the v3.0.x line and is removed in v3.1.
Rationale:
go build Just Works-tags gtk3 escape hatch lets us defer the breaking change of dropping GTK3 entirely to v3.1Implementation (issue #5459): Build-tag flip + file renames. Files previously named *_linux_gtk4.go become the bare *_linux.go defaults with constraint !gtk3; files previously named *_linux.go become *_linux_gtk3.go with constraint gtk3. The gtk4 build tag is retired in favor of gtk3 as the toggle. Legacy wails3 doctor package-manager polarity inverted (gtk4/webkitgtk-6.0 required, gtk3/webkit2gtk-4.1 optional legacy). doctor-ng already had the correct polarity.
Context: Need to support modern Linux distributions with GTK4 while maintaining stability for existing apps.
Decision: GTK3 remains the stable default (no build tag required). GTK4 is available as experimental via -tags gtk4.
Rationale:
Build Tags (post-Decision 1.1, post-#5463 review fixes):
//go:build linux && cgo && !gtk3 && !android && !server//go:build linux && cgo && gtk3 && !android && !server-tags server) excludes both paths so no GTK/cgo code is linked.GTK4/WebKitGTK 6.0:
#cgo linux pkg-config: gtk4 webkitgtk-6.0 libsoup-3.0
GTK3/WebKit2GTK 4.1 (legacy):
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1 libsoup-3.0
Context: GTK4/Wayland doesn't support arbitrary window positioning - this is a Wayland protocol limitation.
Decision: Window positioning functions (move(), setPosition(), center()) are documented NO-OPs on GTK4/Wayland.
Rationale: This is a fundamental Wayland design decision, not a limitation we can work around. Users need to be aware of this behavioral difference.
Context: GTK4 removes GtkMenu/GtkMenuItem in favor of GMenu/GAction.
Decision: Complete rewrite of menu system for GTK4 using GMenu/GAction/GtkPopoverMenuBar.
Status: Stub implementations only. Full implementation pending.
Context: v3's system tray uses D-Bus StatusNotifierItem protocol.
Decision: No changes needed - system tray is already GTK-agnostic.
Commit: a0ca13fdc (2026-01-04)
Files modified:
v3/pkg/application/application_linux.go - Added gtk3 constraintv3/pkg/application/linux_cgo.go - Added gtk3 constraintv3/internal/assetserver/webview/request_linux.go - Added gtk3 constraintv3/internal/assetserver/webview/responsewriter_linux.go - Added gtk3 constraintv3/internal/assetserver/webview/webkit2.go - Added gtk3 constraintFiles created:
v3/pkg/application/linux_cgo_gtk4.go (~1000 lines)
gtk4 webkitgtk-6.0 pkg-configv3/pkg/application/application_linux_gtk4.go (~250 lines)
Files created:
v3/internal/assetserver/webview/webkit6.gov3/internal/assetserver/webview/request_linux_gtk4.gov3/internal/assetserver/webview/responsewriter_linux_gtk4.goGoal: Update wails doctor to check for GTK4 as primary, GTK3 as secondary.
All 7 package managers updated to check GTK4/WebKitGTK 6.0 as primary, GTK3 as optional/legacy:
v3/internal/doctor/packagemanager/apt.go ✅v3/internal/doctor/packagemanager/dnf.go ✅v3/internal/doctor/packagemanager/pacman.go ✅v3/internal/doctor/packagemanager/zypper.go ✅v3/internal/doctor/packagemanager/emerge.go ✅v3/internal/doctor/packagemanager/eopkg.go ✅v3/internal/doctor/packagemanager/nixpkgs.go ✅Package key naming convention (post-#5463 default flip): gtk4, webkitgtk-6.0 (primary/default), gtk3 (legacy), webkit2gtk (legacy) (optional, removed in v3.1)
Files created/updated:
v3/internal/capabilities/capabilities.go - Added GTKVersion (int) and WebKitVersion (string) fieldsv3/internal/capabilities/capabilities_linux.go - GTK4 default: GTKVersion: 4, WebKitVersion: "6.0"v3/internal/capabilities/capabilities_linux_gtk3.go - GTK3 legacy: GTKVersion: 3, WebKitVersion: "4.1"TODO (deferred to Phase 3):
v3/internal/doctor/doctor_linux.go - Improve output to show GTK4 vs GTK3 statusGTK4 replaces direct signal handlers with GtkEventController objects:
GtkEventControllerFocus for focus in/out eventsGtkGestureClick for button press/release eventsGtkEventControllerKey for keyboard eventsclose-request, notify::maximized, notify::fullscreenedNew C function setupWindowEventControllers() sets up all event controllers.
GTK4 uses GdkToplevel API instead of GTK3's gtk_window_begin_move_drag:
gdk_toplevel_begin_move() for window draggdk_toplevel_begin_resize() for window resizegtk_native_get_surface() to get the GdkSurfaceComplete implementation using GTK4's GtkDropTarget:
on_drop_enter / on_drop_leave for drag enter/exit eventson_drop_motion for drag position updateson_drop handles file drops via GDK_TYPE_FILE_LISTonDropEnter, onDropLeave, onDropMotion, onDropFilesisMinimised() uses gdk_toplevel_get_state() with GDK_TOPLEVEL_STATE_MINIMIZEDisMaximised() uses gtk_window_is_maximized()isFullscreen() uses gtk_window_is_fullscreen()GTK4 removed gtk_window_set_geometry_hints(). Now using gtk_widget_set_size_request() for minimum size.
TODO (deferred):
GTK4 completely replaced the menu system. GTK3's GtkMenu/GtkMenuItem are gone.
GMenu - Menu model (data structure, not a widget)GMenuItem - Individual menu item in the modelGSimpleAction - Action that gets triggered when menu item is activatedGSimpleActionGroup - Container for actions, attached to widgetsGtkPopoverMenuBar created from GMenu model via create_menu_bar_from_model()attach_action_group_to_widget()v3/pkg/application/menu_linux_gtk4.go - GTK4 menu processingv3/pkg/application/menuitem_linux_gtk4.go - GTK4 menu item handlingmenu_linux.go - Added gtk3 tagmenuitem_linux.go - Added gtk3 tagmenuActionActivated() - Callback when GAction is triggeredmenuItemNewWithId() - Creates GMenuItem + associated GSimpleActionmenuCheckItemNewWithId() - Creates stateful toggle actionmenuRadioItemNewWithId() - Creates radio actionset_action_enabled() / set_action_state() - Manage action stateTODO (deferred):
WebKitGTK 6.0 uses the same URI scheme handler API as WebKitGTK 4.1. The asset server implementation is identical between GTK3 and GTK4.
v3/internal/assetserver/webview/webkit6.go - WebKitGTK 6.0 helpersv3/internal/assetserver/webview/request_linux_gtk4.go - Request handlingv3/internal/assetserver/webview/responsewriter_linux_gtk4.go - Response writingThe GTK4 CGO file was missing two critical exports that were in the GTK3 file:
onProcessRequest - Handles URI scheme requests from WebKitsendMessageToBackend - Handles JavaScript to Go communicationBoth exports were added to linux_cgo_gtk4.go.
| Aspect | GTK3 | GTK4 |
|---|---|---|
| pkg-config | webkit2gtk-4.1 | webkitgtk-6.0 |
| Headers | webkit2/webkit2.h | webkit/webkit.h |
| Min version | 2.40 | 6.0 |
| URI scheme API | Same | Same |
TODO (deferred to testing phase):
Updated both Dockerfile.linux-x86_64 and Dockerfile.linux-arm64 to install:
-tags gtk4 builds)Build scripts now support BUILD_TAGS environment variable:
BUILD_TAGS=gtk4: Builds with GTK4/WebKitGTK 6.0 (experimental)New targets added to v3/Taskfile.yaml:
| Target | Description |
|---|---|
test:example:linux | Build single example with GTK3 (native, default) |
test:example:linux:gtk4 | Build single example with GTK4 (native, experimental) |
test:examples:linux:docker:x86_64 | Build all examples with GTK3 in Docker |
test:examples:linux:docker:x86_64:gtk4 | Build all examples with GTK4 in Docker (experimental) |
test:examples:linux:docker:arm64 | Build all examples with GTK3 in Docker (ARM64) |
test:examples:linux:docker:arm64:gtk4 | Build all examples with GTK4 in Docker (ARM64, experimental) |
TODO (deferred):
GTK4 completely replaced the dialog APIs. GTK3's GtkFileChooserDialog and
gtk_message_dialog_new are deprecated/removed.
GTK4 uses GtkFileDialog with async API:
gtk_file_dialog_open() - Open single filegtk_file_dialog_open_multiple() - Open multiple filesgtk_file_dialog_select_folder() - Select foldergtk_file_dialog_select_multiple_folders() - Select multiple foldersgtk_file_dialog_save() - Save fileKey differences:
gtk_dialog_run() - everything is async with callbacksGListStore of GtkFileFilter objectsGAsyncResult callbacksgtk_file_dialog_set_accept_label()GTK4's GtkFileDialog uses xdg-desktop-portal for native file dialogs. This provides
better desktop integration but removes some application control:
| Feature | GTK3 | GTK4 | Notes |
|---|---|---|---|
ShowHiddenFiles() | ✅ Works | ❌ No effect | User controls via portal UI toggle |
CanCreateDirectories() | ✅ Works | ❌ No effect | Always enabled in portal |
ResolvesAliases() | ✅ Works | ❌ No effect | Portal handles symlinks |
SetButtonText() | ✅ Works | ✅ Works | gtk_file_dialog_set_accept_label() |
| Multiple folders | ✅ Works | ✅ Works | gtk_file_dialog_select_multiple_folders() |
Why these limitations exist: GTK4's portal-based dialogs delegate UI control to the desktop environment (GNOME, KDE, etc.). This is intentional - the portal provides consistent UX across applications and respects user preferences.
GTK4 uses GtkAlertDialog:
gtk_alert_dialog_choose() - Show dialog with buttonsfileDialogCallback / alertDialogCallback C exports for resultsrunChooserDialog() and runQuestionDialog() Go wrappersrunOpenFileDialog() and runSaveFileDialog() convenience functions| GTK3 | GTK4 |
|---|---|
GtkFileChooserDialog | GtkFileDialog |
gtk_dialog_run() | Async callbacks |
gtk_message_dialog_new() | GtkAlertDialog |
gtk_widget_destroy() | g_object_unref() |
GTK4 uses gtk_application_set_accels_for_action() to bind keyboard shortcuts to GActions.
C Helper Functions (in linux_cgo_gtk4.go):
set_action_accelerator(app, action_name, accel) - Sets accelerator for a GActionbuild_accelerator_string(key, mods) - Converts key+modifiers to GTK accelerator stringGo Functions (in linux_cgo_gtk4.go):
namedKeysToGTK - Map of key names to GDK keysym values (e.g., "backspace" → 0xff08)parseKeyGTK(key) - Converts Wails key string to GDK keysymparseModifiersGTK(modifiers) - Converts Wails modifiers to GdkModifierTypeacceleratorToGTK(accel) - Converts full accelerator to GTK formatsetMenuItemAccelerator(itemId, accel) - Sets accelerator for a menu itemIntegration (in menuitem_linux_gtk4.go):
setAccelerator() method on linuxMenuItem calls setMenuItemAccelerator()newMenuItemImpl(), newCheckMenuItemImpl(), newRadioMenuItemImpl() all set accelerators during creationGTK accelerator strings use format like:
<Control>q - Ctrl+Q<Control><Shift>s - Ctrl+Shift+S<Alt>F4 - Alt+F4<Super>e - Super+E (Windows/Command key)| Wails Modifier | GDK Modifier |
|---|---|
CmdOrCtrlKey | GDK_CONTROL_MASK |
ControlKey | GDK_CONTROL_MASK |
OptionOrAltKey | GDK_ALT_MASK |
ShiftKey | GDK_SHIFT_MASK |
SuperKey | GDK_SUPER_MASK |
TODO:
| Feature | GTK3 | GTK4 |
|---|---|---|
| Init | gtk_init(&argc, &argv) | gtk_init_check() |
| Container | gtk_container_add() | gtk_window_set_child() |
| Show | gtk_widget_show_all() | Widgets visible by default |
| Hide | gtk_widget_hide() | gtk_widget_set_visible(w, FALSE) |
| Clipboard | GtkClipboard | GdkClipboard |
| Menu | GtkMenu/GtkMenuItem | GMenu/GAction |
| Menu Bar | GtkMenuBar | GtkPopoverMenuBar |
| Window Move | gtk_window_move() | NO-OP on Wayland |
| Window Position | gtk_window_get_position() | Not available on Wayland |
| Destroy | gtk_widget_destroy() | gtk_window_destroy() |
| Drag Start | gtk_window_begin_move_drag() | gtk_native_get_surface() + surface drag |
Post-#5463 default flip — GTK4 is the default; GTK3 is opt-in via -tags gtk3 and scheduled for removal in v3.1.
v3/pkg/application/
linux_cgo.go # Main CGO (!gtk3 tag - default)
linux_cgo.c # cgo C source (!gtk3 tag - default)
linux_cgo.h # cgo C header (!gtk3 tag - default)
application_linux.go # App lifecycle (!gtk3 tag - default)
gtkdispatch_linux.go # GTK main-thread dispatch (!gtk3 tag - default)
menu_linux.go # Menu processing (!gtk3 tag - default)
menuitem_linux.go # Menu item handling (!gtk3 tag - default)
v3/internal/assetserver/webview/
webkit_linux.go # WebKitGTK 6.0 helpers (!gtk3 tag - default)
request_linux.go # Request handling (!gtk3 tag - default)
responsewriter_linux.go # Response writing (!gtk3 tag - default)
v3/internal/capabilities/
capabilities_linux.go # GTK4 capabilities (!gtk3 tag - default)
v3/internal/operatingsystem/
webkit_linux.go # WebKit version info (!gtk3 tag - default)
-tags gtk3v3/pkg/application/
linux_cgo_gtk3.go # Main CGO (gtk3 tag - legacy)
application_linux_gtk3.go # App lifecycle (gtk3 tag - legacy)
gtkdispatch_linux_gtk3.go # GTK main-thread dispatch (gtk3 tag - legacy)
menu_linux_gtk3.go # Menu processing (gtk3 tag - legacy)
menuitem_linux_gtk3.go # Menu item handling (gtk3 tag - legacy)
v3/internal/assetserver/webview/
webkit_linux_gtk3.go # WebKit2GTK 4.1 helpers (gtk3 tag - legacy)
request_linux_gtk3.go # Request handling (gtk3 tag - legacy)
responsewriter_linux_gtk3.go # Response writing (gtk3 tag - legacy)
v3/internal/capabilities/
capabilities_linux_gtk3.go # GTK3 capabilities (gtk3 tag - legacy)
v3/internal/operatingsystem/
webkit_linux_gtk3.go # WebKit version info (gtk3 tag - legacy)
Historical note: The Phase tracker blocks earlier in this document (Phases 1–4) reference the pre-flip filenames (
*_linux_gtk4.go,webkit6.go, etc.) as a record of work done at the time. Those references are historical and have not been retconned; new work should reference the post-flip layout above.
v3/pkg/application/
webview_window_linux.go # Window wrapper (uses methods from CGO files)
systemtray_linux.go # D-Bus based, GTK-agnostic
v3/internal/assetserver/webview/
request.go # Interface definitions
responsewriter.go # Interface definitions
g_object_unref() that freed dialog before async callback
6f9c5beb5g_object_unref(dialog) from show_alert_dialog() (same issue as file dialogs)dialogs_linux.go - InvokeAsync → go func() since runQuestionDialog blocks internallyrunQuestionDialog to use options.Title as message (was using options.Message)1a77e6091-race flag by changing C signal functions to accept uintptr_t (3999f1f24)runtimeLoaded/pendingJS (8e386034e)CGO_CFLAGS="-DWAILS_GTK_DEBUG" go build ...v3/test/manual/dialog/gtk_file_dialog_set_accept_label() for custom button textgtk_file_dialog_select_multiple_folders() for multiple directory selectionapplication.go cleanup - was using RLock() when writing a.windows = nilv3/pkg/application/linux_cgo_gtk4.go - dialog fixes, race fixes, accept label, multiple foldersv3/pkg/application/linux_cgo_gtk4.c - DEBUG_LOG macro, alert dialog lifecycle fix, select_multiple_folders callbackv3/pkg/application/linux_cgo_gtk4.h - uintptr_t for signal functionsv3/pkg/application/dialogs_linux.go - deadlock fixv3/pkg/application/webview_window.go - pendingJS mutexv3/pkg/application/application.go - RLock → Lock for cleanup writesdocs/src/content/docs/reference/dialogs.mdx - documented GTK4 limitationscreate_radio_menu_item() C helper and menuRadioItemNewWithGroup() Go wrappergtk_window_minimize() works on GNOME/KDE (sends xdg_toplevel_set_minimized)unregisterWindow() in application_linux_gtk4.goDisableQuitOnLastWindowClosed optionmenuNewSection(), menuAppendSection(), menuAppendItemToSection() helpersv3/pkg/application/linux_cgo_gtk4.go - added radio group support, section helpersv3/pkg/application/linux_cgo_gtk4.c - added create_radio_menu_item(), init_menu_css()v3/pkg/application/linux_cgo_gtk4.h - added function declarationv3/pkg/application/application_linux_gtk4.go - added quit-on-last-window logicv3/pkg/application/menu_linux_gtk4.go - section-based menu processing, radio groupsv3/pkg/application/menuitem_linux_gtk4.go - updated radio item creationv3/pkg/application/webview_window_linux.go - fixed zoom() to toggle maximizev3/pkg/application/window_manager.go - removed debug outputactivated channel and sync.Once to linuxApp structmarkActivated() method called from activateLinux() callbackwaitForActivation() method for callers to block until readyWebviewWindow.Run() to wait for activation before InvokeSyncv3/pkg/application/application_linux_gtk4.go - activation gatev3/pkg/application/linux_cgo_gtk4.go - call markActivated() in activateLinuxv3/pkg/application/webview_window.go - wait for activation on GTK4gtk3 build tag to v3/internal/operatingsystem/webkit_linux.gov3/internal/operatingsystem/webkit_linux_gtk4.go with GTK4/WebKitGTK 6.0init() to newPlatformApp() for cleaner setuponProcessRequest export to linux_cgo_gtk4.gosendMessageToBackend export to linux_cgo_gtk4.gogtk3 constraint to 5 existing files