Skip to content

Pet mode: window resize jitter and mouse passthrough non-functional on Wayland/Hyprland #31

Description

@yuuhikaze

Environment

  • Operating system: NixOS 26.05 (upgraded from 25.11)
  • Display server: Wayland (via Hyprland, wlroots-based compositor)
  • Hardware: Intel i5-1135G7 (Iris Xe graphics), single 1366×768 monitor
  • Electron versions tested:
    • 31.0.2 (downloaded from npm via package.json)
    • 38.8.4 (from nixpkgs, pre-built binary)
    • 41.7.1–41.7.2 (from nixpkgs, both pre-built and source-built via electron-source)
  • App mode: Pet mode (frameless, transparent, always-on-top overlay spanning the full monitor)
  • Reproducible NixOS setup: https://codeberg.org/yuuhikaze/nixos/commit/d241c58a23a00ee136e5bf91611f378a74099676

Intuitive explanation

Pet mode turns the application into a transparent overlay that sits on top of all other windows. It is meant to be a "desktop pet" — the user sees the Live2D character floating over their workspace, and clicks should pass through the transparent areas to reach whatever window is behind.

Two things break in this mode:

1. Dragging the overlay makes its size jump erratically.
When the user grabs the overlay window (via the compositor's SUPER+click window-drag shortcut) and moves it, the window does not stay at a fixed size. Instead it visibly stretches, shrinks, or jitters. This makes positioning the avatar difficult (the user has to OS-window-drag since mouse passthrough does not work either).

2. Mouse clicks do not pass through the transparent areas.
Clicks that land on transparent parts of the overlay should reach the window underneath (e.g. clicking through to a terminal behind the pet). Instead, the overlay absorbs the clicks. The Electron API that enables this behaviour (setIgnoreMouseEvents) was never implemented on Wayland — it is a no-op that does absolutely nothing.


Technical deep-dive

Symptom A: Erratic window resize when dragging

What the application does:
When entering pet mode, the window manager (window-manager.ts) calls:

this.window.setResizable(false);

On Wayland, this maps to the xdg_toplevel protocol. Under the hood, Electron's Chromium backend calls xdg_toplevel.set_min_size and xdg_toplevel.set_max_size with the current window dimensions, signalling to the compositor that the window should not be resized. The application also explicitly calls setMinimumSize() and setMaximumSize() with the full monitor dimensions as additional guards.

Why it fails:
Hyprland (wlroots-based) does not honour these protocol-level size constraints for floating, pinned windows. When the user initiates a compositor window drag (SUPER+click), Hyprland sends xdg_toplevel.configure events that include new dimensions — effectively resizing the window despite the client's size constraints. The window jitters because each configure event triggers a canvas resize in the renderer, which in turn calls delegate.onResize() on the Live2D view, shifting the model's visual position.

What was tested:

  • setResizable(false) alone — no effect.
  • setMinimumSize(W, H) + setMaximumSize(W, H) — no effect on Hyprland.
  • A resize-event handler that immediately restores the original bounds via setBounds() — this did not fix the issue either (it did not make it worse, it just never solved the underlying problem).
  • Electron 38.8.4 (older Chromium) — same behaviour. The issue is not Electron-version-specific.
  • The only effective measure was adding a Hyprland windowrule with min_size and max_size set to the monitor dimensions, which forces the compositor to clamp the window at the server level. This is a compositor-specific workaround, not a portable fix.

Symptom B: setIgnoreMouseEvents is a no-op on Wayland

What the application does:
When entering pet mode:

this.window.setIgnoreMouseEvents(true, { forward: true });

This should tell the compositor to stop delivering pointer events to the window's surface. On X11 this works via the ShapeInput X extension. On macOS it works via WS_EX_TRANSPARENT. On Wayland — it does nothing.

Why it fails:

Electron's missing implementation:
The Wayland Ozone backend in Chromium never implemented SetIgnoreMouseEvents. The function in native_window_views.cc is gated behind #if defined(USE_X11) and only contains the X11 ShapeInput path. On Wayland builds, USE_X11 may still be defined, but even when it is not, there is no Wayland code path. This was reported in electron#40935 and remained unfixed for years.

A draft PR exists:
electron#51769 (submitted May 2026 by contributor wehos) implements the Wayland path by calling ElectronDesktopWindowTreeHostLinux::SetIgnoreMouseEvents(), which calls platform_window()->SetInputRegion({}) — setting the wl_surface input region to an empty rect. This tells the Wayland compositor that no part of the surface should receive pointer events. A follow-up issue (electron#51808) documents a residual reliability problem on the non-GL rendering path, where the input region change is queued in pending state until the next frame is committed. The PR mitigates this with compositor()->ScheduleFullRedraw().

The PR is still a draft (not merged) as of June 2026.

Blocker 1: XWayland interference
The PR's code is structured as:

#if defined(USE_X11)
  if (auto* connection = x11::Connection::Get()) {
    // X11 ShapeInput path (existing code)
  } else {
    // Wayland input-region path (PR's addition)
  }
#endif

The Wayland path is inside an else branch of the X11 connection check. On any modern Wayland session, XWayland is running as a compatibility layer for legacy X11 applications. Because XWayland is present, x11::Connection::Get() returns a valid connection object. The if branch executes (using X11 ShapeInput through the XWayland proxy), and the else branch — the actual Wayland fix — is never reached.

This is a subtle but critical issue: even on a pure Wayland session, the mere presence of the XWayland compositor process causes the fix to be skipped. The fix would only work on a Wayland session where XWayland has been fully disabled (no XWayland binary running), which is uncommon.

The PR would need to either:

  • Move the Wayland call outside the X11 if/else so it executes unconditionally, or
  • Check specifically whether the window is using the Wayland Ozone backend rather than checking for an X11 connection.

Blocker 2: GPU backend selection on the Ozone/Wayland path
The Wayland Ozone backend in Chromium 130+ (Electron 40+) defaults to Vulkan via SkiaRenderer. When Electron runs with --ozone-platform-hint=wayland (set by the ELECTRON_OZONE_PLATFORM_HINT environment variable), the GPU process attempts to initialise a Vulkan context. On wlroots-based compositors, Chrome prints:

--ozone-platform=wayland is not compatible with Vulkan.

and falls back. The fallback path tries ANGLE (Chromium's OpenGL ES translator) with various backends. The Electron 41.7.2 source build allows:

(gl=egl-angle,angle=opengl)
(gl=egl-angle,angle=opengles)
(gl=egl-angle,angle=vulkan)
(gl=egl-angle,angle=swiftshader)

The issue is that --use-gl and --use-angle command-line flags injected via a wrapper script or makeWrapper do not propagate from the browser process to the GPU child process on the Wayland Ozone path. Chromium's GpuProcessHost forwards specific switches; on Ozone/Wayland, SwiftShader/Angle flags appear to be stripped or ignored.

What worked (partially):
Using app.commandLine.appendSwitch('use-gl', 'angle') and app.commandLine.appendSwitch('use-angle', 'swiftshader') in the application's main process (src/main/index.ts) does propagate the flags correctly, because they are set before Chromium's command-line parser runs. With these switches in place, the GPU process successfully initialises SwiftShader (software GLES).

Why this still fails (two reasons):

  1. Performance: SwiftShader software rendering on the Intel i5-1135G7 (Iris Xe) is noticeably laggy for Live2D rendering. The WebGL context initialises and the canvas draws, but frame rates are too low for smooth character animation.
  2. Mouse passthrough still does not work: Even with a working GL context and the PR fix patch applied, the mouse passthrough fix is blocked by the XWayland interference issue described above — the Wayland input-region code path is never reached.

Alternatives that were tested:

  • --disable-gpu — completely disables the GPU process. The application starts and runs, but WebGL is unavailable. Live2D rendering requires WebGL, so this is not acceptable.
  • --use-gl=egl-angle --use-angle=opengles — these specific flag formats were attempted. The GPU process reported gl=none,angle=none in all cases where flags came from a wrapper script rather than from app.commandLine.
  • --in-process-gpu — runs the GPU in the browser process. Still exhibited the same flag-propagation issue on the Ozone backend.

Summary of attempted solutions

Approach Result Reason
setResizable(false) + setMinimumSize/setMaximumSize ❌ Does not prevent resize Hyprland ignores xdg-toplevel size constraints for floating windows
Resize-event handler restoring bounds via setBounds() ❌ Does not fix resize Never addressed the underlying cause
Hyprland windowrule with min_size/max_size ✅ Prevents resize Compositor-level clamp, not a portable fix
Electron 41.7.2 source-built with PR #51769 patch ❌ Wayland path not reached XWayland presence causes else branch to be skipped
--use-gl=angle --use-angle=swiftshader (wrapper flags) ❌ Flags not propagated GPU process on Ozone does not inherit wrapper-injected flags
app.commandLine.appendSwitch() in main process ⚠️ SwiftShader works but too slow + passthrough still broken Software rendering lags on Iris Xe; XWayland blocker remains
--disable-gpu ❌ WebGL unavailable Live2D requires WebGL
--use-gl=angle --use-angle=opengles or --use-gl=egl-angle (any source) gl=none,angle=none Ozone backend ignores these flags for the GPU process

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions