On macOS the window frame event reports the frame size, but every other path treats those numbers as a content size. They disagree by the height of the titlebar, so an app lays its shell out taller than the view it lives in and anything on the bottom edge is placed below the window.
Reproduced on v0.9.0 (9ce03701).
What you see
A standard titlebar window whose manifest asks for 760x760 is created with a 760x760 content view and then told its shell is 760x792. Everything laid out relative to the bottom edge is 32 points too low: a status bar at the bottom of the app is simply not visible.
It comes back the first time you resize the window, which is the clue to where the disagreement is: the resize path reports contentView.bounds and is correct, so the wrong number is only the one sent on open and restore.
Why
Three places, two of which agree with each other and one of which does not.
Windows are created with a content rect. src/platform/macos/appkit_host.m:7763 and :7767 both use initWithContentRect:, so the rect an app asks for in its manifest becomes the size of the content view, not of the frame.
The runtime lays out into the frame event's width and height as though they were a content size. src/runtime/window_storage.zig:216:
pub fn shellBoundsForWindow(self: *const Runtime, window_id: platform.WindowId) geometry.RectF {
const index = Self.findWindowIndexById(self, window_id) orelse return geometry.RectF.init(0, 0, 0, 0);
const frame_value = self.windows[index].info.frame;
const bounds = geometry.RectF.init(0, 0, frame_value.width, frame_value.height);
The origin is 0, 0, which is content-relative, and the width and height come straight off the reported frame.
But the event reports the frame. src/platform/macos/appkit_host.m:9980:
- (void)emitWindowFrameForWindowId:(uint64_t)windowId open:(BOOL)open {
NSWindow *window = self.windows[@(windowId)] ?: self.window;
NSString *label = ...;
NSRect frame = window.frame;
[self emitEvent:(native_sdk_appkit_event_t){
...
.width = frame.size.width,
.height = frame.size.height,
window.frame includes the titlebar. So the two numbers the runtime lays out into are 32 points taller than the view they will be drawn in.
The fix
Send the content size, keeping the frame's origin. The origin is what positions the window on screen and what the restore path round-trips, so it should stay as it is; only the size is wrong.
In emitWindowFrameForWindowId:
NSRect frame = window.frame;
NSRect content = [window contentRectForFrameRect:frame];
[self emitEvent:(native_sdk_appkit_event_t){
.kind = NATIVE_SDK_APPKIT_EVENT_WINDOW_FRAME,
.window_id = windowId,
.x = frame.origin.x,
.y = frame.origin.y,
.width = content.size.width,
.height = content.size.height,
contentRectForFrameRect: is the exact inverse of the initWithContentRect: used to create the window, so this makes the open and restore events agree with both the creation path and the resize path.
A chromeless window has no titlebar, so its content rect equals its frame and this changes nothing for it.
Checking it
The cheapest check is an app with anything anchored to the bottom of its shell, opened at a size its manifest declares. Before, the element is missing and appears after the first resize. After, it is in place on the first frame and the resize changes nothing.
On macOS the window frame event reports the frame size, but every other path treats those numbers as a content size. They disagree by the height of the titlebar, so an app lays its shell out taller than the view it lives in and anything on the bottom edge is placed below the window.
Reproduced on v0.9.0 (
9ce03701).What you see
A standard titlebar window whose manifest asks for 760x760 is created with a 760x760 content view and then told its shell is 760x792. Everything laid out relative to the bottom edge is 32 points too low: a status bar at the bottom of the app is simply not visible.
It comes back the first time you resize the window, which is the clue to where the disagreement is: the resize path reports
contentView.boundsand is correct, so the wrong number is only the one sent on open and restore.Why
Three places, two of which agree with each other and one of which does not.
Windows are created with a content rect.
src/platform/macos/appkit_host.m:7763and:7767both useinitWithContentRect:, so the rect an app asks for in its manifest becomes the size of the content view, not of the frame.The runtime lays out into the frame event's width and height as though they were a content size.
src/runtime/window_storage.zig:216:The origin is
0, 0, which is content-relative, and the width and height come straight off the reported frame.But the event reports the frame.
src/platform/macos/appkit_host.m:9980:window.frameincludes the titlebar. So the two numbers the runtime lays out into are 32 points taller than the view they will be drawn in.The fix
Send the content size, keeping the frame's origin. The origin is what positions the window on screen and what the restore path round-trips, so it should stay as it is; only the size is wrong.
In
emitWindowFrameForWindowId:contentRectForFrameRect:is the exact inverse of theinitWithContentRect:used to create the window, so this makes the open and restore events agree with both the creation path and the resize path.A chromeless window has no titlebar, so its content rect equals its frame and this changes nothing for it.
Checking it
The cheapest check is an app with anything anchored to the bottom of its shell, opened at a size its manifest declares. Before, the element is missing and appears after the first resize. After, it is in place on the first frame and the resize changes nothing.