Skip to content

Commit 6f7cdcf

Browse files
committed
Handle selection UI updates on the CoreGraphics renderer
1 parent 7eea891 commit 6f7cdcf

5 files changed

Lines changed: 31 additions & 136 deletions

File tree

Sources/SwiftTerm/Apple/TerminalRenderOwner.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,18 @@ final class TerminalRenderOwner: Sendable {
481481
cursor: session.snapshot.cursor,
482482
cursorRowCount: session.snapshot.rowCount,
483483
scrollPosition: capturedScrollPosition)
484+
} else if session.snapshot.appearanceChanged {
485+
let region = request.viewState.viewBounds
486+
session.snapshot.cgRegion = region
487+
session.snapshot.rangeChanged = nil
488+
result = TerminalView.PreparedFrame(
489+
region: region,
490+
rangeChanged: nil,
491+
notifyAccessibility: false,
492+
needsMetalDisplay: metalActive,
493+
cursor: session.snapshot.cursor,
494+
cursorRowCount: session.snapshot.rowCount,
495+
scrollPosition: capturedScrollPosition)
484496
} else {
485497
let changed = request.viewState.notifyUpdateChanges
486498
? (start: buffer.yDisp + buffer.y, end: buffer.yDisp + buffer.y)

Sources/SwiftTerm/Apple/TerminalSnapshot.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,9 @@ final class TerminalSnapshot {
325325
var ansiColors: [Color] = []
326326
var cgRegion: CGRect?
327327
var rangeChanged: (start: Int, end: Int)?
328+
/// True when view-dependent appearance changed without a cell-content
329+
/// change. Core Graphics uses this to invalidate the visible surface.
330+
private(set) var appearanceChanged = false
328331
/// The context that goes with this snapshot's contents, built by `refresh`
329332
/// from the view state it was handed. Nil until the first refresh.
330333
private(set) var renderContext: SnapshotRenderContext?
@@ -385,6 +388,7 @@ final class TerminalSnapshot {
385388
textBlinkVisible: viewState.textBlinkVisible)
386389
let styleChanged = previousStyle?.hasSameValue(as: newStyle) != true ||
387390
previousAnsiColors != terminal.ansiColors
391+
appearanceChanged = styleChanged
388392
let bidiFont = ObjectIdentifier(viewState.fonts.normal)
389393
let bidiInputsChanged = previousBidiHostPolicy != viewState.bidiHostPolicy ||
390394
previousBidiFont != bidiFont

TerminalApp/MacTerminal/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1313
@IBOutlet var loggingMenuItem: NSMenuItem?
1414

1515
func applicationDidFinishLaunching(_ aNotification: Notification) {
16-
ResizeTrace.reset()
16+
// Insert code here to initialize your application
1717
}
1818

1919
func applicationWillTerminate(_ aNotification: Notification) {

TerminalApp/MacTerminal/ViewController.swift

Lines changed: 2 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -10,50 +10,6 @@ import Cocoa
1010
import SwiftTerm
1111
import UniformTypeIdentifiers
1212

13-
@MainActor
14-
enum ResizeTrace {
15-
static let path = "/tmp/MacTerminal-resize.log"
16-
17-
private static var fileHandle: FileHandle?
18-
private static var startTime = ProcessInfo.processInfo.systemUptime
19-
20-
static func reset() {
21-
try? fileHandle?.close()
22-
_ = FileManager.default.createFile(atPath: path, contents: nil)
23-
fileHandle = FileHandle(forWritingAtPath: path)
24-
startTime = ProcessInfo.processInfo.systemUptime
25-
log("trace-start pid=\(ProcessInfo.processInfo.processIdentifier)")
26-
}
27-
28-
static func log(_ message: String) {
29-
if fileHandle == nil {
30-
reset()
31-
return
32-
}
33-
let elapsed = ProcessInfo.processInfo.systemUptime - startTime
34-
let line = String(format: "%10.6f %@\n", elapsed, message)
35-
guard let data = line.data(using: .utf8) else { return }
36-
try? fileHandle?.write(contentsOf: data)
37-
try? fileHandle?.synchronize()
38-
}
39-
40-
static func rect(_ value: NSRect) -> String {
41-
String(
42-
format: "{x=%.1f y=%.1f w=%.1f h=%.1f maxX=%.1f maxY=%.1f}",
43-
value.origin.x,
44-
value.origin.y,
45-
value.size.width,
46-
value.size.height,
47-
value.maxX,
48-
value.maxY
49-
)
50-
}
51-
52-
static func point(_ value: NSPoint) -> String {
53-
String(format: "{x=%.1f y=%.1f}", value.x, value.y)
54-
}
55-
}
56-
5713
private actor InputLatencyPendingFrame {
5814
private var awaitingNanoseconds: UInt64?
5915
private var recordedMilliseconds: Double?
@@ -93,17 +49,9 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
9349
var zoomGesture: NSMagnificationGestureRecognizer?
9450
var postedTitle: String = ""
9551
var postedDirectory: String? = nil
96-
private var resizeTraceObservers = [NSObjectProtocol]()
97-
private var resizeTraceLastFrame: NSRect?
98-
private var resizeTraceHasOriginStack = false
9952

100-
func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {
53+
func sizeChanged(source _: LocalProcessTerminalView, newCols _: Int, newRows _: Int) {
10154
// LocalProcessTerminalView updates the PTY. Do not resize the window here.
102-
let windowFrame = source.window.map { ResizeTrace.rect($0.frame) } ?? "nil"
103-
ResizeTrace.log(
104-
"terminal-size cols=\(newCols) rows=\(newRows) "
105-
+ "view=\(ResizeTrace.rect(source.frame)) window=\(windowFrame)"
106-
)
10755
}
10856

10957
func updateWindowTitle ()
@@ -272,7 +220,6 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
272220
override func viewDidAppear() {
273221
super.viewDidAppear()
274222
guard let window = view.window else { return }
275-
installResizeTrace(for: window)
276223
if let terminalWindowCloseObserver {
277224
NotificationCenter.default.removeObserver(terminalWindowCloseObserver)
278225
}
@@ -314,7 +261,7 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
314261
}
315262

316263
override func viewWillDisappear() {
317-
removeResizeTraceObservers()
264+
//terminal = nil
318265
}
319266

320267
@objc
@@ -328,86 +275,8 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
328275

329276
override func viewDidLayout() {
330277
super.viewDidLayout()
331-
recordLayoutTrace("viewDidLayout-before")
332278
terminal.frame = view.frame
333279
terminal.needsLayout = true
334-
recordLayoutTrace("viewDidLayout-after")
335-
}
336-
337-
private func installResizeTrace(for window: NSWindow) {
338-
removeResizeTraceObservers()
339-
resizeTraceLastFrame = window.frame
340-
resizeTraceHasOriginStack = false
341-
recordResizeTrace("viewDidAppear", window: window)
342-
343-
let center = NotificationCenter.default
344-
let events: [(Notification.Name, String)] = [
345-
(NSWindow.willStartLiveResizeNotification, "willStartLiveResize"),
346-
(NSWindow.didResizeNotification, "didResize"),
347-
(NSWindow.didMoveNotification, "didMove"),
348-
(NSWindow.didEndLiveResizeNotification, "didEndLiveResize"),
349-
(NSWindow.didChangeScreenNotification, "didChangeScreen"),
350-
(NSWindow.didChangeBackingPropertiesNotification, "didChangeBackingProperties"),
351-
]
352-
353-
resizeTraceObservers = events.map { name, label in
354-
center.addObserver(forName: name, object: window, queue: .main) {
355-
[weak self, weak window] _ in
356-
MainActor.assumeIsolated {
357-
guard let self, let window else { return }
358-
if name == NSWindow.willStartLiveResizeNotification {
359-
self.resizeTraceLastFrame = window.frame
360-
self.resizeTraceHasOriginStack = false
361-
}
362-
self.recordResizeTrace(label, window: window)
363-
}
364-
}
365-
}
366-
}
367-
368-
private func removeResizeTraceObservers() {
369-
resizeTraceObservers.forEach(NotificationCenter.default.removeObserver)
370-
resizeTraceObservers.removeAll()
371-
}
372-
373-
private func recordResizeTrace(_ label: String, window: NSWindow) {
374-
let frame = window.frame
375-
let previous = resizeTraceLastFrame ?? frame
376-
let delta = NSRect(
377-
x: frame.minX - previous.minX,
378-
y: frame.minY - previous.minY,
379-
width: frame.width - previous.width,
380-
height: frame.height - previous.height
381-
)
382-
let currentEvent = NSApp.currentEvent
383-
let eventName = currentEvent.map { String(describing: $0.type) } ?? "nil"
384-
let eventLocation = currentEvent.map { ResizeTrace.point($0.locationInWindow) } ?? "nil"
385-
let contentFrame = window.contentView.map { ResizeTrace.rect($0.frame) } ?? "nil"
386-
ResizeTrace.log(
387-
"window-event=\(label) live=\(window.inLiveResize) "
388-
+ "frame=\(ResizeTrace.rect(frame)) delta=\(ResizeTrace.rect(delta)) "
389-
+ "content=\(contentFrame) mouseScreen=\(ResizeTrace.point(NSEvent.mouseLocation)) "
390-
+ "event=\(eventName) eventWindow=\(eventLocation) "
391-
+ "buttons=\(NSEvent.pressedMouseButtons)"
392-
)
393-
394-
let originChanged = abs(frame.minX - previous.minX) > 0.01
395-
|| abs(frame.minY - previous.minY) > 0.01
396-
if originChanged && !resizeTraceHasOriginStack {
397-
resizeTraceHasOriginStack = true
398-
ResizeTrace.log("first-origin-change-stack " + Thread.callStackSymbols.prefix(16).joined(separator: " <- "))
399-
}
400-
resizeTraceLastFrame = frame
401-
}
402-
403-
private func recordLayoutTrace(_ label: String) {
404-
let windowFrame = view.window.map { ResizeTrace.rect($0.frame) } ?? "nil"
405-
ResizeTrace.log(
406-
"layout=\(label) viewFrame=\(ResizeTrace.rect(view.frame)) "
407-
+ "viewBounds=\(ResizeTrace.rect(view.bounds)) "
408-
+ "terminalFrame=\(ResizeTrace.rect(terminal.frame)) "
409-
+ "terminalBounds=\(ResizeTrace.rect(terminal.bounds)) window=\(windowFrame)"
410-
)
411280
}
412281

413282
@objc @IBAction

Tests/SwiftTermTests/SelectionRedrawTests.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ struct SelectionRedrawTests {
4646
#expect(view.selection.active)
4747

4848
// No explicit markDirty: the selection change alone must wake the driver.
49-
await drain(); source.tick(); await drain()
49+
await drain()
50+
51+
let prepared = view.prepareFrame(viewState: FrameViewState(view: view))
52+
#expect(prepared?.region == view.bounds)
53+
54+
source.tick(); await drain()
5055

5156
let selected = view.renderOwner.inspection()
5257
#expect(selected.selectionActive)
@@ -66,7 +71,12 @@ struct SelectionRedrawTests {
6671
#expect(view.renderOwner.inspection().selectionActive)
6772

6873
view.selection.selectNone()
69-
await drain(); source.tick(); await drain()
74+
await drain()
75+
76+
let prepared = view.prepareFrame(viewState: FrameViewState(view: view))
77+
#expect(prepared?.region == view.bounds)
78+
79+
source.tick(); await drain()
7080

7181
#expect(!view.renderOwner.inspection().selectionActive)
7282
}

0 commit comments

Comments
 (0)