wails/pr_4570.diff
Lea Anthony 5dc3e21699
feat(linux): GTK4 + WebKitGTK 6.0 support (opt-in via -tags gtk4) (#4958)
* chore: add WebKitGTK 6.0/GTK4 epic and beads issue tracking

Initialize beads (bd) issue tracker with comprehensive epic for
WebKitGTK 6.0 / GTK4 support as the new default for Wails v3 Linux.

Epic: wails-webview2gtk6-t4e (40 tasks)
- GTK4/WebKit6 as default (no build tag needed)
- GTK3/WebKit4.1 via -tags gtk3 for legacy
- Docker container with both library sets for cross-compilation
- Comprehensive test strategy including benchmarks
- task build:linux (GTK4) and task build:linux:gtk3 (legacy)

* feat(linux): add WebKitGTK 6.0 / GTK4 support infrastructure [WIP]

Architecture change for modern Linux desktop support:
- GTK4/WebKitGTK 6.0 is the new DEFAULT (no build tag)
- GTK3/WebKit2GTK 4.1 is LEGACY (requires -tags gtk3)

Changes:
- Add gtk3 build constraint to existing GTK3 CGO files
- Create GTK4 stub implementations (linux_cgo_gtk4.go, application_linux_gtk4.go)
- Create WebKitGTK 6.0 asset server stubs (webkit6.go, request/responsewriter)

Known limitations (documented):
- Window positioning is NO-OP on GTK4/Wayland (protocol limitation)
- Menu system needs GMenu/GAction rewrite (stub only)
- Some methods have TODO markers for full implementation

This establishes the build infrastructure for GTK4 support.
Full implementation requires GTK4 dev environment for testing.

* docs: add implementation tracker for WebKitGTK 6.0/GTK4 work

- Create IMPLEMENTATION.md to track progress, decisions, and API differences
- Update AGENTS.md with instructions to maintain IMPLEMENTATION.md
- Document Phase 1 completion and remaining phases

* feat(linux): update doctor and capabilities for GTK4/WebKitGTK 6.0 support

- Update all 7 package managers (apt, dnf, pacman, zypper, emerge, eopkg, nixpkgs)
  to check for GTK4/WebKitGTK 6.0 as primary dependencies
- Mark GTK3/WebKit2GTK packages as optional/legacy
- Add GTKVersion and WebKitVersion fields to Capabilities struct
- Create capabilities_linux_gtk3.go for legacy build path
- Update IMPLEMENTATION.md to mark Phase 2 complete

GTK4 packages are now checked by default. Legacy GTK3 packages
are marked optional and only needed when building with -tags gtk3.

* feat(linux): implement GTK4 window management and event handling

- Add GtkEventController-based event handling for GTK4:
  - GtkEventControllerFocus for focus in/out
  - GtkGestureClick for button press/release
  - GtkEventControllerKey for keyboard events
- Implement window drag/resize using GdkToplevel API
- Add complete drag-and-drop support with GtkDropTarget
- Fix window state detection (minimized, maximized, fullscreen)
- Fix size() to properly return window dimensions in GTK4
- Update IMPLEMENTATION.md to mark Phase 3 complete

GTK4 uses a fundamentally different event model with controllers
instead of direct signal handlers. This commit implements all the
necessary event handling for window management.

* feat(linux): implement GTK4 menu system with GMenu/GAction

Phase 4 of WebKitGTK 6.0/GTK4 implementation.

GTK4 completely replaces the menu system. GTK3's GtkMenu/GtkMenuItem
are replaced by:
- GMenu: Menu model (data structure, not a widget)
- GMenuItem: Individual menu item in the model
- GSimpleAction: Action triggered when menu item is activated
- GSimpleActionGroup: Container for actions, attached to widgets
- GtkPopoverMenuBar: Menu bar widget created from GMenu model

Key changes:
- linux_cgo_gtk4.go: Added C helpers and Go functions for GMenu/GAction
  - menuActionActivated() callback for action triggers
  - menuItemNewWithId/menuCheckItemNewWithId/menuRadioItemNewWithId
  - set_action_enabled/set_action_state for state management
- menu_linux_gtk4.go: GTK4 menu processing (processMenu, addMenuItem)
- menuitem_linux_gtk4.go: GTK4 menu item handling and role menus
- menu_linux.go: Added gtk3 build tag
- menuitem_linux.go: Added gtk3 build tag

Deferred to future work:
- Context menus with GtkPopoverMenu
- Keyboard accelerators with GtkShortcut

* feat(linux): add missing CGO exports for GTK4 asset server

Phase 5 of WebKitGTK 6.0/GTK4 implementation.

The GTK4 CGO file was missing two critical exports that existed in the
GTK3 version:

1. onProcessRequest - Handles WebKit URI scheme requests. This callback
   is registered with webkit_web_context_register_uri_scheme and routes
   asset requests to the webviewRequests channel for processing.

2. sendMessageToBackend - Handles JavaScript to Go communication. This
   is called when JavaScript sends messages via the webkit user content
   manager, enabling the IPC bridge.

The asset server files (webkit6.go, request_linux_gtk4.go,
responsewriter_linux_gtk4.go) were already complete from Phase 1.
WebKitGTK 6.0 uses the same URI scheme handler API as WebKitGTK 4.1.

* build(linux): add GTK4 support to Docker and Taskfile

Phase 6 of WebKitGTK 6.0/GTK4 implementation.

Docker containers (Ubuntu 24.04):
- Install both GTK4/WebKitGTK 6.0 (default) and GTK3/WebKit2GTK 4.1 (legacy)
- Build scripts support BUILD_TAGS environment variable
- Default build uses GTK4, BUILD_TAGS=gtk3 uses legacy GTK3

Taskfile targets:
- test:example:linux - Build with GTK4 (default)
- test:example:linux:gtk3 - Build with GTK3 (legacy)
- test:examples:linux:docker:x86_64 - Docker build with GTK4
- test:examples:linux:docker:x86_64:gtk3 - Docker build with GTK3
- test:examples:linux:docker:arm64 - Docker build with GTK4 (ARM64)
- test:examples:linux:docker:arm64:gtk3 - Docker build with GTK3 (ARM64)

This allows testing both the new GTK4 default and legacy GTK3 builds.

* feat(linux): implement GTK4 dialog system with GtkFileDialog and GtkAlertDialog

Phase 8 of WebKitGTK 6.0/GTK4 implementation.

GTK4 completely replaced the dialog APIs. GTK3's GtkFileChooserDialog
and gtk_dialog_run() are deprecated/removed in GTK4.

File Dialogs (GtkFileDialog):
- gtk_file_dialog_open() for single file selection
- gtk_file_dialog_open_multiple() for multiple files
- gtk_file_dialog_select_folder() for folder selection
- gtk_file_dialog_save() for save dialogs
- Filters use GListStore of GtkFileFilter objects
- All operations are async with GAsyncResult callbacks

Message Dialogs (GtkAlertDialog):
- gtk_alert_dialog_choose() with button array
- Configurable default and cancel button indices
- Async response via callback

Implementation:
- Request ID tracking for async callback matching
- fileDialogCallback/alertDialogCallback C exports
- runChooserDialog/runQuestionDialog Go wrappers
- runOpenFileDialog/runSaveFileDialog convenience functions

* feat(linux): implement GTK4 keyboard accelerators for menu items

Add keyboard accelerator support using gtk_application_set_accels_for_action():

- Add namedKeysToGTK map with GDK keysym values for special keys
- Add parseKeyGTK() to convert key names to GDK keysyms
- Add parseModifiersGTK() to convert Wails modifiers to GDK modifier masks
- Add acceleratorToGTK() for full accelerator conversion
- Add setMenuItemAccelerator() Go wrapper calling C helpers
- Integrate accelerator setting in newMenuItemImpl, newCheckMenuItemImpl,
  and newRadioMenuItemImpl during menu item creation
- Update setAccelerator() method on linuxMenuItem to use new function

Completes Phase 9 of GTK4 implementation.

* refactor(linux): extract GTK4 C code to separate files and fix WebKitGTK 6.0 API

Extract C code from linux_cgo_gtk4.go to dedicated C files for better
IDE support and maintainability:
- linux_cgo_gtk4.h: Function declarations and type definitions
- linux_cgo_gtk4.c: C implementations for GTK4/WebKitGTK 6.0

WebKitGTK 6.0 API fixes:
- webkit_web_view_new_with_user_content_manager() removed
  -> Use create_webview_with_user_content_manager() with g_object_new()
- WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND removed
  -> Default to ALWAYS (only ALWAYS/NEVER available in 6.0)
- WebKitJavascriptResult replaced with JSCValue in callbacks
  -> sendMessageToBackend now receives JSCValue* directly

Also:
- Remove duplicate show()/hide() methods (use shared file)
- Remove duplicate startResize() (wrong signature)
- Add set_app_menu_model() setter for C global variable access
- Fix webview.Scheme reference to use hardcoded 'wails' string

Note: Some pre-existing compilation errors remain in the codebase
that are unrelated to this refactoring.

* fix(linux): resolve GTK4 compilation errors and add missing platform methods

- Add missing App methods: logPlatformInfo, platformEnvironment, fatalHandler
- Add missing linuxApp methods: hide, show, on, isOnMainThread, getAccentColor
- Add missing CGO functions: getPrimaryScreen, openDevTools, enableDevTools, handleLoadChanged
- Fix options.Linux nil check (struct not pointer)
- Fix runSaveFileDialog return type to match interface
- Fix registerWindow signature to accept pointer type
- Fix GdkRGBA to use float instead of double
- Add webview import for asset request handling
- Add sanity check task to Taskfile for quick compilation verification

* fix(linux): resolve GTK3/GTK4 symbol conflict in operatingsystem package

- Add gtk3 build tag to webkit_linux.go to prevent GTK3 linking in GTK4 builds
- Create webkit_linux_gtk4.go with GTK4/WebKitGTK 6.0 pkg-config
- Move app initialization from init() to newPlatformApp() for cleaner setup
- Fixes runtime crash: 'GTK 2/3 symbols detected in GTK 4 process'

* docs: update implementation tracker for GTK3/GTK4 symbol conflict fix

* fix(linux): add GTK4 activation gate to prevent window creation before app activation

GTK4 requires the application to be 'activated' before gtk_application_window_new()
can be called. This adds a synchronization mechanism:

- Add activated channel and sync.Once to linuxApp struct
- Mark application as activated in activateLinux callback
- Wait for activation in WebviewWindow.Run() before creating windows

Fixes SIGSEGV crash when creating windows on GTK4.

* feat(linux): add primary menu style option and fix GTK4 menu issues

- Add LinuxMenuStyle option for MenuBar vs PrimaryMenu (hamburger) display
- Fix menu separators using GMenu sections instead of separator items
- Fix radio button styling with proper string-valued stateful actions
- Fix app not terminating when last window closed
- Fix Window→Zoom to toggle maximize instead of webview zoom
- Add build constraints to .c/.h files for GTK3 compatibility
- Document MenuStyle option in window reference docs
- Update implementation tracker with session changes

* chore(examples): use PrimaryMenu style in menu example

* feat(linux): implement Systray API v2 with smart defaults and window options

- Add smart defaults for systray click behavior:
  - Window only: left-click toggles window
  - Menu only: right-click shows menu
  - Window + Menu: left-click toggles, right-click shows menu

- Add HideOnEscape and HideOnFocusLost window options:
  - HideOnEscape: hides window when Escape key pressed
  - HideOnFocusLost: hides window on focus lost (auto-disabled on
    focus-follows-mouse WMs like Hyprland, Sway, i3)

- Add WebviewWindow.RegisterKeyBinding() public method

- Fix Linux systray handlers:
  - Activate() now calls clickHandler (was doubleClickHandler)
  - SecondaryActivate() calls rightClickHandler or opens menu
  - ItemIsMenu always false to let handlers control behavior

- Add environment_linux.go with compositor detection:
  - detectCompositor(), detectFocusFollowsMouse(), isTilingWM()
  - Cursor position detection for Hyprland/Sway

- Add comprehensive manual test suite in v3/test/manual/systray/
  - window-only, menu-only, window-menu, custom-handlers, hide-options
  - Builds for both GTK3 and GTK4
  - README with test matrix for different environments

- Update systray-basic example to use new options

* feat: add doctor-ng package with modern TUI for system diagnostics

Introduces a new pkg/doctor-ng package with a clean public API designed
for reuse by both CLI and future GUI tools. Features include:

- Public API types (Report, SystemInfo, Dependency, DiagnosticResult)
- Platform-specific dependency detection (Linux, macOS, Windows)
- Package manager support (apt, dnf, pacman, emerge, eopkg, nixpkgs, zypper)
- Modern TUI using bubbletea/lipgloss with:
  - Interactive dependency navigation (j/k keys)
  - Install missing dependencies prompt (i key)
  - Refresh/rescan capability (r key)
- Non-interactive mode for CI/scripts (-n flag)

The new command is available as 'wails3 doctor-ng' for testing while
the existing 'wails3 doctor' command remains unchanged.

* fix(doctor-ng): stabilize display order, conditional cursor, add copy to clipboard

- Sort platform extras alphabetically to prevent bouncing
- Only show dependency cursor when there are missing deps to act on
- Add 'c' key to copy sanitized report to clipboard
- Update help text to be contextual based on system state

* feat(doctor-ng): add package manager detection for macOS/Windows, remove unused code

- macOS: detect homebrew, macports, nix; show in platform extras
- Windows: detect winget, scoop, choco; show in platform extras
- Remove unused tui/install.go (replaced by tea.ExecProcess)
- Remove unused stateInstall/viewInstall from model.go
- Remove j/k navigation from help (cursor was already removed)

* feat(cli): add wails3 tool capabilities command

Checks system build capabilities via pkg-config:
- GTK4 and WebKitGTK 6.0 availability
- GTK3 and WebKit2GTK 4.1 availability
- Recommends gtk4 or gtk3 based on what's installed

Output is JSON for easy parsing by Taskfile/scripts.

* fix(linux/gtk4): avoid checkptr errors when building with -race

Go's race detector enables checkptr, which flags storing integers
as pointers (a common GLib/C pattern using GINT_TO_POINTER).

Changes:
- Change signal_connect to accept uintptr_t instead of void* for data
- Change enableDND/disableDND to accept uintptr_t instead of gpointer
- Replace unsafe.Pointer(uintptr(id)) with C.uintptr_t(id) in Go code
- Replace g_object_set/get_data for menu item IDs with Go-side map
- Pass 0 instead of nil for unused signal data parameters

This allows building with 'go build -race' for debugging without
triggering 'checkptr: pointer arithmetic computed bad pointer value'
fatal errors.

* fix(examples/dialogs): use window menu for GTK4 compatibility

GTK4 requires menus to be set on windows, not the application.
Use LinuxMenuStylePrimaryMenu to show menu in header bar.

* test(linux): add manual dialog test suite

Comprehensive test programs for GTK4 dialogs:
- message-info, message-question, message-warning, message-error
- file-open, file-open-multi, file-save, file-directory

Each test has multiple test cases accessible via menu.
Use 'go-task build:gtk4' or 'go-task build:gtk3' to build.

* fix(linux/gtk4): fix file dialog hang by not prematurely freeing dialog

GtkFileDialog is async - gtk_file_dialog_select_folder() returns
immediately and the callback fires later. The defer g_object_unref
was freeing the dialog before the user could interact with it.

GTK manages the dialog lifecycle internally for async operations.

* fix: add mutex to protect runtimeLoaded and pendingJS from races

Multiple goroutines access runtimeLoaded and pendingJS concurrently:
- ExecJS reads/writes from window event handlers
- HandleMessage writes when runtime becomes ready
- InitiateFrontendDropProcessing reads/writes during drag-drop

Added pendingJSMutex to synchronize access. Also changed HandleMessage
to copy pending slice before releasing lock to avoid holding it during
InvokeSync calls.

* fix(linux/gtk4): fix dialog deadlock and alert dialog lifecycle

- dialogs_linux.go: Change InvokeAsync to go func() to prevent deadlock
  when show() is called - runQuestionDialog uses InvokeAsync internally
  and blocks on channel, which deadlocks if caller is also using InvokeAsync
- linux_cgo_gtk4.c: Remove premature g_object_unref from show_alert_dialog
  as GtkAlertDialog is async and GTK manages the lifecycle
- linux_cgo_gtk4.c: Add DEBUG_LOG macro for compile-time debug output
  (CGO_CFLAGS="-DWAILS_GTK_DEBUG" go build ...)
- linux_cgo_gtk4.c: Handle cancelled-with-no-error case in file dialogs
- linux_cgo_gtk4.go: Fix runQuestionDialog to use options.Title as message
- linux_cgo_gtk4.go: Add default OK button when no buttons specified

* feat(linux/gtk4): implement custom message dialogs with proper styling

GTK4's GtkAlertDialog lacks icon support and visual differentiation.
This implements a custom GtkWindow-based dialog with:

- Escape key triggers cancel button via GtkEventControllerKey
- Enter key activates default button via gtk_window_set_default_widget
- Custom icons from bytes with gtk_image_set_pixel_size (64px max)
- Symbolic icons for info/warning/error/question dialogs
- 300px minimum width for better short message appearance
- Proper memory cleanup via message_dialog_cleanup()
- close-request returns cancel button index or -1

* fix(linux/gtk4): use native size for custom dialog icons

Custom icons now display at their native size.
Built-in symbolic icons remain at 32px as designed.

* fix(linux/gtk4): implement native file drag-and-drop

Use GtkDropControllerMotion and GtkDropTarget with GTK_PHASE_CAPTURE
to intercept file drops before WebKit's internal GtkDropTargetAsync
handler in the bubble phase.

- Add on_drop_accept to filter for GDK_TYPE_FILE_LIST
- Add motion controller for enter/leave/motion events
- Set capture phase so our handlers run before WebKit's
- Both controllers attached to WebKitWebView widget

* docs: update implementation tracker and dialog docs

- Update IMPLEMENTATION.md with GTK4 dialog progress
- Add GTK4 dialog documentation to reference docs
- Fix RLock -> Lock in cleanup to allow window modification
- Simplify manual dialog test menus (remove nested submenus)

* fix(linux/gtk4): parse runtime call params from query string

WebKitGTK 6.0 sends POST data as URL query parameters for custom URI
schemes instead of in the request body. Add fallback to parse object,
method, and args from query params when body is empty.

* fix(linux): fallback to application menu when no window menu set

Windows without an explicit Linux.Menu option now inherit the
application-level menu set via app.Menu.Set().

* fix(linux/gtk4): implement sync clipboard API

GTK4 uses async clipboard operations. Implement clipboard_get_text_sync
which iterates the GLib main context until the async read completes.
This avoids deadlock when called from the main thread (e.g., menu handlers).

* fix(linux/gtk4): DPI scaling and menu duplication fixes

- Implement proper DPI scaling using gdk_monitor_get_scale (GTK 4.14+)
  for fractional scaling support on Linux/GTK4
- Calculate PhysicalBounds correctly by multiplying logical coords by scale
- Fix menu items duplicating when creating new windows by adding
  processed flag to prevent re-processing menus
- Add safe type assertion helpers in screen example to prevent crashes
- Add CSS to prevent text selection during drag in screen example
- Document tiling WM limitations (Hyprland, Sway, i3) in official docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(tests): add GTK3 vs GTK4 benchmark suite

Add comprehensive benchmark suite for comparing GTK3 and GTK4 performance
in Wails applications. Benchmarks cover:

- Screen enumeration and primary screen query
- Window create/destroy, resize, show/hide operations
- Menu creation (simple, complex, with accelerators)
- Event emit and receive timing
- Dialog setup

Includes comparison tool for side-by-side analysis of results.

Usage:
  go build -tags gtk3 -o benchmark-gtk3 .
  go build -tags gtk4 -o benchmark-gtk4 .
  ./benchmark-gtk3 && ./benchmark-gtk4
  go run compare.go benchmark-GTK3-*.json benchmark-GTK4-*.json

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(examples): add WebView API compatibility checker

Cross-platform example that tests and reports which Web APIs are
available in the current WebView engine. Tests 200+ APIs across
categories:

- Storage (localStorage, IndexedDB, Cache API, File System)
- Network (Fetch, WebSocket, WebTransport, SSE)
- Media (Web Audio, MediaRecorder, Speech APIs)
- Graphics (Canvas, WebGL, WebGL2, WebGPU)
- Device (Geolocation, Sensors, Bluetooth, USB, Serial)
- Workers (Web Workers, Service Workers, Shared Workers)
- Performance (Observers, Timing APIs)
- Security (Web Crypto, WebAuthn, Credentials)
- UI/DOM (Custom Elements, Shadow DOM, Clipboard)
- CSS (CSSOM, Container Queries, Modern Selectors)
- JavaScript (ES Modules, BigInt, Private Fields)

Useful for understanding API availability differences between:
- WebKitGTK (Linux) vs WebView2 (Windows) vs WKWebView (macOS)
- GTK3/WebKit2GTK 4.1 vs GTK4/WebKitGTK 6.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(examples): add Web API examples demonstrating browser capabilities

Add 15 interactive Web API examples in v3/examples/web-apis/:
- Storage: localStorage, IndexedDB
- Network: Fetch API, WebSocket
- Media: Canvas 2D, WebGL, Web Audio
- Device: Geolocation, Clipboard, Fullscreen
- Security: WebCrypto
- Notifications API
- Workers: Web Workers
- Observers: Intersection Observer, Resize Observer

Each example includes an interactive demo with API documentation
and feature detection to help developers understand what's
available in WebView environments.

Also updates webview-api-check with autorun support for
automated API compatibility testing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(examples): add 26 more Web API examples

Expand web-apis examples from 15 to 41 total, covering:

Storage: sessionStorage, Cache API, Page Visibility
Network: XMLHttpRequest, EventSource (SSE), Beacon API
Media: MediaDevices, MediaRecorder, Speech Synthesis
Device: Device Orientation, Vibration, Gamepad
Performance: Performance API, Mutation Observer
UI/DOM: Web Components, Pointer Events, Selection, Dialog
Messaging: Drag and Drop, Broadcast Channel, History API
Data: Streams, File API, Blob, Share, Permissions

Each example includes interactive demos, API detection,
and follows the consistent dark-themed styling pattern.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update changelog with full web-api examples count

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(examples): simplify beacon demo with local server

Replace the complex beacon demo with a simpler version that includes:
- Local HTTP server on port 9999 that receives beacon data
- Go service to retrieve and display received beacons
- Quick buttons for common beacon types (pageview, click, error, timing)
- Live display of received beacon data with auto-refresh
- Clear explanation of how the demo works

This makes the demo more educational by showing both the sending
and receiving sides of the Beacon API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(examples): streamline beacon demo UI

Revert to original standalone implementation with httpbin.org endpoint
but with a compact two-column layout that fits without scrolling:
- Left: endpoint config, data type selector, data input, example buttons
- Right: stats (sent/queued/failed/bytes), auto-unload option, event log

Features retained: String/JSON/FormData/Blob data types, analytics/error/
timing examples, auto-beacon on page unload.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(examples): streamline blob demo with tabbed layout

Redesign blob demo to fit without scrolling using:
- Three-column layout: Create | Stored Blobs | Output
- Tabbed interface for blob creation (Text/JSON/Binary/SVG)
- Compact blob list with download and delete actions
- Operations panel for conversions and slicing
- Feature badges showing API support status

Reduced from 846 lines to 349 lines while keeping core functionality.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(examples): fix dropdown styling in blob demo

Style select option elements with dark background to match theme.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(examples): add MDN links to demo titles

Link API names in titles to their MDN documentation pages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(examples): streamline broadcast-channel with Wails windows

Redesign broadcast-channel demo for Wails environment:
- Replace browser tabs with Wails windows via WindowService
- Compact two-column layout: Channel/Send | Messages
- "Open New Window" button creates new Wails window
- Each window gets unique ID for message tracking
- Join/leave notifications when windows open/close
- Quick message buttons, ping all, stats display
- MDN link in title

Reduced from 737 lines to 245 lines.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(examples): simplify broadcast-channel to use multiple app instances

Remove WindowService that required generated bindings. Instead, instruct
users to run multiple instances of the app to test cross-window messaging.
BroadcastChannel API works across windows of the same origin.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(examples): add API feature badges to broadcast-channel demo

Show supported features: BroadcastChannel, postMessage, close,
onmessage, onmessageerror, MessageChannel - consistent with other demos.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(examples): add multi-window support to broadcast-channel demo

Use Wails runtime.js and WindowService to open new windows for
cross-window BroadcastChannel API testing. Streamlined UI with
feature detection badges and MDN link.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(linux): make GTK4 opt-in via -tags gtk4, keep GTK3 as default

This change inverts the build tag logic so that:
- GTK3/WebKit2GTK 4.1 is the stable default (no tag required)
- GTK4/WebKitGTK 6.0 is experimental opt-in via `-tags gtk4`

This allows the branch to be merged into v3-alpha without breaking
existing apps, while enabling early adopters to test GTK4 support.

Changes:
- Updated 20 Go files: `gtk3` → `!gtk4`, `!gtk3` → `gtk4`
- Updated IMPLEMENTATION.md to reflect new build strategy
- Updated benchmark README with correct build commands
- Added GTK4_FEEDBACK_ISSUE.md template for community testing
- Added Armaan's signing guide link to docs

Build commands after this change:
  go build ./v3/...            # GTK3 (default)
  go build -tags gtk4 ./v3/... # GTK4 (experimental)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(linux): rename capabilities files to follow naming convention

Renamed for consistency with other GTK3/GTK4 file pairs:
- capabilities_linux.go (default, GTK3)
- capabilities_linux_gtk4.go (opt-in, GTK4)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(linux/gtk4): add experimental notice with feedback issue link

When building with -tags gtk4, the app now displays a notice at startup
directing users to the feedback issue for reporting problems.

Issue: https://github.com/wailsapp/wails/issues/4957

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci(linux): add GTK4 testing for webkit-gtk6-support branch

- Fix box alignment in experimental notice
- Add GTK4 dependency installation for this branch only
- Run Go tests with both default (GTK3) and -tags gtk4
- Build examples with both GTK versions
- Build templates with both GTK versions

The GTK4 tests only run when PR source branch is feature/webkit-gtk6-support.
This ensures existing PRs are not affected while enabling full GTK4 CI coverage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(doctor): make GTK3 primary, GTK4 experimental in package checks

Updated all 7 package managers to match new build tag strategy:
- GTK3/WebKit2GTK 4.1 → primary (required for default builds)
- GTK4/WebKitGTK 6.0 → optional/experimental (for -tags gtk4)

Affected: apt, dnf, pacman, zypper, emerge, eopkg, nixpkgs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs(dialogs): fix GTK3/GTK4 documentation to reflect default behavior

GTK3 is the default, GTK4 is opt-in via -tags gtk4. Updated the dialogs
documentation to clarify this instead of suggesting GTK3 is opt-in.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(examples): escape HTML in web-apis examples to prevent DOM XSS

Add escapeHtml() helper function and escape all user-controlled or
dynamic values before inserting them into innerHTML to address CodeQL
security alerts.

Files fixed:
- beacon: escape log type, message, and class names
- eventsource: escape time and type in log entries
- file-api: escape file name, size, and type
- mediadevices: escape time, type, and message in log entries
- selection: escape text content before applying highlight regex
- share: escape file name, size, and type in file list
- speech-synthesis: escape time, type, and message in log entries
- web-components: escape title and color in shadow DOM template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(linux): correct GTK4 build tags and Taskfile for GTK3 default

- Fix build tags in linux_cgo_gtk4.c and linux_cgo_gtk4.h from
  `!gtk3` to `gtk4` to match the Go file constraints
- Update Taskfile.yaml to reflect GTK3 as default, GTK4 as opt-in
- Rename test:example:linux:gtk3 to test:example:linux:gtk4
- Comment out GTK4 tests in test:examples since CI doesn't have GTK4 deps

This fixes the CI failure where GTK4 C files were being compiled
by default due to incorrect build constraints.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(systemtray): add missing defaultClickHandler method

Add the defaultClickHandler method that was in v3-alpha but not
properly merged. This method is called from systemtray_darwin.go
when handling tray icon clicks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(tests): add linux build constraint to gtk4-benchmark

The gtk4-benchmark test is Linux-only but was missing a build
constraint on main.go, causing build failures on macOS/Windows.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci(linux): skip hanging GTK4 service tests in CI

The service startup/shutdown tests hang in GTK4 CI environment due to
display initialization issues with xvfb. Skip these specific tests for
now while keeping other GTK4 tests running.

Skipped tests:
- TestServiceStartup
- TestServiceShutdown
- TestServiceStartupShutdown

The *Error variants of these tests still run as they fail fast before
the hang occurs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci(linux): skip all service tests for GTK4 in CI

All service tests hang in GTK4 CI because they require a fully
functional GTK4 display that xvfb cannot provide. Skip all tests
matching "TestService" pattern.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci(linux): remove unsupported GTK4 template build test

The wails build command doesn't support the -tags flag yet.
GTK4 compilation is already verified by Go tests, so this
additional template build step is not necessary.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix Copilot review feedback on PR #4958

- Use JSON.stringify() for onclick handlers in storage examples to safely
  handle keys with quotes (sessionstorage, localstorage)
- Guard DeviceOrientationEvent check to prevent ReferenceError on
  unsupported browsers (device-orientation)
- Add type assertion check for Bounds to prevent panic on malformed
  JSON (screens.go)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 21:55:45 +11: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...)
+}