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-requestGdkSurface notifications for configured width, height, and toplevel stateNew C function setupWindowEventControllers() sets up all event controllers.
The realised window surface now emits the common resize, minimise, maximise, and fullscreen events used by the other desktop backends.
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 |
The Beta verification gate is covered continuously by the v3 CI matrix:
BUILD_TAGS=gtk3.Recent successful runs of this matrix include PR #5870 and PR #5877. Native dual-stack validation was also performed on Fedora 44 while reproducing and fixing #5845.
Known constraints are documented rather than hidden: GTK4/Wayland window positioning is a protocol-level no-op, portal-backed GTK4 dialogs have the documented option limitations, and remaining Linux regressions stay in focused issues such as #5838, #5839, and #5465. Performance and long-running leak benchmarking are ongoing quality work, not a condition for the v3 Beta gate.
| 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 |
| Window Size | Configure event dimensions | Live GdkSurface width/height properties |
| Window State Events | Configure/window-state events | GdkToplevel:state notifications |
| 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
Size() returning the requested default instead of the live configured window size.GdkSurface resize and toplevel-state notifications, including common maximise, minimise, and fullscreen events.v3/pkg/application/linux_cgo.c, v3/pkg/application/linux_cgo.h,
v3/pkg/application/linux_cgo.go, v3/pkg/application/webview_window_linux.go,
and v3/pkg/application/webview_window_linux_test.go.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