Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,13 @@ pub fn build(b: *std.Build) void {
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "_surfaceCursor = cursor ?: [NSCursor arrowCursor]" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSCursor pointingHandCursor" },
});
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-magnify-input", "Verify AppKit GPU surfaces forward trackpad magnify gestures", &.{
.{ .path = "src/platform/macos/appkit_host.h", .pattern = "NATIVE_SDK_APPKIT_GPU_INPUT_MAGNIFY = 12" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "- (void)magnifyWithEvent:(NSEvent *)event" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NATIVE_SDK_APPKIT_GPU_INPUT_MAGNIFY" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "deltaY:event.magnification" },
.{ .path = "src/platform/macos/root.zig", .pattern = "12 => .magnify" },
});
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-widget-accessibility-actions", "Verify AppKit GPU widget accessibility actions route to the runtime", &.{
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityPerformPress" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "emitWidgetAccessibilityActionWithId" },
Expand Down
1 change: 1 addition & 0 deletions changelog.d/gpu-surface-magnify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feature: **macOS trackpad magnify on GPU surfaces**: pinch gestures arrive as `GpuSurfaceInputKind.magnify` with cursor-local coordinates and incremental `NSEvent.magnification` in `delta_y` (positive zooms in).
4 changes: 2 additions & 2 deletions docs/src/app/platform-support/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ macOS, Linux, and Windows run full desktop apps through their own platform hosts
</tr>
<tr>
<td>Pointer, keyboard &amp; IME input</td>
<td><Tier tier="full" /></td>
<td><Tier tier="full" note="includes trackpad magnify (pinch) on gpu_surface" /></td>
<td><Tier tier="caveats" note="IME composition is mapped; real-hardware IME verification is pending" fn={3} /></td>
<td><Tier tier="full" note="pointer, keyboard, scroll, IME composition, HiDPI" /></td>
<td><Tier tier="caveats" note="touch, system keyboard, and IME through the toolkit host" fn={3} /></td>
Expand Down Expand Up @@ -391,7 +391,7 @@ Mobile hosts own safe areas, orientation, keyboard avoidance, back gestures, and

Canvas-scene apps also run through a hand-written embed host: `addMobileLib` compiles a `Model`/`Msg`/`update`/`view` app into the embed static library with a single `gpu_surface` scene, frames render through the deterministic CPU reference renderer, and the host shim blits the presented pixels (`native_sdk_app_render_pixels`) into its own surface. The `examples/mobile-canvas` iOS shim — the same architecture the toolkit's own UIKit host uses — is exercised on the simulator: rendering, safe-area layout, real touch/keyboard/IME input, and accessibility snapshots. The Android `NativeActivity` shim cross-compiles for both Android arches from the same library build.

`gpu_surface` is implemented for the macOS system-WebView host as a Metal-backed child surface and for the Linux and Windows system-WebView hosts as a software-rendered (CPU reference renderer + GTK pixel blit on Linux, GDI DIB blit on Windows) child surface; Linux and Windows frame events report `backend=software`, and a manifest that declares another backend (for example `gpu_backend = "metal"`) falls back to software on those hosts instead of erroring. Windows maps native IME composition (`WM_IME_COMPOSITION`) onto the same shared IME events the macOS and Linux hosts emit — inline preedit, cursor position, and the commit contract — with real-hardware IME verification still pending. Other current hosts report unsupported operations for that view kind and `runtime.supports(.gpu_surfaces)` / `window.zero.platform.supports("gpuSurfaces")` return `false`.
`gpu_surface` is implemented for the macOS system-WebView host as a Metal-backed child surface and for the Linux and Windows system-WebView hosts as a software-rendered (CPU reference renderer + GTK pixel blit on Linux, GDI DIB blit on Windows) child surface; Linux and Windows frame events report `backend=software`, and a manifest that declares another backend (for example `gpu_backend = "metal"`) falls back to software on those hosts instead of erroring. Windows maps native IME composition (`WM_IME_COMPOSITION`) onto the same shared IME events the macOS and Linux hosts emit — inline preedit, cursor position, and the commit contract — with real-hardware IME verification still pending. On macOS, trackpad pinch gestures arrive as `GpuSurfaceInputKind.magnify` with cursor-local coordinates and incremental `NSEvent.magnification` in `delta_y` (positive zooms in); other hosts do not synthesize magnify. Other current hosts report unsupported operations for that view kind and `runtime.supports(.gpu_surfaces)` / `window.zero.platform.supports("gpuSurfaces")` return `false`.

