lib/windowing/src/x11.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const native_xkb = @import("xkb");
   3 const sys = @import("sys");
   4 const clipboard = @import("clipboard.zig");
   5 const event_module = @import("event.zig");
   6 const Event = event_module.Event;
   7 const EventIterator = event_module.EventIterator;
   8 const CursorShape = @import("cursor.zig").CursorShape;
   9 const gamepad = @import("gamepad.zig");
  10 const input = @import("input.zig");
  11 const surface = @import("surface.zig");
  12 const Key = @import("key.zig").Key;
  13 const MouseButton = @import("mouse.zig").MouseButton;
  14 const Modifier = @import("modifier.zig").Modifier;
  15 const WindowConfig = @import("config.zig").WindowConfig;
  16 const CreateError = @import("window.zig").CreateError;
  17 const PollError = @import("window.zig").PollError;
  18 const PresentError = @import("window.zig").PresentError;
  19 const PresentRegion = @import("window.zig").PresentRegion;
  20 const Scale = @import("window.zig").Scale;
  21 const Size = @import("window.zig").Size;
  22 const presentation = @import("presentation.zig");
  23 const PresentationOutcome = presentation.Outcome;
  24 const PresentationStatus = presentation.Status;
  25 const PresentationStorageStatus = presentation.StorageStatus;
  26 const WaylandStorageStatus = @import("wayland").runtime.StorageStatus;
  27 
  28 const x11 = sys.x11;
  29 const protocol = x11.protocol;
  30 
  31 const max_keycode_slots = 256;
  32 const keysym_columns = 2;
  33 const clipboard_wait_ms = 1000;
  34 const max_core_request_bytes = 65535 * 4;
  35 
  36 const iconic_state: u32 = 3;
  37 const net_wm_state_remove: u32 = 0;
  38 const net_wm_state_add: u32 = 1;
  39 const substructure_mask: u32 = 0x0018_0000;
  40 
  41 const cursor_font_name = "cursor";
  42 
  43 fn deriveConnectionCapacity(
  44     event_capacity: event_module.Capacity,
  45     clipboard_capacity: clipboard.Capacity,
  46     request_byte_count: usize,
  47     reply_byte_count: usize,
  48 ) x11.CapacityError!x11.Capacity {
  49     const publication_padded_byte_count = std.math.add(
  50         usize,
  51         clipboard_capacity.published_text_byte_count,
  52         protocol.pad4(clipboard_capacity.published_text_byte_count),
  53     ) catch return error.CapacityOverflow;
  54     const publication_request_byte_count = std.math.add(
  55         usize,
  56         24,
  57         publication_padded_byte_count,
  58     ) catch return error.CapacityOverflow;
  59     if (request_byte_count < publication_request_byte_count) {
  60         return error.RequestStorageTooSmall;
  61     }
  62     const retained_message_count = std.math.add(
  63         usize,
  64         event_capacity.retained_event_count,
  65         clipboard_capacity.retained_wait_event_count,
  66     ) catch return error.CapacityOverflow;
  67     const clipboard_reply_byte_count = std.math.mul(
  68         usize,
  69         clipboard_capacity.x11_property_long_count,
  70         4,
  71     ) catch return error.CapacityOverflow;
  72     const minimum_reply_byte_count = @max(
  73         clipboard_reply_byte_count,
  74         protocol.maximum_keyboard_mapping_reply_byte_count,
  75     );
  76     if (reply_byte_count < minimum_reply_byte_count) {
  77         return error.ReplyStorageTooSmall;
  78     }
  79     return x11.Capacity.derive(.{
  80         .retained_message_count = retained_message_count,
  81         .request_byte_count = request_byte_count,
  82         .reply_byte_count = reply_byte_count,
  83     });
  84 }
  85 
  86 const Atoms = struct {
  87     wm_protocols: u32,
  88     wm_delete_window: u32,
  89     net_wm_name: u32,
  90     utf8_string: u32,
  91     wm_normal_hints: u32,
  92     wm_size_hints: u32,
  93     wm_change_state: u32,
  94     net_wm_state: u32,
  95     net_wm_state_max_horz: u32,
  96     net_wm_state_max_vert: u32,
  97     clipboard: u32,
  98     windowing_selection: u32,
  99     targets: u32,
 100     atom: u32,
 101 };
 102 
 103 pub const X11Backend = struct {
 104     connection: x11.Connection,
 105     window: u32,
 106     gc: u32,
 107     atoms: Atoms,
 108     width: u32,
 109     height: u32,
 110     should_close: bool = false,
 111     input: input.State,
 112     gamepads: gamepad.State,
 113     clipboard_capacity: clipboard.Capacity,
 114     clipboard_source: clipboard.Source,
 115     clipboard_wait_events: clipboard.WaitEvents,
 116     events: event_module.Queue,
 117     keysyms: [max_keycode_slots][keysym_columns]u32 = @splat(@splat(0)),
 118     cursor_font: u32 = 0,
 119     cursors: [cursor_shape_count]u32 = @splat(0),
 120     cursor_on_screen: bool = false,
 121     red_shift: u5 = 16,
 122     green_shift: u5 = 8,
 123     blue_shift: u5 = 0,
 124     packed_region: PackedRegionStorage,
 125 
 126     const cursor_shape_count = @typeInfo(CursorShape).@"enum".field_names.len;
 127 
 128     pub fn init(self: *X11Backend, allocator: std.mem.Allocator, config: WindowConfig) CreateError!void {
 129         const clipboard_capacity = clipboard.Capacity.derive(config.clipboard) catch
 130             return error.InvalidCapacity;
 131         const event_capacity = event_module.Capacity.derive(config.events) catch
 132             return error.InvalidCapacity;
 133         const connection_capacity = deriveConnectionCapacity(
 134             event_capacity,
 135             clipboard_capacity,
 136             config.x11_request_byte_count,
 137             config.x11_reply_byte_count,
 138         ) catch return error.InvalidCapacity;
 139         const input_capacity = input.Capacity.derive(config.input) catch
 140             return error.InvalidCapacity;
 141         const gamepad_capacity = gamepad.Capacity.derive(config.gamepads) catch
 142             return error.InvalidCapacity;
 143         const presentation_capacity = presentation.Capacity.derive(
 144             config.presentation,
 145         ) catch return error.InvalidCapacity;
 146         var packed_region = PackedRegionStorage.init(
 147             allocator,
 148             presentation_capacity.x11_packed_region_bytes,
 149         ) catch return error.OutOfMemory;
 150         errdefer packed_region.deinit();
 151         self.* = .{
 152             .connection = x11.Connection.connect(allocator, connection_capacity) catch |err| switch (err) {
 153                 error.OutOfMemory, error.ConnectionFailed, error.AuthenticationFailed => return error.ConnectionFailed,
 154                 else => return error.UnsupportedPlatform,
 155             },
 156             .window = 0,
 157             .gc = 0,
 158             .atoms = undefined,
 159             .width = config.width,
 160             .height = config.height,
 161             .input = undefined,
 162             .gamepads = undefined,
 163             .clipboard_capacity = clipboard_capacity,
 164             .clipboard_source = undefined,
 165             .clipboard_wait_events = undefined,
 166             .events = undefined,
 167             .packed_region = packed_region,
 168         };
 169         errdefer self.connection.close();
 170         self.events = event_module.Queue.init(allocator, event_capacity) catch
 171             return error.OutOfMemory;
 172         errdefer self.events.deinit();
 173         self.input = input.State.init(allocator, input_capacity) catch
 174             return error.OutOfMemory;
 175         errdefer self.input.deinit();
 176         self.gamepads = gamepad.State.init(allocator, gamepad_capacity) catch
 177             return error.OutOfMemory;
 178         errdefer self.gamepads.deinit();
 179         self.clipboard_wait_events = clipboard.WaitEvents.init(
 180             allocator,
 181             clipboard_capacity,
 182         ) catch return error.OutOfMemory;
 183         errdefer self.clipboard_wait_events.deinit();
 184         self.clipboard_source = clipboard.Source.init(
 185             allocator,
 186             clipboard_capacity,
 187         ) catch return error.OutOfMemory;
 188         errdefer self.clipboard_source.deinit();
 189 
 190         self.atoms = internAtoms(&self.connection) catch return error.ConnectionFailed;
 191         self.red_shift = maskShift(self.connection.setup.red_mask, 16);
 192         self.green_shift = maskShift(self.connection.setup.green_mask, 8);
 193         self.blue_shift = maskShift(self.connection.setup.blue_mask, 0);
 194 
 195         self.window = self.connection.generateId();
 196         self.gc = self.connection.generateId();
 197 
 198         self.createWindow(config) catch return error.WindowCreationFailed;
 199         self.loadKeyboardMapping() catch return error.WindowCreationFailed;
 200     }
 201 
 202     pub fn deinit(self: *X11Backend) void {
 203         self.gamepads.deinit();
 204         self.input.deinit();
 205         self.events.deinit();
 206         self.clipboard_source.deinit();
 207         self.clipboard_wait_events.deinit();
 208         self.packed_region.deinit();
 209         const request = self.connection.beginRequest();
 210         protocol.freeResource(request, .free_gc, self.gc) catch {};
 211         protocol.windowRequest(request, .destroy_window, self.window) catch {};
 212         self.connection.sendRequest() catch {};
 213         self.connection.close();
 214     }
 215 
 216     fn createWindow(self: *X11Backend, config: WindowConfig) !void {
 217         const setup = self.connection.setup;
 218         const events = protocol.event_mask.key_press |
 219             protocol.event_mask.key_release |
 220             protocol.event_mask.button_press |
 221             protocol.event_mask.button_release |
 222             protocol.event_mask.enter_window |
 223             protocol.event_mask.leave_window |
 224             protocol.event_mask.pointer_motion |
 225             protocol.event_mask.exposure |
 226             protocol.event_mask.structure_notify |
 227             protocol.event_mask.focus_change;
 228 
 229         const request = self.connection.beginRequest();
 230         try protocol.createWindow(request, .{
 231             .window = self.window,
 232             .parent = setup.root,
 233             .depth = setup.root_depth,
 234             .visual = setup.root_visual,
 235             .width = clampU16(config.width),
 236             .height = clampU16(config.height),
 237             .events = events,
 238         });
 239         try protocol.createGc(request, self.gc, self.window);
 240         try self.connection.sendRequest();
 241 
 242         var delete_atom: [4]u8 = undefined;
 243         std.mem.writeInt(u32, &delete_atom, self.atoms.wm_delete_window, .little);
 244         try self.changePropertyBytes(self.atoms.wm_protocols, self.atoms.atom, 32, &delete_atom);
 245         self.applyTitle(config.title);
 246         if (!config.resizable) {
 247             self.applySizeBounds(config.width, config.height, config.width, config.height);
 248         }
 249 
 250         if (config.visible) {
 251             const map = self.connection.beginRequest();
 252             try protocol.windowRequest(map, .map_window, self.window);
 253             try self.connection.sendRequest();
 254         }
 255     }
 256 
 257     fn loadKeyboardMapping(self: *X11Backend) !void {
 258         const setup = self.connection.setup;
 259         const count = setup.max_keycode - setup.min_keycode + 1;
 260         const request = self.connection.beginRequest();
 261         try protocol.getKeyboardMapping(request, setup.min_keycode, count);
 262         try self.connection.sendRequest();
 263         const reply = try self.connection.waitReply();
 264 
 265         const per_keycode = reply.dataByte();
 266         if (per_keycode == 0) return;
 267         var keycode: usize = setup.min_keycode;
 268         var offset: usize = 0;
 269         while (keycode <= setup.max_keycode and offset + 4 <= reply.extra.len) : (keycode += 1) {
 270             var column: usize = 0;
 271             while (column < keysym_columns) : (column += 1) {
 272                 const source = offset + column * 4;
 273                 if (column < per_keycode and source + 4 <= reply.extra.len) {
 274                     self.keysyms[keycode][column] = std.mem.readInt(u32, reply.extra[source..][0..4], .little);
 275                 }
 276             }
 277             offset += @as(usize, per_keycode) * 4;
 278         }
 279     }
 280 
 281     pub fn pollEvents(self: *X11Backend) PollError!EventIterator {
 282         self.input.beginPoll();
 283         self.gamepads.poll();
 284         self.events.beginPoll();
 285 
 286         while (true) {
 287             const message = self.connection.nextMessage() catch return error.ConnectionLost;
 288             const raw = message orelse break;
 289             self.translate(protocol.decodeMessage(&raw));
 290         }
 291 
 292         return self.events.iterator();
 293     }
 294 
 295     pub fn shouldClose(self: *const X11Backend) bool {
 296         return self.should_close;
 297     }
 298 
 299     pub fn eventSource(self: *const X11Backend) ?@import("window.zig").EventSource {
 300         return .{ .descriptor = self.connection.stream.handle };
 301     }
 302 
 303     pub fn getSize(self: *const X11Backend) Size {
 304         return .{ .width = self.width, .height = self.height };
 305     }
 306 
 307     pub fn inputState(self: *const X11Backend) *const input.State {
 308         return &self.input;
 309     }
 310 
 311     pub fn inputStateMut(self: *X11Backend) *input.State {
 312         return &self.input;
 313     }
 314 
 315     pub fn gamepadState(self: *const X11Backend) *const gamepad.State {
 316         return &self.gamepads;
 317     }
 318 
 319     fn translate(self: *X11Backend, message: protocol.Message) void {
 320         switch (message) {
 321             .key_press => |key| {
 322                 const modifiers = modifiersFromState(key.state);
 323                 const keysym = self.keysymFor(key.keycode, key.state);
 324                 const mapped = Key.fromKeysym(
 325                     @fromBackingInt(self.keysyms[key.keycode][0]),
 326                 );
 327                 self.input.pressKey(mapped);
 328                 self.push(.{ .key_press = .{ .key = mapped, .modifiers = modifiers } });
 329                 if (!modifiers.ctrl) {
 330                     var buffer: [4]u8 = undefined;
 331                     if (textFromKeysym(keysym, &buffer)) |bytes| {
 332                         if (Event.textInput(bytes, modifiers)) |text_event| {
 333                             if (self.input.pushTextInput(bytes)) {
 334                                 self.push(text_event);
 335                             }
 336                         }
 337                     }
 338                 }
 339             },
 340             .key_release => |key| {
 341                 const mapped = Key.fromKeysym(
 342                     @fromBackingInt(self.keysyms[key.keycode][0]),
 343                 );
 344                 self.input.releaseKey(mapped);
 345                 self.push(.{ .key_release = .{ .key = mapped, .modifiers = modifiersFromState(key.state) } });
 346             },
 347             .button_press => |button| {
 348                 const modifiers = modifiersFromState(button.state);
 349                 if (wheelFromButton(button.button)) |wheel| {
 350                     self.input.mouse_wheel.x += wheel[0];
 351                     self.input.mouse_wheel.y += wheel[1];
 352                     self.push(.{ .mouse_wheel = .{
 353                         .x = button.x,
 354                         .y = button.y,
 355                         .delta_x = wheel[0],
 356                         .delta_y = wheel[1],
 357                         .modifiers = modifiers,
 358                     } });
 359                 } else if (mouseButtonFromCode(button.button)) |mapped| {
 360                     self.input.pressMouse(mapped);
 361                     self.push(.{ .mouse_press = .{
 362                         .button = mapped,
 363                         .x = button.x,
 364                         .y = button.y,
 365                         .modifiers = modifiers,
 366                     } });
 367                 }
 368             },
 369             .button_release => |button| {
 370                 if (wheelFromButton(button.button) != null) return;
 371                 if (mouseButtonFromCode(button.button)) |mapped| {
 372                     self.input.releaseMouse(mapped);
 373                     self.push(.{ .mouse_release = .{
 374                         .button = mapped,
 375                         .x = button.x,
 376                         .y = button.y,
 377                         .modifiers = modifiersFromState(button.state),
 378                     } });
 379                 }
 380             },
 381             .motion => |motion| {
 382                 const previous = self.input.mouse_position;
 383                 self.input.mouse_position = .{ .x = motion.x, .y = motion.y };
 384                 self.input.mouse_delta.x += motion.x - previous.x;
 385                 self.input.mouse_delta.y += motion.y - previous.y;
 386                 self.push(.{ .mouse_move = .{ .x = motion.x, .y = motion.y } });
 387             },
 388             .configure => |configure| {
 389                 if (configure.window != self.window) return;
 390                 const width: u32 = configure.width;
 391                 const height: u32 = configure.height;
 392                 if (width == self.width and height == self.height) return;
 393                 self.width = width;
 394                 self.height = height;
 395                 self.push(.{ .window_resize = .{ .width = width, .height = height } });
 396             },
 397             .client_message => |client| {
 398                 if (client.message_type == self.atoms.wm_protocols and client.data[0] == self.atoms.wm_delete_window) {
 399                     self.should_close = true;
 400                     self.push(.window_close);
 401                 }
 402             },
 403             .focus => |focus| {
 404                 if (focus.mode != 0) return;
 405                 self.push(.{ .window_focus = .{ .focused = focus.focused } });
 406             },
 407             .destroy_notify => {
 408                 self.should_close = true;
 409                 self.push(.window_close);
 410             },
 411             .selection_clear => |selection| {
 412                 if (selection.owner == self.window and selection.selection == self.atoms.clipboard) {
 413                     self.clipboard_source.clear();
 414                 }
 415             },
 416             .selection_request => |selection| self.serveClipboardRequest(selection),
 417             .enter => self.cursor_on_screen = true,
 418             .leave => self.cursor_on_screen = false,
 419             .expose, .selection_notify, .protocol_error, .reply, .ignored => {},
 420         }
 421     }
 422 
 423     fn push(self: *X11Backend, event: Event) void {
 424         _ = self.events.push(event);
 425     }
 426 
 427     fn keysymFor(
 428         self: *const X11Backend,
 429         keycode: u8,
 430         state: u16,
 431     ) native_xkb.keysym.Keysym {
 432         const shifted = (state & protocol.state_mask.shift) != 0;
 433         const locked = (state & protocol.state_mask.lock) != 0;
 434         var keysym = self.keysyms[keycode][0];
 435         if (shifted and self.keysyms[keycode][1] != 0) {
 436             keysym = self.keysyms[keycode][1];
 437         }
 438         if (locked and !shifted and keysym >= 'a' and keysym <= 'z') {
 439             keysym = keysym - 'a' + 'A';
 440         }
 441         return @fromBackingInt(keysym);
 442     }
 443 
 444     pub fn getFramebufferSize(self: *const X11Backend) Size {
 445         return .{ .width = self.width, .height = self.height };
 446     }
 447 
 448     pub fn nativeSurface(self: *X11Backend) surface.Surface {
 449         const extent = surface.extentFromSizes(
 450             self.width,
 451             self.height,
 452             self.width,
 453             self.height,
 454             1,
 455             1,
 456         );
 457         return .{ .x11 = .{
 458             .connection = &self.connection,
 459             .display = self.connection.target,
 460             .window = self.window,
 461             .screen = self.connection.screen,
 462             .root = self.connection.setup.root,
 463             .visual = self.connection.setup.visual,
 464             .depth = self.connection.setup.visual_depth,
 465             .red_mask = self.connection.setup.red_mask,
 466             .green_mask = self.connection.setup.green_mask,
 467             .blue_mask = self.connection.setup.blue_mask,
 468             .extent = extent,
 469         } };
 470     }
 471 
 472     pub fn getContentScale(_: *const X11Backend) Scale {
 473         return .{ .x = 1, .y = 1 };
 474     }
 475 
 476     pub fn presentRgba8(
 477         self: *X11Backend,
 478         rgba8: []const u8,
 479         width: u32,
 480         height: u32,
 481     ) PresentError!void {
 482         return self.presentRgba8Region(rgba8, width, height, PresentRegion.full(width, height));
 483     }
 484 
 485     pub fn presentRgba8Region(
 486         self: *X11Backend,
 487         rgba8: []const u8,
 488         width: u32,
 489         height: u32,
 490         region: PresentRegion,
 491     ) PresentError!void {
 492         if (width == 0 or height == 0) return;
 493         const frame_byte_count = presentation.rgba8ByteCount(width, height) orelse
 494             return error.UnsupportedFormat;
 495         if (rgba8.len < frame_byte_count) return error.UnsupportedFormat;
 496         const active_region = region.clamped(width, height);
 497         if (active_region.pixelCount() == 0) return;
 498 
 499         const packed_byte_count = try regionByteCount(active_region);
 500         const packed_pixels = self.packed_region.prepare(packed_byte_count) catch
 501             return error.CapacityExceeded;
 502         self.packPixelsRegion(rgba8, width, packed_pixels, active_region);
 503 
 504         const row_bytes = @as(usize, active_region.width) * 4;
 505         const budget = @min(self.connection.maxRequestBytes(), max_core_request_bytes) - 32;
 506         const rows_per_request: usize = @max(1, budget / row_bytes);
 507 
 508         var row: usize = 0;
 509         while (row < active_region.height) {
 510             const rows = @min(rows_per_request, active_region.height - row);
 511             const request = self.connection.beginRequest();
 512             protocol.putImageRows(request, .{
 513                 .drawable = self.window,
 514                 .gc = self.gc,
 515                 .width = clampU16(active_region.width),
 516                 .row_count = @intCast(rows),
 517                 .dst_x = clampI16(active_region.x),
 518                 .dst_y = clampI16(active_region.y + @as(u32, @intCast(row))),
 519                 .depth = self.connection.setup.root_depth,
 520                 .pixels = packed_pixels[row * row_bytes .. (row + rows) * row_bytes],
 521             }) catch return error.CapacityExceeded;
 522             self.connection.sendRequest() catch return error.Unexpected;
 523             row += rows;
 524         }
 525     }
 526 
 527     fn packPixelsRegion(self: *const X11Backend, rgba8: []const u8, frame_width: u32, out: []u8, region: PresentRegion) void {
 528         packRegionRgba8(rgba8, frame_width, out, region, self.pixelPacking());
 529     }
 530 
 531     fn pixelPacking(self: *const X11Backend) PixelPacking {
 532         return .{
 533             .endian = if (self.connection.setup.image_byte_order_msb) .big else .little,
 534             .red_shift = self.red_shift,
 535             .green_shift = self.green_shift,
 536             .blue_shift = self.blue_shift,
 537         };
 538     }
 539 
 540     pub fn setTitle(self: *X11Backend, title: [:0]const u8) void {
 541         self.applyTitle(title);
 542     }
 543 
 544     fn applyTitle(self: *X11Backend, title: [:0]const u8) void {
 545         self.changePropertyBytes(self.atoms.net_wm_name, self.atoms.utf8_string, 8, title) catch {};
 546     }
 547 
 548     pub fn setMinSize(self: *X11Backend, width: u32, height: u32) void {
 549         self.applySizeBounds(width, height, 0, 0);
 550     }
 551 
 552     fn applySizeBounds(self: *X11Backend, min_width: u32, min_height: u32, max_width: u32, max_height: u32) void {
 553         var hints = @as([18]u32, @splat(0));
 554         hints[0] = 16;
 555         hints[5] = min_width;
 556         hints[6] = min_height;
 557         if (max_width != 0 and max_height != 0) {
 558             hints[0] |= 32;
 559             hints[7] = max_width;
 560             hints[8] = max_height;
 561         }
 562         var bytes: [18 * 4]u8 = undefined;
 563         for (hints, 0..) |word, i| {
 564             std.mem.writeInt(u32, bytes[i * 4 ..][0..4], word, .little);
 565         }
 566         self.changePropertyBytes(self.atoms.wm_normal_hints, self.atoms.wm_size_hints, 32, &bytes) catch {};
 567     }
 568 
 569     pub fn setCursor(self: *X11Backend, shape: CursorShape) void {
 570         const cursor = self.cursorFor(shape) orelse return;
 571         const request = self.connection.beginRequest();
 572         protocol.changeCursor(request, self.window, cursor) catch return;
 573         self.connection.sendRequest() catch {};
 574     }
 575 
 576     fn cursorFor(self: *X11Backend, shape: CursorShape) ?u32 {
 577         const index = @backingInt(shape);
 578         if (self.cursors[index] != 0) return self.cursors[index];
 579 
 580         if (self.cursor_font == 0) {
 581             self.cursor_font = self.connection.generateId();
 582             const request = self.connection.beginRequest();
 583             protocol.openFont(request, self.cursor_font, cursor_font_name) catch return null;
 584             self.connection.sendRequest() catch return null;
 585         }
 586 
 587         const cursor = self.connection.generateId();
 588         const request = self.connection.beginRequest();
 589         protocol.createGlyphCursor(request, .{
 590             .cursor = cursor,
 591             .font = self.cursor_font,
 592             .glyph = cursorGlyph(shape),
 593         }) catch return null;
 594         self.connection.sendRequest() catch return null;
 595         self.cursors[index] = cursor;
 596         return cursor;
 597     }
 598 
 599     pub fn isCursorOnScreen(self: *const X11Backend) bool {
 600         return self.cursor_on_screen;
 601     }
 602 
 603     pub fn minimize(self: *X11Backend) void {
 604         self.sendStateMessage(self.atoms.wm_change_state, .{ iconic_state, 0, 0, 0, 0 });
 605     }
 606 
 607     pub fn maximize(self: *X11Backend) void {
 608         self.sendStateMessage(self.atoms.net_wm_state, .{
 609             net_wm_state_add,
 610             self.atoms.net_wm_state_max_horz,
 611             self.atoms.net_wm_state_max_vert,
 612             1,
 613             0,
 614         });
 615     }
 616 
 617     pub fn restore(self: *X11Backend) void {
 618         self.sendStateMessage(self.atoms.net_wm_state, .{
 619             net_wm_state_remove,
 620             self.atoms.net_wm_state_max_horz,
 621             self.atoms.net_wm_state_max_vert,
 622             1,
 623             0,
 624         });
 625         const request = self.connection.beginRequest();
 626         protocol.windowRequest(request, .map_window, self.window) catch return;
 627         self.connection.sendRequest() catch {};
 628     }
 629 
 630     fn sendStateMessage(self: *X11Backend, message_type: u32, data: [5]u32) void {
 631         const request = self.connection.beginRequest();
 632         protocol.sendClientMessage(request, self.connection.setup.root, substructure_mask, .{
 633             .window = self.window,
 634             .message_type = message_type,
 635             .data = data,
 636         }) catch return;
 637         self.connection.sendRequest() catch {};
 638     }
 639 
 640     pub fn isMaximized(self: *X11Backend) bool {
 641         const request = self.connection.beginRequest();
 642         protocol.getProperty(request, .{
 643             .window = self.window,
 644             .property = self.atoms.net_wm_state,
 645             .property_type = self.atoms.atom,
 646             .long_length = 64,
 647         }) catch return false;
 648         self.connection.sendRequest() catch return false;
 649         const reply = self.connection.waitReply() catch return false;
 650 
 651         var horizontal = false;
 652         var vertical = false;
 653         var offset: usize = 0;
 654         while (offset + 4 <= reply.extra.len) : (offset += 4) {
 655             const atom = std.mem.readInt(u32, reply.extra[offset..][0..4], .little);
 656             if (atom == self.atoms.net_wm_state_max_horz) horizontal = true;
 657             if (atom == self.atoms.net_wm_state_max_vert) vertical = true;
 658         }
 659         return horizontal and vertical;
 660     }
 661 
 662     pub fn getClipboardTextAlloc(self: *X11Backend, allocator: std.mem.Allocator) !?[]u8 {
 663         const request = self.connection.beginRequest();
 664         protocol.convertSelection(request, .{
 665             .requestor = self.window,
 666             .selection = self.atoms.clipboard,
 667             .target = self.atoms.utf8_string,
 668             .property = self.atoms.windowing_selection,
 669         }) catch return null;
 670         self.connection.sendRequest() catch return null;
 671 
 672         self.clipboard_wait_events.reset();
 673         var restore_wait_events = true;
 674         defer if (restore_wait_events) self.restoreClipboardWaitEvents();
 675         var property: u32 = protocol.atom_none;
 676         var waited: u32 = 0;
 677         wait: while (waited < clipboard_wait_ms) {
 678             if (!self.clipboard_wait_events.admitNext()) {
 679                 return error.ClipboardWaitCapacityExceeded;
 680             }
 681             while (self.connection.nextMessage() catch return null) |raw| {
 682                 const message = protocol.decodeMessage(&raw);
 683                 switch (message) {
 684                     .selection_notify => |selection| {
 685                         property = selection.property;
 686                         break :wait;
 687                     },
 688                     .selection_request => |selection| self.serveClipboardRequest(selection),
 689                     .selection_clear => |selection| {
 690                         if (selection.owner == self.window and selection.selection == self.atoms.clipboard) {
 691                             self.clipboard_source.clear();
 692                         }
 693                     },
 694                     else => {
 695                         self.clipboard_wait_events.appendAssumeCapacity(raw);
 696                         if (self.clipboard_wait_events.full()) continue :wait;
 697                     },
 698                 }
 699             }
 700             const readable = self.connection.waitReadable(50) catch return null;
 701             if (!readable) waited += 50;
 702         }
 703         if (property == protocol.atom_none) return null;
 704         self.restoreClipboardWaitEvents();
 705         restore_wait_events = false;
 706 
 707         const fetch = self.connection.beginRequest();
 708         protocol.getProperty(fetch, .{
 709             .window = self.window,
 710             .property = property,
 711             .property_type = self.atoms.utf8_string,
 712             .long_length = self.clipboard_capacity.x11_property_long_count,
 713             .delete = true,
 714         }) catch return null;
 715         self.connection.sendRequest() catch return null;
 716         const reply = self.connection.waitReply() catch return null;
 717 
 718         const value_len = reply.u32At(16);
 719         if (value_len == 0 or reply.extra.len < value_len) return null;
 720         if (value_len > self.clipboard_capacity.received_text_byte_count) {
 721             return error.TransferTooLarge;
 722         }
 723         const text = try allocator.alloc(u8, value_len);
 724         @memcpy(text, reply.extra[0..value_len]);
 725         return text;
 726     }
 727 
 728     pub fn setClipboardText(self: *X11Backend, text: []const u8) clipboard.PublishError!void {
 729         try self.clipboard_source.replace(text);
 730         const request = self.connection.beginRequest();
 731         protocol.setSelectionOwner(request, self.window, self.atoms.clipboard) catch
 732             return error.Unexpected;
 733         self.connection.sendRequest() catch return error.Unexpected;
 734     }
 735 
 736     fn serveClipboardRequest(
 737         self: *X11Backend,
 738         selection: protocol.SelectionRequestEvent,
 739     ) void {
 740         if (selection.owner != self.window or selection.selection != self.atoms.clipboard) return;
 741         var property = protocol.atom_none;
 742         if (selection.property != protocol.atom_none) {
 743             const kind = clipboardReplyKind(
 744                 selection.target,
 745                 self.atoms.utf8_string,
 746                 self.atoms.targets,
 747             );
 748             const published = if (kind) |reply| switch (reply) {
 749                 .utf8 => result: {
 750                     self.changePropertyForWindow(
 751                         selection.requestor,
 752                         selection.property,
 753                         self.atoms.utf8_string,
 754                         8,
 755                         self.clipboard_source.text(),
 756                     ) catch break :result false;
 757                     break :result true;
 758                 },
 759                 .targets => result: {
 760                     var targets: [8]u8 = undefined;
 761                     std.mem.writeInt(u32, targets[0..4], self.atoms.targets, .little);
 762                     std.mem.writeInt(u32, targets[4..8], self.atoms.utf8_string, .little);
 763                     self.changePropertyForWindow(
 764                         selection.requestor,
 765                         selection.property,
 766                         self.atoms.atom,
 767                         32,
 768                         &targets,
 769                     ) catch break :result false;
 770                     break :result true;
 771                 },
 772             } else false;
 773             if (published) property = selection.property;
 774         }
 775         const request = self.connection.beginRequest();
 776         protocol.sendSelectionNotify(request, .{
 777             .time = selection.time,
 778             .requestor = selection.requestor,
 779             .selection = selection.selection,
 780             .target = selection.target,
 781             .property = property,
 782         }) catch return;
 783         self.connection.sendRequest() catch {};
 784     }
 785 
 786     fn restoreClipboardWaitEvents(self: *X11Backend) void {
 787         for (self.clipboard_wait_events.retained()) |event| {
 788             self.connection.retainMessage(event) catch unreachable;
 789         }
 790         self.clipboard_wait_events.reset();
 791     }
 792 
 793     pub fn clipboardStatus(self: *const X11Backend) clipboard.Status {
 794         return clipboard.Status.combine(
 795             self.clipboard_wait_events.status(),
 796             self.clipboard_source.status(),
 797         );
 798     }
 799 
 800     pub fn x11StorageStatus(self: *const X11Backend) x11.Status {
 801         return self.connection.storageStatus();
 802     }
 803 
 804     pub fn waylandStorageStatus(_: *const X11Backend) WaylandStorageStatus {
 805         return .{};
 806     }
 807 
 808     pub fn nextPresentationOutcome(_: *X11Backend) ?PresentationOutcome {
 809         return null;
 810     }
 811 
 812     pub fn presentationStatus(_: *const X11Backend) PresentationStatus {
 813         return .unavailable;
 814     }
 815 
 816     pub fn presentationStorageStatus(self: *const X11Backend) PresentationStorageStatus {
 817         return .{
 818             .frame_capacity_rejection_count = self.packed_region.capacity_rejection_count,
 819         };
 820     }
 821 
 822     pub fn droppedEventCount(self: *const X11Backend) u64 {
 823         return self.events.droppedEventCount();
 824     }
 825 
 826     fn changePropertyBytes(self: *X11Backend, property: u32, property_type: u32, format: u8, data: []const u8) !void {
 827         return self.changePropertyForWindow(self.window, property, property_type, format, data);
 828     }
 829 
 830     fn changePropertyForWindow(
 831         self: *X11Backend,
 832         target_window: u32,
 833         property: u32,
 834         property_type: u32,
 835         format: u8,
 836         data: []const u8,
 837     ) !void {
 838         const request = self.connection.beginRequest();
 839         try protocol.changeProperty(request, .{
 840             .window = target_window,
 841             .property = property,
 842             .property_type = property_type,
 843             .format = format,
 844             .data = data,
 845         });
 846         try self.connection.sendRequest();
 847     }
 848 };
 849 
 850 const ClipboardReplyKind = enum {
 851     utf8,
 852     targets,
 853 };
 854 
 855 fn clipboardReplyKind(
 856     target: u32,
 857     utf8_string: u32,
 858     targets: u32,
 859 ) ?ClipboardReplyKind {
 860     if (target == utf8_string) return .utf8;
 861     if (target == targets) return .targets;
 862     return null;
 863 }
 864 
 865 fn internAtoms(connection: *x11.Connection) !Atoms {
 866     return .{
 867         .wm_protocols = try connection.internAtom("WM_PROTOCOLS"),
 868         .wm_delete_window = try connection.internAtom("WM_DELETE_WINDOW"),
 869         .net_wm_name = try connection.internAtom("_NET_WM_NAME"),
 870         .utf8_string = try connection.internAtom("UTF8_STRING"),
 871         .wm_normal_hints = try connection.internAtom("WM_NORMAL_HINTS"),
 872         .wm_size_hints = try connection.internAtom("WM_SIZE_HINTS"),
 873         .wm_change_state = try connection.internAtom("WM_CHANGE_STATE"),
 874         .net_wm_state = try connection.internAtom("_NET_WM_STATE"),
 875         .net_wm_state_max_horz = try connection.internAtom("_NET_WM_STATE_MAXIMIZED_HORZ"),
 876         .net_wm_state_max_vert = try connection.internAtom("_NET_WM_STATE_MAXIMIZED_VERT"),
 877         .clipboard = try connection.internAtom("CLIPBOARD"),
 878         .windowing_selection = try connection.internAtom("WINDOWING_SELECTION"),
 879         .targets = try connection.internAtom("TARGETS"),
 880         .atom = 4,
 881     };
 882 }
 883 
 884 fn clampU16(value: u32) u16 {
 885     return if (value > std.math.maxInt(u16)) std.math.maxInt(u16) else @intCast(value);
 886 }
 887 
 888 fn clampI16(value: u32) i16 {
 889     return if (value > @as(u32, @intCast(std.math.maxInt(i16)))) std.math.maxInt(i16) else @intCast(value);
 890 }
 891 
 892 const PixelPacking = struct {
 893     endian: std.builtin.Endian,
 894     red_shift: u5,
 895     green_shift: u5,
 896     blue_shift: u5,
 897 };
 898 
 899 const PackedRegionStorage = struct {
 900     allocator: std.mem.Allocator,
 901     bytes: []u8,
 902     capacity_rejection_count: u64 = 0,
 903 
 904     fn init(
 905         allocator: std.mem.Allocator,
 906         byte_count: usize,
 907     ) std.mem.Allocator.Error!PackedRegionStorage {
 908         std.debug.assert(byte_count > 0);
 909         return .{
 910             .allocator = allocator,
 911             .bytes = try allocator.alloc(u8, byte_count),
 912         };
 913     }
 914 
 915     fn deinit(self: *PackedRegionStorage) void {
 916         self.allocator.free(self.bytes);
 917         self.* = undefined;
 918     }
 919 
 920     fn prepare(self: *PackedRegionStorage, byte_count: usize) error{CapacityExceeded}![]u8 {
 921         if (byte_count > self.bytes.len) {
 922             self.capacity_rejection_count +|= 1;
 923             return error.CapacityExceeded;
 924         }
 925         return self.bytes[0..byte_count];
 926     }
 927 };
 928 
 929 fn packRegionRgba8(rgba8: []const u8, frame_width: u32, out: []u8, region: PresentRegion, packing: PixelPacking) void {
 930     var y: u32 = 0;
 931     while (y < region.height) : (y += 1) {
 932         var x: u32 = 0;
 933         while (x < region.width) : (x += 1) {
 934             const source_index = (@as(usize, region.y + y) * @as(usize, frame_width) + @as(usize, region.x + x)) * 4;
 935             const target_index = (@as(usize, y) * @as(usize, region.width) + @as(usize, x)) * 4;
 936             packRgba8Pixel(rgba8[source_index..][0..4], out[target_index..][0..4], packing);
 937         }
 938     }
 939 }
 940 
 941 fn packRgba8Pixel(source: *const [4]u8, target: *[4]u8, packing: PixelPacking) void {
 942     const word = (@as(u32, source[0]) << packing.red_shift) |
 943         (@as(u32, source[1]) << packing.green_shift) |
 944         (@as(u32, source[2]) << packing.blue_shift);
 945     std.mem.writeInt(u32, target, word, packing.endian);
 946 }
 947 
 948 fn regionByteCount(region: PresentRegion) PresentError!usize {
 949     const pixels = std.math.mul(usize, @as(usize, region.width), @as(usize, region.height)) catch return error.UnsupportedFormat;
 950     return std.math.mul(usize, pixels, 4) catch return error.UnsupportedFormat;
 951 }
 952 
 953 fn maskShift(mask: u32, fallback: u5) u5 {
 954     if (mask == 0) return fallback;
 955     return @intCast(@ctz(mask));
 956 }
 957 
 958 fn modifiersFromState(state: u16) Modifier {
 959     return .{
 960         .shift = (state & protocol.state_mask.shift) != 0,
 961         .ctrl = (state & protocol.state_mask.control) != 0,
 962         .alt = (state & protocol.state_mask.mod1) != 0,
 963         .super = (state & protocol.state_mask.mod4) != 0,
 964     };
 965 }
 966 
 967 fn mouseButtonFromCode(code: u8) ?MouseButton {
 968     return switch (code) {
 969         1 => .left,
 970         2 => .middle,
 971         3 => .right,
 972         8 => .x1,
 973         9 => .x2,
 974         else => null,
 975     };
 976 }
 977 
 978 fn wheelFromButton(code: u8) ?[2]f32 {
 979     return switch (code) {
 980         4 => .{ 0, 1 },
 981         5 => .{ 0, -1 },
 982         6 => .{ -1, 0 },
 983         7 => .{ 1, 0 },
 984         else => null,
 985     };
 986 }
 987 
 988 fn cursorGlyph(shape: CursorShape) u16 {
 989     return switch (shape) {
 990         .default => 68,
 991         .resize_ns => 116,
 992         .resize_ew => 108,
 993         .resize_nwse => 134,
 994         .resize_nesw => 136,
 995     };
 996 }
 997 
 998 fn textFromKeysym(keysym: native_xkb.keysym.Keysym, buffer: *[4]u8) ?[]const u8 {
 999     const codepoint = native_xkb.keysym.codepoint(keysym) orelse return null;
1000     const len = std.unicode.utf8Encode(codepoint, buffer) catch return null;
1001     return buffer[0..len];
1002 }
1003 
1004 test "x11 connection capacity composes event and clipboard retention" {
1005     const defaults = try deriveConnectionCapacity(
1006         try event_module.Capacity.derive(.{}),
1007         try clipboard.Capacity.derive(.{}),
1008         protocol.maximum_request_byte_count,
1009         x11.default_reply_byte_count,
1010     );
1011     try std.testing.expectEqual(@as(usize, 320), defaults.retained_message_count);
1012     try std.testing.expectEqual(
1013         320 * @sizeOf(x11.Message),
1014         defaults.retained_message_bytes,
1015     );
1016     try std.testing.expectEqual(
1017         protocol.maximum_request_byte_count,
1018         defaults.request_byte_count,
1019     );
1020     try std.testing.expectEqual(x11.default_reply_byte_count, defaults.reply_byte_count);
1021     try std.testing.expectEqual(
1022         defaults.retained_message_bytes + defaults.request_byte_count + defaults.reply_byte_count,
1023         defaults.steady_requested_bytes,
1024     );
1025     try std.testing.expectEqual(
1026         defaults.steady_requested_bytes + defaults.initialization_scratch_byte_count,
1027         defaults.initialization_high_water_bytes,
1028     );
1029 
1030     const selected = try deriveConnectionCapacity(
1031         try event_module.Capacity.derive(.{ .retained_event_count = 2 }),
1032         try clipboard.Capacity.derive(.{
1033             .received_text_byte_count = 7,
1034             .published_text_byte_count = 1024,
1035             .retained_wait_event_count = 3,
1036         }),
1037         4096,
1038         protocol.maximum_keyboard_mapping_reply_byte_count,
1039     );
1040     try std.testing.expectEqual(@as(usize, 5), selected.retained_message_count);
1041     try std.testing.expectEqual(
1042         5 * @sizeOf(x11.Message),
1043         selected.retained_message_bytes,
1044     );
1045     try std.testing.expectEqual(@as(usize, 4096), selected.request_byte_count);
1046     try std.testing.expectEqual(
1047         protocol.maximum_keyboard_mapping_reply_byte_count,
1048         selected.reply_byte_count,
1049     );
1050     try std.testing.expectError(error.RequestStorageTooSmall, deriveConnectionCapacity(
1051         try event_module.Capacity.derive(.{}),
1052         try clipboard.Capacity.derive(.{
1053             .published_text_byte_count = 64,
1054         }),
1055         protocol.minimum_request_byte_count - 4,
1056         x11.default_reply_byte_count,
1057     ));
1058     try std.testing.expectError(error.RequestStorageUnaligned, deriveConnectionCapacity(
1059         try event_module.Capacity.derive(.{}),
1060         try clipboard.Capacity.derive(.{
1061             .published_text_byte_count = 32,
1062         }),
1063         89,
1064         x11.default_reply_byte_count,
1065     ));
1066     try std.testing.expectError(error.ReplyStorageTooSmall, deriveConnectionCapacity(
1067         try event_module.Capacity.derive(.{}),
1068         try clipboard.Capacity.derive(.{}),
1069         protocol.maximum_request_byte_count,
1070         x11.default_reply_byte_count - 4,
1071     ));
1072 }
1073 
1074 test "x11 clipboard reply plan accepts utf8 and targets only" {
1075     try std.testing.expectEqual(
1076         ClipboardReplyKind.utf8,
1077         clipboardReplyKind(11, 11, 12).?,
1078     );
1079     try std.testing.expectEqual(
1080         ClipboardReplyKind.targets,
1081         clipboardReplyKind(12, 11, 12).?,
1082     );
1083     try std.testing.expect(clipboardReplyKind(13, 11, 12) == null);
1084 }
1085 
1086 test "keysym text conversion uses XKB Unicode semantics" {
1087     var buffer: [4]u8 = undefined;
1088     try std.testing.expectEqualStrings("a", textFromKeysym(@fromBackingInt('a'), &buffer).?);
1089     try std.testing.expectEqualStrings("é", textFromKeysym(@fromBackingInt(0xe9), &buffer).?);
1090     try std.testing.expectEqualStrings("λ", textFromKeysym(@fromBackingInt(0x07eb), &buffer).?);
1091     try std.testing.expectEqualStrings("7", textFromKeysym(@fromBackingInt(0xffb7), &buffer).?);
1092     try std.testing.expect(textFromKeysym(@fromBackingInt(0xffe1), &buffer) == null);
1093 }
1094 
1095 test "modifier state maps to windowing modifiers" {
1096     const modifiers = modifiersFromState(protocol.state_mask.shift | protocol.state_mask.control);
1097     try std.testing.expect(modifiers.shift);
1098     try std.testing.expect(modifiers.ctrl);
1099     try std.testing.expect(!modifiers.alt);
1100     try std.testing.expect(!modifiers.super);
1101 }
1102 
1103 test "wheel buttons map to scroll deltas and skip button events" {
1104     try std.testing.expectEqual([2]f32{ 0, 1 }, wheelFromButton(4).?);
1105     try std.testing.expectEqual([2]f32{ 0, -1 }, wheelFromButton(5).?);
1106     try std.testing.expect(wheelFromButton(1) == null);
1107     try std.testing.expectEqual(MouseButton.left, mouseButtonFromCode(1).?);
1108     try std.testing.expectEqual(MouseButton.x2, mouseButtonFromCode(9).?);
1109 }
1110 
1111 test "x11 rgba packing reads rectangular regions from retained frame" {
1112     const width: u32 = 4;
1113     const height: u32 = 3;
1114     var frame: [width * height * 4]u8 = undefined;
1115     for (0..height) |y| {
1116         for (0..width) |x| {
1117             const offset = (y * width + x) * 4;
1118             frame[offset + 0] = @intCast(10 + x);
1119             frame[offset + 1] = @intCast(20 + y);
1120             frame[offset + 2] = @intCast(30 + x + y);
1121             frame[offset + 3] = 0xFF;
1122         }
1123     }
1124 
1125     const region = PresentRegion{ .x = 1, .y = 1, .width = 2, .height = 2 };
1126     var packed_pixels: [2 * 2 * 4]u8 = undefined;
1127     packRegionRgba8(&frame, width, &packed_pixels, region, .{
1128         .endian = .little,
1129         .red_shift = 16,
1130         .green_shift = 8,
1131         .blue_shift = 0,
1132     });
1133 
1134     for (0..region.height) |row| {
1135         for (0..region.width) |column| {
1136             const word = std.mem.readInt(u32, packed_pixels[(row * region.width + column) * 4 ..][0..4], .little);
1137             const source_x: u8 = @intCast(region.x + column);
1138             const source_y: u8 = @intCast(region.y + row);
1139             try std.testing.expectEqual(@as(u8, 10 + source_x), @as(u8, @truncate(word >> 16)));
1140             try std.testing.expectEqual(@as(u8, 20 + source_y), @as(u8, @truncate(word >> 8)));
1141             try std.testing.expectEqual(@as(u8, 30 + source_x + source_y), @as(u8, @truncate(word)));
1142         }
1143     }
1144 }
1145 
1146 test "x11 packed region storage admits its exact bound and rejects max plus one" {
1147     var storage = try PackedRegionStorage.init(std.testing.allocator, 4096);
1148     defer storage.deinit();
1149 
1150     const active = try storage.prepare(4096);
1151     @memset(active, 0xA5);
1152     _ = try storage.prepare(1024);
1153     try std.testing.expectError(error.CapacityExceeded, storage.prepare(4097));
1154     try std.testing.expectEqual(@as(u64, 1), storage.capacity_rejection_count);
1155     try std.testing.expectEqual(@as(u8, 0xA5), storage.bytes[4095]);
1156     storage.capacity_rejection_count = std.math.maxInt(u64);
1157     try std.testing.expectError(error.CapacityExceeded, storage.prepare(4097));
1158     try std.testing.expectEqual(std.math.maxInt(u64), storage.capacity_rejection_count);
1159 }
1160 
1161 test "x11 packed region storage reports acquisition failure" {
1162     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
1163         .fail_index = 0,
1164     });
1165     try std.testing.expectError(
1166         error.OutOfMemory,
1167         PackedRegionStorage.init(failing.allocator(), 4096),
1168     );
1169 }
1170 
1171 test "live window presents pixels the server reads back" {
1172     if (comptime sys.capabilities.current.os != .linux) return error.SkipZigTest;
1173     if (sys.env.get("DISPLAY") == null) return error.SkipZigTest;
1174 
1175     const allocator = std.testing.allocator;
1176     var backend: X11Backend = undefined;
1177     backend.init(allocator, .{
1178         .title = "windowing-x11-live",
1179         .width = 64,
1180         .height = 32,
1181         .backend = .x11,
1182     }) catch |err| switch (err) {
1183         error.ConnectionFailed => return error.SkipZigTest,
1184         else => return err,
1185     };
1186     defer backend.deinit();
1187 
1188     try std.testing.expect(backend.window != 0);
1189     const native = backend.nativeSurface();
1190     try std.testing.expectEqual(surface.Kind.x11, native.kind());
1191     try std.testing.expectEqual(backend.window, native.x11.window);
1192     try std.testing.expectEqual(backend.connection.setup.visual, native.x11.visual);
1193     try std.testing.expectEqual(@as(usize, 64 * 32), native.extent().pixelCount().?);
1194     var mapped_keycodes: usize = 0;
1195     for (backend.keysyms) |columns| {
1196         if (columns[0] != 0) mapped_keycodes += 1;
1197     }
1198     try std.testing.expect(mapped_keycodes > 0);
1199 
1200     var iterator = try backend.pollEvents();
1201     while (iterator.next()) |_| {}
1202 
1203     const width: u32 = 64;
1204     const height: u32 = 32;
1205     const frame = try allocator.alloc(u8, width * height * 4);
1206     defer allocator.free(frame);
1207     for (0..height) |y| {
1208         for (0..width) |x| {
1209             const offset = (y * width + x) * 4;
1210             frame[offset + 0] = @intCast(x * 4);
1211             frame[offset + 1] = @intCast(y * 8);
1212             frame[offset + 2] = 0x40;
1213             frame[offset + 3] = 0xFF;
1214         }
1215     }
1216     try backend.presentRgba8(frame, width, height);
1217 
1218     const request = backend.connection.beginRequest();
1219     try protocol.getImage(request, .{
1220         .drawable = backend.window,
1221         .x = 0,
1222         .y = 0,
1223         .width = width,
1224         .height = height,
1225     });
1226     try backend.connection.sendRequest();
1227     const reply = try backend.connection.waitReply();
1228 
1229     try std.testing.expect(reply.extra.len >= width * height * 4);
1230     const endian: std.builtin.Endian = if (backend.connection.setup.image_byte_order_msb) .big else .little;
1231     var mismatches: usize = 0;
1232     for (0..height) |y| {
1233         for (0..width) |x| {
1234             const offset = (y * width + x) * 4;
1235             const word = std.mem.readInt(u32, reply.extra[offset..][0..4], endian);
1236             const red: u8 = @truncate(word >> backend.red_shift);
1237             const green: u8 = @truncate(word >> backend.green_shift);
1238             const blue: u8 = @truncate(word >> backend.blue_shift);
1239             if (red != frame[offset + 0] or green != frame[offset + 1] or blue != frame[offset + 2]) {
1240                 mismatches += 1;
1241             }
1242         }
1243     }
1244     try std.testing.expectEqual(@as(usize, 0), mismatches);
1245 
1246     const dirty = PresentRegion{ .x = 11, .y = 7, .width = 9, .height = 6 };
1247     for (0..height) |y| {
1248         for (0..width) |x| {
1249             const offset = (y * width + x) * 4;
1250             const inside = x >= dirty.x and x < dirty.x + dirty.width and y >= dirty.y and y < dirty.y + dirty.height;
1251             if (inside) {
1252                 frame[offset + 0] = 0xE1;
1253                 frame[offset + 1] = 0x72;
1254                 frame[offset + 2] = 0x29;
1255             } else {
1256                 frame[offset + 0] = 0x11;
1257                 frame[offset + 1] = 0x22;
1258                 frame[offset + 2] = 0x33;
1259             }
1260             frame[offset + 3] = 0xFF;
1261         }
1262     }
1263     try backend.presentRgba8Region(frame, width, height, dirty);
1264 
1265     const region_request = backend.connection.beginRequest();
1266     try protocol.getImage(region_request, .{
1267         .drawable = backend.window,
1268         .x = 0,
1269         .y = 0,
1270         .width = width,
1271         .height = height,
1272     });
1273     try backend.connection.sendRequest();
1274     const region_reply = try backend.connection.waitReply();
1275 
1276     var region_mismatches: usize = 0;
1277     var outside_mutations: usize = 0;
1278     for (0..height) |y| {
1279         for (0..width) |x| {
1280             const offset = (y * width + x) * 4;
1281             const word = std.mem.readInt(u32, region_reply.extra[offset..][0..4], endian);
1282             const red: u8 = @truncate(word >> backend.red_shift);
1283             const green: u8 = @truncate(word >> backend.green_shift);
1284             const blue: u8 = @truncate(word >> backend.blue_shift);
1285             const inside = x >= dirty.x and x < dirty.x + dirty.width and y >= dirty.y and y < dirty.y + dirty.height;
1286             if (inside) {
1287                 if (red != 0xE1 or green != 0x72 or blue != 0x29) region_mismatches += 1;
1288             } else if (red != @as(u8, @intCast(x * 4)) or green != @as(u8, @intCast(y * 8)) or blue != 0x40) {
1289                 outside_mutations += 1;
1290             }
1291         }
1292     }
1293     try std.testing.expectEqual(@as(usize, 0), region_mismatches);
1294     try std.testing.expectEqual(@as(usize, 0), outside_mutations);
1295 
1296     backend.setTitle("windowing-x11-live-renamed");
1297     backend.setCursor(.resize_ew);
1298     _ = backend.isMaximized();
1299     var post = try backend.pollEvents();
1300     while (post.next()) |_| {}
1301 
1302     var keycode_w: u8 = 0;
1303     for (backend.keysyms, 0..) |columns, code| {
1304         if (columns[0] == 'w') {
1305             keycode_w = @intCast(code);
1306             break;
1307         }
1308     }
1309     try std.testing.expect(keycode_w != 0);
1310 
1311     var raw = @as([protocol.message_length]u8, @splat(0));
1312     raw[0] = @backingInt(protocol.EventCode.key_press);
1313     raw[1] = keycode_w;
1314     std.mem.writeInt(u32, raw[12..16], backend.window, .little);
1315     const inject = backend.connection.beginRequest();
1316     try protocol.sendEvent(
1317         inject,
1318         backend.window,
1319         protocol.event_mask.key_press,
1320         &raw,
1321     );
1322     try backend.connection.sendRequest();
1323 
1324     var saw_key_press = false;
1325     var attempts: usize = 0;
1326     while (attempts < 20 and !saw_key_press) : (attempts += 1) {
1327         _ = try backend.connection.waitReadable(50);
1328         var pump = try backend.pollEvents();
1329         while (pump.next()) |event| {
1330             switch (event) {
1331                 .key_press => |key| {
1332                     if (key.key == .w) saw_key_press = true;
1333                 },
1334                 else => {},
1335             }
1336         }
1337         if (backend.input.isKeyDown(.w)) break;
1338     }
1339     try std.testing.expect(saw_key_press);
1340     try std.testing.expect(backend.input.isKeyDown(.w));
1341 }