Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ extension Ghostty {
private var markedText: NSMutableAttributedString
private(set) var focused: Bool = true
private var prevPressureStage: Int = 0

// Auto-scroll state for drag selection beyond viewport
private var autoScrollTimer: Timer?
private var autoScrollUp: Bool = false
private var lastDragEvent: NSEvent?
Comment thread
coderabbitai[bot] marked this conversation as resolved.
private var appearanceObserver: NSKeyValueObservation?

// This is set to non-null during keyDown to accumulate insertText contents
Expand Down Expand Up @@ -428,6 +433,9 @@ extension Ghostty {

// Cancel progress report timer
progressReportTimer?.invalidate()

// Cancel auto-scroll timer
stopAutoScroll()
}

func focusDidChange(_ focused: Bool) {
Expand Down Expand Up @@ -854,6 +862,8 @@ extension Ghostty {
}

override func mouseUp(with event: NSEvent) {
stopAutoScroll()

// Always reset our pressure when the mouse goes up
prevPressureStage = 0

Expand Down Expand Up @@ -980,6 +990,16 @@ extension Ghostty {

override func mouseDragged(with event: NSEvent) {
self.mouseMoved(with: event)

let pos = self.convert(event.locationInWindow, from: nil)
let viewY = frame.height - pos.y

if viewY < 0 || viewY > frame.height {
startAutoScroll(scrollUp: viewY < 0)
lastDragEvent = event
} else {
stopAutoScroll()
}
}
Comment thread
yyanezh marked this conversation as resolved.

override func rightMouseDragged(with event: NSEvent) {
Expand All @@ -990,6 +1010,49 @@ extension Ghostty {
self.mouseMoved(with: event)
}

// MARK: - Auto-scroll during drag selection

/// Starts a repeating timer that sends synthetic scroll and mouse position
/// events so the selection extends while the cursor is held outside the viewport.
private func startAutoScroll(scrollUp: Bool) {
// Already running in the same direction
if autoScrollTimer != nil && autoScrollUp == scrollUp { return }
stopAutoScroll()
autoScrollUp = scrollUp

let timer = Timer(timeInterval: 0.05, repeats: true) { [weak self] _ in
guard let self, let surfaceModel = self.surfaceModel else { return }

let scrollY: Double = scrollUp ? 1.0 : -1.0
let scrollEvent = Ghostty.Input.MouseScrollEvent(
x: 0,
y: scrollY,
mods: .init(precision: false, momentum: .none)
)
surfaceModel.sendMouseScroll(scrollEvent)

// Re-send mouse position so selection extends while scrolling
if let lastEvent = self.lastDragEvent {
let pos = self.convert(lastEvent.locationInWindow, from: nil)
let mouseEvent = Ghostty.Input.MousePosEvent(
x: pos.x,
y: self.frame.height - pos.y,
mods: .init(nsFlags: lastEvent.modifierFlags)
)
surfaceModel.sendMousePos(mouseEvent)
}
}
RunLoop.main.add(timer, forMode: .common)
autoScrollTimer = timer
}

/// Invalidates the auto-scroll timer and clears associated drag state.
private func stopAutoScroll() {
autoScrollTimer?.invalidate()
autoScrollTimer = nil
lastDragEvent = nil
}

override func scrollWheel(with event: NSEvent) {
guard let surfaceModel else { return }

Expand Down