Windowing — Linux X11
Overview
WIP — XRandR DPI handling and the override-redirect tooltip path are still iterating. Most code paths are stable. The X11 backend is LinuxWindow::X11(X11Window) selected by LinuxWindow::select_backend. Selection priority:
AZ_BACKEND=x11/wayland— explicit override.WAYLAND_DISPLAYset → Wayland.DISPLAYset → X11.- Otherwise
WindowError::NoBackendAvailable.
X11Window holds an Rc<Xlib>, Rc<Egl>, Rc<Xkb>, optional Rc<Xrender> (for ARGB visuals), optional Rc<Gtk3Im> (for IME fallback when XIM is not available), the display + window IDs, the WM_DELETE_WINDOW atom, the IME manager, the render mode (Gpu(GlContext, GlFunctions) or Cpu(Option<GC>)), the tooltip window, the GNOME menu manager, the D-Bus connection (for screensaver inhibit), the embedded event::CommonWindowState, and the linux-specific timer_fds (BTreeMap<TimerId, fd>).
Loading Xlib at runtime
X11Window::new_with_resources opens the Xlib + EGL + XKB shared libraries via Library::load, which wraps libc::dlopen with RTLD_LAZY. Each Xlib, Egl, Xkb, Xrender, Gtk3Im struct contains hundreds of function pointers populated by the load_symbol! macro.
load_first_available::<Library>(&["libX11.so.6", "libX11.so"]) from the common dlopen module is used here for each library. The Xrender and GTK IM libs are optional — new_with_resources falls back gracefully when they fail to load (no ARGB transparency, no IME).
The custom error handler x11_error_handler is installed via XSetErrorHandler so non-fatal X errors get logged instead of terminating the process.
Window creation: ARGB visual probe
try_create_argb_window attempts a 32-bit TrueColor visual with an alpha channel for true window-background transparency. The flow:
XMatchVisualInfo(display, screen, 32, TrueColor, &out)— probe for a 32-bit visual.XRenderFindVisualFormat(display, visual)— confirm the visual has a non-zero alpha mask.XCreateColormap(display, root, visual, AllocNone)— needed for non-default visuals.XCreateWindowwith that visual + colormap, depth 32.
The colormap is freed in X11Window::close. If any step fails the path falls back to the default visual and has_argb_visual = false is recorded for the renderer.
Render modes
enum RenderMode {
Gpu(gl::GlContext, GlFunctions),
Cpu(Option<GC>), // X11 GC for XPutImage in CPU path
}
gl::GlContext wraps EGL: eglGetDisplay → eglInitialize → eglBindAPI(EGL_OPENGL_API) → eglChooseConfig with 8/8/8/8 RGBA → eglCreateContext → eglCreateWindowSurface. EGL is preferred over GLX because it works identically on Wayland; the function-pointer loader is the same.
CPU mode gets selected when:
AZ_BACKEND=cpu/AZUL_RENDERER=software.- GPU init fails (any step in
GlContext::newreturns an error). - The GPU is blacklisted (see Common).
In CPU mode rendering goes through cpurender → AzulPixmap → XPutImage over the GC. Caches (glyph_cache, retained_pixmap, previous_display_list, bgra_buffer) are kept on the window so each frame reuses the previous frame's allocations.
Event loop
X11Window::poll_event is the per-iteration body called from the multi-window loop. Sequence:
check_timers_and_threads()— drain any timerfds that have fired.XPending(display)loop — drain the queue:XFilterEventfirst (consumed by IME →continue).XNextEvent→ dispatch byevent.type_.
Per-event-type handlers:
Expose. Callsrender_and_present()directly. Per-rect expose is used when damage rects are available.FocusIn/FocusOut. Setswindow_focusedanddynamic_selector_context.window_focused, then callssync_ime_position_to_os().ConfigureNotify. Resize callsregenerate_layout. Position-only changes trigger a DPI re-check viadisplay::get_display_at_point.ClientMessage. Compares againstwm_delete_window_atomfor window close.ButtonPress/Release. Routes tohandle_mouse_button.MotionNotify. Routes tohandle_mouse_move, which updates the hit test and firesprocess_window_events.KeyPress/Release. Routes tohandle_keyboardfor XKB translation and IME.EnterNotify/LeaveNotify. Routes tohandle_mouse_crossing.- Dynamic XRandR event base. Refreshes the monitor cache via
crate::desktop::display::get_monitors.
If an event yields ProcessEventResult != DoNothing, request_redraw is called.
The X11 run() wraps poll_event in a multi-window loop that also drains pending_window_creates for popup menus and dialogs. Idle is wait_for_x11_connection_activity which uses XConnectionNumber + select(2) with a 16 ms timeout so timers can still fire even if no native events arrive.
request_redraw sends per-rect Expose events when gpu_damage_rects is populated; otherwise sends a single full-window expose. XSendEvent + XFlush to wake the loop.
XRandR breakpoint detection
try_subscribe_xrandr loads libXrandr.so.2, calls XRRQueryExtension to find the event base, and XRRSelectInput(root, RR_SCREEN_CHANGE_NOTIFY_MASK) to subscribe. The Xrandr library is intentionally mem::forget-leaked — the function pointers must outlive the window.
The X11 backend also tracks responsive breakpoints. On every ConfigureNotify resize, dynamic_selector_context is updated and compared with viewport_breakpoint_changed against the constant list:
const CSS_BREAKPOINTS: &[f32] = &[320.0, 480.0, 640.0, 768.0, 1024.0,
1280.0, 1440.0, 1920.0];
Crossing one logs the breakpoint change for the layout subsystem; the relayout itself fires unconditionally on size change.
IME via XIM (with GTK fallback)
ImeManager opens an XIM via XOpenIM, then creates an XIC with XIMPreeditNothing | XIMStatusNothing. XSetLocaleModifiers("") is called first — XIM only works when the locale is initialised.
filter_event (called from poll_event before dispatch) calls XFilterEvent. When XIM consumes the event, the loop continues without further handling. Otherwise XLookupString produces UTF-8 that flows through the standard process_text_input path.
If XOpenIM returns null (no IM server running), the manager falls back to GTK 3's GtkIMContext loaded via Gtk3Im::new. Both code paths feed the same process_text_input(text) on LayoutWindow.
Tooltips
TooltipWindow is a transient override-redirect window:
let attrs = XSetWindowAttributes { override_redirect: True, ... };
XCreateWindow(display, root, 0, 0, w, h, 0, depth, InputOutput,
visual, CWOverrideRedirect | CWBackPixel, &mut attrs);
It exists for the lifetime of the parent window and is shown/hidden via XMapWindow / XUnmapWindow. Text is drawn with XDrawString on a fixed-width font (-misc-fixed-medium-r-normal--13-...). The tooltip moves with XMoveWindow on cursor motion.
This is the legacy path used until per-DOM tooltip rendering is fully wired through LayoutWindow.tooltip_manager. The constants (TOOLTIP_BG_COLOR, TOOLTIP_CHAR_WIDTH_PX, etc.) hard-code an old style; styled tooltips will route through the standard render pipeline like context menus do.
Native menus
X11 has two menu paths:
- GNOME global menu via DBus
org.gtk.Menus/org.gtk.Actions— see Linux DBus. Activated viagnome_menu::should_use_gnome_menus()which checksXDG_CURRENT_DESKTOP. - Popup menu window —
create_menu_window_optionsreturns aWindowCreateOptionswithWindowDecorations::None,is_always_on_top, and aLayoutCallbackthat renders the menu through the standardmenu_renderer::create_menu_dom_with_csspath. The popup is then created by the main event loop viapending_window_creates.
Clipboard
The X11 clipboard module wraps the x11-clipboard crate. On every event-loop iteration sync_clipboard is called — it writes any pending content from ClipboardManager to both the CLIPBOARD and PRIMARY selections (Ctrl+C/V and middle-click respectively). Reads use a 3 second timeout per selection so worst-case wait is 6 s when both selections are stale.
D-Bus integration
The X11 window holds an optional *mut DBusConnection used for two things:
- Screen-saver inhibit —
org.freedesktop.ScreenSaver.Inhibitis called when the user enables theWindowFlags::keep_screen_awakeflag. The cookie is stored inscreensaver_inhibit_cookieso the pairInhibit/UnInhibitcan be matched. - GNOME menu protocol — the same connection is used by
gnome_menu::GnomeMenuManagerto registerorg.gtk.Menus/org.gtk.Actionsobject paths.
The libdbus-1 SO is loaded via dbus::DBusLib::new which uses the same Library / load_first_available machinery as Xlib.
Timers (timerfd)
linux::timer::start_timerfd wraps timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC) and timerfd_settime. Each Azul timer registered on the window allocates one timerfd; when the fd becomes readable the timer has fired. check_timers_and_threads in poll_event calls read(fd, &mut buf, 8) to consume and dispatch.
Timer fds are mixed into the select(2) set in wait_for_x11_connection_activity so the idle wait wakes both for X events and timer fires.
Multi-window support
The global registry lives in linux/registry.rs (thread-local HashMap<X11WindowId, *mut X11Window>). register_x11_window inserts; get_x11_window looks up; get_all_x11_window_ids gives the loop iteration order. Pointers are leaked via Box::into_raw in the run loop; unregister_x11_window returns the pointer for Box::from_raw cleanup when the window is closed.
The same registry is used by both X11 and Wayland — Wayland uses wl_display as u64 as the registry key (see Linux Wayland).
Accessibility
The X11 accessibility module uses accesskit_unix::Adapter (AT-SPI 2 over DBus) when the a11y cargo feature is on. Adapter::new is wrapped in catch_unwind because DBus connection failures can panic from inside accesskit. Action requests are queued in pending_actions: Arc<Mutex<Vec<ActionRequest>>> and drained from the event loop.
Defines and visual-info layout
The X11 defines module is the literal Rust translation of the Xlib C headers: Display, Window, Atom, XEvent union, XErrorEvent, XSetWindowAttributes, XVisualInfo, XClientMessageEvent, event-type constants (Expose = 12, KeyPress = 2, etc.), and event-mask flags. The file is mechanical — fields and constants must match the C ABI exactly. When adding event-mask bits, cross-check against /usr/include/X11/X.h.
Linux-specific helpers
linux/system_style.rs. Reads GTK/Adwaita system colors via gsettings and populatesazul_css::system::SystemStyle.linux/resources.rs. HoldsAppResources, the shared font cache, app data, system style, and icon provider Arc-shared between all windows on Linux.linux/common/gl.rs. HoldsGlFunctions::initialize, which fillsGenericGlContextviaeglGetProcAddresswith adlsymfallback over libGL.so.1.linux/timer.rs. timerfd helpers reused by both X11 and Wayland.linux/registry.rs. Thread-local window registry shared by both Linux backends.
Known issues / TODOs
check_gpu_blacklistis defined but never called from the X11 GPU init path. When wired, an llvmpipe / NVIDIA-with-no-GLSL / old-Intel detection should fall through toRenderMode::Cpu.- Tooltip rendering is still bitmap text via
XDrawString; styled tooltips will rebuild on top of the samepending_window_createsqueue used by menu popups. - The X11 popup menu path does not yet support nested submenus from
Menu::items[i].submenu— each level needs its own popup window parented to the previous.
Coming Up Next
- Linux Wayland — Linux Wayland shell - wl_surface, xdg-shell, libinput
- Linux DBus — Linux DBus integration for a11y, dialogs, notifications
- Common — Shared shell infrastructure across platforms
- Windowing Overview — Per-window aggregate, headless variant, and the platform shell layer