Fix drag hover redraw churn in hosted panes
This commit is contained in:
parent
3566b6ec21
commit
d43d7d6a51
8 changed files with 303 additions and 77 deletions
|
|
@ -2121,6 +2121,10 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
surfaceView.tabId = newTabId
|
||||
}
|
||||
|
||||
func isAttached(to view: GhosttyNSView) -> Bool {
|
||||
attachedView === view && surface != nil
|
||||
}
|
||||
|
||||
func portalBindingGeneration() -> UInt64 {
|
||||
portalLifecycleGeneration
|
||||
}
|
||||
|
|
@ -2262,6 +2266,9 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
// removed/re-added (or briefly have window/screen nil) without recreating the surface.
|
||||
// Ghostty's vsync-driven renderer depends on having a valid display id; if it is missing
|
||||
// or stale, the surface can appear visually frozen until a focus/visibility change.
|
||||
// SwiftUI also re-enters this path for ordinary state propagation (drag hover, active
|
||||
// markers, visibility flags), so avoid forcing a geometry refresh when the attachment
|
||||
// itself is unchanged.
|
||||
if attachedView === view && surface != nil {
|
||||
#if DEBUG
|
||||
dlog("surface.attach.reuse surface=\(id.uuidString.prefix(5)) view=\(Unmanaged.passUnretained(view).toOpaque())")
|
||||
|
|
@ -2272,7 +2279,6 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
let s = surface {
|
||||
ghostty_surface_set_display_id(s, displayID)
|
||||
}
|
||||
view.forceRefreshSurface()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -2570,6 +2576,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
#endif
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func updateSize(
|
||||
width: CGFloat,
|
||||
height: CGFloat,
|
||||
|
|
@ -2577,15 +2584,15 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
yScale: CGFloat,
|
||||
layerScale: CGFloat,
|
||||
backingSize: CGSize? = nil
|
||||
) {
|
||||
guard let surface = surface else { return }
|
||||
) -> Bool {
|
||||
guard let surface = surface else { return false }
|
||||
_ = layerScale
|
||||
|
||||
let resolvedBackingWidth = backingSize?.width ?? (width * xScale)
|
||||
let resolvedBackingHeight = backingSize?.height ?? (height * yScale)
|
||||
let wpx = pixelDimension(from: resolvedBackingWidth)
|
||||
let hpx = pixelDimension(from: resolvedBackingHeight)
|
||||
guard wpx > 0, hpx > 0 else { return }
|
||||
guard wpx > 0, hpx > 0 else { return false }
|
||||
|
||||
let scaleChanged = !scaleApproximatelyEqual(xScale, lastXScale) || !scaleApproximatelyEqual(yScale, lastYScale)
|
||||
let sizeChanged = wpx != lastPixelWidth || hpx != lastPixelHeight
|
||||
|
|
@ -2594,7 +2601,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
Self.sizeLog("updateSize-call surface=\(id.uuidString.prefix(8)) size=\(wpx)x\(hpx) prev=\(lastPixelWidth)x\(lastPixelHeight) changed=\((scaleChanged || sizeChanged) ? 1 : 0)")
|
||||
#endif
|
||||
|
||||
guard scaleChanged || sizeChanged else { return }
|
||||
guard scaleChanged || sizeChanged else { return false }
|
||||
|
||||
#if DEBUG
|
||||
if sizeChanged {
|
||||
|
|
@ -2616,10 +2623,11 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
}
|
||||
|
||||
// Let Ghostty continue rendering on its own wakeups for steady-state frames.
|
||||
return true
|
||||
}
|
||||
|
||||
/// Force a full size recalculation and surface redraw.
|
||||
func forceRefresh() {
|
||||
func forceRefresh(reason: String = "unspecified") {
|
||||
let hasSurface = surface != nil
|
||||
let viewState: String
|
||||
if let view = attachedView {
|
||||
|
|
@ -2632,7 +2640,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
}
|
||||
#if DEBUG
|
||||
let ts = ISO8601DateFormatter().string(from: Date())
|
||||
let line = "[\(ts)] forceRefresh: \(id) \(viewState)\n"
|
||||
let line = "[\(ts)] forceRefresh: \(id) reason=\(reason) \(viewState)\n"
|
||||
let logPath = "/tmp/cmux-refresh-debug.log"
|
||||
if let handle = FileHandle(forWritingAtPath: logPath) {
|
||||
handle.seekToEndOfFile()
|
||||
|
|
@ -2941,6 +2949,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
private var lastScrollEventTime: CFTimeInterval = 0
|
||||
private var visibleInUI: Bool = true
|
||||
private var pendingSurfaceSize: CGSize?
|
||||
private var lastDrawableSize: CGSize = .zero
|
||||
private var isFindEscapeSuppressionArmed = false
|
||||
#if DEBUG
|
||||
private var lastSizeSkipSignature: String?
|
||||
|
|
@ -3114,14 +3123,22 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
}
|
||||
|
||||
func attachSurface(_ surface: TerminalSurface) {
|
||||
appliedColorScheme = nil
|
||||
let isSameSurface = terminalSurface === surface
|
||||
let isAlreadyAttached = surface.isAttached(to: self)
|
||||
if !isSameSurface {
|
||||
appliedColorScheme = nil
|
||||
}
|
||||
terminalSurface = surface
|
||||
tabId = surface.tabId
|
||||
surface.attachToView(self)
|
||||
if !isAlreadyAttached {
|
||||
surface.attachToView(self)
|
||||
}
|
||||
surface.setKeyboardCopyModeActive(keyboardCopyModeActive)
|
||||
updateSurfaceSize()
|
||||
if !isAlreadyAttached {
|
||||
updateSurfaceSize()
|
||||
}
|
||||
applySurfaceBackground()
|
||||
applySurfaceColorScheme(force: true)
|
||||
applySurfaceColorScheme(force: !isSameSurface || !isAlreadyAttached)
|
||||
}
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
|
|
@ -3229,8 +3246,9 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
return currentBounds
|
||||
}
|
||||
|
||||
private func updateSurfaceSize(size: CGSize? = nil) {
|
||||
guard let terminalSurface = terminalSurface else { return }
|
||||
@discardableResult
|
||||
private func updateSurfaceSize(size: CGSize? = nil) -> Bool {
|
||||
guard let terminalSurface = terminalSurface else { return false }
|
||||
let size = resolvedSurfaceSize(preferred: size)
|
||||
guard size.width > 0 && size.height > 0 else {
|
||||
#if DEBUG
|
||||
|
|
@ -3244,7 +3262,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
lastSizeSkipSignature = signature
|
||||
}
|
||||
#endif
|
||||
return
|
||||
return false
|
||||
}
|
||||
pendingSurfaceSize = size
|
||||
guard let window else {
|
||||
|
|
@ -3258,7 +3276,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
lastSizeSkipSignature = signature
|
||||
}
|
||||
#endif
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// First principles: derive pixel size from AppKit's backing conversion for the current
|
||||
|
|
@ -3276,7 +3294,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
lastSizeSkipSignature = signature
|
||||
}
|
||||
#endif
|
||||
return
|
||||
return false
|
||||
}
|
||||
#if DEBUG
|
||||
if lastSizeSkipSignature != nil {
|
||||
|
|
@ -3295,17 +3313,29 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
width: floor(max(0, backingSize.width)),
|
||||
height: floor(max(0, backingSize.height))
|
||||
)
|
||||
var didChange = false
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
if let layer, !nearlyEqual(layer.contentsScale, layerScale) {
|
||||
didChange = true
|
||||
}
|
||||
layer?.contentsScale = layerScale
|
||||
layer?.masksToBounds = true
|
||||
if let metalLayer = layer as? CAMetalLayer {
|
||||
metalLayer.drawableSize = drawablePixelSize
|
||||
if drawablePixelSize != lastDrawableSize || metalLayer.drawableSize != drawablePixelSize {
|
||||
if metalLayer.drawableSize != drawablePixelSize {
|
||||
didChange = true
|
||||
}
|
||||
if metalLayer.drawableSize != drawablePixelSize {
|
||||
metalLayer.drawableSize = drawablePixelSize
|
||||
}
|
||||
lastDrawableSize = drawablePixelSize
|
||||
}
|
||||
}
|
||||
CATransaction.commit()
|
||||
|
||||
terminalSurface.updateSize(
|
||||
let surfaceSizeChanged = terminalSurface.updateSize(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
xScale: xScale,
|
||||
|
|
@ -3313,15 +3343,19 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
layerScale: layerScale,
|
||||
backingSize: backingSize
|
||||
)
|
||||
return didChange || surfaceSizeChanged
|
||||
}
|
||||
|
||||
fileprivate func pushTargetSurfaceSize(_ size: CGSize) {
|
||||
@discardableResult
|
||||
fileprivate func pushTargetSurfaceSize(_ size: CGSize) -> Bool {
|
||||
updateSurfaceSize(size: size)
|
||||
}
|
||||
|
||||
/// Force a full size recalculation and Metal layer refresh.
|
||||
/// Resets cached metrics so updateSurfaceSize() re-runs unconditionally.
|
||||
func forceRefreshSurface() {
|
||||
/// Force a full size reconciliation for the current bounds.
|
||||
/// Keep the drawable-size cache intact so redundant refresh paths do not
|
||||
/// reallocate Metal drawables when the pixel size is unchanged.
|
||||
@discardableResult
|
||||
func forceRefreshSurface() -> Bool {
|
||||
updateSurfaceSize()
|
||||
}
|
||||
|
||||
|
|
@ -4882,6 +4916,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
private let keyboardCopyModeBadgeView: GhosttyPassthroughVisualEffectView
|
||||
private let keyboardCopyModeBadgeLabel: NSTextField
|
||||
private var searchOverlayHostingView: NSHostingView<SurfaceSearchOverlay>?
|
||||
private var lastSearchOverlayStateID: ObjectIdentifier?
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var windowObservers: [NSObjectProtocol] = []
|
||||
private var isLiveScrolling = false
|
||||
|
|
@ -4908,6 +4943,9 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
|
||||
#if DEBUG
|
||||
private var lastDropZoneOverlayLogSignature: String?
|
||||
private var dragLayoutLogSequence: UInt64 = 0
|
||||
private static let tabTransferPasteboardType = NSPasteboard.PasteboardType("com.splittabbar.tabtransfer")
|
||||
private static let sidebarTabReorderPasteboardType = NSPasteboard.PasteboardType("com.cmux.sidebar-tab-reorder")
|
||||
private static var flashCounts: [UUID: Int] = [:]
|
||||
private static var drawCounts: [UUID: Int] = [:]
|
||||
private static var lastDrawTimes: [UUID: CFTimeInterval] = [:]
|
||||
|
|
@ -5238,36 +5276,50 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
/// Reconcile AppKit geometry with ghostty surface geometry synchronously.
|
||||
/// Used after split topology mutations (close/split) to prevent a stale one-frame
|
||||
/// IOSurface size from being presented after pane expansion.
|
||||
func reconcileGeometryNow() {
|
||||
@discardableResult
|
||||
func reconcileGeometryNow() -> Bool {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.reconcileGeometryNow()
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
synchronizeGeometryAndContent()
|
||||
return synchronizeGeometryAndContent()
|
||||
}
|
||||
|
||||
/// Request an immediate terminal redraw after geometry updates so stale IOSurface
|
||||
/// contents do not remain stretched during live resize churn.
|
||||
func refreshSurfaceNow() {
|
||||
surfaceView.terminalSurface?.forceRefresh()
|
||||
func refreshSurfaceNow(reason: String = "portal.refreshSurfaceNow") {
|
||||
surfaceView.terminalSurface?.forceRefresh(reason: reason)
|
||||
}
|
||||
|
||||
private func synchronizeGeometryAndContent() {
|
||||
@discardableResult
|
||||
private func synchronizeGeometryAndContent() -> Bool {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
defer { CATransaction.commit() }
|
||||
|
||||
backgroundView.frame = bounds
|
||||
scrollView.frame = bounds
|
||||
let previousSurfaceSize = surfaceView.frame.size
|
||||
_ = setFrameIfNeeded(backgroundView, to: bounds)
|
||||
_ = setFrameIfNeeded(scrollView, to: bounds)
|
||||
let targetSize = scrollView.bounds.size
|
||||
surfaceView.frame.size = targetSize
|
||||
documentView.frame.size.width = scrollView.bounds.width
|
||||
inactiveOverlayView.frame = bounds
|
||||
#if DEBUG
|
||||
logLayoutDuringActiveDrag(targetSize: targetSize)
|
||||
#endif
|
||||
let targetSurfaceFrame = CGRect(origin: surfaceView.frame.origin, size: targetSize)
|
||||
_ = setFrameIfNeeded(surfaceView, to: targetSurfaceFrame)
|
||||
let targetDocumentFrame = CGRect(
|
||||
origin: documentView.frame.origin,
|
||||
size: CGSize(width: scrollView.bounds.width, height: documentView.frame.height)
|
||||
)
|
||||
_ = setFrameIfNeeded(documentView, to: targetDocumentFrame)
|
||||
_ = setFrameIfNeeded(inactiveOverlayView, to: bounds)
|
||||
if let zone = activeDropZone {
|
||||
dropZoneOverlayView.frame = dropZoneOverlayFrame(for: zone, in: bounds.size)
|
||||
_ = setFrameIfNeeded(
|
||||
dropZoneOverlayView,
|
||||
to: dropZoneOverlayFrame(for: zone, in: bounds.size)
|
||||
)
|
||||
}
|
||||
if let pending = pendingDropZone,
|
||||
bounds.width > 2,
|
||||
|
|
@ -5281,15 +5333,68 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
// same initial animation as direct drop-zone activation.
|
||||
setDropZoneOverlay(zone: pending)
|
||||
}
|
||||
notificationRingOverlayView.frame = bounds
|
||||
flashOverlayView.frame = bounds
|
||||
_ = setFrameIfNeeded(notificationRingOverlayView, to: bounds)
|
||||
_ = setFrameIfNeeded(flashOverlayView, to: bounds)
|
||||
updateNotificationRingPath()
|
||||
updateFlashPath()
|
||||
synchronizeScrollView()
|
||||
synchronizeSurfaceView()
|
||||
synchronizeCoreSurface()
|
||||
let didCoreSurfaceChange = synchronizeCoreSurface()
|
||||
return !sizeApproximatelyEqual(previousSurfaceSize, targetSize) || didCoreSurfaceChange
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func setFrameIfNeeded(_ view: NSView, to frame: CGRect) -> Bool {
|
||||
guard !Self.rectApproximatelyEqual(view.frame, frame) else { return false }
|
||||
view.frame = frame
|
||||
return true
|
||||
}
|
||||
|
||||
private func sizeApproximatelyEqual(_ lhs: CGSize, _ rhs: CGSize, epsilon: CGFloat = 0.0001) -> Bool {
|
||||
abs(lhs.width - rhs.width) <= epsilon && abs(lhs.height - rhs.height) <= epsilon
|
||||
}
|
||||
|
||||
private func pointApproximatelyEqual(_ lhs: CGPoint, _ rhs: CGPoint, epsilon: CGFloat = 0.5) -> Bool {
|
||||
abs(lhs.x - rhs.x) <= epsilon && abs(lhs.y - rhs.y) <= epsilon
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static func isDragMouseEvent(_ eventType: NSEvent.EventType?) -> Bool {
|
||||
switch eventType {
|
||||
case .leftMouseDragged, .rightMouseDragged, .otherMouseDragged:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func logLayoutDuringActiveDrag(targetSize: CGSize) {
|
||||
let pasteboardTypes = NSPasteboard(name: .drag).types
|
||||
let hasTabDrag = pasteboardTypes?.contains(Self.tabTransferPasteboardType) == true
|
||||
let hasSidebarDrag = pasteboardTypes?.contains(Self.sidebarTabReorderPasteboardType) == true
|
||||
let eventType = NSApp.currentEvent?.type
|
||||
let hasActiveDrag =
|
||||
activeDropZone != nil ||
|
||||
pendingDropZone != nil ||
|
||||
((hasTabDrag || hasSidebarDrag) && Self.isDragMouseEvent(eventType))
|
||||
guard hasActiveDrag else { return }
|
||||
|
||||
dragLayoutLogSequence &+= 1
|
||||
let surface = surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"
|
||||
let activeZone = activeDropZone.map { String(describing: $0) } ?? "none"
|
||||
let pendingZone = pendingDropZone.map { String(describing: $0) } ?? "none"
|
||||
let event = eventType.map { String(describing: $0) } ?? "nil"
|
||||
dlog(
|
||||
"terminal.layout.drag surface=\(surface) seq=\(dragLayoutLogSequence) " +
|
||||
"activeZone=\(activeZone) pendingZone=\(pendingZone) " +
|
||||
"hasTabDrag=\(hasTabDrag ? 1 : 0) hasSidebarDrag=\(hasSidebarDrag ? 1 : 0) " +
|
||||
"event=\(event) inWindow=\(window != nil ? 1 : 0) " +
|
||||
"bounds=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " +
|
||||
"target=\(String(format: "%.1fx%.1f", targetSize.width, targetSize.height))"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
windowObservers.forEach { NotificationCenter.default.removeObserver($0) }
|
||||
|
|
@ -5385,10 +5490,15 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
return
|
||||
}
|
||||
|
||||
let targetHidden = !visible
|
||||
let targetOpacity: Float = visible ? 1 : 0
|
||||
guard notificationRingOverlayView.isHidden != targetHidden ||
|
||||
notificationRingLayer.opacity != targetOpacity else { return }
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
notificationRingOverlayView.isHidden = !visible
|
||||
notificationRingLayer.opacity = visible ? 1 : 0
|
||||
notificationRingOverlayView.isHidden = targetHidden
|
||||
notificationRingLayer.opacity = targetOpacity
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
|
|
@ -5405,6 +5515,8 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
guard let terminalSurface = surfaceView.terminalSurface,
|
||||
let searchState else {
|
||||
let hadOverlay = searchOverlayHostingView != nil
|
||||
lastSearchOverlayStateID = nil
|
||||
guard hadOverlay else { return }
|
||||
#if DEBUG
|
||||
dlog("find.setSearchOverlay REMOVE surface=\(surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") hadOverlay=\(hadOverlay)")
|
||||
#endif
|
||||
|
|
@ -5414,6 +5526,16 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
return
|
||||
}
|
||||
|
||||
let searchStateID = ObjectIdentifier(searchState)
|
||||
if let overlay = searchOverlayHostingView,
|
||||
lastSearchOverlayStateID == searchStateID,
|
||||
overlay.superview === self {
|
||||
if !keyboardCopyModeBadgeView.isHidden {
|
||||
addSubview(keyboardCopyModeBadgeView, positioned: .above, relativeTo: overlay)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let hadOverlay = searchOverlayHostingView != nil
|
||||
#if DEBUG
|
||||
dlog("find.setSearchOverlay MOUNT surface=\(terminalSurface.id.uuidString.prefix(5)) existingOverlay=\(hadOverlay ? "yes(update)" : "no(create)")")
|
||||
|
|
@ -5457,6 +5579,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
if !keyboardCopyModeBadgeView.isHidden {
|
||||
addSubview(keyboardCopyModeBadgeView, positioned: .above, relativeTo: overlay)
|
||||
}
|
||||
lastSearchOverlayStateID = searchStateID
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -5474,6 +5597,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
addSubview(keyboardCopyModeBadgeView, positioned: .above, relativeTo: overlay)
|
||||
}
|
||||
searchOverlayHostingView = overlay
|
||||
lastSearchOverlayStateID = searchStateID
|
||||
}
|
||||
|
||||
func setKeyboardCopyModeIndicator(visible: Bool) {
|
||||
|
|
@ -6356,16 +6480,18 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
|
||||
private func synchronizeSurfaceView() {
|
||||
let visibleRect = scrollView.contentView.documentVisibleRect
|
||||
guard !pointApproximatelyEqual(surfaceView.frame.origin, visibleRect.origin) else { return }
|
||||
surfaceView.frame.origin = visibleRect.origin
|
||||
}
|
||||
|
||||
/// Match upstream Ghostty behavior: use content area width (excluding non-content
|
||||
/// regions such as scrollbar space) when telling libghostty the terminal size.
|
||||
private func synchronizeCoreSurface() {
|
||||
@discardableResult
|
||||
private func synchronizeCoreSurface() -> Bool {
|
||||
let width = max(0, scrollView.contentSize.width - overlayScrollbarInsetWidth())
|
||||
let height = surfaceView.frame.height
|
||||
guard width > 0, height > 0 else { return }
|
||||
surfaceView.pushTargetSurfaceSize(CGSize(width: width, height: height))
|
||||
guard width > 0, height > 0 else { return false }
|
||||
return surfaceView.pushTargetSurfaceSize(CGSize(width: width, height: height))
|
||||
}
|
||||
|
||||
/// Reserve overlay scrollbar gutter so wrapped text never sits underneath a visible scroller.
|
||||
|
|
@ -6425,19 +6551,30 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
}
|
||||
|
||||
private func synchronizeScrollView() {
|
||||
documentView.frame.size.height = documentHeight()
|
||||
var didChangeGeometry = false
|
||||
let targetDocumentHeight = documentHeight()
|
||||
if abs(documentView.frame.height - targetDocumentHeight) > 0.5 {
|
||||
documentView.frame.size.height = targetDocumentHeight
|
||||
didChangeGeometry = true
|
||||
}
|
||||
|
||||
if !isLiveScrolling {
|
||||
let cellHeight = surfaceView.cellSize.height
|
||||
if cellHeight > 0, let scrollbar = surfaceView.scrollbar {
|
||||
let offsetY =
|
||||
CGFloat(scrollbar.total - scrollbar.offset - scrollbar.len) * cellHeight
|
||||
scrollView.contentView.scroll(to: CGPoint(x: 0, y: offsetY))
|
||||
let targetOrigin = CGPoint(x: 0, y: offsetY)
|
||||
if !pointApproximatelyEqual(scrollView.contentView.bounds.origin, targetOrigin) {
|
||||
scrollView.contentView.scroll(to: targetOrigin)
|
||||
didChangeGeometry = true
|
||||
}
|
||||
lastSentRow = Int(scrollbar.offset)
|
||||
}
|
||||
}
|
||||
|
||||
scrollView.reflectScrolledClipView(scrollView.contentView)
|
||||
if didChangeGeometry {
|
||||
scrollView.reflectScrolledClipView(scrollView.contentView)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleScrollChange() {
|
||||
|
|
@ -6669,31 +6806,57 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
private final class HostContainerView: NSView {
|
||||
var onDidMoveToWindow: (() -> Void)?
|
||||
var onGeometryChanged: (() -> Void)?
|
||||
private(set) var geometryRevision: UInt64 = 0
|
||||
private var lastReportedGeometryState: GeometryState?
|
||||
|
||||
private struct GeometryState: Equatable {
|
||||
let frame: CGRect
|
||||
let bounds: CGRect
|
||||
let windowNumber: Int?
|
||||
let superviewID: ObjectIdentifier?
|
||||
}
|
||||
|
||||
private func currentGeometryState() -> GeometryState {
|
||||
GeometryState(
|
||||
frame: frame,
|
||||
bounds: bounds,
|
||||
windowNumber: window?.windowNumber,
|
||||
superviewID: superview.map(ObjectIdentifier.init)
|
||||
)
|
||||
}
|
||||
|
||||
private func notifyGeometryChangedIfNeeded() {
|
||||
let state = currentGeometryState()
|
||||
guard state != lastReportedGeometryState else { return }
|
||||
lastReportedGeometryState = state
|
||||
geometryRevision &+= 1
|
||||
onGeometryChanged?()
|
||||
}
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
onDidMoveToWindow?()
|
||||
onGeometryChanged?()
|
||||
notifyGeometryChangedIfNeeded()
|
||||
}
|
||||
|
||||
override func viewDidMoveToSuperview() {
|
||||
super.viewDidMoveToSuperview()
|
||||
onGeometryChanged?()
|
||||
notifyGeometryChangedIfNeeded()
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
onGeometryChanged?()
|
||||
notifyGeometryChangedIfNeeded()
|
||||
}
|
||||
|
||||
override func setFrameOrigin(_ newOrigin: NSPoint) {
|
||||
super.setFrameOrigin(newOrigin)
|
||||
onGeometryChanged?()
|
||||
notifyGeometryChangedIfNeeded()
|
||||
}
|
||||
|
||||
override func setFrameSize(_ newSize: NSSize) {
|
||||
super.setFrameSize(newSize)
|
||||
onGeometryChanged?()
|
||||
notifyGeometryChangedIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6706,6 +6869,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
var desiredPortalZPriority: Int = 0
|
||||
var lastBoundHostId: ObjectIdentifier?
|
||||
var lastPaneDropZone: DropZone?
|
||||
var lastSynchronizedHostGeometryRevision: UInt64 = 0
|
||||
weak var hostedView: GhosttySurfaceScrollView?
|
||||
}
|
||||
|
||||
|
|
@ -6825,6 +6989,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
expectedGeneration: portalExpectedGeneration
|
||||
)
|
||||
coordinator.lastBoundHostId = ObjectIdentifier(host)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = host.geometryRevision
|
||||
hostedView.setVisibleInUI(coordinator.desiredIsVisibleInUI)
|
||||
hostedView.setActive(coordinator.desiredIsActive)
|
||||
hostedView.setNotificationRing(visible: coordinator.desiredShowsUnreadNotificationRing)
|
||||
|
|
@ -6856,10 +7021,12 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
hostedView.setNotificationRing(visible: coordinator.desiredShowsUnreadNotificationRing)
|
||||
}
|
||||
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = host.geometryRevision
|
||||
}
|
||||
|
||||
if host.window != nil {
|
||||
let hostId = ObjectIdentifier(host)
|
||||
let geometryRevision = host.geometryRevision
|
||||
let shouldBindNow =
|
||||
coordinator.lastBoundHostId != hostId ||
|
||||
hostedView.superview == nil ||
|
||||
|
|
@ -6876,8 +7043,11 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
expectedGeneration: portalExpectedGeneration
|
||||
)
|
||||
coordinator.lastBoundHostId = hostId
|
||||
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
|
||||
} else if coordinator.lastSynchronizedHostGeometryRevision != geometryRevision {
|
||||
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
|
||||
}
|
||||
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
|
||||
} else {
|
||||
// Bind is deferred until host moves into a window. Update the
|
||||
// existing portal entry's visibleInUI now so that any portal sync
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue