From a1a8aeb104e9fb47f1396ddb0572b056533ffeaa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 30 Sep 2023 21:35:50 -0700 Subject: [PATCH] initial window size needs to take into account window chrome --- include/ghostty.h | 12 +--- macos/Sources/Ghostty/AppState.swift | 9 ++- macos/Sources/Ghostty/SurfaceView.swift | 79 ++++++++++++------------- src/Surface.zig | 30 ++++++++++ src/apprt/embedded.zig | 38 ++++-------- src/apprt/glfw.zig | 16 +++-- 6 files changed, 97 insertions(+), 87 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 198fe08dc..1a0a7c9c6 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -291,15 +291,6 @@ typedef enum { GHOSTTY_BUILD_MODE_RELEASE_SMALL, } ghostty_build_mode_e; -typedef struct { - bool origin; - bool size; - uint32_t x; - uint32_t y; - uint32_t w; - uint32_t h; -} ghostty_rect_s; - // Fully defined types. This MUST be kept in sync with equivalent Zig // structs. To find the Zig struct, grep for this type name. The documentation // for all of these types is available in the Zig source. @@ -335,6 +326,7 @@ typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direc typedef void (*ghostty_runtime_toggle_split_zoom_cb)(void *); typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t); typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, ghostty_non_native_fullscreen_e); +typedef void (*ghostty_runtime_set_initial_window_size_cb)(void *, uint32_t, uint32_t); typedef struct { void *userdata; @@ -354,6 +346,7 @@ typedef struct { ghostty_runtime_toggle_split_zoom_cb toggle_split_zoom_cb; ghostty_runtime_goto_tab_cb goto_tab_cb; ghostty_runtime_toggle_fullscreen_cb toggle_fullscreen_cb; + ghostty_runtime_set_initial_window_size_cb set_initial_window_size_cb; } ghostty_runtime_config_s; //------------------------------------------------------------------- @@ -389,7 +382,6 @@ ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); void ghostty_surface_free(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t); bool ghostty_surface_transparent(ghostty_surface_t); -ghostty_rect_s ghostty_surface_window_frame(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_focus(ghostty_surface_t, bool); diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 5cf39f044..e01ce96af 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -125,7 +125,8 @@ extension Ghostty { focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) }, goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, - toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) } + toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) }, + set_initial_window_size_cb: { userdata, width, height in AppState.setInitialWindowSize(userdata, width: width, height: height) } ) // Create the ghostty app. @@ -413,6 +414,12 @@ extension Ghostty { ] ) } + + static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { + // We need a window to set the frame + guard let surfaceView = self.surfaceUserdata(from: userdata) else { return } + surfaceView.initialSize = NSMakeSize(Double(width), Double(height)) + } static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { guard let surface = self.surfaceUserdata(from: userdata) else { return } diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index ac2788796..d686055c9 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -71,7 +71,6 @@ extension Ghostty { // We use these notifications to determine when the window our surface is // attached to is or is not focused. let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView) - let pubInitialFrame = NotificationCenter.default.publisher(for: Notification.didReceiveInitialWindowFrame, object: surfaceView) let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification) let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification) @@ -101,26 +100,6 @@ extension Ghostty { surfaceFocus = true } } - .onReceive(pubInitialFrame) { notification in - // We never set the initial frame if we're a split - guard !isSplit else { return } - - // We need a window to set the frame - guard let surfaceWindow = surfaceView.window else { return } - guard let frameAny = notification.userInfo?[Ghostty.Notification.FrameKey] else { return } - guard let frame = frameAny as? NSRect else { return } - - // If we have tabs, then do not change the window size - guard let windowControllerRaw = surfaceWindow.windowController else { return } - guard let windowController = windowControllerRaw as? PrimaryWindowController else { return } - guard !windowController.didInitializeFromSurface else { return } - - // We have no tabs and we are not a split, so set the initial size of the window. - surfaceWindow.setFrame(frame, display: true) - - // Note that we did initialize - windowController.didInitializeFromSurface = true - } .onAppear() { // Welcome to the SwiftUI bug house of horrors. On macOS 12 (at least // 12.5.1, didn't test other versions), the order in which the view @@ -213,6 +192,10 @@ extension Ghostty { // to the app level and it is set from there. @Published var title: String = "👻" + // An initial size to request for a window. This will only affect + // then the view is moved to a new window. + var initialSize: NSSize? = nil + private(set) var surface: ghostty_surface_t? var error: Error? = nil @@ -429,27 +412,39 @@ extension Ghostty { // If we have a blur, set the blur ghostty_set_window_background_blur(surface, Unmanaged.passUnretained(window).toOpaque()) - // Set the window size - let rect = ghostty_surface_window_frame(surface) - if (rect.size || rect.origin) { - var frame = window.frame - if (rect.origin) { - frame.origin.x = Double(rect.x) - frame.origin.y = Double(rect.y) - } - if (rect.size) { - frame.size.width = Double(rect.w) - frame.size.height = Double(rect.h) - } - - NotificationCenter.default.post( - name: Notification.didReceiveInitialWindowFrame, - object: self, - userInfo: [ - Notification.FrameKey: frame, - ] - ) - } + // Try to set the initial window size if we have one + setInitialWindowSize() + } + + /// Sets the initial window size requested by the Ghostty config. + /// + /// This only works under certain conditions: + /// - The window must be "uninitialized" + /// - The window must have no tabs + /// - Ghostty must have requested an initial size + /// + private func setInitialWindowSize() { + guard let initialSize = initialSize else { return } + + // If we have tabs, then do not change the window size + guard let window = self.window else { return } + guard let windowControllerRaw = window.windowController else { return } + guard let windowController = windowControllerRaw as? PrimaryWindowController else { return } + guard !windowController.didInitializeFromSurface else { return } + + // Setup our frame. We need to first subtract the views frame so that we can + // just get the chrome frame so that we only affect the surface view size. + var frame = window.frame + frame.size.width -= self.frame.size.width + frame.size.height -= self.frame.size.height + frame.size.width += initialSize.width + frame.size.height += initialSize.height + + // We have no tabs and we are not a split, so set the initial size of the window. + window.setFrame(frame, display: true) + + // Note that we did initialize + windowController.didInitializeFromSurface = true } override func becomeFirstResponder() -> Bool { diff --git a/src/Surface.zig b/src/Surface.zig index 5711edf08..172d2495a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -471,6 +471,36 @@ pub fn init( .{&self.io_thread}, ); self.io_thr.setName("io") catch {}; + + // Determine our initial window size if configured. We need to do this + // quite late in the process because our height/width are in grid dimensions, + // so we need to know our cell sizes first. + // + // Note: it is important to do this after the renderer is setup above. + // This allows the apprt to fully initialize the surface before we + // start messing with the window. + if (config.@"window-height" > 0 and config.@"window-width" > 0) init: { + const scale = rt_surface.getContentScale() catch break :init; + const height = @max(config.@"window-height" * cell_size.height, 480); + const width = @max(config.@"window-width" * cell_size.width, 640); + const width_f32: f32 = @floatFromInt(width); + const height_f32: f32 = @floatFromInt(height); + + // The final values are affected by content scale and we need to + // account for the padding so we get the exact correct grid size. + const final_width: u32 = + @as(u32, @intFromFloat(@ceil(width_f32 / scale.x))) + + padding.left + + padding.right; + const final_height: u32 = + @as(u32, @intFromFloat(@ceil(height_f32 / scale.y))) + + padding.top + + padding.bottom; + + rt_surface.setInitialWindowSize(final_width, final_height) catch |err| { + log.warn("unable to set initial window size: {s}", .{err}); + }; + } } pub fn deinit(self: *Surface) void { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index ecfb08012..da5a60273 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -87,6 +87,10 @@ pub const App = struct { /// Toggle fullscreen for current window. toggle_fullscreen: ?*const fn (SurfaceUD, configpkg.NonNativeFullscreen) callconv(.C) void = null, + + /// Set the initial window size. It is up to the user of libghostty to + /// determine if it is the initial window and set this appropriately. + set_initial_window_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null, }; /// Special values for the goto_tab callback. @@ -734,6 +738,15 @@ pub const Surface = struct { func(self.opts.userdata, options); } + pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void { + const func = self.app.opts.set_initial_window_size orelse { + log.info("runtime embedder does not set_initial_window_size", .{}); + return; + }; + + func(self.opts.userdata, width, height); + } + fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { const font_size: u16 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; @@ -757,16 +770,6 @@ pub const Surface = struct { pub const CAPI = struct { const global = &@import("../main.zig").state; - /// ghostty_rect_s - const Rect = extern struct { - origin: bool = false, - size: bool = false, - x: u32 = 0, - y: u32 = 0, - w: u32 = 0, - h: u32 = 0, - }; - /// Create a new app. export fn ghostty_app_new( opts: *const apprt.runtime.App.Options, @@ -874,21 +877,6 @@ pub const CAPI = struct { return surface.app.config.@"background-opacity" < 1.0; } - /// The desired window frame for a new window. - export fn ghostty_surface_window_frame(surface: *Surface) Rect { - const config = surface.app.config; - - // If the desired height/width isn't configured, return 0. - if (config.@"window-height" == 0 or config.@"window-width" == 0) return .{}; - - // Return the desired rect - return .{ - .size = true, - .h = @max(config.@"window-height" * surface.core_surface.cell_size.height, 480), - .w = @max(config.@"window-width" * surface.core_surface.cell_size.width, 640), - }; - } - /// Tell the surface that it needs to schedule a render export fn ghostty_surface_refresh(surface: *Surface) void { surface.refresh(); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index e69aa1479..4a40ed22e 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -376,15 +376,6 @@ pub const Surface = struct { self, ); errdefer self.core_surface.deinit(); - - // If we have a desired window size, we can now calculate the size - // because we have the cell size. - if (config.@"window-height" > 0 or config.@"window-width" > 0) { - self.window.setSize(.{ - .height = @max(config.@"window-height" * self.core_surface.cell_size.height, 480), - .width = @max(config.@"window-width" * self.core_surface.cell_size.width, 640), - }); - } } pub fn deinit(self: *Surface) void { @@ -456,6 +447,13 @@ pub const Surface = struct { self.app.app.alloc.destroy(self); } + /// Set the initial window size. This is called exactly once at + /// surface initialization time. This may be called before "self" + /// is fully initialized. + pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void { + self.window.setSize(.{ .width = width, .height = height }); + } + /// Set the size limits of the window. /// Note: this interface is not good, we should redo it if we plan /// to use this more. i.e. you can't set max width but no max height,