Add DEBUG logging for browser omnibar/content focus handoff (#910)

* Add browser focus debug logging around omnibar/content handoff

* Avoid release-only unused-value warnings in focus debug logs

* Add omnibar focus writer trace logs

* Fix omnibar tap focus race

* Stabilize omnibar focus state transitions

* Propagate event context into responder guard

* Fix webview pointer hit testing in focus guard

* Stop omnibar reacquire on pointer blur intent

* Blur omnibar on webview click intent

* Preserve pointer intent through webview focus handoff

* Restore page input focus after omnibar escape

* Fix omnibar escape focus handoff and restore retry

* Track editable focus for omnibar restore and improve test diagnostics

* Add omnibar focus tracker telemetry to failing UI test

* Wait for page readiness before seeding focused input in UI test setup

* Strengthen omnibar escape focus regression with post-click assertion

* Use deterministic window offsets for post-escape web input click test

* Always enforce webview responder on omnibar escape

* Harden omnibar focus restore and address PR review feedback

---------

Co-authored-by: tiffanysun1 <tiffanysun8@gmail.com>
This commit is contained in:
Lawrence Chen 2026-03-07 03:05:13 -08:00 committed by GitHub
parent 58bcc929b2
commit e680f1de55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1659 additions and 71 deletions

View file

@ -5510,7 +5510,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
guard let self else { return }
guard self != nil else { return }
runSetupWhenWindowReady()
}
}
@ -5607,6 +5607,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
"ghosttyGotoSplitDownShortcut": ghosttyGotoSplitDownShortcut?.displayString ?? "",
"webViewFocused": "true"
])
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_GOTO_SPLIT_INPUT_SETUP"] == "1" {
setupFocusedInputForGotoSplitUITest(panel: browserPanel)
}
return
}
@ -5652,6 +5655,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
guard let self else { return }
guard let panelId = notification.object as? UUID else { return }
self.recordGotoSplitUITestWebViewFocus(panelId: panelId, key: "webViewFocusedAfterAddressBarFocus")
self.recordGotoSplitUITestActiveElement(panelId: panelId, keyPrefix: "addressBarFocus")
})
gotoSplitUITestObservers.append(NotificationCenter.default.addObserver(
@ -5662,6 +5666,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
guard let self else { return }
guard let panelId = notification.object as? UUID else { return }
self.recordGotoSplitUITestWebViewFocus(panelId: panelId, key: "webViewFocusedAfterAddressBarExit")
self.recordGotoSplitUITestActiveElement(panelId: panelId, keyPrefix: "addressBarExit")
})
}
@ -5689,6 +5694,329 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}
private func setupFocusedInputForGotoSplitUITest(panel: BrowserPanel, attempt: Int = 0) {
let maxAttempts = 80
guard attempt < maxAttempts else {
writeGotoSplitTestData([
"webInputFocusSeeded": "false",
"setupError": "Timed out focusing page input for omnibar restore test"
])
return
}
let script = """
(() => {
try {
const trackerInstalled = window.__cmuxAddressBarFocusTrackerInstalled === true;
const readyState = String(document.readyState || "");
if (!trackerInstalled || readyState !== "complete") {
const active = document.activeElement;
return {
focused: false,
id: "",
activeId: active && typeof active.id === "string" ? active.id : "",
activeTag: active && active.tagName ? active.tagName.toLowerCase() : "",
trackerInstalled,
trackedStateId:
window.__cmuxAddressBarFocusState &&
typeof window.__cmuxAddressBarFocusState.id === "string"
? window.__cmuxAddressBarFocusState.id
: "",
readyState
};
}
const ensureInput = (id, value) => {
const existing = document.getElementById(id);
const input = (existing && existing.tagName && existing.tagName.toLowerCase() === "input")
? existing
: (() => {
const created = document.createElement("input");
created.id = id;
created.type = "text";
created.value = value;
return created;
})();
input.autocapitalize = "off";
input.autocomplete = "off";
input.spellcheck = false;
input.style.display = "block";
input.style.width = "100%";
input.style.margin = "0";
input.style.padding = "8px 10px";
input.style.border = "1px solid #5f6368";
input.style.borderRadius = "6px";
input.style.boxSizing = "border-box";
input.style.fontSize = "14px";
input.style.fontFamily = "system-ui, -apple-system, sans-serif";
input.style.background = "white";
input.style.color = "black";
return input;
};
let container = document.getElementById("cmux-ui-test-focus-container");
if (!container || !container.tagName || container.tagName.toLowerCase() !== "div") {
container = document.createElement("div");
container.id = "cmux-ui-test-focus-container";
document.body.appendChild(container);
}
container.style.position = "fixed";
container.style.left = "24px";
container.style.top = "24px";
container.style.width = "min(520px, calc(100vw - 48px))";
container.style.display = "grid";
container.style.rowGap = "12px";
container.style.padding = "12px";
container.style.background = "rgba(255,255,255,0.92)";
container.style.border = "1px solid rgba(95,99,104,0.55)";
container.style.borderRadius = "8px";
container.style.boxShadow = "0 2px 10px rgba(0,0,0,0.2)";
container.style.zIndex = "2147483647";
const input = ensureInput("cmux-ui-test-focus-input", "cmux-ui-focus-primary");
const secondaryInput = ensureInput("cmux-ui-test-focus-input-secondary", "cmux-ui-focus-secondary");
if (input.parentElement !== container) {
container.appendChild(input);
}
if (secondaryInput.parentElement !== container) {
container.appendChild(secondaryInput);
}
input.focus({ preventScroll: true });
if (typeof input.setSelectionRange === "function") {
const end = input.value.length;
input.setSelectionRange(end, end);
}
let trackedFocusId = input.getAttribute("data-cmux-addressbar-focus-id");
if (!trackedFocusId) {
trackedFocusId = "cmux-ui-test-focus-input-tracked";
input.setAttribute("data-cmux-addressbar-focus-id", trackedFocusId);
}
const selectionStart = typeof input.selectionStart === "number" ? input.selectionStart : null;
const selectionEnd = typeof input.selectionEnd === "number" ? input.selectionEnd : null;
if (
!window.__cmuxAddressBarFocusState ||
typeof window.__cmuxAddressBarFocusState.id !== "string" ||
window.__cmuxAddressBarFocusState.id !== trackedFocusId
) {
window.__cmuxAddressBarFocusState = { id: trackedFocusId, selectionStart, selectionEnd };
}
const secondaryRect = secondaryInput.getBoundingClientRect();
const viewportWidth = Math.max(Number(window.innerWidth) || 0, 1);
const viewportHeight = Math.max(Number(window.innerHeight) || 0, 1);
const secondaryCenterX = Math.min(
0.98,
Math.max(0.02, (secondaryRect.left + (secondaryRect.width / 2)) / viewportWidth)
);
const secondaryCenterY = Math.min(
0.98,
Math.max(0.02, (secondaryRect.top + (secondaryRect.height / 2)) / viewportHeight)
);
const active = document.activeElement;
return {
focused: active === input,
id: input.id || "",
secondaryId: secondaryInput.id || "",
secondaryCenterX,
secondaryCenterY,
activeId: active && typeof active.id === "string" ? active.id : "",
activeTag: active && active.tagName ? active.tagName.toLowerCase() : "",
trackerInstalled,
trackedStateId:
window.__cmuxAddressBarFocusState &&
typeof window.__cmuxAddressBarFocusState.id === "string"
? window.__cmuxAddressBarFocusState.id
: "",
readyState
};
} catch (_) {
return {
focused: false,
id: "",
secondaryId: "",
secondaryCenterX: -1,
secondaryCenterY: -1,
activeId: "",
activeTag: "",
trackerInstalled: false,
trackedStateId: "",
readyState: ""
};
}
})();
"""
panel.webView.evaluateJavaScript(script) { [weak self] result, _ in
guard let self else { return }
let payload = result as? [String: Any]
let focused = (payload?["focused"] as? Bool) ?? false
let inputId = (payload?["id"] as? String) ?? ""
let secondaryInputId = (payload?["secondaryId"] as? String) ?? ""
let secondaryCenterX = (payload?["secondaryCenterX"] as? NSNumber)?.doubleValue ?? -1
let secondaryCenterY = (payload?["secondaryCenterY"] as? NSNumber)?.doubleValue ?? -1
let activeId = (payload?["activeId"] as? String) ?? ""
let trackerInstalled = (payload?["trackerInstalled"] as? Bool) ?? false
let trackedStateId = (payload?["trackedStateId"] as? String) ?? ""
let readyState = (payload?["readyState"] as? String) ?? ""
var secondaryClickOffsetX = -1.0
var secondaryClickOffsetY = -1.0
if let window = panel.webView.window {
let webFrame = panel.webView.convert(panel.webView.bounds, to: nil)
let contentHeight = Double(window.contentView?.bounds.height ?? 0)
if webFrame.width > 1,
webFrame.height > 1,
contentHeight > 1,
secondaryCenterX > 0,
secondaryCenterX < 1,
secondaryCenterY > 0,
secondaryCenterY < 1 {
let xInContent = Double(webFrame.minX) + (secondaryCenterX * Double(webFrame.width))
let yFromTopInWeb = secondaryCenterY * Double(webFrame.height)
let yInContent = Double(webFrame.maxY) - yFromTopInWeb
let yFromTopInContent = contentHeight - yInContent
let titlebarHeight = max(0, Double(window.frame.height) - contentHeight)
secondaryClickOffsetX = xInContent
secondaryClickOffsetY = titlebarHeight + yFromTopInContent
}
}
if focused,
!inputId.isEmpty,
!secondaryInputId.isEmpty,
inputId == activeId,
trackerInstalled,
!trackedStateId.isEmpty,
secondaryCenterX > 0,
secondaryCenterX < 1,
secondaryCenterY > 0,
secondaryCenterY < 1,
secondaryClickOffsetX > 0,
secondaryClickOffsetY > 0 {
self.writeGotoSplitTestData([
"webInputFocusSeeded": "true",
"webInputFocusElementId": inputId,
"webInputFocusSecondaryElementId": secondaryInputId,
"webInputFocusSecondaryCenterX": "\(secondaryCenterX)",
"webInputFocusSecondaryCenterY": "\(secondaryCenterY)",
"webInputFocusSecondaryClickOffsetX": "\(secondaryClickOffsetX)",
"webInputFocusSecondaryClickOffsetY": "\(secondaryClickOffsetY)",
"webInputFocusActiveElementId": activeId,
"webInputFocusTrackerInstalled": trackerInstalled ? "true" : "false",
"webInputFocusTrackedStateId": trackedStateId,
"webInputFocusReadyState": readyState
])
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
self?.setupFocusedInputForGotoSplitUITest(panel: panel, attempt: attempt + 1)
}
}
}
private func recordGotoSplitUITestActiveElement(panelId: UUID, keyPrefix: String) {
recordGotoSplitUITestActiveElementRetry(panelId: panelId, keyPrefix: keyPrefix, attempt: 0)
}
private func recordGotoSplitUITestActiveElementRetry(panelId: UUID, keyPrefix: String, attempt: Int) {
let delays: [Double] = [0.05, 0.1, 0.25, 0.5]
let delay = attempt < delays.count ? delays[attempt] : delays.last!
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self,
let tabManager,
let tab = tabManager.selectedWorkspace,
let panel = tab.browserPanel(for: panelId) else { return }
self.evaluateGotoSplitUITestActiveElement(panel: panel) { snapshot in
let activeId = snapshot["id"] ?? ""
let expectedInputId = self.gotoSplitUITestExpectedInputId() ?? ""
if keyPrefix == "addressBarExit",
!expectedInputId.isEmpty,
activeId != expectedInputId,
attempt < delays.count - 1 {
self.recordGotoSplitUITestActiveElementRetry(
panelId: panelId,
keyPrefix: keyPrefix,
attempt: attempt + 1
)
return
}
self.writeGotoSplitTestData([
"\(keyPrefix)PanelId": panelId.uuidString,
"\(keyPrefix)ActiveElementId": activeId,
"\(keyPrefix)ActiveElementTag": snapshot["tag"] ?? "",
"\(keyPrefix)ActiveElementType": snapshot["type"] ?? "",
"\(keyPrefix)ActiveElementEditable": snapshot["editable"] ?? "false",
"\(keyPrefix)TrackedFocusStateId": snapshot["trackedFocusStateId"] ?? "",
"\(keyPrefix)FocusTrackerInstalled": snapshot["focusTrackerInstalled"] ?? "false"
])
}
}
}
private func evaluateGotoSplitUITestActiveElement(
panel: BrowserPanel,
completion: @escaping ([String: String]) -> Void
) {
let script = """
(() => {
try {
const active = document.activeElement;
if (!active) {
return { id: "", tag: "", type: "", editable: "false" };
}
const tag = (active.tagName || "").toLowerCase();
const type = (active.type || "").toLowerCase();
const editable =
!!active.isContentEditable ||
tag === "textarea" ||
(tag === "input" && type !== "hidden");
return {
id: typeof active.id === "string" ? active.id : "",
tag,
type,
editable: editable ? "true" : "false",
trackedFocusStateId:
window.__cmuxAddressBarFocusState &&
typeof window.__cmuxAddressBarFocusState.id === "string"
? window.__cmuxAddressBarFocusState.id
: "",
focusTrackerInstalled:
window.__cmuxAddressBarFocusTrackerInstalled === true ? "true" : "false"
};
} catch (_) {
return {
id: "",
tag: "",
type: "",
editable: "false",
trackedFocusStateId: "",
focusTrackerInstalled: "false"
};
}
})();
"""
panel.webView.evaluateJavaScript(script) { result, _ in
let payload = result as? [String: Any]
completion([
"id": (payload?["id"] as? String) ?? "",
"tag": (payload?["tag"] as? String) ?? "",
"type": (payload?["type"] as? String) ?? "",
"editable": (payload?["editable"] as? String) ?? "false",
"trackedFocusStateId": (payload?["trackedFocusStateId"] as? String) ?? "",
"focusTrackerInstalled": (payload?["focusTrackerInstalled"] as? String) ?? "false"
])
}
}
private func gotoSplitUITestExpectedInputId() -> String? {
let env = ProcessInfo.processInfo.environment
guard let path = env["CMUX_UI_TEST_GOTO_SPLIT_PATH"], !path.isEmpty else { return nil }
return loadGotoSplitTestData(at: path)["webInputFocusElementId"]
}
private func recordGotoSplitMoveIfNeeded(direction: NavigationDirection) {
guard isGotoSplitUITestRecordingEnabled() else { return }
guard let tabManager, let workspace = tabManager.selectedWorkspace else { return }
@ -6591,7 +6919,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
if browserAddressBarFocusedPanelId != nil,
cmuxOwningGhosttyView(for: NSApp.keyWindow?.firstResponder) != nil {
#if DEBUG
dlog("handleCustomShortcut: clearing stale browserAddressBarFocusedPanelId")
let stalePanelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
let firstResponderType = NSApp.keyWindow?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
dlog(
"browser.focus.addressBar.staleClear panel=\(stalePanelToken) " +
"reason=terminal_first_responder fr=\(firstResponderType)"
)
#endif
browserAddressBarFocusedPanelId = nil
stopBrowserOmnibarSelectionRepeat()
@ -7274,6 +7607,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
dlog(line)
}
private func browserFocusStateSnapshot() -> String {
let selected = tabManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil"
let focused = tabManager?.selectedWorkspace?.focusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
let addressBar = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
let keyWindow = NSApp.keyWindow?.windowNumber ?? -1
let firstResponderType = NSApp.keyWindow?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
return "selected=\(selected) focused=\(focused) addr=\(addressBar) keyWin=\(keyWindow) fr=\(firstResponderType)"
}
private func redactedDebugURL(_ url: URL?) -> String {
guard let url else { return "nil" }
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return "<invalid>"
}
components.user = nil
components.password = nil
components.query = nil
components.fragment = nil
return components.string ?? "<redacted>"
}
#endif
@discardableResult
@ -7281,9 +7635,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
guard let tabManager,
let workspace = tabManager.selectedWorkspace,
let panel = workspace.browserPanel(for: panelId) else {
#if DEBUG
dlog(
"browser.focus.addressBar.route panel=\(panelId.uuidString.prefix(5)) " +
"result=miss \(browserFocusStateSnapshot())"
)
#endif
return false
}
#if DEBUG
dlog(
"browser.focus.addressBar.route panel=\(panel.id.uuidString.prefix(5)) " +
"workspace=\(workspace.id.uuidString.prefix(5)) result=hit \(browserFocusStateSnapshot())"
)
#endif
workspace.focusPanel(panel.id)
#if DEBUG
let focusedAfter = workspace.focusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
dlog(
"browser.focus.addressBar.route panel=\(panel.id.uuidString.prefix(5)) " +
"workspace=\(workspace.id.uuidString.prefix(5)) focusedAfter=\(focusedAfter)"
)
#endif
focusBrowserAddressBar(in: panel)
return true
}
@ -7291,16 +7664,56 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
@discardableResult
func openBrowserAndFocusAddressBar(url: URL? = nil, insertAtEnd: Bool = false) -> UUID? {
guard let panelId = tabManager?.openBrowser(url: url, insertAtEnd: insertAtEnd) else {
#if DEBUG
dlog(
"browser.focus.openAndFocus result=open_failed insertAtEnd=\(insertAtEnd ? 1 : 0) " +
"url=\(redactedDebugURL(url)) \(browserFocusStateSnapshot())"
)
#endif
return nil
}
#if DEBUG
dlog(
"browser.focus.openAndFocus result=open_ok panel=\(panelId.uuidString.prefix(5)) " +
"insertAtEnd=\(insertAtEnd ? 1 : 0) url=\(redactedDebugURL(url))"
)
#endif
#if DEBUG
let didFocus = focusBrowserAddressBar(panelId: panelId)
dlog(
"browser.focus.openAndFocus result=focus_request panel=\(panelId.uuidString.prefix(5)) " +
"focused=\(didFocus ? 1 : 0) \(browserFocusStateSnapshot())"
)
#else
_ = focusBrowserAddressBar(panelId: panelId)
#endif
return panelId
}
private func focusBrowserAddressBar(in panel: BrowserPanel) {
#if DEBUG
let requestId = panel.requestAddressBarFocus()
dlog(
"browser.focus.addressBar.request panel=\(panel.id.uuidString.prefix(5)) " +
"request=\(requestId.uuidString.prefix(8)) \(browserFocusStateSnapshot())"
)
#else
_ = panel.requestAddressBarFocus()
#endif
browserAddressBarFocusedPanelId = panel.id
#if DEBUG
dlog(
"browser.focus.addressBar.sticky panel=\(panel.id.uuidString.prefix(5)) " +
"request=\(requestId.uuidString.prefix(8)) \(browserFocusStateSnapshot())"
)
#endif
NotificationCenter.default.post(name: .browserFocusAddressBar, object: panel.id)
#if DEBUG
dlog(
"browser.focus.addressBar.notify panel=\(panel.id.uuidString.prefix(5)) " +
"request=\(requestId.uuidString.prefix(8))"
)
#endif
}
func focusedBrowserAddressBarPanelId() -> UUID? {
@ -7309,11 +7722,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private func focusedBrowserAddressBarPanelIdForShortcutEvent(_ event: NSEvent) -> UUID? {
guard let panelId = browserAddressBarFocusedPanelId else { return nil }
guard let context = preferredMainWindowContextForShortcutRouting(event: event),
let workspace = context.tabManager.selectedWorkspace,
workspace.browserPanel(for: panelId) != nil else {
guard let context = preferredMainWindowContextForShortcutRouting(event: event) else {
#if DEBUG
dlog(
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
"accepted=0 reason=no_context event=\(NSWindow.keyDescription(event))"
)
#endif
return nil
}
guard let workspace = context.tabManager.selectedWorkspace else {
#if DEBUG
dlog(
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
"accepted=0 reason=no_workspace event=\(NSWindow.keyDescription(event))"
)
#endif
return nil
}
guard workspace.browserPanel(for: panelId) != nil else {
#if DEBUG
dlog(
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
"accepted=0 reason=panel_not_in_workspace workspace=\(workspace.id.uuidString.prefix(5)) " +
"event=\(NSWindow.keyDescription(event))"
)
#endif
return nil
}
#if DEBUG
dlog(
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
"accepted=1 workspace=\(workspace.id.uuidString.prefix(5)) event=\(NSWindow.keyDescription(event))"
)
#endif
return panelId
}
@ -7330,7 +7776,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
let normalizedFlags = browserOmnibarNormalizedModifierFlags(flags)
let isCommandOrControlOnly = normalizedFlags == [.command] || normalizedFlags == [.control]
guard isCommandOrControlOnly else { return false }
return chars == "n" || chars == "p"
let shouldBypass = chars == "n" || chars == "p"
#if DEBUG
if shouldBypass {
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
dlog(
"browser.focus.addressBar.shortcutBypass panel=\(panelToken) " +
"chars=\(chars) flags=\(normalizedFlags.rawValue)"
)
}
#endif
return shouldBypass
}
private func commandOmnibarSelectionDelta(
@ -7347,6 +7803,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private func dispatchBrowserOmnibarSelectionMove(delta: Int) {
guard delta != 0 else { return }
guard let panelId = browserAddressBarFocusedPanelId else { return }
#if DEBUG
dlog(
"browser.focus.omnibar.selectionMove panel=\(panelId.uuidString.prefix(5)) " +
"delta=\(delta) repeatKey=\(browserOmnibarRepeatKeyCode.map(String.init) ?? "nil")"
)
#endif
NotificationCenter.default.post(
name: .browserMoveOmnibarSelection,
object: panelId,
@ -7356,15 +7818,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private func startBrowserOmnibarSelectionRepeatIfNeeded(keyCode: UInt16, delta: Int) {
guard delta != 0 else { return }
guard browserAddressBarFocusedPanelId != nil else { return }
guard browserAddressBarFocusedPanelId != nil else {
#if DEBUG
dlog(
"browser.focus.omnibar.repeat.start key=\(keyCode) delta=\(delta) " +
"result=skip_no_focused_address_bar"
)
#endif
return
}
if browserOmnibarRepeatKeyCode == keyCode, browserOmnibarRepeatDelta == delta {
#if DEBUG
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
dlog(
"browser.focus.omnibar.repeat.start panel=\(panelToken) " +
"key=\(keyCode) delta=\(delta) result=reuse"
)
#endif
return
}
stopBrowserOmnibarSelectionRepeat()
browserOmnibarRepeatKeyCode = keyCode
browserOmnibarRepeatDelta = delta
#if DEBUG
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
dlog(
"browser.focus.omnibar.repeat.start panel=\(panelToken) " +
"key=\(keyCode) delta=\(delta) result=armed"
)
#endif
let start = DispatchWorkItem { [weak self] in
self?.scheduleBrowserOmnibarSelectionRepeatTick()
@ -7376,11 +7860,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private func scheduleBrowserOmnibarSelectionRepeatTick() {
browserOmnibarRepeatStartWorkItem = nil
guard browserAddressBarFocusedPanelId != nil else {
#if DEBUG
dlog("browser.focus.omnibar.repeat.tick result=stop_no_focused_address_bar")
#endif
stopBrowserOmnibarSelectionRepeat()
return
}
guard browserOmnibarRepeatKeyCode != nil else { return }
#if DEBUG
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
dlog(
"browser.focus.omnibar.repeat.tick panel=\(panelToken) " +
"delta=\(browserOmnibarRepeatDelta)"
)
#endif
dispatchBrowserOmnibarSelectionMove(delta: browserOmnibarRepeatDelta)
let tick = DispatchWorkItem { [weak self] in
@ -7391,12 +7885,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
private func stopBrowserOmnibarSelectionRepeat() {
#if DEBUG
let previousKeyCode = browserOmnibarRepeatKeyCode
let previousDelta = browserOmnibarRepeatDelta
#endif
browserOmnibarRepeatStartWorkItem?.cancel()
browserOmnibarRepeatTickWorkItem?.cancel()
browserOmnibarRepeatStartWorkItem = nil
browserOmnibarRepeatTickWorkItem = nil
browserOmnibarRepeatKeyCode = nil
browserOmnibarRepeatDelta = 0
#if DEBUG
if previousKeyCode != nil || previousDelta != 0 {
dlog(
"browser.focus.omnibar.repeat.stop key=\(previousKeyCode.map(String.init) ?? "nil") " +
"delta=\(previousDelta)"
)
}
#endif
}
private func handleBrowserOmnibarSelectionRepeatLifecycleEvent(_ event: NSEvent) {
@ -7405,11 +7911,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
switch event.type {
case .keyUp:
if event.keyCode == browserOmnibarRepeatKeyCode {
#if DEBUG
dlog(
"browser.focus.omnibar.repeat.lifecycle event=keyUp key=\(event.keyCode) " +
"action=stop"
)
#endif
stopBrowserOmnibarSelectionRepeat()
}
case .flagsChanged:
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
if !flags.contains(.command) {
#if DEBUG
dlog(
"browser.focus.omnibar.repeat.lifecycle event=flagsChanged " +
"flags=\(flags.rawValue) action=stop"
)
#endif
stopBrowserOmnibarSelectionRepeat()
}
default:
@ -9180,6 +9698,9 @@ enum MenuBarIconRenderer {
private var cmuxFirstResponderGuardCurrentEventOverride: NSEvent?
private var cmuxFirstResponderGuardHitViewOverride: NSView?
#endif
private var cmuxFirstResponderGuardCurrentEventContext: NSEvent?
private var cmuxFirstResponderGuardHitViewContext: NSView?
private var cmuxFirstResponderGuardContextWindowNumber: Int?
private var cmuxBrowserReturnForwardingDepth = 0
private var cmuxWindowFirstResponderBypassDepth = 0
private var cmuxFieldEditorOwningWebViewAssociationKey: UInt8 = 0
@ -9221,6 +9742,7 @@ private extension NSWindow {
let responderWebView = responder.flatMap {
Self.cmuxOwningWebView(for: $0, in: self, event: currentEvent)
}
var pointerInitiatedWebFocus = false
if AppDelegate.shared?.shouldBlockFirstResponderChangeWhileCommandPaletteVisible(
window: self,
@ -9244,6 +9766,7 @@ private extension NSWindow {
event: currentEvent
)
if pointerInitiatedFocus {
pointerInitiatedWebFocus = true
#if DEBUG
dlog(
"focus.guard allowPointerFirstResponder responder=\(String(describing: type(of: responder))) " +
@ -9280,7 +9803,16 @@ private extension NSWindow {
)
}
#endif
let result = cmux_makeFirstResponder(responder)
let result: Bool
if pointerInitiatedWebFocus, let webView = responderWebView {
// `NSWindow.makeFirstResponder` may run before `CmuxWebView.mouseDown(with:)`.
// Preserve pointer intent during this synchronous responder change.
result = webView.withPointerFocusAllowance {
cmux_makeFirstResponder(responder)
}
} else {
result = cmux_makeFirstResponder(responder)
}
if result {
if let fieldEditor = responder as? NSTextView, fieldEditor.isFieldEditor {
Self.cmuxTrackFieldEditor(fieldEditor, owningWebView: responderWebView)
@ -9292,6 +9824,18 @@ private extension NSWindow {
}
@objc func cmux_sendEvent(_ event: NSEvent) {
let previousContextEvent = cmuxFirstResponderGuardCurrentEventContext
let previousContextHitView = cmuxFirstResponderGuardHitViewContext
let previousContextWindowNumber = cmuxFirstResponderGuardContextWindowNumber
cmuxFirstResponderGuardCurrentEventContext = event
cmuxFirstResponderGuardHitViewContext = Self.cmuxHitViewForEventDispatch(in: self, event: event)
cmuxFirstResponderGuardContextWindowNumber = self.windowNumber
defer {
cmuxFirstResponderGuardCurrentEventContext = previousContextEvent
cmuxFirstResponderGuardHitViewContext = previousContextHitView
cmuxFirstResponderGuardContextWindowNumber = previousContextWindowNumber
}
guard shouldSuppressWindowMoveForFolderDrag(window: self, event: event),
let contentView = self.contentView else {
cmux_sendEvent(event)
@ -9549,37 +10093,63 @@ private extension NSWindow {
return found
}
private static func cmuxCurrentEvent(for _: NSWindow) -> NSEvent? {
private static func cmuxCurrentEvent(for window: NSWindow) -> NSEvent? {
#if DEBUG
if let override = cmuxFirstResponderGuardCurrentEventOverride {
return override
}
#endif
if cmuxFirstResponderGuardContextWindowNumber == window.windowNumber {
return cmuxFirstResponderGuardCurrentEventContext
}
return NSApp.currentEvent
}
private static func cmuxHitViewInThemeFrame(in window: NSWindow, event: NSEvent) -> NSView? {
guard let contentView = window.contentView,
let themeFrame = contentView.superview else {
return nil
}
let pointInTheme = themeFrame.convert(event.locationInWindow, from: nil)
return themeFrame.hitTest(pointInTheme)
}
private static func cmuxHitViewInContentView(in window: NSWindow, event: NSEvent) -> NSView? {
guard let contentView = window.contentView else {
return nil
}
let pointInContent = contentView.convert(event.locationInWindow, from: nil)
return contentView.hitTest(pointInContent)
}
private static func cmuxTopHitViewForEvent(in window: NSWindow, event: NSEvent) -> NSView? {
if let hitInThemeFrame = cmuxHitViewInThemeFrame(in: window, event: event) {
return hitInThemeFrame
}
return cmuxHitViewInContentView(in: window, event: event)
}
private static func cmuxHitViewForEventDispatch(in window: NSWindow, event: NSEvent) -> NSView? {
if event.windowNumber != 0, event.windowNumber != window.windowNumber {
return nil
}
if let eventWindow = event.window, eventWindow !== window {
return nil
}
return cmuxTopHitViewForEvent(in: window, event: event)
}
private static func cmuxHitViewForCurrentEvent(in window: NSWindow, event: NSEvent) -> NSView? {
#if DEBUG
if let override = cmuxFirstResponderGuardHitViewOverride {
return override
}
#endif
guard let contentView = window.contentView else { return nil }
if contentView.className == "NSGlassEffectView" {
let pointInContent = contentView.convert(event.locationInWindow, from: nil)
return contentView.hitTest(pointInContent)
if cmuxFirstResponderGuardContextWindowNumber == window.windowNumber,
let contextHitView = cmuxFirstResponderGuardHitViewContext {
return contextHitView
}
if let themeFrame = contentView.superview {
let pointInTheme = themeFrame.convert(event.locationInWindow, from: nil)
if let hit = themeFrame.hitTest(pointInTheme) {
return hit
}
}
let pointInContent = contentView.convert(event.locationInWindow, from: nil)
return contentView.hitTest(pointInContent)
return cmuxTopHitViewForEvent(in: window, event: event)
}
private static func cmuxTrackFieldEditor(_ fieldEditor: NSTextView, owningWebView webView: CmuxWebView?) {