## Related Docs

Expand Down
2 changes: 1 addition & 1 deletion src/embed/host.zig
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ pub const MobileHostApp = struct {
self.last_input_composition_cursor = input.composition_cursor;
self.last_input_modifiers = input.modifiers;
switch (input.kind) {
.pointer_down, .pointer_up, .pointer_cancel, .pointer_move, .pointer_drag, .scroll => {
.pointer_down, .pointer_up, .pointer_cancel, .pointer_move, .pointer_drag, .scroll, .magnify => {
self.touch_count += 1;
self.last_touch_id = input.pointer_id;
self.last_touch_kind = input.kind;
Expand Down
25 changes: 24 additions & 1 deletion src/embed/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,31 @@ test "mobile C ABI forwards surface resize and touch input" {
try std.testing.expectEqual(@as(f32, 0), self.last_touch_pressure);
try std.testing.expectEqualStrings("", std.mem.span(native_sdk_app_last_error_name(app)));

// Magnify is a desktop (macOS) kind; the embed host still records it
// when the runtime delivers one so shared GpuSurfaceInputKind handling
// stays exhaustive across hosts.
try self.embedded.runtime.dispatchPlatformEvent(self.embedded.app, .{ .gpu_surface_input = .{
.window_id = 1,
.label = mobile_gpu_surface_label,
.kind = .magnify,
.timestamp_ns = 9_000_000,
.pointer_id = 7,
.x = 40,
.y = 50,
.delta_y = 0.08,
} });
try std.testing.expectEqual(@as(usize, 5), self.touch_count);
try std.testing.expectEqual(@as(usize, 5), self.input_count);
try std.testing.expectEqual(platform.GpuSurfaceInputKind.magnify, self.last_touch_kind);
try std.testing.expectEqual(platform.GpuSurfaceInputKind.magnify, self.last_input_kind);
try std.testing.expectEqual(@as(u64, 7), self.last_touch_id);
try std.testing.expectEqual(@as(f32, 40), self.last_touch_x);
try std.testing.expectEqual(@as(f32, 50), self.last_touch_y);
try std.testing.expectEqual(@as(f32, 0), self.last_touch_delta_x);
try std.testing.expectEqual(@as(f32, 0.08), self.last_touch_delta_y);

native_sdk_app_touch(app, 42, 99, 13, 25, 0);
try std.testing.expectEqual(@as(usize, 4), self.touch_count);
try std.testing.expectEqual(@as(usize, 5), self.touch_count);
try std.testing.expectEqualStrings("InvalidTouchPhase", std.mem.span(native_sdk_app_last_error_name(app)));
}

Expand Down
11 changes: 11 additions & 0 deletions src/platform/linux/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ fn gpuSurfaceInputKindFromInt(value: c_int) platform_mod.GpuSurfaceInputKind {
9 => .ime_commit_composition,
10 => .ime_cancel_composition,
11 => .pointer_cancel,
12 => .magnify, // macOS-synthesized; Linux does not emit magnify
else => .pointer_move,
};
}
Expand Down Expand Up @@ -1409,6 +1410,16 @@ test "linux gpu surface input maps pointer cancel" {
try std.testing.expectEqual(platform_mod.GpuSurfaceInputKind.pointer_cancel, gpuSurfaceInputEventFromGtkEvent(&event).kind);
}

test "linux gpu surface input maps magnify abi ordinal without synthesizing" {
// Shared ABI ordinal 12 (macOS magnify); Linux does not emit it.
var event = std.mem.zeroes(GtkEvent);
event.input_kind = 12;
event.delta_y = -0.02;
const input = gpuSurfaceInputEventFromGtkEvent(&event);
try std.testing.expectEqual(platform_mod.GpuSurfaceInputKind.magnify, input.kind);
try std.testing.expectEqual(@as(f32, -0.02), input.delta_y);
}

test "linux gpu surface input maps ime text and composition events" {
const preedit = "e\xcc\x81"; // "é" as e + combining acute
var set_event = std.mem.zeroes(GtkEvent);
Expand Down
3 changes: 3 additions & 0 deletions src/platform/macos/appkit_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ typedef enum {
NATIVE_SDK_APPKIT_GPU_INPUT_IME_COMMIT_COMPOSITION = 9,
NATIVE_SDK_APPKIT_GPU_INPUT_IME_CANCEL_COMPOSITION = 10,
NATIVE_SDK_APPKIT_GPU_INPUT_POINTER_CANCEL = 11,
/* Trackpad pinch: cursor-local x/y, incremental NSEvent.magnification
* in delta_y (positive = zoom in). Appended so existing ordinals stay. */
NATIVE_SDK_APPKIT_GPU_INPUT_MAGNIFY = 12,
} native_sdk_appkit_gpu_input_kind_t;

typedef enum {
Expand Down
12 changes: 12 additions & 0 deletions src/platform/macos/appkit_host.m
Original file line number Diff line number Diff line change
Expand Up @@ -5530,6 +5530,18 @@ - (void)scrollWheel:(NSEvent *)event {
[self queueScrollInputEvent:event deltaX:-event.scrollingDeltaX deltaY:-event.scrollingDeltaY];
}

- (void)magnifyWithEvent:(NSEvent *)event {
// Pinch-to-zoom: forward cursor-local coordinates and the incremental
// magnification factor in delta_y (NSEvent.magnification; positive =
// zoom in). delta_x stays 0 — see GpuSurfaceInputEvent.delta_y.
if (event.magnification == 0) return;
[self emitInputEventWithKind:NATIVE_SDK_APPKIT_GPU_INPUT_MAGNIFY
event:event
button:0
deltaX:0
deltaY:event.magnification];
}

- (void)keyDown:(NSEvent *)event {
if ([self focusedTextAccessibilityElement]) {
self.interpretedKeyEventEmittedInput = NO;
Expand Down
27 changes: 27 additions & 0 deletions src/platform/macos/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1813,6 +1813,7 @@ fn gpuSurfaceInputKindFromInt(value: c_int) platform_mod.GpuSurfaceInputKind {
9 => .ime_commit_composition,
10 => .ime_cancel_composition,
11 => .pointer_cancel,
12 => .magnify,
else => .pointer_move,
};
}
Expand Down Expand Up @@ -2242,6 +2243,32 @@ test "mac gpu surface input maps pointer cancel" {
try std.testing.expectEqual(platform_mod.GpuSurfaceInputKind.pointer_cancel, input.kind);
}

test "mac gpu surface input maps magnify with incremental magnification in delta_y" {
const label = "canvas";
var event = std.mem.zeroes(AppKitEvent);
event.window_id = 3;
event.view_label = label.ptr;
event.view_label_len = label.len;
event.input_kind = 12;
event.timestamp_ns = 456_000_000;
event.x = 120;
event.y = 80;
event.delta_x = 0;
event.delta_y = 0.05;
event.shortcut_modifiers = shortcut_modifier_option;

const input = gpuSurfaceInputEventFromAppKitEvent(&event);
try std.testing.expectEqual(@as(platform_mod.WindowId, 3), input.window_id);
try std.testing.expectEqualStrings("canvas", input.label);
try std.testing.expectEqual(platform_mod.GpuSurfaceInputKind.magnify, input.kind);
try std.testing.expectEqual(@as(u64, 456_000_000), input.timestamp_ns);
try std.testing.expectEqual(@as(f32, 120), input.x);
try std.testing.expectEqual(@as(f32, 80), input.y);
try std.testing.expectEqual(@as(f32, 0), input.delta_x);
try std.testing.expectEqual(@as(f32, 0.05), input.delta_y);
try std.testing.expect(input.modifiers.option);
}

test "mac appearance event maps color scheme" {
try std.testing.expectEqual(platform_mod.ColorScheme.light, appKitColorScheme(0));
try std.testing.expectEqual(platform_mod.ColorScheme.dark, appKitColorScheme(1));
Expand Down
7 changes: 7 additions & 0 deletions src/platform/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,9 @@ pub const GpuSurfaceInputKind = enum {
ime_set_composition,
ime_commit_composition,
ime_cancel_composition,
/// Trackpad pinch / magnify gesture (macOS AppKit `-magnifyWithEvent:`).
/// Host ABI ordinal is 12; see `GpuSurfaceInputEvent.delta_y`.
magnify,
};

pub const GpuSurfaceInputEvent = struct {
Expand All @@ -1563,6 +1566,10 @@ pub const GpuSurfaceInputEvent = struct {
button: i32 = 0,
pressure: f32 = 0,
delta_x: f32 = 0,
/// For `.scroll`, the wheel/trackpad scroll delta in canvas points.
/// For `.magnify`, the incremental magnification factor from the host
/// (`NSEvent.magnification` on macOS): positive zooms in, negative
/// zooms out. `delta_x` is 0 for magnify.
delta_y: f32 = 0,
key: []const u8 = "",
text: []const u8 = "",
Expand Down
11 changes: 11 additions & 0 deletions src/platform/windows/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ fn gpuSurfaceInputKindFromInt(value: c_int) platform_mod.GpuSurfaceInputKind {
9 => .ime_commit_composition,
10 => .ime_cancel_composition,
11 => .pointer_cancel,
12 => .magnify, // macOS-synthesized; Windows does not emit magnify
else => .pointer_move,
};
}
Expand Down Expand Up @@ -1385,6 +1386,16 @@ test "windows gpu surface input maps pointer cancel" {
try std.testing.expectEqual(platform_mod.GpuSurfaceInputKind.pointer_cancel, gpuSurfaceInputEventFromWindowsEvent(&event).kind);
}

test "windows gpu surface input maps magnify abi ordinal without synthesizing" {
// Shared ABI ordinal 12 (macOS magnify); Windows does not emit it.
var event = std.mem.zeroes(WindowsEvent);
event.input_kind = 12;
event.delta_y = 0.1;
const input = gpuSurfaceInputEventFromWindowsEvent(&event);
try std.testing.expectEqual(platform_mod.GpuSurfaceInputKind.magnify, input.kind);
try std.testing.expectEqual(@as(f32, 0.1), input.delta_y);
}

test "windows gpu surface input maps ime text and composition events" {
// A GCS_COMPSTR preedit update: kind 8 carries the full preedit text
// plus a UTF-8 byte cursor (the host converts GCS_CURSORPOS UTF-16
Expand Down
17 changes: 17 additions & 0 deletions src/runtime/canvas_frame_helpers.zig
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pub fn canvasWidgetPointerEventFromGpuInput(input_event: GpuSurfaceInputEvent) ?
.ime_set_composition,
.ime_commit_composition,
.ime_cancel_composition,
.magnify,
=> return null,
};
return .{
Expand All @@ -163,6 +164,7 @@ pub fn canvasWidgetInputBatchesDisplayListRefresh(kind: platform.GpuSurfaceInput
.ime_set_composition,
.ime_commit_composition,
.ime_cancel_composition,
.magnify,
=> true,
};
}
Expand All @@ -181,6 +183,7 @@ pub fn canvasWidgetKeyboardEventFromGpuInput(input_event: GpuSurfaceInputEvent,
.ime_set_composition,
.ime_commit_composition,
.ime_cancel_composition,
.magnify,
=> return null,
};
return .{
Expand Down Expand Up @@ -221,6 +224,7 @@ fn canvasWidgetTextEditEventFromGpuInput(input_event: GpuSurfaceInputEvent) ?can
.pointer_move,
.pointer_drag,
.scroll,
.magnify,
=> null,
};
}
Expand Down Expand Up @@ -412,3 +416,16 @@ pub fn gpuSurfaceFrameEventFromGpuFrame(frame: platform.GpuFrame) platform.GpuSu
.widget_semantics_count = frame.widget_semantics_count,
};
}

test "magnify gpu input is not a widget pointer or keyboard event" {
const input = GpuSurfaceInputEvent{
.label = "canvas",
.kind = .magnify,
.x = 10,
.y = 20,
.delta_y = 0.05,
};
try std.testing.expect(canvasWidgetPointerEventFromGpuInput(input) == null);
try std.testing.expect(canvasWidgetKeyboardEventFromGpuInput(input, 1) == null);
try std.testing.expect(canvasWidgetInputBatchesDisplayListRefresh(.magnify));
}