From f27b0670814f18bde672838606ff5d22b2d112e4 Mon Sep 17 00:00:00 2001 From: Alvaro Gaona Date: Mon, 10 Aug 2026 23:34:01 +0200 Subject: [PATCH 1/2] feat(platform): let apps offset the window-control cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A titlebar style fixes both the band's height and where the system centres the window controls in it. An app whose own header is a different height than any offered band therefore cannot line its content up with them — the only lever was picking a different band, which changes the window's whole shape (on macOS 26 the tall band also rounds the window corner harder). `WindowOptions.window_controls_offset` decouples the two: keep the band you want and put the controls on your own centreline. Declared in app.zon as `.window_controls_offset_x` / `_y`, positive right and down, zero (the default) leaving the platform's placement untouched. macOS moves the three `standardWindowButton` views; every other platform ignores it, the same honest no-op `WindowChrome.insets` already reports on the edge a platform does not use. The offset is REMEMBERED rather than applied once. AppKit relays those views out on its own schedule — entering and leaving fullscreen — so the host stores it and re-applies after each, and each apply sets an absolute origin from a captured base rather than nudging what is there, so repeated applies cannot walk the buttons across the titlebar. Nothing to add for reporting: `chromeInsetsForWindowId:` already derives `WindowChrome.buttons` by unioning the real button frames, so an app centring against the cluster sees the moved position with no second source of truth. --- src/app_runner/root.zig | 21 +++++ src/platform/macos/appkit_host.m | 78 +++++++++++++++++++ src/platform/macos/root.zig | 15 ++++ src/platform/types.zig | 17 ++++ .../app_manifest/comptime_scene.zig | 14 ++++ src/primitives/app_manifest/types.zig | 9 +++ 6 files changed, 154 insertions(+) diff --git a/src/app_runner/root.zig b/src/app_runner/root.zig index b60ae23dd..16a9e4619 100644 --- a/src/app_runner/root.zig +++ b/src/app_runner/root.zig @@ -138,6 +138,12 @@ pub const RunOptions = struct { // Close handling is host window state like the titlebar: // the manifest's declaration rides the host create. info.main_window.close_policy = manifestShellStartupClosePolicy(); + // Same for the window-control offset: the host re-applies it + // after every relayout, so it has to know it from create on. + info.main_window.window_controls_offset = .{ + .x = manifestShellStartupFloat("window_controls_offset_x", 0), + .y = manifestShellStartupFloat("window_controls_offset_y", 0), + }; } return info; } @@ -272,6 +278,10 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi .min_width = windowMinSize(window, "min_width"), .min_height = windowMinSize(window, "min_height"), .close_policy = windowClosePolicy(window), + .window_controls_offset = .{ + .x = windowFloat(window, "window_controls_offset_x", 0), + .y = windowFloat(window, "window_controls_offset_y", 0), + }, }; } @@ -350,6 +360,17 @@ fn manifestShellStartupMinSize(comptime field: []const u8) f32 { return windowMinSize(shell.windows[0], field); } +/// A plain float off the STARTUP window declaration. Unlike +/// `manifestShellStartupMinSize`, negative values are meaningful here: +/// the window-control offset moves the cluster left and up too. +fn manifestShellStartupFloat(comptime field: []const u8, comptime default_value: f32) f32 { + if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return default_value; + const shell = app_manifest.shell; + if (comptime !@hasField(@TypeOf(shell), "windows")) return default_value; + if (comptime shell.windows.len == 0) return default_value; + return windowFloat(shell.windows[0], field, default_value); +} + /// Present-before-show for the STARTUP window: when app.zon's first /// shell window hosts a canvas (`gpu_surface` view), the host creates /// it ordered-out and it becomes visible after the first canvas frame diff --git a/src/platform/macos/appkit_host.m b/src/platform/macos/appkit_host.m index ca6d7d1a1..82df643ef 100644 --- a/src/platform/macos/appkit_host.m +++ b/src/platform/macos/appkit_host.m @@ -802,6 +802,16 @@ @interface NativeSdkAppKitHost : NSObject *windows; +/// Declared window-control offsets, by window id. Kept rather than +/// applied once: AppKit lays the standard buttons out again on its own +/// schedule (fullscreen transitions, toolbar visibility), so the offset +/// has to be re-appliable. +@property(nonatomic, strong) NSMutableDictionary *windowControlOffsets; +/// Where AppKit puts the three buttons when left alone, captured the +/// first time a window's cluster moves. Applies use base + offset, so +/// re-applying lands in the same place instead of nudging the buttons +/// further along each time. +@property(nonatomic, strong) NSMutableDictionary *> *windowControlBaseOrigins; @property(nonatomic, strong) NSMutableDictionary *webViews; @property(nonatomic, strong) NSMutableDictionary *delegates; @property(nonatomic, strong) NSMutableDictionary *bridgeScriptHandlers; @@ -1033,6 +1043,7 @@ @interface NativeSdkAppKitHost : NSObject 0 ? windowLabel : @"main"; self.windows = [[NSMutableDictionary alloc] init]; + self.windowControlOffsets = [[NSMutableDictionary alloc] init]; + self.windowControlBaseOrigins = [[NSMutableDictionary alloc] init]; self.webViews = [[NSMutableDictionary alloc] init]; self.delegates = [[NSMutableDictionary alloc] init]; self.bridgeScriptHandlers = [[NSMutableDictionary alloc] init]; @@ -8349,6 +8371,48 @@ - (BOOL)startWindowDragWithId:(uint64_t)windowId { return YES; } +/// Put the window's three standard buttons at their declared offset. +/// Callable as often as AppKit relayouts them: it sets an absolute +/// origin from the captured base rather than nudging the current +/// frames, which would walk the buttons across the titlebar one +/// fullscreen toggle at a time. +/// +/// The declared offset is positive-down like the rest of the SDK's +/// geometry, but the titlebar's view space is positive-up — hence the +/// negated y here and nowhere else. +- (void)applyWindowControlsOffsetForWindowId:(uint64_t)windowId { + NSValue *stored = self.windowControlOffsets[@(windowId)]; + if (!stored) return; + NSWindow *window = self.windows[@(windowId)]; + if (!window) return; + NSButton *buttons[3] = { + [window standardWindowButton:NSWindowCloseButton], + [window standardWindowButton:NSWindowMiniaturizeButton], + [window standardWindowButton:NSWindowZoomButton], + }; + for (int index = 0; index < 3; index += 1) { + if (!buttons[index]) return; + } + + NSArray *base = self.windowControlBaseOrigins[@(windowId)]; + if (!base) { + base = @[ + [NSValue valueWithPoint:buttons[0].frame.origin], + [NSValue valueWithPoint:buttons[1].frame.origin], + [NSValue valueWithPoint:buttons[2].frame.origin], + ]; + self.windowControlBaseOrigins[@(windowId)] = base; + } + + NSPoint offset = stored.pointValue; + for (int index = 0; index < 3; index += 1) { + NSPoint origin = base[index].pointValue; + NSRect frame = buttons[index].frame; + frame.origin = NSMakePoint(origin.x + offset.x, origin.y - offset.y); + buttons[index].frame = frame; + } +} + // Chrome overlay geometry for hidden-titlebar windows: how far the // transparent titlebar (top) and the traffic lights (leading edge) // overlay the content view, plus the traffic-light cluster's bounding @@ -12860,6 +12924,20 @@ int native_sdk_appkit_create_window(native_sdk_appkit_host_t *host, uint64_t win return [object createWindowWithId:window_id title:titleString ?: @"" label:labelString ?: @"" x:x y:y width:width height:height restoreFrame:(restore_frame != 0) initialPlacement:initial_placement restorePolicy:restore_policy resizable:(resizable != 0) titlebarStyle:titlebar_style showPolicy:show_policy windowFlags:window_flags makeMain:NO] ? 1 : 0; } +int native_sdk_appkit_set_window_controls_offset(native_sdk_appkit_host_t *host, uint64_t window_id, double dx, double dy) { + NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + if (dx == 0 && dy == 0) { + [object.windowControlOffsets removeObjectForKey:@(window_id)]; + return 1; + } + // Store before applying: the apply reads this back on every relayout. + object.windowControlOffsets[@(window_id)] = [NSValue valueWithPoint:NSMakePoint(dx, dy)]; + [object applyWindowControlsOffsetForWindowId:window_id]; + return 1; +} + int native_sdk_appkit_set_window_content_min_size(native_sdk_appkit_host_t *host, uint64_t window_id, double min_width, double min_height) { NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; NSWindow *window = object.windows[@(window_id)]; diff --git a/src/platform/macos/root.zig b/src/platform/macos/root.zig index 0fbc51bb7..ffee3c1de 100644 --- a/src/platform/macos/root.zig +++ b/src/platform/macos/root.zig @@ -190,6 +190,7 @@ extern fn native_sdk_appkit_set_shortcuts(host: *AppKitHost, ids: [*]const [*]co extern fn native_sdk_appkit_request_frame(host: *AppKitHost) void; extern fn native_sdk_appkit_create_window(host: *AppKitHost, window_id: u64, window_title: [*]const u8, window_title_len: usize, window_label: [*]const u8, window_label_len: usize, x: f64, y: f64, width: f64, height: f64, restore_frame: c_int, initial_placement: c_int, restore_policy: c_int, resizable: c_int, titlebar_style: c_int, show_policy: c_int, window_flags: u32) c_int; extern fn native_sdk_appkit_set_window_content_min_size(host: *AppKitHost, window_id: u64, min_width: f64, min_height: f64) c_int; +extern fn native_sdk_appkit_set_window_controls_offset(host: *AppKitHost, window_id: u64, dx: f64, dy: f64) c_int; extern fn native_sdk_appkit_focus_window(host: *AppKitHost, window_id: u64) c_int; extern fn native_sdk_appkit_close_window(host: *AppKitHost, window_id: u64) c_int; extern fn native_sdk_appkit_minimize_window(host: *AppKitHost, window_id: u64) c_int; @@ -690,6 +691,10 @@ pub const MacPlatform = struct { // .hide threads through here so the STARTUP window's red // button hides from the first frame on. applyWindowClosePolicy(host, window_options.id, window_options.close_policy); + // And the control offset, which has to land before the first + // chrome query: `WindowChrome.buttons` is measured off the real + // button frames, so a late apply reports the unmoved cluster. + applyWindowControlsOffset(host, window_options.id, window_options.window_controls_offset); return .{ .host = host, .web_engine = web_engine, @@ -1473,6 +1478,15 @@ fn applyWindowContentMinSize(host: *AppKitHost, window_id: u64, min_width: f32, _ = native_sdk_appkit_set_window_content_min_size(host, window_id, width, height); } +/// Move a created window's control cluster to its declared offset. Zero +/// means "leave AppKit's placement alone" and skips the call, so a +/// window that declares nothing never registers an offset at all. +fn applyWindowControlsOffset(host: *AppKitHost, window_id: u64, offset: geometry.PointF) void { + if (!std.math.isFinite(offset.x) or !std.math.isFinite(offset.y)) return; + if (offset.x == 0 and offset.y == 0) return; + _ = native_sdk_appkit_set_window_controls_offset(host, window_id, offset.x, offset.y); +} + fn createWindow(context: ?*anyopaque, options: platform_mod.WindowOptions) anyerror!platform_mod.WindowInfo { const self: *MacPlatform = @ptrCast(@alignCast(context.?)); try refuseUnsupportedTransparentWindow(self.web_engine, options); @@ -1481,6 +1495,7 @@ fn createWindow(context: ?*anyopaque, options: platform_mod.WindowOptions) anyer if (native_sdk_appkit_create_window(self.host, options.id, title.ptr, title.len, options.label.ptr, options.label.len, frame.x, frame.y, frame.width, frame.height, if (options.restore_state) 1 else 0, initialPlacementInt(options.initial_placement), restorePolicyInt(options.restore_policy), if (options.resizable) 1 else 0, titlebarStyleInt(options.titlebar), showModeInt(options.show), windowFlags(options)) == 0) return error.CreateFailed; applyWindowContentMinSize(self.host, options.id, options.min_width, options.min_height); applyWindowClosePolicy(self.host, options.id, options.close_policy); + applyWindowControlsOffset(self.host, options.id, options.window_controls_offset); return .{ .id = options.id, .label = options.label, diff --git a/src/platform/types.zig b/src/platform/types.zig index d47e05b23..1f93d194c 100644 --- a/src/platform/types.zig +++ b/src/platform/types.zig @@ -676,6 +676,21 @@ pub const WindowOptions = struct { /// What the user's close affordance does — see `WindowClosePolicy`. /// Fixed at create like the titlebar: it is host window state. close_policy: WindowClosePolicy = .quit, + /// Move the window-control cluster from where the platform puts it, + /// in points, positive right and down — macOS's traffic lights, + /// ignored elsewhere. Zero, the default, leaves it where it is. + /// + /// A titlebar style fixes both the band's height and where the + /// system centres the controls in it, so an app whose header is a + /// different height than any offered band cannot line up with them. + /// This decouples the two: keep the band, move the controls. + /// + /// Host window state like the titlebar, so it rides the create; the + /// host re-applies it wherever the platform relayouts the controls + /// (macOS: fullscreen transitions). `WindowChrome.buttons` reports + /// the moved cluster, so a header centring against it needs no + /// second source of truth. + window_controls_offset: geometry.PointF = geometry.PointF.init(0, 0), pub fn resolvedTitle(self: WindowOptions, app_name: []const u8) []const u8 { return if (self.title.len > 0) self.title else app_name; @@ -750,6 +765,8 @@ pub const WindowCreateOptions = struct { min_height: f32 = 0, /// See `WindowOptions.close_policy`. close_policy: WindowClosePolicy = .quit, + /// See `WindowOptions.window_controls_offset`. + window_controls_offset: geometry.PointF = geometry.PointF.init(0, 0), source: ?WebViewSource = null, pub fn windowOptions(self: WindowCreateOptions, id: WindowId, label: []const u8) WindowOptions { diff --git a/src/primitives/app_manifest/comptime_scene.zig b/src/primitives/app_manifest/comptime_scene.zig index 71368d01c..1d7ffb6b3 100644 --- a/src/primitives/app_manifest/comptime_scene.zig +++ b/src/primitives/app_manifest/comptime_scene.zig @@ -92,6 +92,12 @@ fn shellWindowFrom(comptime window: anytype) types.ShellWindow { if (@hasField(@TypeOf(window), "close_policy")) { out.close_policy = enumField(types.WindowClosePolicy, window.close_policy, "close_policy"); } + if (@hasField(@TypeOf(window), "window_controls_offset_x")) { + out.window_controls_offset_x = window.window_controls_offset_x; + } + if (@hasField(@TypeOf(window), "window_controls_offset_y")) { + out.window_controls_offset_y = window.window_controls_offset_y; + } if (@hasField(@TypeOf(window), "views")) { var views: []const types.ShellView = &.{}; for (window.views) |view| { @@ -293,6 +299,8 @@ test "shellConfigFrom converts a rich scene without exhausting the branch quota" .titlebar = "hidden_inset_tall", .min_width = 1144, .min_height = 720, + .window_controls_offset_x = 4, + .window_controls_offset_y = 8, .views = .{ rich_view, .{ .label = "side", .kind = "sidebar", .edge = "right", .axis = "row", .width = 240 }, @@ -336,6 +344,12 @@ test "shellConfigFrom converts a rich scene without exhausting the branch quota" try std.testing.expectEqual(@as(usize, 3), scene.windows.len); try std.testing.expectEqual(types.WindowTitlebarStyle.hidden_inset_tall, scene.windows[0].titlebar); try std.testing.expectEqual(types.WindowRestorePolicy.center_on_primary, scene.windows[0].restore_policy); + // A declared offset survives the ZON read; a window that declares + // none reads zero, the sentinel the host checks before applying. + try std.testing.expectEqual(@as(f32, 4), scene.windows[0].window_controls_offset_x); + try std.testing.expectEqual(@as(f32, 8), scene.windows[0].window_controls_offset_y); + try std.testing.expectEqual(@as(f32, 0), scene.windows[1].window_controls_offset_x); + try std.testing.expectEqual(@as(f32, 0), scene.windows[1].window_controls_offset_y); try std.testing.expectEqual(@as(usize, 4), scene.windows[0].views.len); try std.testing.expectEqual(types.ShellEdge.left, scene.windows[0].views[0].edge.?); try std.testing.expectEqual(types.ShellAxis.column, scene.windows[0].views[0].axis.?); diff --git a/src/primitives/app_manifest/types.zig b/src/primitives/app_manifest/types.zig index 5507b8ebf..5f56d84d9 100644 --- a/src/primitives/app_manifest/types.zig +++ b/src/primitives/app_manifest/types.zig @@ -529,6 +529,15 @@ pub const ShellWindow = struct { /// STARTUP window threads it through the host create, and /// runtime-created windows apply their own declaration at create. close_policy: WindowClosePolicy = .quit, + /// Move the window-control cluster from where the platform puts it, + /// in points, positive right and down — macOS's traffic lights, + /// ignored elsewhere. A titlebar style fixes both the band height + /// and where the controls sit inside it, so a header of any other + /// height cannot line up with them; this decouples the two. Zero + /// leaves the platform's placement alone. Two scalars rather than a + /// point, to keep the ZON flat like `min_width`/`min_height`. + window_controls_offset_x: f32 = 0, + window_controls_offset_y: f32 = 0, views: []const ShellView = &.{}, }; From 98296c480b2f571fb71620300245f77ee52b2d71 Mon Sep 17 00:00:00 2001 From: Alvaro Gaona Date: Tue, 11 Aug 2026 00:59:05 +0200 Subject: [PATCH 2/2] fix(platform): carry window_controls_offset through WindowCreateOptions The conversion to WindowOptions dropped the field, so imperatively created windows always got the default (0, 0) offset. The null platform now captures the offset at create, and a test covers both a window that declares one and a window that does not. --- src/platform/null_platform.zig | 5 ++++ src/platform/types.zig | 1 + src/runtime/window_command_bridge_tests.zig | 30 +++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/platform/null_platform.zig b/src/platform/null_platform.zig index 986e1a54e..2a70d2432 100644 --- a/src/platform/null_platform.zig +++ b/src/platform/null_platform.zig @@ -464,6 +464,10 @@ pub const NullPlatform = struct { /// seam-regression purpose as `window_resizable`. window_min_width: [max_windows]f32 = [_]f32{0} ** max_windows, window_min_height: [max_windows]f32 = [_]f32{0} ** max_windows, + /// Captured `WindowOptions.window_controls_offset` per created + /// window: only macOS acts on it, so nothing else would notice a + /// caller's offset being dropped before the create. + window_controls_offset: [max_windows]geometry.PointF = [_]geometry.PointF{geometry.PointF.init(0, 0)} ** max_windows, /// Live visibility per window, modeling the macOS host: immediate /// windows are visible at create; `.on_first_present` windows stay /// hidden until their first gpu-surface present (or an explicit @@ -1187,6 +1191,7 @@ pub const NullPlatform = struct { self.window_close_policy[self.window_count] = options.close_policy; self.window_min_width[self.window_count] = options.min_width; self.window_min_height[self.window_count] = options.min_height; + self.window_controls_offset[self.window_count] = options.window_controls_offset; self.window_first_present_seq[self.window_count] = 0; // Present-before-show: deferred windows are created hidden and // become visible on their first gpu-surface present. diff --git a/src/platform/types.zig b/src/platform/types.zig index 1f93d194c..b0f77f876 100644 --- a/src/platform/types.zig +++ b/src/platform/types.zig @@ -797,6 +797,7 @@ pub const WindowCreateOptions = struct { .min_width = self.min_width, .min_height = self.min_height, .close_policy = self.close_policy, + .window_controls_offset = self.window_controls_offset, }; } }; diff --git a/src/runtime/window_command_bridge_tests.zig b/src/runtime/window_command_bridge_tests.zig index b6a8f25c5..2f722ac6a 100644 --- a/src/runtime/window_command_bridge_tests.zig +++ b/src/runtime/window_command_bridge_tests.zig @@ -182,6 +182,36 @@ test "runtime-created window store miss preserves authored placement" { try std.testing.expectEqual(platform.WindowInitialPlacement.explicit, harness.null_platform.window_placement[1]); } +test "an imperative window's control offset survives to the platform create" { + const TestApp = struct { + fn app(self: *@This()) App { + return .{ .context = self, .name = "controls-offset", .source = platform.WebViewSource.html("

