initial window size needs to take into account window chrome

This commit is contained in:
Mitchell Hashimoto
2023-09-30 21:35:50 -07:00
parent cc8e1cd936
commit a1a8aeb104
6 changed files with 97 additions and 87 deletions

View File

@ -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);

View File

@ -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.
@ -414,6 +415,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 }

View File

@ -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)
// Try to set the initial window size if we have one
setInitialWindowSize()
}
NotificationCenter.default.post(
name: Notification.didReceiveInitialWindowFrame,
object: self,
userInfo: [
Notification.FrameKey: frame,
]
)
}
/// 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 {

View File

@ -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 {

View File

@ -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();

View File

@ -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,