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
21 changes: 21 additions & 0 deletions src/app_runner/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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),
},
};
}

Expand Down Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions src/platform/macos/appkit_host.m
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,16 @@ @interface NativeSdkAppKitHost : NSObject <WKNavigationDelegate, NSMenuDelegate,
@property(nonatomic, strong) NativeSdkBridgeScriptHandler *bridgeScriptHandler;
@property(nonatomic, strong) NativeSdkAssetSchemeHandler *assetSchemeHandler;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSWindow *> *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<NSNumber *, NSValue *> *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<NSNumber *, NSArray<NSValue *> *> *windowControlBaseOrigins;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, WKWebView *> *webViews;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NativeSdkWindowDelegate *> *delegates;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NativeSdkBridgeScriptHandler *> *bridgeScriptHandlers;
Expand Down Expand Up @@ -1033,6 +1043,7 @@ @interface NativeSdkAppKitHost : NSObject <WKNavigationDelegate, NSMenuDelegate,
- (instancetype)initWithAppName:(NSString *)appName displayName:(NSString *)displayName version:(NSString *)version aboutDescription:(NSString *)aboutDescription hasWebContent:(BOOL)hasWebContent dockVisible:(BOOL)dockVisible windowTitle:(NSString *)windowTitle bundleIdentifier:(NSString *)bundleIdentifier iconPath:(NSString *)iconPath windowLabel:(NSString *)windowLabel x:(double)x y:(double)y width:(double)width height:(double)height restoreFrame:(BOOL)restoreFrame initialPlacement:(int)initialPlacement restorePolicy:(int)restorePolicy resizable:(BOOL)resizable titlebarStyle:(int)titlebarStyle showPolicy:(int)showPolicy windowFlags:(uint32_t)windowFlags;
- (BOOL)createWindowWithId:(uint64_t)windowId title:(NSString *)title label:(NSString *)label x:(double)x y:(double)y width:(double)width height:(double)height restoreFrame:(BOOL)restoreFrame initialPlacement:(int)initialPlacement restorePolicy:(int)restorePolicy resizable:(BOOL)resizable titlebarStyle:(int)titlebarStyle showPolicy:(int)showPolicy windowFlags:(uint32_t)windowFlags makeMain:(BOOL)makeMain;
- (void)orderWindowForImplicitShow:(uint64_t)windowId;
- (void)applyWindowControlsOffsetForWindowId:(uint64_t)windowId;
- (void)showDeferredWindowIfPending:(uint64_t)windowId reason:(const char *)reason;
- (void)applyWindowClearColor:(uint64_t)windowId red:(uint8_t)red green:(uint8_t)green blue:(uint8_t)blue alpha:(uint8_t)alpha;
- (void)focusWindowWithId:(uint64_t)windowId;
Expand Down Expand Up @@ -1268,6 +1279,7 @@ - (void)windowWillEnterFullScreen:(NSNotification *)notification {
- (void)windowDidEnterFullScreen:(NSNotification *)notification {
(void)notification;
[self setToolbarVisible:NO];
[self reapplyWindowControlsOffset];
}

- (void)windowWillExitFullScreen:(NSNotification *)notification {
Expand All @@ -1278,6 +1290,7 @@ - (void)windowWillExitFullScreen:(NSNotification *)notification {
- (void)windowDidExitFullScreen:(NSNotification *)notification {
(void)notification;
[self setToolbarVisible:YES];
[self reapplyWindowControlsOffset];
}

- (void)setToolbarVisible:(BOOL)visible {
Expand All @@ -1286,6 +1299,13 @@ - (void)setToolbarVisible:(BOOL)visible {
window.toolbar.visible = visible;
}

/// Fullscreen transitions relayout the standard buttons and discard our
/// placement, so re-apply the declared offset after each. A window that
/// declared none returns immediately.
- (void)reapplyWindowControlsOffset {
[self.host applyWindowControlsOffsetForWindowId:self.windowId];
}

// Registered for tall-titlebar windows only (`observesContentLayout`):
// whenever the titlebar band actually grows or shrinks — toolbar
// visibility flips settle a layout pass after the notification edges —
Expand Down Expand Up @@ -7915,6 +7935,8 @@ - (instancetype)initWithAppName:(NSString *)appName displayName:(NSString *)disp
self.iconPath = iconPath ?: @"";
self.windowLabel = windowLabel.length > 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];
Expand Down Expand Up @@ -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<NSValue *> *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
Expand Down Expand Up @@ -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)];
Expand Down
15 changes: 15 additions & 0 deletions src/platform/macos/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/platform/null_platform.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions src/platform/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Comment thread
vercel[bot] marked this conversation as resolved.
source: ?WebViewSource = null,

pub fn windowOptions(self: WindowCreateOptions, id: WindowId, label: []const u8) WindowOptions {
Expand Down Expand Up @@ -780,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,
};
}
};
Expand Down
14 changes: 14 additions & 0 deletions src/primitives/app_manifest/comptime_scene.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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.?);
Expand Down
9 changes: 9 additions & 0 deletions src/primitives/app_manifest/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 = &.{},
};

Expand Down
Loading