Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 6 additions & 2 deletions Sources/DefiDaemon/DaemonDesktopSynchronization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ extension Daemon {
}
let previousSelectedWindowID = previousActiveMonitorID.flatMap {
state.selectedWindowID(on: $0)
}
}.map { snapshot.windowIDReplacements[$0] ?? $0 }
let mouseGestureEnded =
snapshot.mouseResizeGestureObserved && !snapshot.leftMouseButtonDown
let mouseInteractionEnded =
Expand Down Expand Up @@ -178,13 +178,16 @@ extension Daemon {
var nativeCursorWarpWindowID: WindowID?
var nativeCursorWarpInputTimestamp: TimeInterval?
var nativeFocusFrameMonitorID: MonitorID?
let previouslyManagedWindowIDs = Set(state.windows.keys)
let previouslyManagedWindowIDs = Set(state.windows.keys.map {
snapshot.windowIDReplacements[$0] ?? $0
})
let enteringNativeFullscreenWindowIDs = snapshot.nativeFullscreenWindowIDs
.subtracting(state.nativeFullscreenWindowIDs)
platform.updateNativeFullscreenWindowIDs(
snapshot.nativeFullscreenWindowIDs,
activeWindowIDs: snapshot.activeNativeFullscreenWindowIDs
)
rebindFocusRequests(using: snapshot.windowIDReplacements)
if pendingAnimatedFocus.map({
enteringNativeFullscreenWindowIDs.contains($0.windowID)
}) == true {
Expand All @@ -199,6 +202,7 @@ extension Daemon {
snapshot.windows,
config: config,
placementPreferences: placementPreferences,
windowIDReplacements: snapshot.windowIDReplacements,
Comment thread
greptile-apps[bot] marked this conversation as resolved.
externallyChangedWindowIDs: Set(snapshot.externallyChangedFrames.keys),
nativeFullscreenWindowIDs: snapshot.nativeFullscreenWindowIDs,
viewports: viewportsByMonitor,
Expand Down
93 changes: 93 additions & 0 deletions Sources/DefiDaemon/DaemonPointerFocusRecovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,101 @@ import DefiModel
import DefiRuntime
import Foundation

func reboundPendingAnimatedFocus(
_ request: PendingAnimatedFocus,
using replacements: [WindowID: WindowID]
) -> PendingAnimatedFocus {
PendingAnimatedFocus(
windowID: replacements[request.windowID] ?? request.windowID,
previousSelectedWindowID: request.previousSelectedWindowID.map {
replacements[$0] ?? $0
},
monitorID: request.monitorID,
sourceWorkspaceID: request.sourceWorkspaceID,
commandGeneration: request.commandGeneration,
focusInputTimestamp: request.focusInputTimestamp,
cursorWarpInputTimestamp: request.cursorWarpInputTimestamp,
retryCount: request.retryCount
)
}

func reboundPendingWorkspaceFocus(
_ request: PendingWorkspaceFocus,
using replacements: [WindowID: WindowID]
) -> PendingWorkspaceFocus {
PendingWorkspaceFocus(
monitorID: request.monitorID,
requestedWorkspaceID: request.requestedWorkspaceID,
previousWorkspaceID: request.previousWorkspaceID,
requestedWindowID:
replacements[request.requestedWindowID] ?? request.requestedWindowID,
restoresPreviousWorkspaceOnCancellation:
request.restoresPreviousWorkspaceOnCancellation,
commandGeneration: request.commandGeneration,
focusInputTimestamp: request.focusInputTimestamp,
cursorWarpInputTimestamp: request.cursorWarpInputTimestamp,
retryCount: request.retryCount
)
}

func reboundDisplacedPointerFocusRecovery(
_ recovery: DisplacedPointerFocusRecovery,
using replacements: [WindowID: WindowID]
) -> DisplacedPointerFocusRecovery {
switch recovery {
case .command(let request, let timestamp):
.command(
reboundPendingAnimatedFocus(request, using: replacements),
timestamp: timestamp
)
case .workspace(let request, let timestamp):
.workspace(
reboundPendingWorkspaceFocus(request, using: replacements),
timestamp: timestamp
)
}
}

@MainActor
extension Daemon {
func rebindFocusRequests(using replacements: [WindowID: WindowID]) {
guard replacements.isEmpty == false else { return }

if let submittedCommandFocus {
let rebound = reboundPendingAnimatedFocus(
submittedCommandFocus,
using: replacements
)
if rebound != submittedCommandFocus {
invalidateSubmittedCommandFocus(recoveringTo: rebound.windowID)
pendingAnimatedFocus = rebound
}
}
pendingAnimatedFocus = pendingAnimatedFocus.map {
reboundPendingAnimatedFocus($0, using: replacements)
}

if let pendingWorkspaceFocus {
let rebound = reboundPendingWorkspaceFocus(
pendingWorkspaceFocus,
using: replacements
)
if rebound != pendingWorkspaceFocus,
submittedWorkspaceFocusGeneration != nil
|| submittedWorkspaceFocusRequestID != nil
{
invalidateSubmittedWorkspaceFocus(
recoveringTo: rebound.requestedWindowID
)
}
self.pendingWorkspaceFocus = rebound
}

displacedPointerFocusRecovery = displacedPointerFocusRecovery.map {
reboundDisplacedPointerFocusRecovery($0, using: replacements)
}
}

func requeueDisplacedPointerFocusAfterDisplayChange(
_ recovery: DisplacedPointerFocusRecovery
) {
Expand Down
14 changes: 13 additions & 1 deletion Sources/DefiMacOS/MacOSPlatform+Snapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,18 @@ extension SnapshotEngine {
retainedWindowIDs: nextRetainedWindowIDs,
cachedWindowIDs: cachedSnapshotWindowIDs
)
let removedWindowIDs = Set(previousElements.keys).subtracting(nextWindowIDs)
let windowIDReplacements = discovery.windowIDReplacements
let removedWindowIDs = Set(previousElements.keys)
.subtracting(nextWindowIDs)
.subtracting(windowIDReplacements.keys)
if windowIDReplacements.isEmpty == false {
let replacements = windowIDReplacements.sorted {
$0.key.rawValue < $1.key.rawValue
}.map {
"\($0.key.rawValue)->\($0.value.rawValue)"
}.joined(separator: ",")
frameCoordinator.recordTrace("window-identity-replaced [\(replacements)]")
}
newlyDiscoveredWindowIDs =
hasCompletedWindowSnapshot
? nextWindowIDs.subtracting(previousElements.keys)
Expand Down Expand Up @@ -735,6 +746,7 @@ extension SnapshotEngine {
focusedWindowID: focusedWindowID,
nativeFocusChanged: nativeFocusChanged,
removedWindowIDs: removedWindowIDs,
windowIDReplacements: windowIDReplacements,
latestUserInputTimestamp: userInput.latestEventTimestamp,
userInputAfterWindowTopology: userInputOccurredAfterWindowTopology(
topologyInputTimestamp: topologyInputTimestamp,
Expand Down
126 changes: 123 additions & 3 deletions Sources/DefiMacOS/MacOSPlatform+WindowSnapshotDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct SnapshotWindowDiscoveryResult {
let nextRetainedWindowIDs: Set<WindowID>
let cachedSnapshotWindowIDs: Set<WindowID>
let previouslyManagedApplicationWindows: [pid_t: [AXUIElement]]
let windowIDReplacements: [WindowID: WindowID]
}

extension SnapshotEngine {
Expand Down Expand Up @@ -125,6 +126,7 @@ extension SnapshotEngine {
var minimizedWindows = minimizedWindowElementsByProcess
var transientGeometryWindows = transientGeometryWindowElementsByProcess
var windows: [Window] = []
var nextNativeWindowTabGroups: [WindowID: NativeWindowTabGroup] = [:]
var nextRetainedWindowIDs = Set<WindowID>()
var cachedSnapshotWindowIDs = Set<WindowID>()

Expand Down Expand Up @@ -161,6 +163,8 @@ extension SnapshotEngine {
for (windowID, element) in cachedElements {
nextElements[windowID] = element
nextProcessIDs[windowID] = processID
nextNativeWindowTabGroups[windowID] =
nativeWindowTabGroupsByWindowID[windowID]
}
}
processIDsToRefresh = requestedProcessIDs
Expand Down Expand Up @@ -290,10 +294,27 @@ onMain { $0.eventMonitor?.prepareForWindowDiscovery(
}
)

for element in appWindows ?? [] {
let orderedWindowCandidates = (appWindows ?? []).enumerated().map {
index, element in
let previousWindowID = previousWindowIDsByProcessAndElementHash[processID]?[
CFHash(element)
]?.first { CFEqual(previousElements[$0], element) }
return (
index: index,
element: element,
previousWindowID: previousWindowID
)
}.sorted { lhs, rhs in
windowDiscoveryCandidateComesFirst(
lhsPreviousWindowID: lhs.previousWindowID,
lhsIndex: lhs.index,
rhsPreviousWindowID: rhs.previousWindowID,
rhsIndex: rhs.index
)
}
for candidate in orderedWindowCandidates {
let element = candidate.element
let previousWindowID = candidate.previousWindowID
if previousWindowID.map(explicitlyDestroyedWindowIDs.contains) == true {
continue
}
Expand Down Expand Up @@ -411,11 +432,46 @@ onMain { $0.eventMonitor?.prepareForWindowDiscovery(
for: disposition,
configuredFloating: decision.floating
)
var nativeTabGroup: NativeWindowTabGroup?
if refreshesWindowList || previousWindowID == nil {
// ponytail: native tab groups are small; index physical IDs if this scan grows.
let belongsToKnownNativeTabGroup =
nativeWindowTabGroupsByWindowID[tracked.id] != nil
|| nativeWindowTabGroupsByWindowID.values.contains {
$0.backingWindowIDs.contains(tracked.id)
}
nativeTabGroup = AXMessagingTimeoutAccess.shared.withTimeout(
snapshotAccessibilityTimeoutSeconds,
elements: [element]
) {
self.nativeWindowTabGroup(
in: element,
windowFrame: tracked.frame,
allowsTransientFrameMismatch:
belongsToKnownNativeTabGroup
)
}
} else {
nativeTabGroup = previousWindowID.flatMap {
nativeWindowTabGroupsByWindowID[$0]
}
}
if let detectedGroup = nativeTabGroup {
nativeTabGroup = nativeWindowTabGroupRebindingKnownMembers(
detectedGroup,
representativeID: tracked.id,
processID: processID,
previousGroupsByRepresentativeID:
nativeWindowTabGroupsByWindowID,
previousProcessIDs: previousProcessIDs
)
}
windows.append(tracked)
nextElements[tracked.id] = element
nextProcessIDs[tracked.id] = processID
nextNativeWindowTabGroups[tracked.id] = nativeTabGroup
}

let previousWindows = previousWindowsByProcess[processID] ?? []
let discoveredWindowIDs = Set(nextElements.keys)
let needsCachedWindowValidation = previousWindows.contains {
Expand Down Expand Up @@ -474,12 +530,66 @@ onMain { $0.eventMonitor?.prepareForWindowDiscovery(
windows.append(previousWindow)
nextElements[previousWindow.id] = previousElement
nextProcessIDs[previousWindow.id] = processID
nextNativeWindowTabGroups[previousWindow.id] =
nativeWindowTabGroupsByWindowID[previousWindow.id]
if applicationWindows[processID]?.contains(where: {
CFEqual($0, previousElement)
}) != true {
applicationWindows[processID, default: []].append(previousElement)
}
}

let processNativeTabGroups = nextNativeWindowTabGroups.filter {
nextProcessIDs[$0.key] == processID
}
let newlyObservedProcessWindowIDs = Set(
windows.lazy.filter { $0.processID == processID }.map(\.id)
).subtracting(previousElements.keys)
let additionalBackingWindowIDsByRepresentative:
[WindowID: Set<WindowID>] = Dictionary(
uniqueKeysWithValues: processNativeTabGroups.compactMap {
representativeID, group in
guard let previousGroup =
nativeWindowTabGroupsByWindowID[representativeID],
group.tabTitles.count == previousGroup.tabTitles.count + 1
else { return nil }
return (representativeID, newlyObservedProcessWindowIDs)
}
)
let nativeTabBackingIDsByRepresentative =
nativeTabBackingWindowIDsByRepresentative(
windows: windows.filter { $0.processID == processID },
groupsByRepresentativeID: processNativeTabGroups,
retainedWindowIDs: processRetainedWindowIDs,
additionalBackingWindowIDsByRepresentative:
additionalBackingWindowIDsByRepresentative
)
let nativeTabBackingIDs = Set(
nativeTabBackingIDsByRepresentative.values.flatMap { $0 }
)
if nativeTabBackingIDs.isEmpty == false {
for (representativeID, backingWindowIDs) in
nativeTabBackingIDsByRepresentative
{
nextNativeWindowTabGroups[representativeID]?.backingWindowIDs =
backingWindowIDs
}
windows.removeAll { nativeTabBackingIDs.contains($0.id) }
for windowID in nativeTabBackingIDs {
nextElements[windowID] = nil
nextProcessIDs[windowID] = nil
nextNativeWindowTabGroups[windowID] = nil
}
let representativeIDs = nativeTabBackingIDsByRepresentative.keys.sorted {
$0.rawValue < $1.rawValue
}.map { String($0.rawValue) }.joined(separator: ",")
let backingIDs = nativeTabBackingIDs.sorted {
$0.rawValue < $1.rawValue
}.map { String($0.rawValue) }.joined(separator: ",")
frameCoordinator.recordTrace(
"native-tabs pid=\(processID) representatives=[\(representativeIDs)] backing=[\(backingIDs)]"
)
}
}
resolveTransientOwners(
windows: &windows,
Expand All @@ -491,6 +601,15 @@ onMain { $0.eventMonitor?.prepareForWindowDiscovery(
? Set(nextProcessIDs.values)
: topologyProcessIDs
)
let liveNativeWindowTabGroups = nextNativeWindowTabGroups.filter {
nextElements[$0.key] != nil
}
let windowIDReplacements = nativeWindowTabRepresentativeReplacements(
previousWindowIDs: Set(previousElements.keys),
nextWindowIDs: Set(nextElements.keys),
groupsByRepresentativeID: liveNativeWindowTabGroups
)
nativeWindowTabGroupsByWindowID = liveNativeWindowTabGroups
return SnapshotWindowDiscoveryResult(
nextElements: nextElements,
nextProcessIDs: nextProcessIDs,
Expand All @@ -503,7 +622,8 @@ onMain { $0.eventMonitor?.prepareForWindowDiscovery(
nextRetainedWindowIDs: nextRetainedWindowIDs,
cachedSnapshotWindowIDs: cachedSnapshotWindowIDs,
previouslyManagedApplicationWindows:
previouslyManagedApplicationWindows
previouslyManagedApplicationWindows,
windowIDReplacements: windowIDReplacements
)
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/DefiMacOS/PlatformModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public struct DesktopSnapshot: Sendable {
public let focusedWindowID: WindowID?
public let nativeFocusChanged: Bool
public let removedWindowIDs: Set<WindowID>
public let windowIDReplacements: [WindowID: WindowID]
public let latestUserInputTimestamp: TimeInterval
public let userInputAfterWindowTopology: Bool
public let externallyChangedFrames: [WindowID: Rect]
Expand All @@ -80,6 +81,7 @@ public struct DesktopSnapshot: Sendable {
focusedWindowID: WindowID?,
nativeFocusChanged: Bool = false,
removedWindowIDs: Set<WindowID> = [],
windowIDReplacements: [WindowID: WindowID] = [:],
latestUserInputTimestamp: TimeInterval = 0,
userInputAfterWindowTopology: Bool = false,
externallyChangedFrames: [WindowID: Rect] = [:],
Expand All @@ -101,6 +103,7 @@ public struct DesktopSnapshot: Sendable {
self.focusedWindowID = focusedWindowID
self.nativeFocusChanged = nativeFocusChanged
self.removedWindowIDs = removedWindowIDs
self.windowIDReplacements = windowIDReplacements
self.latestUserInputTimestamp = latestUserInputTimestamp
self.userInputAfterWindowTopology = userInputAfterWindowTopology
self.externallyChangedFrames = externallyChangedFrames
Expand Down
Loading