Skip to content

macOS: the window frame event reports the frame size where everything else expects the content size #355

Description

@sepehr-safari

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions