mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
initial window size needs to take into account window chrome
This commit is contained in:
@ -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);
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user