Handle scale on screen changes

This commit is contained in:
Lawrence Chen 2026-01-29 17:46:55 -08:00
parent a77a684548
commit af2ab0955c
2 changed files with 83 additions and 22 deletions

View file

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

View file

@ -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<CChar>, UnsafeMutablePointer<CChar>)] = []
@ -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 {