From af2ab0955c995c92b7c003c6046d2baa0e96497e Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:46:55 -0800 Subject: [PATCH] Handle scale on screen changes --- GhosttyTabs.xcodeproj/project.pbxproj | 16 ++--- Sources/GhosttyTerminalView.swift | 89 ++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 22 deletions(-) diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 807c6d87..024a3ab0 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -522,7 +522,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -538,7 +538,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.11.1; + MARKETING_VERSION = 1.12.0; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -567,7 +567,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -583,7 +583,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.11.1; + MARKETING_VERSION = 1.12.0; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -636,10 +636,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.11.1; + MARKETING_VERSION = 1.12.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -653,10 +653,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.11.1; + MARKETING_VERSION = 1.12.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index c70d9e53..26af51ba 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -617,6 +617,17 @@ class TerminalSurface: Identifiable { hostedView.attachSurface(self) } + private func scaleFactors(for view: GhosttyNSView) -> (x: CGFloat, y: CGFloat, layer: CGFloat) { + let layerScale = view.window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2.0 + guard view.bounds.width > 0 && view.bounds.height > 0 else { + return (layerScale, layerScale, layerScale) + } + let backingBounds = view.convertToBacking(view.bounds) + let xScale = backingBounds.width / view.bounds.width + let yScale = backingBounds.height / view.bounds.height + return (xScale, yScale, layerScale) + } + func attachToView(_ view: GhosttyNSView) { // If already attached to this view, nothing to do if attachedView === view && surface != nil { @@ -642,7 +653,7 @@ class TerminalSurface: Identifiable { return } - let scale = view.window?.screen?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2.0 + let scaleFactors = scaleFactors(for: view) updateMetalLayer(for: view) @@ -650,7 +661,7 @@ class TerminalSurface: Identifiable { surfaceConfig.platform_tag = GHOSTTY_PLATFORM_MACOS surfaceConfig.platform.macos.nsview = Unmanaged.passUnretained(view).toOpaque() surfaceConfig.userdata = Unmanaged.passUnretained(view).toOpaque() - surfaceConfig.scale_factor = scale + surfaceConfig.scale_factor = scaleFactors.layer surfaceConfig.context = surfaceContext var envVars: [ghostty_env_var_s] = [] var envStorage: [(UnsafeMutablePointer, UnsafeMutablePointer)] = [] @@ -726,11 +737,11 @@ class TerminalSurface: Identifiable { return } - ghostty_surface_set_content_scale(surface, scale, scale) + ghostty_surface_set_content_scale(surface, scaleFactors.x, scaleFactors.y) ghostty_surface_set_size( surface, - UInt32(view.bounds.width * scale), - UInt32(view.bounds.height * scale) + UInt32(view.bounds.width * scaleFactors.x), + UInt32(view.bounds.height * scaleFactors.y) ) ghostty_surface_refresh(surface) if !ownsDisplayLink { @@ -740,7 +751,7 @@ class TerminalSurface: Identifiable { } private func updateMetalLayer(for view: GhosttyNSView) { - let scale = view.window?.screen?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2.0 + let scale = view.window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2.0 if let metalLayer = view.layer as? CAMetalLayer { metalLayer.contentsScale = scale if view.bounds.width > 0 && view.bounds.height > 0 { @@ -752,15 +763,15 @@ class TerminalSurface: Identifiable { } } - func updateSize(width: CGFloat, height: CGFloat, scale: CGFloat) { + func updateSize(width: CGFloat, height: CGFloat, xScale: CGFloat, yScale: CGFloat, layerScale: CGFloat) { guard let surface = surface else { return } - ghostty_surface_set_content_scale(surface, scale, scale) - ghostty_surface_set_size(surface, UInt32(width * scale), UInt32(height * scale)) + ghostty_surface_set_content_scale(surface, xScale, yScale) + ghostty_surface_set_size(surface, UInt32(width * xScale), UInt32(height * yScale)) ghostty_surface_refresh(surface) if let view = attachedView, let metalLayer = view.layer as? CAMetalLayer { - metalLayer.contentsScale = scale - metalLayer.drawableSize = CGSize(width: width * scale, height: height * scale) + metalLayer.contentsScale = layerScale + metalLayer.drawableSize = CGSize(width: width * layerScale, height: height * layerScale) } } @@ -819,6 +830,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { private var keyTables: [String] = [] private var eventMonitor: Any? private var trackingArea: NSTrackingArea? + private var windowObserver: NSObjectProtocol? override func makeBackingLayer() -> CALayer { let metalLayer = CAMetalLayer() @@ -928,7 +940,18 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { override func viewDidMoveToWindow() { super.viewDidMoveToWindow() - if window != nil { + if let windowObserver { + NotificationCenter.default.removeObserver(windowObserver) + self.windowObserver = nil + } + if let window { + windowObserver = NotificationCenter.default.addObserver( + forName: NSWindow.didChangeScreenNotification, + object: window, + queue: .main + ) { [weak self] notification in + self?.windowDidChangeScreen(notification) + } attachSurfaceIfNeeded() updateSurfaceSize() applySurfaceBackground() @@ -938,6 +961,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { override func viewDidChangeBackingProperties() { super.viewDidChangeBackingProperties() + if let window { + CATransaction.begin() + CATransaction.setDisableActions(true) + layer?.contentsScale = window.backingScaleFactor + CATransaction.commit() + } updateSurfaceSize() } @@ -956,8 +985,18 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { private func updateSurfaceSize() { guard let terminalSurface = terminalSurface else { return } - let scale = window?.screen?.backingScaleFactor ?? 2.0 - terminalSurface.updateSize(width: bounds.width, height: bounds.height, scale: scale) + guard bounds.width > 0 && bounds.height > 0 else { return } + let backingBounds = convertToBacking(bounds) + let xScale = backingBounds.width / bounds.width + let yScale = backingBounds.height / bounds.height + let layerScale = window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2.0 + terminalSurface.updateSize( + width: bounds.width, + height: bounds.height, + xScale: xScale, + yScale: yScale, + layerScale: layerScale + ) } // Convenience accessor for the ghostty surface @@ -1512,6 +1551,9 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { if let eventMonitor { NSEvent.removeMonitor(eventMonitor) } + if let windowObserver { + NotificationCenter.default.removeObserver(windowObserver) + } terminalSurface = nil } @@ -1538,6 +1580,25 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { addTrackingArea(trackingArea) } } + + private func windowDidChangeScreen(_ notification: Notification) { + guard let window else { return } + guard let object = notification.object as? NSWindow, window == object else { return } + guard let screen = window.screen else { return } + guard let surface = terminalSurface?.surface else { return } + + ghostty_surface_set_display_id(surface, screen.displayID ?? 0) + + DispatchQueue.main.async { [weak self] in + self?.viewDidChangeBackingProperties() + } + } +} + +private extension NSScreen { + var displayID: UInt32? { + deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? UInt32 + } } struct GhosttyScrollbar {