lib/windowing/src/cocoa.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 event_module = @import("event.zig");
6 const Event = event_module.Event;
7 const MouseButtonEvent = event_module.MouseButtonEvent;
8 const CursorShape = @import("cursor.zig").CursorShape;
9 const gamepad = @import("gamepad.zig");
10 const input = @import("input.zig");
11 const Key = @import("key.zig").Key;
12 const MouseButton = @import("mouse.zig").MouseButton;
13 const Modifier = @import("modifier.zig").Modifier;
14 const surface = @import("surface.zig");
15 const WindowConfig = @import("config.zig").WindowConfig;
16 const CreateError = @import("window.zig").CreateError;
17 const Size = @import("window.zig").Size;
18 const Scale = @import("window.zig").Scale;
19 const PollError = @import("window.zig").PollError;
20 const PresentError = @import("window.zig").PresentError;
21 const PresentRegion = @import("window.zig").PresentRegion;
22 const EventIterator = event_module.EventIterator;
23 const presentation = @import("presentation.zig");
24 const PresentationOutcome = presentation.Outcome;
25 const PresentationStatus = presentation.Status;
26 const PresentationStorageStatus = presentation.StorageStatus;
27 const WaylandStorageStatus = @import("wayland").runtime.StorageStatus;
28
29 const cocoa = sys.apple.cocoa;
30 const foundation = sys.apple.foundation;
31 const coregraphics = sys.apple.coregraphics;
32 const objc = sys.apple.objc;
33
34 fn cls(name: [*:0]const u8) CreateError!objc.Id {
35 return objc.class(name) orelse return error.WindowCreationFailed;
36 }
37
38 const window_delegate_class_name: [*:0]const u8 = "TinyWindowingWindowDelegate";
39 const window_delegate_backend_ivar: [*:0]const u8 = "tinyBackend";
40 const window_delegate_bool_method: [*:0]const u8 = if (objc.BOOL == bool) "B@:@" else "c@:@";
41 const window_delegate_void_method: [*:0]const u8 = "v@:@";
42
43 fn windowDelegateClass() CreateError!objc.Class {
44 if (objc.lookupClass(window_delegate_class_name)) |class_value| return class_value;
45
46 const superclass = objc.lookupClass("NSObject") orelse return error.WindowCreationFailed;
47 const class_value = objc.allocateClassPair(
48 superclass,
49 window_delegate_class_name,
50 ) orelse return error.WindowCreationFailed;
51 errdefer objc.disposeClassPair(class_value);
52 if (!objc.addPointerIvar(class_value, window_delegate_backend_ivar)) {
53 return error.WindowCreationFailed;
54 }
55 if (!objc.addMethod(
56 class_value,
57 objc.selector("windowShouldClose:"),
58 @ptrCast(&windowShouldCloseCallback),
59 window_delegate_bool_method,
60 )) return error.WindowCreationFailed;
61 if (!objc.addMethod(
62 class_value,
63 objc.selector("windowDidBecomeKey:"),
64 @ptrCast(&windowDidBecomeKeyCallback),
65 window_delegate_void_method,
66 )) return error.WindowCreationFailed;
67 if (!objc.addMethod(
68 class_value,
69 objc.selector("windowDidResignKey:"),
70 @ptrCast(&windowDidResignKeyCallback),
71 window_delegate_void_method,
72 )) return error.WindowCreationFailed;
73 objc.registerClassPair(class_value);
74 return class_value;
75 }
76
77 fn delegateBackend(delegate: objc.Id) *CocoaBackend {
78 const pointer = objc.instancePointer(delegate, window_delegate_backend_ivar).?;
79 return @ptrCast(@alignCast(pointer));
80 }
81
82 fn windowShouldCloseCallback(
83 delegate: objc.Id,
84 _: objc.SEL,
85 _: objc.Id,
86 ) callconv(.c) objc.BOOL {
87 delegateBackend(delegate).recordClose();
88 return objc.boolean(true);
89 }
90
91 fn windowDidBecomeKeyCallback(
92 delegate: objc.Id,
93 _: objc.SEL,
94 _: objc.Id,
95 ) callconv(.c) void {
96 delegateBackend(delegate).recordFocus(true);
97 }
98
99 fn windowDidResignKeyCallback(
100 delegate: objc.Id,
101 _: objc.SEL,
102 _: objc.Id,
103 ) callconv(.c) void {
104 delegateBackend(delegate).recordFocus(false);
105 }
106
107 const Selectors = struct {
108 shared_application: objc.SEL,
109 set_activation_policy: objc.SEL,
110 alloc: objc.SEL,
111 object_init: objc.SEL,
112 init_with_content_rect: objc.SEL,
113 set_delegate: objc.SEL,
114 set_released_when_closed: objc.SEL,
115 set_title: objc.SEL,
116 set_content_min_size: objc.SEL,
117 backing_scale_factor: objc.SEL,
118 perform_miniaturize: objc.SEL,
119 zoom: objc.SEL,
120 is_zoomed: objc.SEL,
121 mouse_location_outside_of_event_stream: objc.SEL,
122 make_key_and_order_front: objc.SEL,
123 next_event: objc.SEL,
124 send_event: objc.SEL,
125 event_type: objc.SEL,
126 key_code: objc.SEL,
127 modifier_flags: objc.SEL,
128 is_a_repeat: objc.SEL,
129 button_number: objc.SEL,
130 location_in_window: objc.SEL,
131 scrolling_delta_x: objc.SEL,
132 scrolling_delta_y: objc.SEL,
133 characters: objc.SEL,
134 utf8_string: objc.SEL,
135 general_pasteboard: objc.SEL,
136 string_for_type: objc.SEL,
137 clear_contents: objc.SEL,
138 set_string_for_type: objc.SEL,
139 distant_past: objc.SEL,
140 close: objc.SEL,
141 content_view: objc.SEL,
142 frame: objc.SEL,
143 set_wants_layer: objc.SEL,
144 layer: objc.SEL,
145 set_contents: objc.SEL,
146 set_contents_scale: objc.SEL,
147 finish_launching: objc.SEL,
148 init_with_utf8: objc.SEL,
149 arrow_cursor: objc.SEL,
150 resize_up_down_cursor: objc.SEL,
151 resize_left_right_cursor: objc.SEL,
152 set_cursor: objc.SEL,
153
154 fn init() Selectors {
155 return .{
156 .shared_application = objc.selector("sharedApplication"),
157 .set_activation_policy = objc.selector("setActivationPolicy:"),
158 .alloc = objc.selector("alloc"),
159 .object_init = objc.selector("init"),
160 .init_with_content_rect = objc.selector("initWithContentRect:styleMask:backing:defer:"),
161 .set_delegate = objc.selector("setDelegate:"),
162 .set_released_when_closed = objc.selector("setReleasedWhenClosed:"),
163 .set_title = objc.selector("setTitle:"),
164 .set_content_min_size = objc.selector("setContentMinSize:"),
165 .backing_scale_factor = objc.selector("backingScaleFactor"),
166 .perform_miniaturize = objc.selector("performMiniaturize:"),
167 .zoom = objc.selector("zoom:"),
168 .is_zoomed = objc.selector("isZoomed"),
169 .mouse_location_outside_of_event_stream = objc.selector("mouseLocationOutsideOfEventStream"),
170 .make_key_and_order_front = objc.selector("makeKeyAndOrderFront:"),
171 .next_event = objc.selector("nextEventMatchingMask:untilDate:inMode:dequeue:"),
172 .send_event = objc.selector("sendEvent:"),
173 .event_type = objc.selector("type"),
174 .key_code = objc.selector("keyCode"),
175 .modifier_flags = objc.selector("modifierFlags"),
176 .is_a_repeat = objc.selector("isARepeat"),
177 .button_number = objc.selector("buttonNumber"),
178 .location_in_window = objc.selector("locationInWindow"),
179 .scrolling_delta_x = objc.selector("scrollingDeltaX"),
180 .scrolling_delta_y = objc.selector("scrollingDeltaY"),
181 .characters = objc.selector("characters"),
182 .utf8_string = objc.selector("UTF8String"),
183 .general_pasteboard = objc.selector("generalPasteboard"),
184 .string_for_type = objc.selector("stringForType:"),
185 .clear_contents = objc.selector("clearContents"),
186 .set_string_for_type = objc.selector("setString:forType:"),
187 .distant_past = objc.selector("distantPast"),
188 .close = objc.selector("close"),
189 .content_view = objc.selector("contentView"),
190 .frame = objc.selector("frame"),
191 .set_wants_layer = objc.selector("setWantsLayer:"),
192 .layer = objc.selector("layer"),
193 .set_contents = objc.selector("setContents:"),
194 .set_contents_scale = objc.selector("setContentsScale:"),
195 .finish_launching = objc.selector("finishLaunching"),
196 .init_with_utf8 = objc.selector("initWithUTF8String:"),
197 .arrow_cursor = objc.selector("arrowCursor"),
198 .resize_up_down_cursor = objc.selector("resizeUpDownCursor"),
199 .resize_left_right_cursor = objc.selector("resizeLeftRightCursor"),
200 .set_cursor = objc.selector("set"),
201 };
202 }
203 };
204
205 const Lifecycle = struct {
206 should_close: bool = false,
207 focused: bool = false,
208 pending_focus: ?bool = null,
209 pending_close: bool = false,
210 polling: bool = false,
211 };
212
213 const CocoaStorage = struct {
214 clipboard_capacity: clipboard.Capacity,
215 clipboard_source: clipboard.Source,
216 gamepads: gamepad.State,
217 input: input.State,
218 events: event_module.Queue,
219 frames: FrameStorage,
220
221 fn init(allocator: std.mem.Allocator, config: WindowConfig) CreateError!CocoaStorage {
222 const clipboard_capacity = clipboard.Capacity.derive(config.clipboard) catch
223 return error.InvalidCapacity;
224 const event_capacity = event_module.Capacity.derive(config.events) catch
225 return error.InvalidCapacity;
226 const input_capacity = input.Capacity.derive(config.input) catch
227 return error.InvalidCapacity;
228 const gamepad_capacity = gamepad.Capacity.derive(config.gamepads) catch
229 return error.InvalidCapacity;
230 const presentation_capacity = presentation.Capacity.derive(config.presentation) catch
231 return error.InvalidCapacity;
232 var frames = FrameStorage.init(allocator, presentation_capacity) catch
233 return error.OutOfMemory;
234 errdefer frames.deinit();
235 var events = event_module.Queue.init(allocator, event_capacity) catch
236 return error.OutOfMemory;
237 errdefer events.deinit();
238 var input_state = input.State.init(allocator, input_capacity) catch
239 return error.OutOfMemory;
240 errdefer input_state.deinit();
241 var gamepad_state = gamepad.State.init(allocator, gamepad_capacity) catch
242 return error.OutOfMemory;
243 errdefer gamepad_state.deinit();
244 const clipboard_source = clipboard.Source.init(allocator, clipboard_capacity) catch
245 return error.OutOfMemory;
246 return .{
247 .clipboard_capacity = clipboard_capacity,
248 .clipboard_source = clipboard_source,
249 .gamepads = gamepad_state,
250 .input = input_state,
251 .events = events,
252 .frames = frames,
253 };
254 }
255
256 fn deinit(self: *CocoaStorage) void {
257 self.clipboard_source.deinit();
258 self.gamepads.deinit();
259 self.input.deinit();
260 self.events.deinit();
261 self.frames.deinit();
262 self.* = undefined;
263 }
264 };
265
266 const NativeWindow = struct {
267 app: objc.Id,
268 window: objc.Id,
269 delegate: objc.Id,
270 layer: objc.Id,
271
272 fn init(sels: Selectors, config: WindowConfig) CreateError!NativeWindow {
273 const delegate_class = try windowDelegateClass();
274 const delegate_alloc = objc.send(?objc.Id, @ptrCast(delegate_class), sels.alloc, .{}) orelse
275 return error.WindowCreationFailed;
276 const delegate = objc.send(?objc.Id, delegate_alloc, sels.object_init, .{}) orelse
277 return error.WindowCreationFailed;
278 errdefer objc.release(delegate);
279
280 const app_class = try cls("NSApplication");
281 const app = objc.send(objc.Id, app_class, sels.shared_application, .{});
282 objc.send(void, app, sels.set_activation_policy, .{@as(foundation.NSInteger, 0)});
283
284 const window_class = try cls("NSWindow");
285 const allocated = objc.send(objc.Id, window_class, sels.alloc, .{});
286 const rect = cocoa.NSRect{
287 .origin = .{ .x = 100, .y = 100 },
288 .size = .{
289 .width = @floatFromInt(config.width),
290 .height = @floatFromInt(config.height),
291 },
292 };
293 const window = objc.send(?objc.Id, allocated, sels.init_with_content_rect, .{
294 rect,
295 windowStyle(config.resizable),
296 @as(foundation.NSUInteger, 2),
297 objc.boolean(false),
298 }) orelse return error.WindowCreationFailed;
299 errdefer releaseWindow(window, sels);
300 objc.send(void, window, sels.set_released_when_closed, .{objc.boolean(false)});
301
302 const title = retainedString(sels, config.title) orelse
303 return error.WindowCreationFailed;
304 defer objc.release(title);
305 objc.send(void, window, sels.set_title, .{title});
306 const content_view = objc.send(objc.Id, window, sels.content_view, .{});
307 objc.send(void, content_view, sels.set_wants_layer, .{objc.boolean(true)});
308 return .{
309 .app = app,
310 .window = window,
311 .delegate = delegate,
312 .layer = objc.send(objc.Id, content_view, sels.layer, .{}),
313 };
314 }
315
316 fn deinit(self: NativeWindow, sels: Selectors) void {
317 releaseWindow(self.window, sels);
318 objc.release(self.delegate);
319 }
320 };
321
322 fn windowStyle(resizable: bool) foundation.NSUInteger {
323 var style = cocoa.NSWindowStyleMaskTitled |
324 cocoa.NSWindowStyleMaskClosable |
325 cocoa.NSWindowStyleMaskMiniaturizable;
326 if (resizable) style |= cocoa.NSWindowStyleMaskResizable;
327 return style;
328 }
329
330 fn releaseWindow(window: objc.Id, sels: Selectors) void {
331 objc.send(void, window, sels.close, .{});
332 objc.release(window);
333 }
334
335 fn retainedString(sels: Selectors, bytes: [:0]const u8) ?objc.Id {
336 const string_class = objc.class("NSString") orelse return null;
337 const allocated = objc.send(?objc.Id, string_class, sels.alloc, .{}) orelse return null;
338 return objc.send(?objc.Id, allocated, sels.init_with_utf8, .{bytes.ptr});
339 }
340
341 pub const CocoaBackend = struct {
342 app: objc.Id,
343 window: objc.Id,
344 delegate: objc.Id,
345 layer: objc.Id,
346 sels: Selectors,
347 lifecycle: Lifecycle,
348 width: u32,
349 height: u32,
350 clipboard_capacity: clipboard.Capacity,
351 clipboard_source: clipboard.Source,
352 gamepads: gamepad.State,
353 input: input.State,
354 events: event_module.Queue,
355 frames: FrameStorage,
356
357 pub fn init(
358 self: *CocoaBackend,
359 allocator: std.mem.Allocator,
360 config: WindowConfig,
361 ) CreateError!void {
362 if (config.backend != .auto) return error.UnsupportedPlatform;
363 var autorelease_pool = objc.AutoreleasePool.init();
364 defer autorelease_pool.deinit();
365 const sels = Selectors.init();
366 var storage = try CocoaStorage.init(allocator, config);
367 errdefer storage.deinit();
368 const native = try NativeWindow.init(sels, config);
369 errdefer native.deinit(sels);
370
371 self.* = .{
372 .app = native.app,
373 .window = native.window,
374 .delegate = native.delegate,
375 .layer = native.layer,
376 .sels = sels,
377 .lifecycle = .{},
378 .width = config.width,
379 .height = config.height,
380 .clipboard_capacity = storage.clipboard_capacity,
381 .clipboard_source = storage.clipboard_source,
382 .gamepads = storage.gamepads,
383 .input = storage.input,
384 .events = storage.events,
385 .frames = storage.frames,
386 };
387 const backend_set = objc.setInstancePointer(
388 native.delegate,
389 window_delegate_backend_ivar,
390 @ptrCast(self),
391 );
392 if (!backend_set) {
393 return error.WindowCreationFailed;
394 }
395 objc.send(void, native.window, sels.set_delegate, .{native.delegate});
396
397 if (config.visible) {
398 objc.send(void, native.window, sels.make_key_and_order_front, .{objc.nil});
399 }
400 objc.send(void, native.app, sels.finish_launching, .{});
401 }
402
403 pub fn deinit(self: *CocoaBackend) void {
404 objc.send(void, self.window, self.sels.set_delegate, .{objc.nil});
405 _ = objc.setInstancePointer(self.delegate, window_delegate_backend_ivar, null);
406 objc.send(void, self.layer, self.sels.set_contents, .{objc.nil});
407 self.frames.deinit();
408 objc.send(void, self.window, self.sels.close, .{});
409 objc.release(self.window);
410 objc.release(self.delegate);
411 self.gamepads.deinit();
412 self.input.deinit();
413 self.events.deinit();
414 self.clipboard_source.deinit();
415 self.* = undefined;
416 }
417
418 pub fn pollEvents(self: *CocoaBackend) PollError!EventIterator {
419 var autorelease_pool = objc.AutoreleasePool.init();
420 defer autorelease_pool.deinit();
421 self.input.beginPoll();
422 self.gamepads.poll();
423 self.events.beginPoll();
424 self.flushPendingLifecycle();
425 self.lifecycle.polling = true;
426 defer self.lifecycle.polling = false;
427
428 const ns_date_cls = cls("NSDate") catch return self.events.iterator();
429 const distant_past = objc.send(objc.Id, ns_date_cls, self.sels.distant_past, .{});
430
431 const mode = foundation.stringFromBytes("kCFRunLoopDefaultMode") orelse
432 return self.events.iterator();
433 defer objc.release(mode);
434
435 while (true) {
436 const ns_event = objc.send(
437 ?objc.Id,
438 self.app,
439 self.sels.next_event,
440 .{
441 cocoa.NSAnyEventMask,
442 distant_past,
443 mode,
444 objc.boolean(true),
445 },
446 ) orelse break;
447
448 self.translateEvent(ns_event);
449
450 objc.send(void, self.app, self.sels.send_event, .{ns_event});
451 }
452
453 self.syncContentSize();
454
455 return self.events.iterator();
456 }
457
458 pub fn shouldClose(self: *const CocoaBackend) bool {
459 return self.lifecycle.should_close;
460 }
461
462 fn recordClose(self: *CocoaBackend) void {
463 if (self.lifecycle.should_close) return;
464 self.lifecycle.should_close = true;
465 if (self.lifecycle.polling) {
466 self.pushEvent(.window_close);
467 } else {
468 self.lifecycle.pending_close = true;
469 }
470 }
471
472 fn recordFocus(self: *CocoaBackend, focused: bool) void {
473 if (self.lifecycle.focused == focused) return;
474 self.lifecycle.focused = focused;
475 if (self.lifecycle.polling) {
476 self.pushEvent(.{ .window_focus = .{ .focused = focused } });
477 } else {
478 self.lifecycle.pending_focus = focused;
479 }
480 }
481
482 fn flushPendingLifecycle(self: *CocoaBackend) void {
483 std.debug.assert(!self.lifecycle.polling);
484 if (self.lifecycle.pending_focus) |focused| {
485 self.lifecycle.pending_focus = null;
486 self.pushEvent(.{ .window_focus = .{ .focused = focused } });
487 }
488 if (self.lifecycle.pending_close) {
489 self.lifecycle.pending_close = false;
490 self.pushEvent(.window_close);
491 }
492 }
493
494 pub fn eventSource(_: *const CocoaBackend) ?@import("window.zig").EventSource {
495 return null;
496 }
497
498 pub fn getSize(self: *const CocoaBackend) Size {
499 return .{ .width = self.width, .height = self.height };
500 }
501
502 pub fn inputState(self: *const CocoaBackend) *const input.State {
503 return &self.input;
504 }
505
506 pub fn inputStateMut(self: *CocoaBackend) *input.State {
507 return &self.input;
508 }
509
510 pub fn modifiers(self: *const CocoaBackend) Modifier {
511 return self.input.modifiers();
512 }
513
514 pub fn gamepadState(self: *const CocoaBackend) *const gamepad.State {
515 return &self.gamepads;
516 }
517
518 pub fn setTitle(self: *CocoaBackend, title: [:0]const u8) void {
519 const title_str = retainedString(self.sels, title) orelse return;
520 defer objc.release(title_str);
521 objc.send(void, self.window, self.sels.set_title, .{title_str});
522 }
523
524 pub fn setMinSize(self: *CocoaBackend, width: u32, height: u32) void {
525 objc.send(void, self.window, self.sels.set_content_min_size, .{cocoa.NSSize{
526 .width = @floatFromInt(width),
527 .height = @floatFromInt(height),
528 }});
529 }
530
531 pub fn setCursor(self: *CocoaBackend, shape: CursorShape) void {
532 const ns_cursor_cls = cls("NSCursor") catch return;
533 const cursor = objc.send(objc.Id, ns_cursor_cls, self.cursorSelector(shape), .{});
534 objc.send(void, cursor, self.sels.set_cursor, .{});
535 }
536
537 pub fn isCursorOnScreen(self: *const CocoaBackend) bool {
538 const loc = objc.send(cocoa.NSPoint, self.window, self.sels.mouse_location_outside_of_event_stream, .{});
539 return pointInsideSize(loc, self.width, self.height);
540 }
541
542 pub fn minimize(self: *CocoaBackend) void {
543 objc.send(void, self.window, self.sels.perform_miniaturize, .{objc.nil});
544 }
545
546 pub fn maximize(self: *CocoaBackend) void {
547 if (!self.isMaximized()) objc.send(void, self.window, self.sels.zoom, .{objc.nil});
548 }
549
550 pub fn restore(self: *CocoaBackend) void {
551 if (self.isMaximized()) objc.send(void, self.window, self.sels.zoom, .{objc.nil});
552 }
553
554 pub fn isMaximized(self: *const CocoaBackend) bool {
555 return objc.isTrue(objc.send(objc.BOOL, self.window, self.sels.is_zoomed, .{}));
556 }
557
558 pub fn getClipboardTextAlloc(self: *CocoaBackend, allocator: std.mem.Allocator) !?[]u8 {
559 const pasteboard_cls = cls("NSPasteboard") catch return null;
560 const pasteboard = objc.send(objc.Id, pasteboard_cls, self.sels.general_pasteboard, .{});
561 const type_string = foundation.stringFromBytes(std.mem.span(pasteboard_string_type)) orelse
562 return null;
563 defer objc.release(type_string);
564 const string = objc.send(?objc.Id, pasteboard, self.sels.string_for_type, .{type_string}) orelse return null;
565 const raw = objc.send(?[*:0]const u8, string, self.sels.utf8_string, .{}) orelse return null;
566 const text = std.mem.span(raw);
567 if (text.len > self.clipboard_capacity.received_text_byte_count) {
568 return error.TransferTooLarge;
569 }
570 return try allocator.dupe(u8, text);
571 }
572
573 pub fn setClipboardText(self: *CocoaBackend, text: []const u8) clipboard.PublishError!void {
574 try self.clipboard_source.replace(text);
575 const publication = clipboardPublication(&self.clipboard_source);
576 var autorelease_pool = objc.AutoreleasePool.init();
577 defer autorelease_pool.deinit();
578 const pasteboard_cls = cls("NSPasteboard") catch return error.Unavailable;
579 const pasteboard = objc.send(objc.Id, pasteboard_cls, self.sels.general_pasteboard, .{});
580 const type_string = foundation.stringFromBytes(publication.pasteboard_type) orelse
581 return error.Unexpected;
582 defer objc.release(type_string);
583 const string = foundation.stringFromBytes(publication.text) orelse
584 return error.Unexpected;
585 defer objc.release(string);
586 _ = objc.send(foundation.NSInteger, pasteboard, self.sels.clear_contents, .{});
587 const accepted = objc.send(objc.BOOL, pasteboard, self.sels.set_string_for_type, .{
588 string,
589 type_string,
590 });
591 if (!objc.isTrue(accepted)) return error.Unexpected;
592 }
593
594 pub fn clipboardStatus(self: *const CocoaBackend) clipboard.Status {
595 return self.clipboard_source.status();
596 }
597
598 pub fn x11StorageStatus(_: *const CocoaBackend) sys.x11.Status {
599 return .{};
600 }
601
602 pub fn waylandDmabuf(_: *CocoaBackend) ?@import("wayland/root.zig").DmabufSurface {
603 return null;
604 }
605
606 pub fn waylandStorageStatus(_: *const CocoaBackend) WaylandStorageStatus {
607 return .{};
608 }
609
610 pub fn nextPresentationOutcome(_: *CocoaBackend) ?PresentationOutcome {
611 return null;
612 }
613
614 pub fn presentationStatus(_: *const CocoaBackend) PresentationStatus {
615 return .unavailable;
616 }
617
618 pub fn presentationStorageStatus(self: *const CocoaBackend) PresentationStorageStatus {
619 return .{
620 .frame_capacity_rejection_count = self.frames.capacity_rejection_count,
621 };
622 }
623
624 pub fn droppedEventCount(self: *const CocoaBackend) u64 {
625 return self.events.droppedEventCount();
626 }
627
628 pub fn getFramebufferSize(self: *const CocoaBackend) Size {
629 const scale = self.getContentScale();
630 return .{
631 .width = scaledToU32(self.width, scale.x),
632 .height = scaledToU32(self.height, scale.y),
633 };
634 }
635
636 pub fn nativeSurface(self: *CocoaBackend) surface.Surface {
637 const scale = self.getContentScale();
638 const framebuffer = self.getFramebufferSize();
639 return .{ .cocoa = .{
640 .app = self.app,
641 .window = self.window,
642 .layer = self.layer,
643 .extent = surface.extentFromSizes(
644 self.width,
645 self.height,
646 framebuffer.width,
647 framebuffer.height,
648 scale.x,
649 scale.y,
650 ),
651 } };
652 }
653
654 pub fn presentRgba8(
655 self: *CocoaBackend,
656 rgba8: []const u8,
657 width: u32,
658 height: u32,
659 ) PresentError!void {
660 const byte_count = presentation.rgba8ByteCount(width, height) orelse
661 return error.UnsupportedFormat;
662 if (rgba8.len < byte_count) return error.UnsupportedFormat;
663
664 const next = try self.frames.prepare(byte_count);
665 @memcpy(next.pixels, rgba8[0..byte_count]);
666 try self.setLayerRgba8(next.pixels, width, height);
667 self.frames.commit(next.index, width, height);
668 }
669
670 pub fn presentRgba8Region(
671 self: *CocoaBackend,
672 rgba8: []const u8,
673 width: u32,
674 height: u32,
675 region: PresentRegion,
676 ) PresentError!void {
677 const byte_count = presentation.rgba8ByteCount(width, height) orelse
678 return error.UnsupportedFormat;
679 if (rgba8.len < byte_count) return error.UnsupportedFormat;
680 const active_region = region.clamped(width, height);
681 if (active_region.pixelCount() == 0) return;
682
683 if (!self.frames.matches(width, height)) {
684 return self.presentRgba8(rgba8, width, height);
685 }
686
687 const next = try self.frames.prepare(byte_count);
688 @memcpy(next.pixels, self.frames.active(byte_count));
689 copyRgba8Region(next.pixels, rgba8, width, active_region);
690 try self.setLayerRgba8(next.pixels, width, height);
691 self.frames.commit(next.index, width, height);
692 }
693
694 pub fn getContentScale(self: *const CocoaBackend) Scale {
695 const scale: f32 = @floatCast(objc.send(coregraphics.CGFloat, self.window, self.sels.backing_scale_factor, .{}));
696 const sanitized = sanitizeScale(scale);
697 return .{ .x = sanitized, .y = sanitized };
698 }
699
700 fn cursorSelector(self: *const CocoaBackend, shape: CursorShape) objc.SEL {
701 return switch (shape) {
702 .default => self.sels.arrow_cursor,
703 .resize_ns => self.sels.resize_up_down_cursor,
704 .resize_ew => self.sels.resize_left_right_cursor,
705 .resize_nwse, .resize_nesw => self.sels.arrow_cursor,
706 };
707 }
708
709 fn translateEvent(self: *CocoaBackend, ns_event: objc.Id) void {
710 const event_type = objc.send(foundation.NSUInteger, ns_event, self.sels.event_type, .{});
711
712 switch (event_type) {
713 cocoa.NSEventTypeKeyDown => self.translateKeyDown(ns_event),
714 cocoa.NSEventTypeKeyUp => self.translateKeyUp(ns_event),
715 cocoa.NSEventTypeFlagsChanged => self.translateModifier(ns_event),
716 cocoa.NSEventTypeLeftMouseDown,
717 cocoa.NSEventTypeRightMouseDown,
718 cocoa.NSEventTypeOtherMouseDown,
719 => self.translateMouseButton(ns_event, true),
720 cocoa.NSEventTypeLeftMouseUp,
721 cocoa.NSEventTypeRightMouseUp,
722 cocoa.NSEventTypeOtherMouseUp,
723 => self.translateMouseButton(ns_event, false),
724 cocoa.NSEventTypeMouseMoved,
725 cocoa.NSEventTypeLeftMouseDragged,
726 cocoa.NSEventTypeRightMouseDragged,
727 cocoa.NSEventTypeOtherMouseDragged,
728 => {
729 const loc = objc.send(cocoa.NSPoint, ns_event, self.sels.location_in_window, .{});
730 self.input.moveMouse(@intFromFloat(loc.x), @intFromFloat(loc.y));
731 self.pushEvent(.{ .mouse_move = .{
732 .x = @intFromFloat(loc.x),
733 .y = @intFromFloat(loc.y),
734 } });
735 },
736 cocoa.NSEventTypeScrollWheel => {
737 const loc = objc.send(cocoa.NSPoint, ns_event, self.sels.location_in_window, .{});
738 const mods = objc.send(foundation.NSUInteger, ns_event, self.sels.modifier_flags, .{});
739 const delta_x: f32 = @floatCast(objc.send(coregraphics.CGFloat, ns_event, self.sels.scrolling_delta_x, .{}));
740 const delta_y: f32 = @floatCast(objc.send(coregraphics.CGFloat, ns_event, self.sels.scrolling_delta_y, .{}));
741 self.input.setMousePosition(@intFromFloat(loc.x), @intFromFloat(loc.y));
742 self.input.addMouseWheel(delta_x, delta_y);
743 self.pushEvent(.{ .mouse_wheel = .{
744 .x = @intFromFloat(loc.x),
745 .y = @intFromFloat(loc.y),
746 .delta_x = delta_x,
747 .delta_y = delta_y,
748 .modifiers = cocoaModsToModifier(mods),
749 } });
750 },
751 else => {},
752 }
753 }
754
755 fn translateMouseButton(self: *CocoaBackend, ns_event: objc.Id, pressed: bool) void {
756 const location = objc.send(
757 cocoa.NSPoint,
758 ns_event,
759 self.sels.location_in_window,
760 .{},
761 );
762 const flags = objc.send(
763 foundation.NSUInteger,
764 ns_event,
765 self.sels.modifier_flags,
766 .{},
767 );
768 const number = objc.send(
769 foundation.NSInteger,
770 ns_event,
771 self.sels.button_number,
772 .{},
773 );
774 const button = cocoaMouseButton(number) orelse return;
775 const event: MouseButtonEvent = .{
776 .button = button,
777 .x = @as(i32, @intFromFloat(location.x)),
778 .y = @as(i32, @intFromFloat(location.y)),
779 .modifiers = cocoaModsToModifier(flags),
780 };
781 self.input.setMousePosition(event.x, event.y);
782 if (pressed) {
783 self.input.pressMouse(button);
784 self.pushEvent(.{ .mouse_press = event });
785 } else {
786 self.input.releaseMouse(button);
787 self.pushEvent(.{ .mouse_release = event });
788 }
789 }
790
791 fn translateKeyDown(self: *CocoaBackend, ns_event: objc.Id) void {
792 const keycode = objc.send(u16, ns_event, self.sels.key_code, .{});
793 const flags = objc.send(
794 foundation.NSUInteger,
795 ns_event,
796 self.sels.modifier_flags,
797 .{},
798 );
799 const repeated = objc.isTrue(objc.send(
800 objc.BOOL,
801 ns_event,
802 self.sels.is_a_repeat,
803 .{},
804 ));
805 const active_modifiers = cocoaModsToModifier(flags);
806 self.recordKey(macKeycodeToKey(keycode), active_modifiers, true, repeated);
807 self.pushTextInput(ns_event, active_modifiers);
808 }
809
810 fn translateKeyUp(self: *CocoaBackend, ns_event: objc.Id) void {
811 const keycode = objc.send(u16, ns_event, self.sels.key_code, .{});
812 const flags = objc.send(
813 foundation.NSUInteger,
814 ns_event,
815 self.sels.modifier_flags,
816 .{},
817 );
818 self.recordKey(macKeycodeToKey(keycode), cocoaModsToModifier(flags), false, false);
819 }
820
821 fn translateModifier(self: *CocoaBackend, ns_event: objc.Id) void {
822 const keycode = objc.send(u16, ns_event, self.sels.key_code, .{});
823 const flags = objc.send(
824 foundation.NSUInteger,
825 ns_event,
826 self.sels.modifier_flags,
827 .{},
828 );
829 const key = macKeycodeToKey(keycode);
830 const key_flag = cocoaModifierFlag(key) orelse return;
831 const pressed = cocoaModifierPressed(self.input.isKeyDown(key), key_flag, flags);
832 self.recordKey(key, cocoaModsToModifier(flags), pressed, false);
833 }
834
835 fn recordKey(
836 self: *CocoaBackend,
837 key: Key,
838 active_modifiers: Modifier,
839 pressed: bool,
840 repeated: bool,
841 ) void {
842 if (pressed) {
843 self.input.pressKey(key);
844 self.pushEvent(.{ .key_press = .{
845 .key = key,
846 .modifiers = active_modifiers,
847 .repeated = repeated,
848 } });
849 } else {
850 self.input.releaseKey(key);
851 self.pushEvent(.{ .key_release = .{
852 .key = key,
853 .modifiers = active_modifiers,
854 } });
855 }
856 }
857
858 fn pushEvent(self: *CocoaBackend, event: Event) void {
859 _ = self.events.push(event);
860 }
861
862 fn pushTextInput(self: *CocoaBackend, ns_event: objc.Id, active_modifiers: Modifier) void {
863 const string = objc.send(?objc.Id, ns_event, self.sels.characters, .{}) orelse return;
864 const raw = objc.send(?[*:0]const u8, string, self.sels.utf8_string, .{}) orelse return;
865 if (Event.textInput(std.mem.span(raw), active_modifiers)) |event| {
866 _ = self.input.pushTextInput(event.text_input.text());
867 self.pushEvent(event);
868 }
869 }
870
871 fn syncContentSize(self: *CocoaBackend) void {
872 const view = objc.send(objc.Id, self.window, self.sels.content_view, .{});
873 const frame = objc.send(cocoa.NSRect, view, self.sels.frame, .{});
874 const size = frameToSize(frame);
875 if (size.width != self.width or size.height != self.height) {
876 self.width = size.width;
877 self.height = size.height;
878 self.pushEvent(.{ .window_resize = .{ .width = size.width, .height = size.height } });
879 }
880 }
881
882 fn setLayerRgba8(self: *CocoaBackend, pixels: []u8, width: u32, height: u32) PresentError!void {
883 const color_space = coregraphics.createDeviceRgbColorSpace() orelse return error.Unexpected;
884 defer coregraphics.releaseColorSpace(color_space);
885
886 const provider = coregraphics.createDataProvider(pixels) orelse return error.Unexpected;
887 defer coregraphics.releaseDataProvider(provider);
888
889 const image = coregraphics.createRgba8Image(width, height, @as(usize, width) * 4, color_space, provider) orelse return error.Unexpected;
890 defer coregraphics.releaseImage(image);
891
892 objc.send(void, self.layer, self.sels.set_contents_scale, .{layerContentsScale(width, self.width)});
893 objc.send(void, self.layer, self.sels.set_contents, .{@as(objc.Id, @ptrCast(image))});
894 }
895 };
896
897 const PreparedFrame = struct {
898 index: usize,
899 pixels: []u8,
900 };
901
902 const FrameStorage = struct {
903 allocator: std.mem.Allocator,
904 bytes: []u8,
905 slot_byte_count: usize,
906 active_index: ?usize = null,
907 width: u32 = 0,
908 height: u32 = 0,
909 capacity_rejection_count: u64 = 0,
910
911 fn init(
912 allocator: std.mem.Allocator,
913 capacity: presentation.Capacity,
914 ) std.mem.Allocator.Error!FrameStorage {
915 std.debug.assert(capacity.cocoa_frame_slot_count == 2);
916 std.debug.assert(
917 capacity.cocoa_frame_storage_bytes / capacity.cocoa_frame_slot_count ==
918 capacity.retained_frame_byte_count,
919 );
920 return .{
921 .allocator = allocator,
922 .bytes = try allocator.alloc(u8, capacity.cocoa_frame_storage_bytes),
923 .slot_byte_count = capacity.retained_frame_byte_count,
924 };
925 }
926
927 fn deinit(self: *FrameStorage) void {
928 self.allocator.free(self.bytes);
929 self.* = undefined;
930 }
931
932 fn prepare(self: *FrameStorage, byte_count: usize) error{CapacityExceeded}!PreparedFrame {
933 if (byte_count > self.slot_byte_count) {
934 self.capacity_rejection_count +|= 1;
935 return error.CapacityExceeded;
936 }
937 const index = if (self.active_index) |active_index| active_index ^ 1 else 0;
938 return .{ .index = index, .pixels = self.slot(index)[0..byte_count] };
939 }
940
941 fn active(self: *FrameStorage, byte_count: usize) []const u8 {
942 std.debug.assert(byte_count <= self.slot_byte_count);
943 return self.slot(self.active_index.?)[0..byte_count];
944 }
945
946 fn matches(self: *const FrameStorage, width: u32, height: u32) bool {
947 return self.active_index != null and self.width == width and self.height == height;
948 }
949
950 fn commit(self: *FrameStorage, index: usize, width: u32, height: u32) void {
951 std.debug.assert(index < 2);
952 self.active_index = index;
953 self.width = width;
954 self.height = height;
955 }
956
957 fn slot(self: *FrameStorage, index: usize) []u8 {
958 std.debug.assert(index < 2);
959 const start = index * self.slot_byte_count;
960 return self.bytes[start..][0..self.slot_byte_count];
961 }
962 };
963
964 const pasteboard_string_type: [*:0]const u8 = "public.utf8-plain-text";
965
966 const ClipboardPublication = struct {
967 pasteboard_type: []const u8,
968 text: []const u8,
969 };
970
971 fn clipboardPublication(source: *const clipboard.Source) ClipboardPublication {
972 return .{
973 .pasteboard_type = std.mem.span(pasteboard_string_type),
974 .text = source.text(),
975 };
976 }
977
978 fn frameToSize(frame: cocoa.NSRect) Size {
979 return .{
980 .width = floatToU32(frame.size.width),
981 .height = floatToU32(frame.size.height),
982 };
983 }
984
985 fn sanitizeScale(scale: f32) f32 {
986 if (!std.math.isFinite(scale) or scale <= 0) return 1;
987 return scale;
988 }
989
990 fn layerContentsScale(pixel_width: u32, logical_width: u32) coregraphics.CGFloat {
991 if (pixel_width == 0 or logical_width == 0) return 1;
992 return @as(coregraphics.CGFloat, @floatFromInt(pixel_width)) / @as(coregraphics.CGFloat, @floatFromInt(logical_width));
993 }
994
995 fn scaledToU32(value: u32, scale: f32) u32 {
996 return floatToU32(@as(f64, @floatFromInt(value)) * @as(f64, sanitizeScale(scale)));
997 }
998
999 fn copyRgba8Region(dst: []u8, src: []const u8, frame_width: u32, region: PresentRegion) void {
1000 const row_bytes = @as(usize, region.width) * 4;
1001 var row: u32 = 0;
1002 while (row < region.height) : (row += 1) {
1003 const offset = (@as(usize, region.y + row) * @as(usize, frame_width) + @as(usize, region.x)) * 4;
1004 @memcpy(dst[offset..][0..row_bytes], src[offset..][0..row_bytes]);
1005 }
1006 }
1007
1008 fn floatToU32(value: f64) u32 {
1009 if (!std.math.isFinite(value) or value <= 0) return 0;
1010 if (value >= @as(f64, @floatFromInt(std.math.maxInt(u32)))) return std.math.maxInt(u32);
1011 return @intFromFloat(@round(value));
1012 }
1013
1014 fn pointInsideSize(point: cocoa.NSPoint, width: u32, height: u32) bool {
1015 return point.x >= 0 and
1016 point.y >= 0 and
1017 point.x < @as(coregraphics.CGFloat, @floatFromInt(width)) and
1018 point.y < @as(coregraphics.CGFloat, @floatFromInt(height));
1019 }
1020
1021 fn macKeycodeToKey(keycode: u16) Key {
1022 return switch (keycode) {
1023 0x00 => .a,
1024 0x01 => .s,
1025 0x02 => .d,
1026 0x03 => .f,
1027 0x04 => .h,
1028 0x05 => .g,
1029 0x06 => .z,
1030 0x07 => .x,
1031 0x08 => .c,
1032 0x09 => .v,
1033 0x0B => .b,
1034 0x0C => .q,
1035 0x0D => .w,
1036 0x0E => .e,
1037 0x0F => .r,
1038 0x10 => .y,
1039 0x11 => .t,
1040 0x12 => .@"1",
1041 0x13 => .@"2",
1042 0x14 => .@"3",
1043 0x15 => .@"4",
1044 0x16 => .@"6",
1045 0x17 => .@"5",
1046 0x18 => .equal,
1047 0x19 => .@"9",
1048 0x1A => .@"7",
1049 0x1B => .minus,
1050 0x1C => .@"8",
1051 0x1D => .@"0",
1052 0x1E => .right_bracket,
1053 0x1F => .o,
1054 0x20 => .u,
1055 0x21 => .left_bracket,
1056 0x22 => .i,
1057 0x23 => .p,
1058 0x24 => .enter,
1059 0x25 => .l,
1060 0x26 => .j,
1061 0x27 => .apostrophe,
1062 0x28 => .k,
1063 0x29 => .semicolon,
1064 0x2A => .backslash,
1065 0x2B => .comma,
1066 0x2C => .slash,
1067 0x2D => .n,
1068 0x2E => .m,
1069 0x2F => .period,
1070 0x30 => .tab,
1071 0x31 => .space,
1072 0x32 => .grave,
1073 0x33 => .backspace,
1074 0x35 => .escape,
1075 0x36 => .right_super,
1076 0x37 => .left_super,
1077 0x38 => .left_shift,
1078 0x39 => .caps_lock,
1079 0x3A => .left_alt,
1080 0x3B => .left_ctrl,
1081 0x3C => .right_shift,
1082 0x3D => .right_alt,
1083 0x3E => .right_ctrl,
1084 0x60 => .f5,
1085 0x61 => .f6,
1086 0x62 => .f7,
1087 0x63 => .f3,
1088 0x64 => .f8,
1089 0x65 => .f9,
1090 0x67 => .f11,
1091 0x6D => .f10,
1092 0x6F => .f12,
1093 0x72 => .insert,
1094 0x73 => .home,
1095 0x74 => .page_up,
1096 0x75 => .delete,
1097 0x76 => .f4,
1098 0x77 => .end,
1099 0x78 => .f2,
1100 0x79 => .page_down,
1101 0x7A => .f1,
1102 0x7B => .left,
1103 0x7C => .right,
1104 0x7D => .down,
1105 0x7E => .up,
1106 else => .unknown,
1107 };
1108 }
1109
1110 fn cocoaModifierFlag(key: Key) ?foundation.NSUInteger {
1111 return switch (key) {
1112 .caps_lock => cocoa.NSEventModifierFlagCapsLock,
1113 .left_shift, .right_shift => cocoa.NSEventModifierFlagShift,
1114 .left_ctrl, .right_ctrl => cocoa.NSEventModifierFlagControl,
1115 .left_alt, .right_alt => cocoa.NSEventModifierFlagOption,
1116 .left_super, .right_super => cocoa.NSEventModifierFlagCommand,
1117 else => null,
1118 };
1119 }
1120
1121 fn cocoaModifierPressed(
1122 key_down: bool,
1123 key_flag: foundation.NSUInteger,
1124 flags: foundation.NSUInteger,
1125 ) bool {
1126 return flags & key_flag != 0 and !key_down;
1127 }
1128
1129 fn cocoaMouseButton(number: foundation.NSInteger) ?MouseButton {
1130 return switch (number) {
1131 0 => .left,
1132 1 => .right,
1133 2 => .middle,
1134 3 => .x1,
1135 4 => .x2,
1136 else => null,
1137 };
1138 }
1139
1140 fn cocoaModsToModifier(flags: foundation.NSUInteger) Modifier {
1141 return .{
1142 .shift = (flags & cocoa.NSEventModifierFlagShift) != 0,
1143 .ctrl = (flags & cocoa.NSEventModifierFlagControl) != 0,
1144 .alt = (flags & cocoa.NSEventModifierFlagOption) != 0,
1145 .super = (flags & cocoa.NSEventModifierFlagCommand) != 0,
1146 };
1147 }
1148
1149 test "Cocoa cursor screen check compares point to window size" {
1150 try std.testing.expect(pointInsideSize(.{ .x = 0, .y = 0 }, 10, 10));
1151 try std.testing.expect(pointInsideSize(.{ .x = 9.5, .y = 9.5 }, 10, 10));
1152 try std.testing.expect(!pointInsideSize(.{ .x = -0.5, .y = 0 }, 10, 10));
1153 try std.testing.expect(!pointInsideSize(.{ .x = 10, .y = 0 }, 10, 10));
1154 try std.testing.expect(!pointInsideSize(.{ .x = 0, .y = 10 }, 10, 10));
1155 }
1156
1157 test "Cocoa modifier flags preserve side transitions and caps lock" {
1158 const shift = cocoa.NSEventModifierFlagShift;
1159 try std.testing.expectEqual(shift, cocoaModifierFlag(.left_shift).?);
1160 try std.testing.expectEqual(shift, cocoaModifierFlag(.right_shift).?);
1161 try std.testing.expectEqual(
1162 cocoa.NSEventModifierFlagCapsLock,
1163 cocoaModifierFlag(.caps_lock).?,
1164 );
1165 try std.testing.expect(cocoaModifierFlag(.a) == null);
1166 try std.testing.expect(cocoaModifierPressed(false, shift, shift));
1167 try std.testing.expect(!cocoaModifierPressed(true, shift, shift));
1168 try std.testing.expect(!cocoaModifierPressed(false, shift, 0));
1169 try std.testing.expectEqual(Key.caps_lock, macKeycodeToKey(0x39));
1170 }
1171
1172 test "Cocoa mouse button numbers map the public domain" {
1173 try std.testing.expectEqual(MouseButton.left, cocoaMouseButton(0).?);
1174 try std.testing.expectEqual(MouseButton.right, cocoaMouseButton(1).?);
1175 try std.testing.expectEqual(MouseButton.middle, cocoaMouseButton(2).?);
1176 try std.testing.expectEqual(MouseButton.x1, cocoaMouseButton(3).?);
1177 try std.testing.expectEqual(MouseButton.x2, cocoaMouseButton(4).?);
1178 try std.testing.expect(cocoaMouseButton(-1) == null);
1179 try std.testing.expect(cocoaMouseButton(5) == null);
1180 }
1181
1182 test "Cocoa backend declarations compile against Apple ABIs" {
1183 if (comptime builtin.os.tag != .macos) return;
1184 std.testing.refAllDecls(CocoaBackend);
1185 }
1186
1187 test "Cocoa clipboard publication retains exact UTF-8 source bytes" {
1188 const capacity = try clipboard.Capacity.derive(.{
1189 .published_text_byte_count = 8,
1190 });
1191 var source = try clipboard.Source.init(std.testing.allocator, capacity);
1192 defer source.deinit();
1193 try source.replace("copy λ");
1194 const publication = clipboardPublication(&source);
1195 try std.testing.expectEqualStrings(
1196 "public.utf8-plain-text",
1197 publication.pasteboard_type,
1198 );
1199 try std.testing.expectEqualStrings("copy λ", publication.text);
1200 try std.testing.expectError(error.CapacityExceeded, source.replace("123456789"));
1201 try std.testing.expectEqualStrings("copy λ", clipboardPublication(&source).text);
1202 }
1203
1204 test "Cocoa frame storage alternates exact bounded handoff slots" {
1205 const capacity = try presentation.Capacity.derive(.{
1206 .retained_frame_byte_count = 16,
1207 });
1208 var storage = try FrameStorage.init(std.testing.allocator, capacity);
1209 defer storage.deinit();
1210
1211 const first = try storage.prepare(16);
1212 @memset(first.pixels, 0x31);
1213 storage.commit(first.index, 2, 2);
1214 try std.testing.expect(storage.matches(2, 2));
1215 const second = try storage.prepare(16);
1216 try std.testing.expect(first.pixels.ptr != second.pixels.ptr);
1217 @memcpy(second.pixels, storage.active(16));
1218 storage.commit(second.index, 2, 2);
1219 const third = try storage.prepare(16);
1220 try std.testing.expectEqual(first.pixels.ptr, third.pixels.ptr);
1221 try std.testing.expectError(error.CapacityExceeded, storage.prepare(17));
1222 try std.testing.expectEqual(@as(u64, 1), storage.capacity_rejection_count);
1223 storage.capacity_rejection_count = std.math.maxInt(u64);
1224 try std.testing.expectError(error.CapacityExceeded, storage.prepare(17));
1225 try std.testing.expectEqual(std.math.maxInt(u64), storage.capacity_rejection_count);
1226 }
1227
1228 test "Cocoa frame storage reports acquisition failure" {
1229 const capacity = try presentation.Capacity.derive(.{
1230 .retained_frame_byte_count = 16,
1231 });
1232 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
1233 .fail_index = 0,
1234 });
1235 try std.testing.expectError(
1236 error.OutOfMemory,
1237 FrameStorage.init(failing.allocator(), capacity),
1238 );
1239 }
1240
1241 test "Cocoa region copy only mutates dirty rows" {
1242 const width: u32 = 4;
1243 const height: u32 = 3;
1244 var dst: [width * height * 4]u8 = @as([(width * height * 4)]u8, @splat(0x10));
1245 var src: [width * height * 4]u8 = undefined;
1246 for (0..height) |y| {
1247 for (0..width) |x| {
1248 const offset = (y * width + x) * 4;
1249 src[offset + 0] = @intCast(40 + x);
1250 src[offset + 1] = @intCast(50 + y);
1251 src[offset + 2] = @intCast(60 + x + y);
1252 src[offset + 3] = 0xFF;
1253 }
1254 }
1255
1256 const region = PresentRegion{ .x = 1, .y = 1, .width = 2, .height = 1 };
1257 copyRgba8Region(&dst, &src, width, region);
1258
1259 for (0..height) |y| {
1260 for (0..width) |x| {
1261 const offset = (y * width + x) * 4;
1262 const inside = x >= region.x and x < region.x + region.width and y >= region.y and y < region.y + region.height;
1263 if (inside) {
1264 try std.testing.expectEqualSlices(u8, src[offset..][0..4], dst[offset..][0..4]);
1265 } else {
1266 try std.testing.expectEqualSlices(u8, &[_]u8{ 0x10, 0x10, 0x10, 0x10 }, dst[offset..][0..4]);
1267 }
1268 }
1269 }
1270 }