Skip to content

Commit 9c2ae77

Browse files
linesightclaude
andcommitted
feat: add native Wayland support (explicit opt-in, X11/XWayland default)
On Wayland sessions, X11/XWayland is used by default so CEF's browser widget is parented inside a GTK frame window that the window manager decorates normally (title bar, resize handles, close button). Native Wayland mode (ozone-platform=wayland) is available as an explicit opt-in via switches={"ozone-platform": "wayland"} in cef.Initialize(). It is not auto-selected from WAYLAND_DISPLAY because CEF's NativeWidgetDelegate hardcodes params.remove_standard_frame=true / params.type=TYPE_CONTROL on the standalone Wayland path, which prevents the Wayland compositor from adding Server Side Decorations. Key changes: - _linux_apply_initialize_defaults(): detects wayland vs x11 mode; defaults to X11/XWayland; native Wayland only when ozone-platform=wayland is explicit - Initialize(): skips GTK/X11 init and X11ErrorHandlers in Wayland mode - CreateBrowserSync(): in Wayland mode passes parent=0 so CEF creates its own xdg_toplevel; in X11 mode creates a GTK frame window as before - HideX11ShellWindow(): unmaps the empty CefWindowX11 shell that CEF creates alongside the NativeWidgetDelegate content window when SUPPORTS_OZONE_X11 is compiled in, leaving only the content-bearing Wayland window visible - _linux_register_wayland_close_handler(): installs a DoClose callback that calls QuitMessageLoop() since CloseHostWindow() is a no-op on Wayland - _linux_wayland_message_loop(): bare GLib loop driving CefDoMessageLoopWork() every 10 ms without any GTK window; QuitMessageLoop() calls g_main_loop_quit() - WindowInfo.SetAsChild: suppresses spurious handle=0 warning in Wayland mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 133cbb3 commit 9c2ae77

6 files changed

Lines changed: 194 additions & 39 deletions

File tree

src/cefpython.pyx

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -578,12 +578,15 @@ def Initialize(applicationSettings=None, commandLineSwitches=None, **kwargs):
578578
g_commandLineSwitches["use-angle"] = "gl"
579579

580580
IF UNAME_SYSNAME == "Linux":
581-
# Initialize GTK so GDK has a display connection before CefInitialize.
582-
_linux_gtk_init()
583-
# Auto-apply switches/settings required for CEF 146 on Linux/Xwayland.
584-
# Uses setdefault so user-supplied values are never overwritten.
581+
# Detect Wayland/X11 mode and apply switches BEFORE gtk_init so we
582+
# know whether to open a GTK/X11 display connection at all.
585583
_linux_apply_initialize_defaults(application_settings,
586584
g_commandLineSwitches)
585+
# In X11/Xwayland mode, open a GDK display connection before
586+
# CefInitialize so the Ozone X11 backend can use it.
587+
# In native Wayland mode, no GTK/X11 connection is needed or wanted.
588+
if not _g_linux_wayland_mode:
589+
_linux_gtk_init()
587590
# Pre-seed Chrome profile files to prevent the profile-picker keepalive
588591
# from blocking OnContextInitialized (Chrome 146).
589592
if application_settings.get("cache_path"):
@@ -694,8 +697,7 @@ def Initialize(applicationSettings=None, commandLineSwitches=None, **kwargs):
694697
Debug("CefInitialize() WARNING: OnContextInitialized not received"
695698
" within 30 seconds")
696699

697-
if sys.platform.startswith("linux"):
698-
# Install by default.
700+
if sys.platform.startswith("linux") and not _g_linux_wayland_mode:
699701
WindowUtils.InstallX11ErrorHandlers()
700702

701703

