mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
apprt: unify split-click focus behavior across macOS and GTK; suppress focus-transfer mouse events (#11167)
## Summary This PR aligns split-pane click behavior across macOS and GTK when focus changes due to click. When a left-click is used to transfer focus (window activation or switching to another split), Ghostty now treats that click as focus-only and suppresses forwarding mouse press/release events for that focus-transfer click. ## Changes 1. macOS: suppress focus-transfer left mouse-down and matching mouse-up in `SurfaceView_AppKit.swift`. 1. GTK: suppress focus-transfer left mouse-down and matching mouse-up in `src/apprt/gtk/class/surface.zig`. 1. macOS: defer key-window focus sync to next runloop tick to reduce transient focus churn in `BaseTerminalController.swift`. 1. macOS build/lint: exclude generated/dependency paths from SwiftLint during build in `.swiftlint.yml` and `Ghostty.xcodeproj/project.pbxproj`. ## Behavior 1. Focus-transfer split clicks are now focus-only on both macOS and GTK. 1. Matching release is also suppressed for those clicks, avoiding release-without-press sequences. 1. Platform behavior is consistent for split focus transitions. ## Validation 1. Built macOS target with `xcodebuild -target Ghostty -configuration Debug -arch arm64`. 1. Ran targeted Zig test command `zig build test -Dtest-filter=computeFraction`. 1. Ran format/lint for touched files (`swiftlint lint --fix`, `zig fmt`). 4. Build and (human) tested click scenarios on macOS ## AI Disclosure AI-assisted. Thread: https://ampcode.com/threads/T-019cb9fe-b11b-753f-99e7-8ecc52b73ec4
This commit is contained in:
@@ -1239,9 +1239,11 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
}
|
||||
|
||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||
// so things like cursors blink, pty events are sent, etc.
|
||||
self.syncFocusToSurfaceTree()
|
||||
// Becoming key can race with responder updates when activating a window.
|
||||
// Sync on the next runloop so split focus has settled first.
|
||||
DispatchQueue.main.async {
|
||||
self.syncFocusToSurfaceTree()
|
||||
}
|
||||
}
|
||||
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
|
||||
@@ -221,6 +221,10 @@ extension Ghostty {
|
||||
// This is set to non-null during keyDown to accumulate insertText contents
|
||||
private var keyTextAccumulator: [String]?
|
||||
|
||||
// True when we've consumed a left mouse-down only to move focus and
|
||||
// should suppress the matching mouse-up from being reported.
|
||||
private var suppressNextLeftMouseUp: Bool = false
|
||||
|
||||
// A small delay that is introduced before a title change to avoid flickers
|
||||
private var titleChangeTimer: Timer?
|
||||
|
||||
@@ -644,12 +648,18 @@ extension Ghostty {
|
||||
let location = convert(event.locationInWindow, from: nil)
|
||||
guard hitTest(location) == self else { return event }
|
||||
|
||||
// We only want to grab focus if either our app or window was
|
||||
// not focused.
|
||||
guard !NSApp.isActive || !window.isKeyWindow else { return event }
|
||||
// If we're already the first responder then no focus transfer is
|
||||
// happening, so the click should continue as normal.
|
||||
guard window.firstResponder !== self else { return event }
|
||||
|
||||
// If we're already focused we do nothing
|
||||
guard !focused else { return event }
|
||||
// If our window/app is already focused, then this click is only
|
||||
// being used to transfer split focus. Consume it so it does not
|
||||
// get forwarded to the terminal as a mouse click.
|
||||
if NSApp.isActive && window.isKeyWindow {
|
||||
window.makeFirstResponder(self)
|
||||
suppressNextLeftMouseUp = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make ourselves the first responder
|
||||
window.makeFirstResponder(self)
|
||||
@@ -854,6 +864,13 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
// If this mouse-up corresponds to a focus-only click transfer,
|
||||
// suppress it so we don't emit a release without a press.
|
||||
if suppressNextLeftMouseUp {
|
||||
suppressNextLeftMouseUp = false
|
||||
return
|
||||
}
|
||||
|
||||
// Always reset our pressure when the mouse goes up
|
||||
prevPressureStage = 0
|
||||
|
||||
|
||||
@@ -693,6 +693,10 @@ pub const Surface = extern struct {
|
||||
/// Whether primary paste (middle-click paste) is enabled.
|
||||
gtk_enable_primary_paste: bool = true,
|
||||
|
||||
/// True when a left mouse down was consumed purely for a focus change,
|
||||
/// and the matching left mouse release should also be suppressed.
|
||||
suppress_left_mouse_release: bool = false,
|
||||
|
||||
/// How much pending horizontal scroll do we have?
|
||||
pending_horizontal_scroll: f64 = 0.0,
|
||||
|
||||
@@ -2733,13 +2737,21 @@ pub const Surface = extern struct {
|
||||
|
||||
// If we don't have focus, grab it.
|
||||
const gl_area_widget = priv.gl_area.as(gtk.Widget);
|
||||
if (gl_area_widget.hasFocus() == 0) {
|
||||
const had_focus = gl_area_widget.hasFocus() != 0;
|
||||
if (!had_focus) {
|
||||
_ = gl_area_widget.grabFocus();
|
||||
}
|
||||
|
||||
// Report the event
|
||||
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
||||
|
||||
// If this click is only transitioning split focus, suppress it so
|
||||
// it doesn't get forwarded to the terminal as a mouse event.
|
||||
if (!had_focus and button == .left) {
|
||||
priv.suppress_left_mouse_release = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (button == .middle and !priv.gtk_enable_primary_paste) {
|
||||
return;
|
||||
}
|
||||
@@ -2795,6 +2807,11 @@ pub const Surface = extern struct {
|
||||
const gtk_mods = event.getModifierState();
|
||||
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
||||
|
||||
if (button == .left and priv.suppress_left_mouse_release) {
|
||||
priv.suppress_left_mouse_release = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (button == .middle and !priv.gtk_enable_primary_paste) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user