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:
Mitchell Hashimoto
2026-03-04 11:11:36 -08:00
committed by GitHub
3 changed files with 45 additions and 9 deletions

View File

@@ -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;
}