@@ -799,17 +801,24 @@ def CreateBrowserSync(windowInfo=None,
799801
elif not isinstance(windowInfo, WindowInfo):
800802
raise Exception("CreateBrowserSync() failed: windowInfo: invalid object")
801803

802-
# On Linux, when no parent window is given, auto-create a GTK toplevel
803-
# so callers need no GTK-specific code (same API as Windows/Mac).
804+
# On Linux, when no parent window is given, provide a top-level window
805+
# so callers need no toolkit-specific code (same API as Windows/Mac).
804806
_linux_toplevel_state = None
805807
IF UNAME_SYSNAME == "Linux":
806808
if windowInfo.windowType == "child" and windowInfo.parentWindowHandle == 0:
807-
_linux_toplevel_state = _linux_create_toplevel(
808-
window_title or "CEF Browser")
809-
windowInfo.SetAsChild(
810-
_linux_toplevel_state['xid'],
811-
[0, 0, _linux_toplevel_state['width'],
812-
_linux_toplevel_state['height']])
809+
if _g_linux_wayland_mode:
810+
# Native Wayland: CEF creates its own xdg_toplevel surface.
811+
# Pass parent=0 with default bounds; no GTK window is needed.
812+
_linux_toplevel_state = {'wayland': True}
813+
windowInfo.SetAsChild(0, [0, 0, _LINUX_DEFAULT_WIDTH,
814+
_LINUX_DEFAULT_HEIGHT])
815+
else:
816+
_linux_toplevel_state = _linux_create_toplevel(
817+
window_title or "CEF Browser")
818+
windowInfo.SetAsChild(
819+
_linux_toplevel_state['xid'],
820+
[0, 0, _linux_toplevel_state['width'],
821+
_linux_toplevel_state['height']])
813822

814823
if window_title and windowInfo.parentWindowHandle == 0:
815824
windowInfo.windowName = window_title
@@ -903,8 +912,12 @@ def CreateBrowserSync(windowInfo=None,
903912
and windowInfo.windowName:
904913
# Set window title in hello_world.py example
905914
IF UNAME_SYSNAME == "Linux":
906-
x11.SetX11WindowTitle(cefBrowser,
907-
PyStringToChar(windowInfo.windowName))
915+
# X11 title setting uses XStoreName which requires an X11 display.
916+
# Skip on native Wayland; CEF's Ozone backend propagates the page
917+
# title to the compositor via xdg_toplevel_set_title automatically.
918+
if not _g_linux_wayland_mode:
919+
x11.SetX11WindowTitle(cefBrowser,
920+
PyStringToChar(windowInfo.windowName))
908921
ELIF UNAME_SYSNAME == "Darwin":
909922
MacSetWindowTitle(cefBrowser,
910923
PyStringToChar(windowInfo.windowName))
@@ -913,7 +926,20 @@ def CreateBrowserSync(windowInfo=None,
913926
if windowInfo._linux_embed_info:
914927
_linux_schedule_xembed(pyBrowser, windowInfo._linux_embed_info)
915928
if _linux_toplevel_state is not None:
916-
_linux_register_window_callbacks(pyBrowser, _linux_toplevel_state)
929+
if _linux_toplevel_state.get('wayland'):
930+
# Native Wayland top-level: register a DoClose callback that
931+
# quits the GLib loop. On Wayland, CloseHostWindow() is a
932+
# compile-time no-op, so we must exit the loop ourselves when
933+
# DoClose fires (either from CloseBrowser() or xdg_toplevel.close).
934+
_linux_register_wayland_close_handler(pyBrowser)
935+
# When SUPPORTS_OZONE_X11 is compiled, CreateHostWindow() creates
936+
# both an X11/XWayland shell window (empty) and a Wayland
937+
# xdg_toplevel with the actual browser content. Hide the empty
938+
# X11 shell so only the content-bearing Wayland window is visible.
939+
x11.HideX11ShellWindow(cefBrowser)
940+
else:
941+
# X11/XWayland top-level: attach GTK resize and close callbacks.
942+
_linux_register_window_callbacks(pyBrowser, _linux_toplevel_state)
917943

918944
return pyBrowser
919945

@@ -956,13 +982,21 @@ def QuitMessageLoop():
956982
Debug("QuitMessageLoop()")
957983
IF UNAME_SYSNAME == "Linux":
958984
import ctypes as _ct
959-
_gtk = _ct.CDLL("libgtk-3.so.0")
960-
# Only call gtk_main_quit() when a GTK main loop is actually running.
961-
# _on_delete() may have already called it (via gtk_main_quit directly),
962-
# and calling it a second time during the drain would generate a
963-
# spurious "assertion 'main_loops != NULL' failed" warning.
964-
if _gtk.gtk_main_level() > 0:
965-
_gtk.gtk_main_quit()
985+
if _g_linux_wayland_mode:
986+
# Native Wayland: quit the GLib main loop stored by
987+
# _linux_wayland_message_loop(). g_main_loop_quit() is
988+
# thread-safe and safe to call even from a CEF UI-thread task.
989+
if _g_wayland_main_loop is not None:
990+
_glib = _ct.CDLL("libglib-2.0.so.0")
991+
_glib.g_main_loop_quit(_ct.c_void_p(_g_wayland_main_loop))
992+
else:
993+
_gtk = _ct.CDLL("libgtk-3.so.0")
994+
# Only call gtk_main_quit() when a GTK main loop is actually running.
995+
# _on_delete() may have already called it (via gtk_main_quit directly),
996+
# and calling it a second time during the drain would generate a
997+
# spurious "assertion 'main_loops != NULL' failed" warning.
998+
if _gtk.gtk_main_level() > 0:
999+
_gtk.gtk_main_quit()
9661000
with nogil:
9671001
CefQuitMessageLoop()
9681002

src/client_handler/x11.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ void SetX11WindowTitle(CefRefPtr<CefBrowser> browser, char* title) {
5454
XStoreName(xdisplay, xwindow, title);
5555
}
5656

57+
void HideX11ShellWindow(CefRefPtr<CefBrowser> browser) {
58+
// On native Wayland (ozone-platform=wayland) with SUPPORTS_OZONE_X11
59+
// compiled in, CEF's CreateHostWindow() creates two separate windows:
60+
// 1. An X11/XWayland top-level shell (CefWindowX11) — empty, visible
61+
// 2. A Wayland xdg_toplevel (NativeWidgetDelegate) — holds the content
62+
// Unmap the empty X11 shell so only the content-bearing Wayland window
63+
// is visible to the user.
64+
::Window xwindow = browser->GetHost()->GetWindowHandle();
65+
::Display* xdisplay = cef_get_xdisplay();
66+
if (!xdisplay || !xwindow) return;
67+
XUnmapWindow(xdisplay, xwindow);
68+
XFlush(xdisplay);
69+
}
70+
5771
GtkWindow* CefBrowser_GetGtkWindow(CefRefPtr<CefBrowser> browser) {
5872
// TODO: Should return NULL when using the Views framework
5973
// -- REWRITTEN FOR CEF PYTHON USE CASE --

src/client_handler/x11.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ void InstallX11ErrorHandlers();
1919
void SetX11WindowBounds(CefRefPtr<CefBrowser> browser,
2020
int x, int y, int width, int height);
2121
void SetX11WindowTitle(CefRefPtr<CefBrowser> browser, char* title);
22+
void HideX11ShellWindow(CefRefPtr<CefBrowser> browser);
2223

2324
GtkWindow* CefBrowser_GetGtkWindow(CefRefPtr<CefBrowser> browser);
2425
XImage* CefBrowser_GetImage(CefRefPtr<CefBrowser> browser);

src/extern/x11.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ cdef extern from "client_handler/x11.h" nogil:
1212
void SetX11WindowBounds(CefRefPtr[CefBrowser] browser,
1313
int x, int y, int width, int height)
1414
void SetX11WindowTitle(CefRefPtr[CefBrowser] browser, char* title)
15+
void HideX11ShellWindow(CefRefPtr[CefBrowser] browser)
1516
XImage* CefBrowser_GetImage(CefRefPtr[CefBrowser] browser)

src/window_info.pyx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ cdef class WindowInfo:
116116
if parentWindowHandle == 0:
117117
import os as _os
118118
import warnings
119-
if "WAYLAND_DISPLAY" in _os.environ:
119+
# In native Wayland mode, parentWindowHandle=0 is correct and
120+
# expected — CEF creates its own xdg_toplevel surface. Only
121+
# warn when the user is likely using an X11-incompatible toolkit
122+
# without having opted into native Wayland.
123+
if "WAYLAND_DISPLAY" in _os.environ and not _g_linux_wayland_mode:
120124
warnings.warn(
121125
"WindowInfo.SetAsChild: parentWindowHandle is 0 on Linux "
122126
"in a Wayland session. The GUI toolkit is likely using the "

src/window_utils_linux.pyx

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44

55
include "cefpython.pyx"
66

7+
# Set to True when native Wayland mode is active (ozone-platform=wayland).
8+
# Initialised by _linux_apply_initialize_defaults() inside Initialize().
9+
_g_linux_wayland_mode = False
10+
11+
# GMainLoop* pointer (raw integer) used by _linux_wayland_message_loop().
12+
# Stored here so QuitMessageLoop() can call g_main_loop_quit() on it.
13+
_g_wayland_main_loop = None
14+
15+
# Default size for auto-created top-level windows (X11 and Wayland paths).
16+
_LINUX_DEFAULT_WIDTH = 800
17+
_LINUX_DEFAULT_HEIGHT = 600
18+
19+
720
class WindowUtils:
821
# You have to overwrite this class and provide implementations
922
# for these methods.
@@ -86,20 +99,40 @@ def _linux_get_root_xid():
8699

87100

88101
def _linux_apply_initialize_defaults(app_settings, cmd_switches):
89-
"""Auto-apply Linux/Xwayland CEF 146 defaults that every embedding app needs.
102+
"""Auto-apply Linux CEF 146 defaults that every app needs.
103+
104+
X11/XWayland is the default on all Linux systems (even Wayland sessions).
105+
Native Wayland mode must be requested explicitly by passing
106+
switches={"ozone-platform": "wayland"} to cef.Initialize().
107+
108+
Reason: CEF's NativeWidgetDelegate hardcodes params.remove_standard_frame=true
109+
and params.type=TYPE_CONTROL for the standalone Wayland path, so the
110+
compositor never adds Server Side Decorations. In X11/XWayland mode CEF
111+
parents the browser widget inside the GTK window we create, and the window
112+
manager decorates the GTK frame window normally.
90113
91114
Uses setdefault so users can still override any individual entry by passing
92115
it explicitly to cef.Initialize(switches={...}).
93116
"""
117+
global _g_linux_wayland_mode
94118
import os as _os
95119

96-
# Must be set before GTK is first initialized (gtk_init). Belt-and-
97-
# suspenders: also set it here in case the user forgot to set it before
98-
# the cefpython3 import.
99-
_os.environ.setdefault("GDK_BACKEND", "x11")
120+
# Native Wayland mode only when the caller explicitly opts in.
121+
_ozone_explicit = cmd_switches.get("ozone-platform", "")
122+
_wayland_mode = (_ozone_explicit == "wayland")
123+
_g_linux_wayland_mode = _wayland_mode
100124

101-
# Force X11 mode in Chrome's Ozone platform selection.
102-
_os.environ.pop("WAYLAND_DISPLAY", None)
125+
if not _wayland_mode:
126+
# X11/Xwayland mode: force GDK and Chrome onto X11.
127+
# Must be set before gtk_init() so GDK opens an X11/Xwayland display.
128+
_os.environ.setdefault("GDK_BACKEND", "x11")
129+
# Remove WAYLAND_DISPLAY so Chrome's Ozone platform selection picks X11.
130+
_os.environ.pop("WAYLAND_DISPLAY", None)
131+
cmd_switches.setdefault("ozone-platform", "x11")
132+
else:
133+
# Native Wayland mode: use the Ozone Wayland backend.
134+
# Keep WAYLAND_DISPLAY so Chrome connects to the Wayland compositor.
135+
cmd_switches.setdefault("ozone-platform", "wayland")
103136

104137
# Point the Vulkan loader at the SwiftShader ICD shipped with CEF.
105138
import cefpython3 as _cef3_pkg
@@ -121,7 +154,8 @@ def _linux_apply_initialize_defaults(app_settings, cmd_switches):
121154

122155
# Chromium switches required for stable embedded operation on CEF 146.
123156
sw = cmd_switches
124-
# Force X11 backend (not Wayland) — cefpython uses raw X11 window handles.
157+
# Ozone platform: already set above (wayland or x11); setdefault is a no-op
158+
# if the user already passed ozone-platform explicitly.
125159
sw.setdefault("ozone-platform", "x11")
126160
# Bypass Zygote to avoid stack-smash crash from --change-stack-guard-on-fork.
127161
sw.setdefault("disable-zygote", "")
@@ -221,7 +255,7 @@ def _linux_setup_profile(cache_path):
221255
}, _f)
222256

223257

224-
def _linux_create_toplevel(title, width=800, height=600):
258+
def _linux_create_toplevel(title, width=_LINUX_DEFAULT_WIDTH, height=_LINUX_DEFAULT_HEIGHT):
225259
"""Create a standalone GTK toplevel window for embedded browser use.
226260
227261
Called from CreateBrowserSync when no parent window handle is given on
@@ -345,14 +379,81 @@ def _linux_register_window_callbacks(browser, ws):
345379
_del_cb, None, None, 0)
346380

347381

382+
def _linux_register_wayland_close_handler(browser):
383+
"""Register a DoClose callback for a native Wayland auto-created top-level window.
384+
385+
On Wayland with the Alloy runtime, CloseHostWindow() is a compile-time no-op
386+
(guarded by #if BUILDFLAG(SUPPORTS_OZONE_X11)), so WindowDestroyed() is never
387+
called and OnBeforeClose never fires through the normal CEF destroy chain.
388+
389+
This callback bridges the gap: when the user closes the native Wayland window
390+
(xdg_toplevel.close) or when CloseBrowser(True) is called, DoClose fires.
391+
We quit the GLib main loop so MessageLoop() returns and the caller can proceed
392+
to cef.Shutdown(). Returning False tells CEF to proceed with the close;
393+
CloseHostWindow() then no-ops, but the drain loop in
394+
_linux_wayland_message_loop() handles final CEF cleanup.
395+
"""
396+
_existing = browser.GetClientCallback("DoClose")
397+
398+
def _wayland_do_close(browser, **_kw):
399+
suppress = False
400+
if _existing:
401+
try:
402+
suppress = bool(_existing(browser=browser))
403+
except Exception:
404+
pass
405+
if not suppress:
406+
QuitMessageLoop()
407+
return suppress
408+
409+
browser.SetClientCallback("DoClose", _wayland_do_close)
410+
411+
412+
def _linux_wayland_message_loop():
413+
"""Run a GLib main loop driving CefDoMessageLoopWork() for native Wayland.
414+
415+
No GTK window is involved; CEF's Ozone Wayland backend creates and owns its
416+
own wl_surface. The GLib main loop is used only as a portable timer source
417+
to pump the CEF message queue every 10 ms. QuitMessageLoop() calls
418+
g_main_loop_quit() on the stored loop pointer to stop it.
419+
"""
420+
global _g_wayland_main_loop
421+
import ctypes as _ct, time as _t
422+
423+
_glib = _ct.CDLL("libglib-2.0.so.0")
424+
_glib.g_main_loop_new.restype = _ct.c_void_p
425+
426+
_loop = _glib.g_main_loop_new(None, False)
427+
_g_wayland_main_loop = _loop
428+
429+
_WorkCb = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p)
430+
def _cef_work(_ud):
431+
MessageLoopWork()
432+
return True
433+
_cb = _WorkCb(_cef_work)
434+
g_linux_reparent_callbacks.append(_cb)
435+
_glib.g_timeout_add(10, _cb, None)
436+
437+
_glib.g_main_loop_run(_ct.c_void_p(_loop))
438+
439+
for _ in range(50):
440+
MessageLoopWork()
441+
_t.sleep(0.01)
442+
443+
_glib.g_main_loop_unref(_ct.c_void_p(_loop))
444+
_g_wayland_main_loop = None
445+
446+
348447
def _linux_message_loop():
349-
"""Run gtk_main() with a GLib timer driving CefDoMessageLoopWork().
448+
"""Run the appropriate message loop for the active platform backend.
350449
351-
Used by cef.MessageLoop() on Linux. CEF's Ozone X11 backend requires a
352-
running GLib main loop; gtk_main() provides that while the timer pumps
353-
CEF's internal work queue every 10 ms. After gtk_main() returns, pump
354-
CEF briefly so browsers can close cleanly before cef.Shutdown().
450+
Dispatches to the Wayland GLib loop or the GTK/X11 loop depending on
451+
whether native Wayland mode was detected during Initialize().
355452
"""
453+
if _g_linux_wayland_mode:
454+
_linux_wayland_message_loop()
455+
return
456+
356457
import ctypes as _ct, time as _t
357458

358459
_gtk = _ct.CDLL("libgtk-3.so.0")

0 commit comments

Comments
 (0)