diff --git a/build.zig b/build.zig index bf2cfd5be..a7f42383e 100644 --- a/build.zig +++ b/build.zig @@ -147,6 +147,9 @@ pub fn build(b: *std.build.Builder) !void { lib.linkLibC(); lib.addOptions("build_options", exe_options); + // See the comment in this file + lib.addCSourceFile("src/renderer/metal_workaround.c", &.{}); + // Create a single static lib with all our dependencies merged var lib_list = try addDeps(b, lib, true); try lib_list.append(.{ .generated = &lib.output_path_source }); @@ -172,6 +175,9 @@ pub fn build(b: *std.build.Builder) !void { lib.linkLibC(); lib.addOptions("build_options", exe_options); + // See the comment in this file + lib.addCSourceFile("src/renderer/metal_workaround.c", &.{}); + // Create a single static lib with all our dependencies merged var lib_list = try addDeps(b, lib, true); try lib_list.append(.{ .generated = &lib.output_path_source }); @@ -425,10 +431,12 @@ fn addDeps( } // stb_image_resize - _ = try stb_image_resize.link(b, step, .{}); + const stb_image_resize_step = try stb_image_resize.link(b, step, .{}); + try static_libs.append(.{ .generated = &stb_image_resize_step.output_path_source }); // utf8proc - _ = try utf8proc.link(b, step); + const utf8proc_step = try utf8proc.link(b, step); + try static_libs.append(.{ .generated = &utf8proc_step.output_path_source }); // Imgui, we have to do this later since we need some information const imgui_backends = if (step.target.isDarwin()) @@ -499,7 +507,7 @@ fn addDeps( // Pixman const pixman_step = try pixman.link(b, step, .{}); - _ = pixman_step; + try static_libs.append(.{ .generated = &pixman_step.output_path_source }); // Only Linux gets fontconfig if (enable_fontconfig) { diff --git a/include/ghostty.h b/include/ghostty.h index 7c612e8a0..367f14207 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -27,7 +27,10 @@ typedef struct { void *userdata; ghostty_runtime_wakeup_cb wakeup_cb; } ghostty_runtime_config_s; -typedef struct {} ghostty_surface_config_s; +typedef struct { + void *nsview; + double scale_factor; +} ghostty_surface_config_s; // Opaque types typedef void *ghostty_app_t; @@ -44,12 +47,12 @@ void ghostty_config_free(ghostty_config_t); void ghostty_config_load_string(ghostty_config_t, const char *, uintptr_t); void ghostty_config_finalize(ghostty_config_t); -ghostty_app_t ghostty_app_new(ghostty_runtime_config_s *, ghostty_config_t); +ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t); void ghostty_app_free(ghostty_app_t); int ghostty_app_tick(ghostty_app_t); ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); -void ghostty_surface_free(ghostty_app_t, ghostty_surface_t); +void ghostty_surface_free(ghostty_surface_t); #ifdef __cplusplus } diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 77399b2e8..7eef0edd0 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -9,8 +9,8 @@ /* Begin PBXBuildFile section */ A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; + A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; }; - A5B30537299BEAAA0047F10C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30536299BEAAA0047F10C /* ContentView.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5D495A2299BEC7E00DD1313 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; /* End PBXBuildFile section */ @@ -18,9 +18,9 @@ /* Begin PBXFileReference section */ A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurfaceView.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30534299BEAAA0047F10C /* GhosttyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyApp.swift; sourceTree = ""; }; - A5B30536299BEAAA0047F10C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; @@ -43,9 +43,9 @@ children = ( A5D495A0299BEC2200DD1313 /* Preview Content */, A5B30534299BEAAA0047F10C /* GhosttyApp.swift */, - A5B30536299BEAAA0047F10C /* ContentView.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */, + A55685DF29A03A9F004303CE /* AppError.swift */, ); path = Sources; sourceTree = ""; @@ -153,8 +153,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A55685E029A03A9F004303CE /* AppError.swift in Sources */, A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */, - A5B30537299BEAAA0047F10C /* ContentView.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */, ); diff --git a/macos/Sources/AppError.swift b/macos/Sources/AppError.swift new file mode 100644 index 000000000..55f191d3d --- /dev/null +++ b/macos/Sources/AppError.swift @@ -0,0 +1,3 @@ +enum AppError: Error { + case surfaceCreateError +} diff --git a/macos/Sources/ContentView.swift b/macos/Sources/ContentView.swift deleted file mode 100644 index f54deaba8..000000000 --- a/macos/Sources/ContentView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index 11f6a80e7..2654ac5b3 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -20,7 +20,7 @@ struct GhosttyApp: App { case .error: ErrorView() case .ready: - TerminalSurfaceView() + TerminalSurfaceView(app: ghostty.app!) } } } @@ -37,6 +37,10 @@ class GhosttyState: ObservableObject { /// The ghostty global configuration. var config: ghostty_config_t? = nil + /// The ghostty app instance. We only have one of these for the entire app, although I guess + /// in theory you can have multiple... I don't know why you would... + var app: ghostty_app_t? = nil + init() { // Initialize ghostty global state. This happens once per process. guard ghostty_init() == GHOSTTY_SUCCESS else { @@ -51,6 +55,7 @@ class GhosttyState: ObservableObject { readiness = .error return } + self.config = cfg; // TODO: we'd probably do some config loading here... for now we'd // have to do this synchronously. When we support config updating we can do @@ -59,11 +64,29 @@ class GhosttyState: ObservableObject { // Finalize will make our defaults available. ghostty_config_finalize(cfg) - config = cfg - readiness = .ready + // Create our "runtime" config. The "runtime" is the configuration that ghostty + // uses to interface with the application runtime environment. + var runtime_cfg = ghostty_runtime_config_s( + userdata: nil, + wakeup_cb: { userdata in GhosttyState.wakeup() }) + + // Create the ghostty app. + guard let app = ghostty_app_new(&runtime_cfg, cfg) else { + GhosttyApp.logger.critical("ghostty_app_new failed") + readiness = .error + return + } + self.app = app + + self.readiness = .ready + } + + static func wakeup() { + // TODO } deinit { + ghostty_app_free(app) ghostty_config_free(config) } } diff --git a/macos/Sources/TerminalSurfaceView.swift b/macos/Sources/TerminalSurfaceView.swift index dcff76146..41505c5a3 100644 --- a/macos/Sources/TerminalSurfaceView.swift +++ b/macos/Sources/TerminalSurfaceView.swift @@ -1,4 +1,6 @@ +import OSLog import SwiftUI +import GhosttyKit /// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn /// and interacted with. The word "surface" is used because a surface may represent a window, a tab, @@ -8,7 +10,11 @@ import SwiftUI /// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to /// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with. struct TerminalSurfaceView: NSViewRepresentable { - @StateObject private var state = TerminalSurfaceState() + @StateObject private var state: TerminalSurfaceState + + init(app: ghostty_app_t) { + self._state = StateObject(wrappedValue: TerminalSurfaceState(app)) + } func makeNSView(context: Context) -> TerminalSurfaceView_Real { // We need the view as part of the state to be created previously because @@ -24,10 +30,33 @@ struct TerminalSurfaceView: NSViewRepresentable { /// The state for the terminal surface view. class TerminalSurfaceState: ObservableObject { - var view: TerminalSurfaceView_Real; + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: TerminalSurfaceState.self) + ) - init() { + var view: TerminalSurfaceView_Real + private var surface: ghostty_surface_t? = nil + private var error: Error? = nil + + init(_ app: ghostty_app_t) { view = TerminalSurfaceView_Real() + + var surface_cfg = ghostty_surface_config_s( + nsview: Unmanaged.passUnretained(view).toOpaque(), + scale_factor: 2.0) + guard let surface = ghostty_surface_new(app, &surface_cfg) else { + self.error = AppError.surfaceCreateError + return + } + + self.surface = surface; + } + + deinit { + if let surface = self.surface { + ghostty_surface_free(surface) + } } } @@ -52,8 +81,10 @@ class TerminalSurfaceView_Real: NSView { } } -struct TerminalSurfaceView_Previews: PreviewProvider { - static var previews: some View { - TerminalSurfaceView() - } -} +/* + struct TerminalSurfaceView_Previews: PreviewProvider { + static var previews: some View { + TerminalSurfaceView() + } + } + */ diff --git a/pkg/pixman/build.zig b/pkg/pixman/build.zig index 16df1f85d..4a1650a21 100644 --- a/pkg/pixman/build.zig +++ b/pkg/pixman/build.zig @@ -138,7 +138,7 @@ const srcs = &.{ root ++ "pixman/pixman-region16.c", root ++ "pixman/pixman-region32.c", root ++ "pixman/pixman-solid-fill.c", - root ++ "pixman/pixman-timer.c", + //root ++ "pixman/pixman-timer.c", root ++ "pixman/pixman-trap.c", root ++ "pixman/pixman-utils.c", }; diff --git a/src/App.zig b/src/App.zig index 2c70d399f..1d4b833f8 100644 --- a/src/App.zig +++ b/src/App.zig @@ -387,8 +387,9 @@ pub const CAPI = struct { app: *App, opts: *const apprt.runtime.Window.Options, ) !*Window { - _ = opts; - const w = try app.newWindow(.{}); + const w = try app.newWindow(.{ + .runtime = opts.*, + }); return w; } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 97f1ca58f..38e5a1193 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -8,6 +8,7 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const objc = @import("objc"); const apprt = @import("../apprt.zig"); const CoreApp = @import("../App.zig"); const CoreWindow = @import("../Window.zig"); @@ -45,15 +46,25 @@ pub const App = struct { }; pub const Window = struct { + nsview: objc.Object, + scale_factor: f64, + pub const Options = extern struct { - id: usize = 0, + /// The pointer to the backing NSView for the surface. + nsview: *anyopaque = undefined, + + /// The scale factor of the screen. + scale_factor: f64 = 1, }; pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { _ = app; _ = core_win; - _ = opts; - return .{}; + + return .{ + .nsview = objc.Object.fromId(opts.nsview), + .scale_factor = opts.scale_factor, + }; } pub fn deinit(self: *Window) void { @@ -67,7 +78,10 @@ pub const Window = struct { pub fn getSize(self: *const Window) !apprt.WindowSize { _ = self; - return apprt.WindowSize{ .width = 1, .height = 1 }; + + // Initially our window will have a zero size. Until we can determine + // the size of the window, we just send down this value. + return apprt.WindowSize{ .width = 800, .height = 600 }; } pub fn setSizeLimits(self: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e15233d44..c57dd7c99 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -305,22 +305,42 @@ pub fn deinit(self: *Metal) void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void { - // Set our window backing layer to be our swapchain - const nswindow = switch (apprt.runtime) { - apprt.glfw => objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?), - apprt.embedded => @panic("TODO"), + const Info = struct { + view: objc.Object, + scaleFactor: f64, + }; + + // Get the view and scale factor for our surface. + const info: Info = switch (apprt.runtime) { + apprt.glfw => info: { + // Everything in glfw is window-oriented so we grab the backing + // window, then derive everything from that. + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?); + const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); + const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor"); + break :info .{ + .view = contentView, + .scaleFactor = scaleFactor, + }; + }, + + apprt.embedded => .{ + .view = win.nsview, + .scaleFactor = win.scale_factor, + }, + else => @compileError("unsupported apprt for metal"), }; - const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); - contentView.setProperty("layer", self.swapchain.value); - contentView.setProperty("wantsLayer", true); + + // Make our view layer-backed with our Metal layer + info.view.setProperty("layer", self.swapchain.value); + info.view.setProperty("wantsLayer", true); // Ensure that our metal layer has a content scale set to match the // scale factor of the window. This avoids magnification issues leading // to blurry rendering. - const layer = contentView.getProperty(objc.Object, "layer"); - const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor"); - layer.setProperty("contentsScale", scaleFactor); + const layer = info.view.getProperty(objc.Object, "layer"); + layer.setProperty("contentsScale", info.scaleFactor); } /// This is called if this renderer runs DevMode. diff --git a/src/renderer/size.zig b/src/renderer/size.zig index a96ae160e..0be4e6476 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -106,10 +106,10 @@ pub const Padding = struct { const padding_bot = space_bot - padding_top; return .{ - .top = padding_top, - .bottom = padding_bot, - .right = padding_right, - .left = padding_left, + .top = @max(0, padding_top), + .bottom = @max(0, padding_bot), + .right = @max(0, padding_right), + .left = @max(0, padding_left), }; } @@ -124,6 +124,17 @@ pub const Padding = struct { } }; +test "Padding balanced on zero" { + // On some systems, our screen can be zero-sized for a bit, and we + // don't want to end up with negative padding. + const testing = std.testing; + const grid: GridSize = .{ .columns = 100, .rows = 37 }; + const cell: CellSize = .{ .width = 10, .height = 20 }; + const screen: ScreenSize = .{ .width = 0, .height = 0 }; + const padding = Padding.balanced(screen, grid, cell); + try testing.expectEqual(padding, .{}); +} + test "GridSize update exact" { const testing = std.testing;