lib/windowing/src/window.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const clipboard = @import("clipboard.zig");
4 const EventIterator = @import("event.zig").EventIterator;
5 const WindowConfig = @import("config.zig").WindowConfig;
6 const Backend = @import("backend.zig").Backend;
7 const input = @import("input.zig");
8 const Key = @import("key.zig").Key;
9 const MouseButton = @import("mouse.zig").MouseButton;
10 const Modifier = @import("modifier.zig").Modifier;
11 const CursorShape = @import("cursor.zig").CursorShape;
12 const gamepads = @import("gamepad.zig");
13 const presentation = @import("presentation.zig");
14 const surface = @import("surface.zig");
15 const WaylandStorageStatus = @import("wayland").runtime.StorageStatus;
16 const WaylandDmabuf = @import("wayland/root.zig").DmabufSurface;
17
18 pub const Size = struct {
19 width: u32,
20 height: u32,
21 };
22
23 pub const Scale = struct {
24 x: f32,
25 y: f32,
26 };
27
28 pub const EventSource = struct {
29 descriptor: sys.fd.Descriptor,
30 readable: bool = true,
31 writable: bool = false,
32 };
33
34 pub const PresentRegion = struct {
35 x: u32 = 0,
36 y: u32 = 0,
37 width: u32 = 0,
38 height: u32 = 0,
39
40 pub fn full(width: u32, height: u32) PresentRegion {
41 return .{ .width = width, .height = height };
42 }
43
44 pub fn pixelCount(self: PresentRegion) usize {
45 return @as(usize, self.width) * @as(usize, self.height);
46 }
47
48 pub fn clamped(self: PresentRegion, width: u32, height: u32) PresentRegion {
49 if (self.x >= width or self.y >= height) return .{ .x = self.x, .y = self.y };
50 return .{
51 .x = self.x,
52 .y = self.y,
53 .width = @min(self.width, width - self.x),
54 .height = @min(self.height, height - self.y),
55 };
56 }
57 };
58
59 pub const CreateError = error{
60 ConnectionFailed,
61 InvalidCapacity,
62 WindowCreationFailed,
63 UnsupportedPlatform,
64 OutOfMemory,
65 };
66
67 pub const PollError = error{
68 ConnectionLost,
69 PresentationFailed,
70 };
71
72 pub const PresentError = error{
73 UnsupportedPlatform,
74 UnsupportedFormat,
75 CapacityExceeded,
76 Unexpected,
77 OutOfMemory,
78 /// The Wayland surface presents dma-bufs with explicit synchronization, and every commit of
79 /// it must carry sync points that a shared-memory frame lacks.
80 ExplicitSyncSurface,
81 };
82
83 pub const Window = struct {
84 allocator: std.mem.Allocator,
85 backend: *Backend,
86
87 pub fn create(allocator: std.mem.Allocator, config: WindowConfig) CreateError!Window {
88 const backend = allocator.create(Backend) catch return error.OutOfMemory;
89 errdefer allocator.destroy(backend);
90 try backend.init(allocator, config);
91 return .{ .allocator = allocator, .backend = backend };
92 }
93
94 pub fn destroy(self: *Window) void {
95 self.backend.deinit();
96 self.allocator.destroy(self.backend);
97 self.* = undefined;
98 }
99
100 pub fn pollEvents(self: *Window) PollError!EventIterator {
101 return self.backend.pollEvents();
102 }
103
104 pub fn droppedEventCount(self: *const Window) u64 {
105 return self.backend.droppedEventCount();
106 }
107
108 pub fn eventSource(self: *const Window) ?EventSource {
109 return self.backend.eventSource();
110 }
111
112 pub fn shouldClose(self: *const Window) bool {
113 return self.backend.shouldClose();
114 }
115
116 pub fn getSize(self: *const Window) Size {
117 return self.backend.getSize();
118 }
119
120 pub fn getFramebufferSize(self: *const Window) Size {
121 return self.backend.getFramebufferSize();
122 }
123
124 pub fn getDrawArea(self: *const Window) sys.desktop.DrawArea {
125 return sizeToDrawArea(self.getSize());
126 }
127
128 pub fn getFramebufferArea(self: *const Window) sys.desktop.DrawArea {
129 return sizeToDrawArea(self.getFramebufferSize());
130 }
131
132 pub fn getNativeSurface(self: *Window) surface.Surface {
133 return self.backend.nativeSurface();
134 }
135
136 pub fn getScaleEvidence(self: *const Window) sys.desktop.WindowScaleEvidence {
137 return scaleEvidenceFromSizes(self.getSize(), self.getFramebufferSize(), self.getContentScale());
138 }
139
140 pub fn presentRgba8(
141 self: *Window,
142 rgba8: []const u8,
143 width: u32,
144 height: u32,
145 ) PresentError!void {
146 return self.backend.presentRgba8(rgba8, width, height);
147 }
148
149 pub fn presentRgba8Region(
150 self: *Window,
151 rgba8: []const u8,
152 width: u32,
153 height: u32,
154 region: PresentRegion,
155 ) PresentError!void {
156 return self.backend.presentRgba8Region(rgba8, width, height, region);
157 }
158
159 pub fn getContentScale(self: *const Window) Scale {
160 return self.backend.getContentScale();
161 }
162
163 pub fn isKeyDown(self: *const Window, key: Key) bool {
164 return self.backend.inputState().isKeyDown(key);
165 }
166
167 pub fn isKeyPressed(self: *const Window, key: Key) bool {
168 return self.backend.inputState().isKeyPressed(key);
169 }
170
171 pub fn isKeyReleased(self: *const Window, key: Key) bool {
172 return self.backend.inputState().isKeyReleased(key);
173 }
174
175 pub fn nextPressedKey(self: *Window) ?Key {
176 return self.backend.inputStateMut().nextPressedKey();
177 }
178
179 pub fn modifiers(self: *const Window) Modifier {
180 return self.backend.modifiers();
181 }
182
183 pub fn isMouseButtonDown(self: *const Window, button: MouseButton) bool {
184 return self.backend.inputState().isMouseButtonDown(button);
185 }
186
187 pub fn isMouseButtonPressed(self: *const Window, button: MouseButton) bool {
188 return self.backend.inputState().isMouseButtonPressed(button);
189 }
190
191 pub fn isMouseButtonReleased(self: *const Window, button: MouseButton) bool {
192 return self.backend.inputState().isMouseButtonReleased(button);
193 }
194
195 pub fn getMousePosition(self: *const Window) input.Point {
196 return self.backend.inputState().mouse_position;
197 }
198
199 pub fn getMouseDelta(self: *const Window) input.Point {
200 return self.backend.inputState().mouse_delta;
201 }
202
203 pub fn getMouseWheel(self: *const Window) input.Wheel {
204 return self.backend.inputState().mouse_wheel;
205 }
206
207 pub fn nextTextCodepoint(self: *Window) ?u21 {
208 return self.backend.inputStateMut().nextTextCodepoint();
209 }
210
211 pub fn inputStatus(self: *const Window) input.Status {
212 return self.backend.inputState().status();
213 }
214
215 pub fn setCursor(self: *Window, shape: CursorShape) void {
216 self.backend.setCursor(shape);
217 }
218
219 pub fn isCursorOnScreen(self: *const Window) bool {
220 return self.backend.isCursorOnScreen();
221 }
222
223 pub fn setTitle(self: *Window, title: [:0]const u8) void {
224 self.backend.setTitle(title);
225 }
226
227 pub fn setMinSize(self: *Window, width: u32, height: u32) void {
228 self.backend.setMinSize(width, height);
229 }
230
231 pub fn minimize(self: *Window) void {
232 self.backend.minimize();
233 }
234
235 pub fn maximize(self: *Window) void {
236 self.backend.maximize();
237 }
238
239 pub fn restore(self: *Window) void {
240 self.backend.restore();
241 }
242
243 pub fn isMaximized(self: *Window) bool {
244 return self.backend.isMaximized();
245 }
246
247 pub fn toggleMaximize(self: *Window) void {
248 if (self.isMaximized()) {
249 self.restore();
250 } else {
251 self.maximize();
252 }
253 }
254
255 pub fn getClipboardTextAlloc(self: *Window, allocator: std.mem.Allocator) !?[]u8 {
256 return self.backend.getClipboardTextAlloc(allocator);
257 }
258
259 pub fn setClipboardText(self: *Window, text: []const u8) clipboard.PublishError!void {
260 return self.backend.setClipboardText(text);
261 }
262
263 pub fn clipboardStatus(self: *const Window) clipboard.Status {
264 return self.backend.clipboardStatus();
265 }
266
267 pub fn x11StorageStatus(self: *const Window) sys.x11.Status {
268 return self.backend.x11StorageStatus();
269 }
270
271 /// The window's dma-buf presentation with explicit synchronization, when it is a Wayland
272 /// window and the compositor offers `zwp_linux_dmabuf_v1` 4 or later and
273 /// `wp_linux_drm_syncobj_manager_v1`. Its first commit makes shared-memory presents of the
274 /// window fail with `ExplicitSyncSurface`.
275 pub fn waylandDmabuf(self: *Window) ?WaylandDmabuf {
276 return self.backend.waylandDmabuf();
277 }
278
279 pub fn waylandStorageStatus(self: *const Window) WaylandStorageStatus {
280 return self.backend.waylandStorageStatus();
281 }
282
283 pub fn nextPresentationOutcome(self: *Window) ?presentation.Outcome {
284 return self.backend.nextPresentationOutcome();
285 }
286
287 pub fn presentationStatus(self: *const Window) presentation.Status {
288 return self.backend.presentationStatus();
289 }
290
291 pub fn presentationStorageStatus(self: *const Window) presentation.StorageStatus {
292 return self.backend.presentationStorageStatus();
293 }
294
295 pub fn gamepadCount(self: *const Window) usize {
296 return self.backend.gamepadState().connectedCount();
297 }
298
299 pub fn gamepad(self: *const Window, index: usize) *const gamepads.Gamepad {
300 return self.backend.gamepadState().gamepad(index);
301 }
302
303 pub fn gamepadStatus(self: *const Window) gamepads.Status {
304 return self.backend.gamepadState().status();
305 }
306 };
307
308 pub fn scaleEvidenceFromSizes(size: Size, framebuffer: Size, content_scale: Scale) sys.desktop.WindowScaleEvidence {
309 return .{
310 .has_window_content_scale = true,
311 .window_display = usableScale(@max(content_scale.x, content_scale.y)),
312 .display_content = usableScale(@max(content_scale.x, content_scale.y)),
313 .pixel_density = sys.desktop.framebufferScaleFromDimensions(
314 @floatFromInt(size.width),
315 @floatFromInt(size.height),
316 @floatFromInt(framebuffer.width),
317 @floatFromInt(framebuffer.height),
318 ) orelse 1.0,
319 };
320 }
321
322 fn sizeToDrawArea(size: Size) sys.desktop.DrawArea {
323 return .{
324 .width = u32ToI32(size.width),
325 .height = u32ToI32(size.height),
326 };
327 }
328
329 fn u32ToI32(value: u32) i32 {
330 return if (value > @as(u32, @intCast(std.math.maxInt(i32)))) std.math.maxInt(i32) else @intCast(value);
331 }
332
333 fn usableScale(scale: f32) f32 {
334 if (!std.math.isFinite(scale) or scale <= 0) return 1.0;
335 return scale;
336 }
337
338 test "scale evidence derives pixel density from framebuffer dimensions" {
339 const evidence = scaleEvidenceFromSizes(
340 .{ .width = 640, .height = 480 },
341 .{ .width = 1280, .height = 960 },
342 .{ .x = 2, .y = 2 },
343 );
344 try std.testing.expect(evidence.has_window_content_scale);
345 try std.testing.expectEqual(@as(f32, 2), evidence.display_content);
346 try std.testing.expectEqual(@as(f32, 2), evidence.pixel_density);
347 }
348
349 test "draw areas clamp to signed GUI dimensions" {
350 try std.testing.expectEqual(
351 sys.desktop.DrawArea{ .width = 10, .height = std.math.maxInt(i32) },
352 sizeToDrawArea(.{ .width = 10, .height = std.math.maxInt(u32) }),
353 );
354 }
355
356 test "present region clamps to framebuffer bounds" {
357 try std.testing.expectEqual(
358 PresentRegion{ .x = 4, .y = 3, .width = 6, .height = 5 },
359 (PresentRegion{ .x = 4, .y = 3, .width = 20, .height = 20 }).clamped(10, 8),
360 );
361 try std.testing.expectEqual(
362 PresentRegion{ .x = 12, .y = 3 },
363 (PresentRegion{ .x = 12, .y = 3, .width = 2, .height = 2 }).clamped(10, 8),
364 );
365 try std.testing.expectEqual(@as(usize, 30), (PresentRegion{ .width = 6, .height = 5 }).pixelCount());
366 }