lib/windowing/src/backend.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3 const sys = @import("sys");
4 const clipboard = @import("clipboard.zig");
5 const EventIterator = @import("event.zig").EventIterator;
6 const CursorShape = @import("cursor.zig").CursorShape;
7 const gamepad = @import("gamepad.zig");
8 const input = @import("input.zig");
9 const Modifier = @import("modifier.zig").Modifier;
10 const surface = @import("surface.zig");
11 const presentation = @import("presentation.zig");
12 const BackendPreference = @import("config.zig").BackendPreference;
13 const WindowConfig = @import("config.zig").WindowConfig;
14 const window = @import("window.zig");
15 const WaylandStorageStatus = @import("wayland").runtime.StorageStatus;
16
17 const Allocator = std.mem.Allocator;
18 const CocoaBackend = @import("cocoa.zig").CocoaBackend;
19 const UnsupportedBackend = @import("unsupported.zig").UnsupportedBackend;
20 const WaylandBackend = @import("wayland/root.zig").WaylandBackend;
21 const WaylandDmabuf = @import("wayland/root.zig").DmabufSurface;
22 const X11Backend = @import("x11.zig").X11Backend;
23
24 pub const Backend = switch (builtin.os.tag) {
25 .linux => LinuxBackend,
26 .macos => CocoaBackend,
27 else => UnsupportedBackend,
28 };
29
30 const SelectionOrder = struct {
31 first: BackendPreference,
32 fallback: ?BackendPreference,
33 };
34
35 pub const LinuxBackend = union(enum) {
36 wayland: WaylandBackend,
37 x11: X11Backend,
38
39 pub fn init(self: *LinuxBackend, allocator: Allocator, config: WindowConfig) window.CreateError!void {
40 const order = selectionOrder(config.backend, waylandAdvertised());
41 self.initPreferred(allocator, config, order.first) catch |err| {
42 const fallback = order.fallback orelse return err;
43 if (!shouldFallback(err)) return err;
44 return self.initPreferred(allocator, config, fallback);
45 };
46 }
47
48 fn initPreferred(self: *LinuxBackend, allocator: Allocator, config: WindowConfig, preference: BackendPreference) window.CreateError!void {
49 switch (preference) {
50 .auto => unreachable,
51 .wayland => {
52 self.* = .{ .wayland = undefined };
53 try self.wayland.init(allocator, config);
54 },
55 .x11 => {
56 self.* = .{ .x11 = undefined };
57 try self.x11.init(allocator, config);
58 },
59 }
60 }
61
62 pub fn deinit(self: *LinuxBackend) void {
63 switch (self.*) {
64 .wayland => |*backend| backend.deinit(),
65 .x11 => |*backend| backend.deinit(),
66 }
67 }
68
69 pub fn pollEvents(self: *LinuxBackend) window.PollError!EventIterator {
70 return switch (self.*) {
71 .wayland => |*backend| backend.pollEvents(),
72 .x11 => |*backend| backend.pollEvents(),
73 };
74 }
75
76 pub fn droppedEventCount(self: *const LinuxBackend) u64 {
77 return switch (self.*) {
78 .wayland => |*backend| backend.droppedEventCount(),
79 .x11 => |*backend| backend.droppedEventCount(),
80 };
81 }
82
83 pub fn eventSource(self: *const LinuxBackend) ?window.EventSource {
84 return switch (self.*) {
85 .wayland => |*backend| backend.eventSource(),
86 .x11 => |*backend| backend.eventSource(),
87 };
88 }
89
90 pub fn shouldClose(self: *const LinuxBackend) bool {
91 return switch (self.*) {
92 .wayland => |*backend| backend.shouldClose(),
93 .x11 => |*backend| backend.shouldClose(),
94 };
95 }
96
97 pub fn getSize(self: *const LinuxBackend) window.Size {
98 return switch (self.*) {
99 .wayland => |*backend| backend.getSize(),
100 .x11 => |*backend| backend.getSize(),
101 };
102 }
103
104 pub fn getFramebufferSize(self: *const LinuxBackend) window.Size {
105 return switch (self.*) {
106 .wayland => |*backend| backend.getFramebufferSize(),
107 .x11 => |*backend| backend.getFramebufferSize(),
108 };
109 }
110
111 pub fn nativeSurface(self: *LinuxBackend) surface.Surface {
112 return switch (self.*) {
113 .wayland => |*backend| backend.nativeSurface(),
114 .x11 => |*backend| backend.nativeSurface(),
115 };
116 }
117
118 pub fn presentRgba8(
119 self: *LinuxBackend,
120 rgba8: []const u8,
121 width: u32,
122 height: u32,
123 ) window.PresentError!void {
124 return switch (self.*) {
125 .wayland => |*backend| backend.presentRgba8(rgba8, width, height),
126 .x11 => |*backend| backend.presentRgba8(rgba8, width, height),
127 };
128 }
129
130 pub fn presentRgba8Region(
131 self: *LinuxBackend,
132 rgba8: []const u8,
133 width: u32,
134 height: u32,
135 region: window.PresentRegion,
136 ) window.PresentError!void {
137 return switch (self.*) {
138 .wayland => |*backend| backend.presentRgba8Region(rgba8, width, height, region),
139 .x11 => |*backend| backend.presentRgba8Region(rgba8, width, height, region),
140 };
141 }
142
143 pub fn getContentScale(self: *const LinuxBackend) window.Scale {
144 return switch (self.*) {
145 .wayland => |*backend| backend.getContentScale(),
146 .x11 => |*backend| backend.getContentScale(),
147 };
148 }
149
150 pub fn inputState(self: *const LinuxBackend) *const input.State {
151 return switch (self.*) {
152 .wayland => |*backend| backend.inputState(),
153 .x11 => |*backend| backend.inputState(),
154 };
155 }
156
157 pub fn inputStateMut(self: *LinuxBackend) *input.State {
158 return switch (self.*) {
159 .wayland => |*backend| backend.inputStateMut(),
160 .x11 => |*backend| backend.inputStateMut(),
161 };
162 }
163
164 pub fn modifiers(self: *const LinuxBackend) Modifier {
165 return switch (self.*) {
166 .wayland => |*backend| backend.modifiers(),
167 .x11 => |*backend| backend.inputState().modifiers(),
168 };
169 }
170
171 pub fn gamepadState(self: *const LinuxBackend) *const gamepad.State {
172 return switch (self.*) {
173 .wayland => |*backend| backend.gamepadState(),
174 .x11 => |*backend| backend.gamepadState(),
175 };
176 }
177
178 pub fn setCursor(self: *LinuxBackend, shape: CursorShape) void {
179 switch (self.*) {
180 .wayland => |*backend| backend.setCursor(shape),
181 .x11 => |*backend| backend.setCursor(shape),
182 }
183 }
184
185 pub fn isCursorOnScreen(self: *const LinuxBackend) bool {
186 return switch (self.*) {
187 .wayland => |*backend| backend.isCursorOnScreen(),
188 .x11 => |*backend| backend.isCursorOnScreen(),
189 };
190 }
191
192 pub fn setTitle(self: *LinuxBackend, title: [:0]const u8) void {
193 switch (self.*) {
194 .wayland => |*backend| backend.setTitle(title),
195 .x11 => |*backend| backend.setTitle(title),
196 }
197 }
198
199 pub fn setMinSize(self: *LinuxBackend, width: u32, height: u32) void {
200 switch (self.*) {
201 .wayland => |*backend| backend.setMinSize(width, height),
202 .x11 => |*backend| backend.setMinSize(width, height),
203 }
204 }
205
206 pub fn minimize(self: *LinuxBackend) void {
207 switch (self.*) {
208 .wayland => |*backend| backend.minimize(),
209 .x11 => |*backend| backend.minimize(),
210 }
211 }
212
213 pub fn maximize(self: *LinuxBackend) void {
214 switch (self.*) {
215 .wayland => |*backend| backend.maximize(),
216 .x11 => |*backend| backend.maximize(),
217 }
218 }
219
220 pub fn restore(self: *LinuxBackend) void {
221 switch (self.*) {
222 .wayland => |*backend| backend.restore(),
223 .x11 => |*backend| backend.restore(),
224 }
225 }
226
227 pub fn isMaximized(self: *LinuxBackend) bool {
228 return switch (self.*) {
229 .wayland => |*backend| backend.isMaximized(),
230 .x11 => |*backend| backend.isMaximized(),
231 };
232 }
233
234 pub fn getClipboardTextAlloc(self: *LinuxBackend, allocator: Allocator) !?[]u8 {
235 return switch (self.*) {
236 .wayland => |*backend| backend.getClipboardTextAlloc(allocator),
237 .x11 => |*backend| backend.getClipboardTextAlloc(allocator),
238 };
239 }
240
241 pub fn setClipboardText(self: *LinuxBackend, text: []const u8) clipboard.PublishError!void {
242 return switch (self.*) {
243 .wayland => |*backend| backend.setClipboardText(text),
244 .x11 => |*backend| backend.setClipboardText(text),
245 };
246 }
247
248 pub fn clipboardStatus(self: *const LinuxBackend) clipboard.Status {
249 return switch (self.*) {
250 .wayland => |*backend| backend.clipboardStatus(),
251 .x11 => |*backend| backend.clipboardStatus(),
252 };
253 }
254
255 pub fn x11StorageStatus(self: *const LinuxBackend) sys.x11.Status {
256 return switch (self.*) {
257 .wayland => |*backend| backend.x11StorageStatus(),
258 .x11 => |*backend| backend.x11StorageStatus(),
259 };
260 }
261
262 pub fn waylandDmabuf(self: *LinuxBackend) ?WaylandDmabuf {
263 return switch (self.*) {
264 .wayland => |*backend| backend.waylandDmabuf(),
265 .x11 => null,
266 };
267 }
268
269 pub fn waylandStorageStatus(self: *const LinuxBackend) WaylandStorageStatus {
270 return switch (self.*) {
271 .wayland => |*backend| backend.waylandStorageStatus(),
272 .x11 => |*backend| backend.waylandStorageStatus(),
273 };
274 }
275
276 pub fn nextPresentationOutcome(self: *LinuxBackend) ?presentation.Outcome {
277 return switch (self.*) {
278 .wayland => |*backend| backend.nextPresentationOutcome(),
279 .x11 => |*backend| backend.nextPresentationOutcome(),
280 };
281 }
282
283 pub fn presentationStatus(self: *const LinuxBackend) presentation.Status {
284 return switch (self.*) {
285 .wayland => |*backend| backend.presentationStatus(),
286 .x11 => |*backend| backend.presentationStatus(),
287 };
288 }
289
290 pub fn presentationStorageStatus(self: *const LinuxBackend) presentation.StorageStatus {
291 return switch (self.*) {
292 .wayland => |*backend| backend.presentationStorageStatus(),
293 .x11 => |*backend| backend.presentationStorageStatus(),
294 };
295 }
296 };
297
298 fn waylandAdvertised() bool {
299 if (sys.env.contains("WAYLAND_DISPLAY") or sys.env.contains("WAYLAND_SOCKET")) return true;
300 return if (sys.env.getConstant("XDG_SESSION_TYPE")) |session_type|
301 std.mem.eql(u8, session_type, "wayland")
302 else
303 false;
304 }
305
306 fn shouldFallback(failure: window.CreateError) bool {
307 return switch (failure) {
308 error.ConnectionFailed, error.WindowCreationFailed, error.UnsupportedPlatform => true,
309 error.InvalidCapacity, error.OutOfMemory => false,
310 };
311 }
312
313 fn selectionOrder(preference: BackendPreference, wayland_display_present: bool) SelectionOrder {
314 return switch (preference) {
315 .auto => if (wayland_display_present)
316 .{ .first = .wayland, .fallback = .x11 }
317 else
318 .{ .first = .x11, .fallback = null },
319 .wayland => .{ .first = .wayland, .fallback = null },
320 .x11 => .{ .first = .x11, .fallback = null },
321 };
322 }
323
324 test "Linux backend selection prefers Wayland only when advertised" {
325 try std.testing.expectEqual(
326 SelectionOrder{ .first = .wayland, .fallback = .x11 },
327 selectionOrder(.auto, true),
328 );
329 try std.testing.expectEqual(
330 SelectionOrder{ .first = .x11, .fallback = null },
331 selectionOrder(.auto, false),
332 );
333 }
334
335 test "explicit Linux backend selections never redirect" {
336 try std.testing.expectEqual(
337 SelectionOrder{ .first = .wayland, .fallback = null },
338 selectionOrder(.wayland, false),
339 );
340 try std.testing.expectEqual(
341 SelectionOrder{ .first = .x11, .fallback = null },
342 selectionOrder(.x11, true),
343 );
344 }
345
346 test "automatic Linux fallback preserves allocation failures" {
347 try std.testing.expect(shouldFallback(error.ConnectionFailed));
348 try std.testing.expect(shouldFallback(error.WindowCreationFailed));
349 try std.testing.expect(shouldFallback(error.UnsupportedPlatform));
350 try std.testing.expect(!shouldFallback(error.InvalidCapacity));
351 try std.testing.expect(!shouldFallback(error.OutOfMemory));
352 }
353
354 test "live automatic unreachable Wayland falls back to invisible X11 window under Xvfb" {
355 if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
356 const marker = sys.env.getConstant("TINY_WINDOWING_LIVE_AUTO_FALLBACK") orelse return error.SkipZigTest;
357 if (!std.mem.eql(u8, marker, "1")) return error.SkipZigTest;
358
359 try std.testing.expect(sys.env.get("WAYLAND_DISPLAY") == null);
360 try std.testing.expect(sys.env.get("WAYLAND_SOCKET") == null);
361 try std.testing.expectEqualStrings(
362 "wayland",
363 sys.env.getConstant("XDG_SESSION_TYPE") orelse return error.TestUnexpectedResult,
364 );
365 try std.testing.expectEqualStrings(
366 "/tmp/windowing-test-missing-runtime",
367 sys.env.getConstant("XDG_RUNTIME_DIR") orelse return error.TestUnexpectedResult,
368 );
369 try std.testing.expect(sys.env.get("DISPLAY") != null);
370
371 const allocator = std.testing.allocator;
372 var live_window = try window.Window.create(allocator, .{
373 .title = "windowing-auto-fallback-live",
374 .width = 37,
375 .height = 19,
376 .visible = false,
377 .backend = .auto,
378 });
379 defer live_window.destroy();
380
381 const native = live_window.getNativeSurface();
382 try std.testing.expectEqual(surface.Kind.x11, native.kind());
383 const extent = native.extent();
384 try std.testing.expectEqual(@as(u32, 37), extent.width);
385 try std.testing.expectEqual(@as(u32, 19), extent.height);
386 try std.testing.expectEqual(@as(u32, 37), extent.framebuffer_width);
387 try std.testing.expectEqual(@as(u32, 19), extent.framebuffer_height);
388 }