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 }