Fix crash on startup when the X11 server supports XInput < 2.4 (#53582)

CanWang created

## Summary

- Fix crash on startup when the X11 server supports XInput < 2.4 (e.g.
XInput 2.3)
- Gesture event mask bits (pinch begin/update/end) are now only
requested when the server advertises XInput >= 2.4
- Zed previously failed to open any window on affected systems, printing
`Zed failed to open a window: X11 XiSelectEvents failed`

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable









## Problem

On X11 systems where the XInput extension version is older than 2.4, Zed
crashes immediately on startup with:

```
Zed failed to open a window: X11 XiSelectEvents failed.

Caused by:
    X11 error X11Error { error_kind: Value, error_code: 2, sequence: 277,
    bad_value: 27, minor_opcode: 46, major_opcode: 131,
    extension_name: Some("XInputExtension"),
    request_name: Some("XISelectEvents") }
```

This makes Zed completely unusable on any X11 display server that only
supports XInput 2.3 or earlier, which includes many current Ubuntu
20.04/22.04 systems, remote X11 sessions, and VNC/Xvfb setups.

### Root cause

During window creation, `X11WindowState::new` calls `XISelectEvents`
with an event mask that unconditionally includes gesture event bits
(`GESTURE_PINCH_BEGIN`, `GESTURE_PINCH_UPDATE`, `GESTURE_PINCH_END`).
These gesture events were introduced in **XInput 2.4**.

When the X server only supports XInput 2.3 (or older), it does not
recognize these mask bits and rejects the entire `XISelectEvents`
request with a `BadValue` error. This is fatal because the error is
propagated up and prevents the window from being created.

A comment in the original code stated:

> If the server only supports an older version, gesture events simply
won't be delivered.

This is incorrect. The X11 protocol does **not** silently ignore unknown
mask bits in `XISelectEvents` — it rejects the whole request.

### How XInput version negotiation works

The client calls `XIQueryVersion(2, 4)` to announce the highest version
it supports. The server responds with the highest version **it**
supports (e.g. `2.3`). The client is then responsible for not using
features beyond the negotiated version. The existing code ignored the
server's response and used 2.4 features unconditionally.

## Fix

### Approach

Check the XInput version returned by the server. Only include gesture
event mask bits in `XISelectEvents` when the negotiated version is >=
2.4. On older servers, basic input events (motion, button press/release,
enter, leave) still work normally — only touchpad pinch gestures are
unavailable.

### Changed files

**`crates/gpui_linux/src/linux/x11/client.rs`**

1. Added `supports_xinput_gestures: bool` field to `X11ClientState`.
2. After the existing `xinput_xi_query_version(2, 4)` call, compute
whether the server version is >= 2.4:
   ```rust
   let supports_xinput_gestures = xinput_version.major_version > 2
|| (xinput_version.major_version == 2 && xinput_version.minor_version >=
4);
   ```
3. Added an `info!` log line reporting the detected XInput version and
gesture support status.
4. Pass `supports_xinput_gestures` through `open_window` into
`X11Window::new`.

**`crates/gpui_linux/src/linux/x11/window.rs`**

1. Added `supports_xinput_gestures: bool` parameter to both
`X11Window::new` and `X11WindowState::new`.
2. The `XISelectEvents` call now builds the event mask conditionally:
- Always includes: `MOTION`, `BUTTON_PRESS`, `BUTTON_RELEASE`, `ENTER`,
`LEAVE`
- Only when `supports_xinput_gestures` is true: `GESTURE_PINCH_BEGIN`,
`GESTURE_PINCH_UPDATE`, `GESTURE_PINCH_END`

### What is NOT changed

- The gesture event **handlers** in `client.rs`
(`XinputGesturePinchBegin`, `XinputGesturePinchUpdate`,
`XinputGesturePinchEnd`) are left as-is. They simply won't be triggered
on servers without gesture support, since the events are never
registered.
- No behavioral change on systems with XInput >= 2.4 — gesture events
continue to work exactly as before.

## Testing

| Test | Before fix | After fix |
|------|-----------|-----------|
| `./target/release/zed .` on XInput 2.3 | Immediate crash (exit code 1)
| Window opens successfully (runs until killed) |
| XInput version detection | Version queried but response ignored |
Version checked and logged |

Verified on an X11 system with XInput 2.3 (X.Org 1.20.13, Ubuntu 20.04).

## Test plan

- [x] Build succeeds (`cargo build --release`)
- [x] Zed launches and opens a window on XInput 2.3 system
- [x] No regression on the basic input event path (motion, clicks,
enter/leave still registered)
- [ ] Verify gesture pinch events still work on a system with XInput >=
2.4

Release Notes:

- Fixed Zed failing to start on X11 systems with XInput version older
than 2.4, which includes many Linux distributions and remote desktop
setups.

Change summary

crates/gpui_linux/src/linux/x11/client.rs | 17 ++++++++++++
crates/gpui_linux/src/linux/x11/window.rs | 31 ++++++++++++++----------
2 files changed, 34 insertions(+), 14 deletions(-)

Detailed changes

crates/gpui_linux/src/linux/x11/client.rs 🔗

@@ -214,6 +214,8 @@ pub struct X11ClientState {
 
     pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
 
+    pub(crate) supports_xinput_gestures: bool,
+
     pub(crate) common: LinuxCommon,
     pub(crate) clipboard: Clipboard,
     pub(crate) clipboard_item: Option<ClipboardItem>,
@@ -345,7 +347,8 @@ impl X11Client {
 
         // Announce to X server that XInput up to 2.4 is supported.
         // Version 2.4 is needed for gesture events (GesturePinchBegin/Update/End).
-        // If the server only supports an older version, gesture events simply won't be delivered.
+        // The server responds with the highest version it supports; if < 2.4,
+        // we must not request gesture event masks in XISelectEvents.
         let xinput_version = get_reply(
             || "XInput XiQueryVersion failed",
             xcb_connection.xinput_xi_query_version(2, 4),
@@ -354,6 +357,14 @@ impl X11Client {
             xinput_version.major_version >= 2,
             "XInput version >= 2 required."
         );
+        let supports_xinput_gestures = xinput_version.major_version > 2
+            || (xinput_version.major_version == 2 && xinput_version.minor_version >= 4);
+        log::info!(
+            "XInput version: {}.{}, gesture support: {}",
+            xinput_version.major_version,
+            xinput_version.minor_version,
+            supports_xinput_gestures,
+        );
 
         let pointer_device_states =
             current_pointer_device_states(&xcb_connection, &BTreeMap::new()).unwrap_or_default();
@@ -535,6 +546,8 @@ impl X11Client {
 
             pointer_device_states,
 
+            supports_xinput_gestures,
+
             clipboard,
             clipboard_item: None,
             xdnd_state: Xdnd::default(),
@@ -1593,6 +1606,7 @@ impl LinuxClient for X11Client {
         let scale_factor = state.scale_factor;
         let appearance = state.common.appearance;
         let compositor_gpu = state.compositor_gpu.take();
+        let supports_xinput_gestures = state.supports_xinput_gestures;
         let window = X11Window::new(
             handle,
             X11ClientStatePtr(Rc::downgrade(&self.0)),
@@ -1608,6 +1622,7 @@ impl LinuxClient for X11Client {
             scale_factor,
             appearance,
             parent_window,
+            supports_xinput_gestures,
         )?;
         check_reply(
             || "Failed to set XdndAware property",

crates/gpui_linux/src/linux/x11/window.rs 🔗

@@ -423,6 +423,7 @@ impl X11WindowState {
         scale_factor: f32,
         appearance: WindowAppearance,
         parent_window: Option<X11WindowStatePtr>,
+        supports_xinput_gestures: bool,
     ) -> anyhow::Result<Self> {
         let x_screen_index = params
             .display_id
@@ -660,25 +661,27 @@ impl X11WindowState {
                 ),
             )?;
 
+            let mut xi_event_mask = xinput::XIEventMask::MOTION
+                | xinput::XIEventMask::BUTTON_PRESS
+                | xinput::XIEventMask::BUTTON_RELEASE
+                | xinput::XIEventMask::ENTER
+                | xinput::XIEventMask::LEAVE;
+            if supports_xinput_gestures {
+                // x11rb 0.13 doesn't define XIEventMask constants for gesture
+                // events, so we construct them from the event opcodes (each
+                // XInput event type N maps to mask bit N).
+                xi_event_mask |=
+                    xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_BEGIN_EVENT)
+                        | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_UPDATE_EVENT)
+                        | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_END_EVENT);
+            }
             check_reply(
                 || "X11 XiSelectEvents failed.",
                 xcb.xinput_xi_select_events(
                     x_window,
                     &[xinput::EventMask {
                         deviceid: XINPUT_ALL_DEVICE_GROUPS,
-                        mask: vec![
-                            xinput::XIEventMask::MOTION
-                                | xinput::XIEventMask::BUTTON_PRESS
-                                | xinput::XIEventMask::BUTTON_RELEASE
-                                | xinput::XIEventMask::ENTER
-                                | xinput::XIEventMask::LEAVE
-                                // x11rb 0.13 doesn't define XIEventMask constants for gesture
-                                // events, so we construct them from the event opcodes (each
-                                // XInput event type N maps to mask bit N).
-                                | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_BEGIN_EVENT)
-                                | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_UPDATE_EVENT)
-                                | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_END_EVENT),
-                        ],
+                        mask: vec![xi_event_mask],
                     }],
                 ),
             )?;
@@ -855,6 +858,7 @@ impl X11Window {
         scale_factor: f32,
         appearance: WindowAppearance,
         parent_window: Option<X11WindowStatePtr>,
+        supports_xinput_gestures: bool,
     ) -> anyhow::Result<Self> {
         let ptr = X11WindowStatePtr {
             state: Rc::new(RefCell::new(X11WindowState::new(
@@ -872,6 +876,7 @@ impl X11Window {
                 scale_factor,
                 appearance,
                 parent_window,
+                supports_xinput_gestures,
             )?)),
             callbacks: Rc::new(RefCell::new(Callbacks::default())),
             xcb: xcb.clone(),