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 }