Main

") }; + } + }; + + const harness = try TestHarness().create(std.testing.allocator, .{}); + defer harness.destroy(std.testing.allocator); + var app_state: TestApp = .{}; + try harness.start(app_state.app()); + + // `WindowCreateOptions` converts to the `WindowOptions` the create + // seam takes, so a field the conversion forgets is dropped in silence. + const tools = try harness.runtime.createWindow(.{ + .label = "tools", + .title = "Tools", + .window_controls_offset = geometry.PointF.init(6, -4), + }); + const index: usize = @intCast(tools.id - 1); + try std.testing.expectEqual(@as(f32, 6), harness.null_platform.window_controls_offset[index].x); + try std.testing.expectEqual(@as(f32, -4), harness.null_platform.window_controls_offset[index].y); + + // A window that declares nothing keeps the zero sentinel. + const plain = try harness.runtime.createWindow(.{ .label = "plain", .title = "Plain" }); + const plain_index: usize = @intCast(plain.id - 1); + try std.testing.expectEqual(@as(f32, 0), harness.null_platform.window_controls_offset[plain_index].x); + try std.testing.expectEqual(@as(f32, 0), harness.null_platform.window_controls_offset[plain_index].y); +} + test "transparent imperative window without explicit source stays canvas-only" { const TestApp = struct { fn app(self: *@This()) App {