Skip to content
Open
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
2 changes: 1 addition & 1 deletion Sources/OmniWM/Core/Ax/AXManager+FrameApplications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ extension AXManager {
else { return frames }
return frames.filter {
let allowed = isFrameAllowedToWrite($0)
if !allowed { workspaceFrameSettlement?.reject($0) }
if !allowed { rejectInWorkspaceFrameSettlements($0) }
return allowed
}
}
Expand Down
35 changes: 24 additions & 11 deletions Sources/OmniWM/Core/Ax/AXManager+FrameBatching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ extension AXManager {
}
}

func rejectInWorkspaceFrameSettlements(_ target: AXFrameApplicationTarget) {
for settlement in workspaceFrameSettlements {
settlement.reject(target)
}
}

private func workspaceFrameSettlementObserver(
for target: AXFrameApplicationTarget,
after observer: FrameApplicationTerminalObserver?
) -> FrameApplicationTerminalObserver? {
let settlementObservers = workspaceFrameSettlements.compactMap { $0.observe(target) }
guard !settlementObservers.isEmpty else { return observer }
return { result in
observer?(result)
for settlementObserver in settlementObservers {
settlementObserver(result)
}
}
}

private func enqueueFrameApplicationsUsingBuffer(
_ frames: [AXFrameApplicationTarget],
options: AXFrameBatchOptions,
Expand All @@ -65,19 +85,12 @@ extension AXManager {
let pid = target.pid
let windowId = target.windowId
if inactiveWorkspaceWindowIds.contains(windowId) {
workspaceFrameSettlement?.reject(target)
rejectInWorkspaceFrameSettlements(target)
continue
}
let settlementObserver = options.isRetry ? nil : workspaceFrameSettlement?.observe(target)
let terminalObserver: FrameApplicationTerminalObserver?
if let settlementObserver {
terminalObserver = { result in
options.terminalObserver?(result)
settlementObserver(result)
}
} else {
terminalObserver = options.terminalObserver
}
let terminalObserver = options.isRetry
? options.terminalObserver
: workspaceFrameSettlementObserver(for: target, after: options.terminalObserver)
let decision = frameLedger.prepareFrameApplication(
target,
isRetry: options.isRetry,
Expand Down
2 changes: 1 addition & 1 deletion Sources/OmniWM/Core/Ax/AXManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class AXManager {
let frameBatchBuffer = AXFrameBatchBuffer()
let managedWindowBindings = AXManagedWindowBindings()
let frameLedger = AXFrameApplicationLedger()
var workspaceFrameSettlement: AXFrameSettlement?
var workspaceFrameSettlements: [AXFrameSettlement] = []
var deferredParkRequests: [AXFrameApplicationRequest] = []
var deferredParkAwaitedWindowIds: Set<Int> = []
var deferredParkTimeout: Task<Void, Never>?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,14 @@ extension LayoutRefreshController {
}

private func handleScreenParametersChanged() {
workspaceSwipe.cancel(reason: "display-change")
workspaceSwipe.cancelAll(reason: "display-change")
detectRefreshRates()
controller?.syncMonitorsToNiriEngine()
controller?.surfaceReconciler.noteWorldChanged()
}

func cleanupForMonitorDisconnect(displayId: CGDirectDisplayID, migrateAnimations: Bool) {
if workspaceSwipe.flight?.preparation.monitor.displayId == displayId {
workspaceSwipe.cancel(reason: "display-disconnected")
}
workspaceSwipe.cancelFlights(onDisplay: displayId, reason: "display-disconnected")
if let workspaceId = niriHandler.scrollAnimationByDisplay[displayId] {
niriHandler.terminateViewportGesture(
for: workspaceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ import QuartzCore
}

func resetState() {
workspaceSwipe.cancel(reason: "reset")
workspaceSwipe.cancelAll(reason: "reset")
let discardedScratchpadIndices = revealGroups.indices()
layoutState.activeRefreshTask?.cancel()
layoutState.activeRefreshTask = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,16 @@ extension MouseEventHandler {
state.activeGestureMode = mode
state.gesturePhase = .committed
if case let .workspaceSwitch(axis) = mode {
controller.layoutRefreshController.workspaceSwipe.begin(
if controller.layoutRefreshController.workspaceSwipe.begin(
axis: axis, cumulative: axis == .horizontal ? metrics.cumulativeX : metrics.cumulativeY,
timestamp: timestamp,
recognitionMovement: SwipeEvent(
delta: Double(axis == .horizontal ? metrics.rawDeltaX : metrics.rawDeltaY),
timestamp: metrics.previousTimestamp
)
)
if controller.layoutRefreshController.workspaceSwipe.hasPresentation { state.workspaceSwipeFired = true }
) { state.workspaceSwipeFired = true }
} else {
controller.layoutRefreshController.workspaceSwipe.cancel(reason: "other-gesture")
controller.layoutRefreshController.workspaceSwipe.otherGestureRecognized(on: lockedContext.monitorId)
}
return true
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/OmniWM/Core/Controller/MouseEventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ final class MouseEventHandler {
}

func resetForMultitouchSourceReplacement() {
controller?.layoutRefreshController.workspaceSwipe.cancel(reason: "source-replaced")
controller?.layoutRefreshController.workspaceSwipe.cancelAll(reason: "source-replaced")
resetGestureState()
state.workspaceSwipeTracker.reset()
clearGestureLatches()
Expand Down
6 changes: 3 additions & 3 deletions Sources/OmniWM/Core/Controller/WindowActionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ final class WindowActionHandler {

func toggleOverview() {
guard controller?.settings.overview.enabled == true else { return }
controller?.layoutRefreshController.workspaceSwipe.cancel(reason: "overview")
controller?.layoutRefreshController.workspaceSwipe.cancelAll(reason: "overview")
overviewController.toggle()
}

func openOverview() {
guard controller?.settings.overview.enabled == true else { return }
controller?.layoutRefreshController.workspaceSwipe.cancel(reason: "overview")
controller?.layoutRefreshController.workspaceSwipe.cancelAll(reason: "overview")
overviewController.input.beginGestureScrollSuppression()
overviewController.open()
}
Expand All @@ -123,7 +123,7 @@ final class WindowActionHandler {

func beginOverviewGesture() -> Bool {
guard controller?.settings.overview.enabled == true else { return false }
controller?.layoutRefreshController.workspaceSwipe.cancel(reason: "overview")
controller?.layoutRefreshController.workspaceSwipe.cancelAll(reason: "overview")
return overviewController.beginInteractiveTransition()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ extension WorkspaceNavigationHandler {
isNext: isNext, from: currentWorkspace.id, on: monitorId, createIfNeeded: true
)
else { return }
controller.layoutRefreshController.workspaceSwipe.noteSwitchWithoutPresentation(on: monitorId)
activateWorkspaceInOrder(targetWorkspace, from: currentWorkspace.id, on: monitorId)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (C) 2026 OmniNull — https://github.com/OmniNull/OmniWM

import AppKit

extension WorkspaceSwipePresentation {
func isCurrent(_ flight: Flight) -> Bool {
flights[flight.preparation.monitor.id] === flight
}

func handOff(
from predecessor: Flight, into preparation: Preparation, toward destination: Workspace,
using preview: WorkspaceSwipePreview
) -> Bool? {
guard preview.replace(
source: preparation.source.items,
destination: destination.items,
monitor: preparation.monitor,
workingFrame: preparation.frame
) else {
return fallBackWithoutAnimation(from: predecessor)
}
if !predecessor.committing { commit(predecessor) }
guard isCurrent(predecessor), predecessor.committing else { return false }
cancel(predecessor, reason: "chained", keepingPreview: true)
return nil
}

func prefetchChainTargets(after flight: Flight) {
let preparation = flight.preparation
guard flight.motion.target == 1,
let preview = previews[preparation.monitor.id],
let next = makePreparation(monitorId: preparation.monitor.id, chainedSource: flight.destination)
else { return }
preview.prefetch(
shown: preparation.source.items + flight.destination.items,
upcoming: (next.previous?.items ?? []) + (next.next?.items ?? []),
monitor: preparation.monitor,
workingFrame: preparation.frame
)
}

func refuseOrFallBack(from predecessor: Flight, axis: WorkspaceSwipeAxis, cumulative: Double) -> Bool {
guard let controller,
let isNext = TrackpadGestureIntent.isNextWorkspace(
axis: axis, displacement: cumulative, naturalDirection: controller.settings.gestures.invertDirection
),
let target = controller.workspaceNavigationHandler.swipeTargetWorkspace(
isNext: isNext,
from: predecessor.destination.id,
on: predecessor.preparation.monitor.id,
createIfNeeded: false
),
target.id != predecessor.destination.id
else {
trace("chain-refused")
return true
}
return fallBackWithoutAnimation(from: predecessor)
}

func fallBackWithoutAnimation(from predecessor: Flight) -> Bool {
if !predecessor.committing { commit(predecessor) }
if isCurrent(predecessor), predecessor.committing { cancel(predecessor, reason: "chain-fallback") }
return false
}

func createdDestination(isNext: Bool, from preparation: Preparation) -> Workspace? {
guard let controller, isNext,
let created = controller.workspaceNavigationHandler.swipeTargetWorkspace(
isNext: true, from: preparation.source.id, on: preparation.monitor.id, createIfNeeded: true
)
else { return nil }
return makeWorkspace(created.id, monitor: preparation.monitor, active: false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import AppKit

extension WorkspaceSwipePresentation {
func warmPreviews() {
guard flight == nil, preparation == nil,
guard preparation == nil,
let controller, controller.hasStartedServices,
controller.motionPolicy.animationsEnabled, controller.settings.gestures.workspaceSwipeEnabled,
!controller.isOverviewOpen(),
let monitor = controller.monitorForInteraction(),
flights[monitor.id] == nil,
let preparation = makePreparation(monitorId: monitor.id)
else { return }
previewSurface(controller).warm(
previewSurface(controller, for: monitor.id).warm(
source: preparation.source.items,
destination: (preparation.previous?.items ?? []) + (preparation.next?.items ?? []),
monitor: preparation.monitor,
Expand All @@ -21,21 +22,25 @@ extension WorkspaceSwipePresentation {
}

func handleInvalidation(workspaceId: WorkspaceDescriptor.ID?, domains: InvalidationDomain) {
guard let flight else { return }
if flight.committing {
if !participantsAreCurrent(flight) { cancel(reason: "commit-superseded") }
} else if !domains.isDisjoint(with: .layoutCommit),
workspaceId == nil || workspaceId == flight.preparation.source.id || workspaceId == flight.destination
.id
{
cancel(reason: "world-invalidated")
for flight in Array(flights.values) {
if flight.committing {
if !participantsAreCurrent(flight) { cancel(flight, reason: "commit-superseded") }
} else if flight.chained {
if !participantsAreCurrent(flight) { cancel(flight, reason: "world-invalidated") }
} else if !domains.isDisjoint(with: .layoutCommit),
workspaceId == nil || workspaceId == flight.preparation.source.id
|| workspaceId == flight.destination.id
{
cancel(flight, reason: "world-invalidated")
}
}
}

func makePreparation(monitorId: Monitor.ID) -> Preparation? {
func makePreparation(monitorId: Monitor.ID, chainedSource: Workspace? = nil) -> Preparation? {
guard let controller, let monitor = controller.workspaceManager.monitor(byId: monitorId),
let current = controller.workspaceManager.activeWorkspaceOrFirst(on: monitorId),
let source = makeWorkspace(current.id, monitor: monitor, active: true)
let current = chainedSource.flatMap({ controller.workspaceManager.descriptor(for: $0.id) })
?? controller.workspaceManager.activeWorkspaceOrFirst(on: monitorId),
let source = chainedSource ?? makeWorkspace(current.id, monitor: monitor, active: true)
else { return nil }
let navigation = controller.workspaceNavigationHandler
let previous = navigation.swipeTargetWorkspace(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ extension WorkspaceSwipePresentation {
}

func windowFrameChanged(_ windowId: UInt32) {
guard let flight, flight.handoff?.windowIds.contains(windowId) == true else { return }
readHandoffGeometry(flight)
for flight in flights.values where flight.handoff?.windowIds.contains(windowId) == true {
readHandoffGeometry(flight)
}
}

private func readHandoffGeometry(_ flight: Flight) {
Expand Down Expand Up @@ -114,8 +115,8 @@ extension WorkspaceSwipePresentation {
}

private func finishHandoff(_ flight: Flight, confirmed: Bool) {
guard self.flight === flight else { return }
guard isCurrent(flight) else { return }
let failed = flight.settlement?.failed == true
cancel(reason: failed ? "placement-failed" : confirmed ? "completed" : "handoff-unconfirmed")
cancel(flight, reason: failed ? "placement-failed" : confirmed ? "completed" : "handoff-unconfirmed")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (C) 2026 OmniNull — https://github.com/OmniNull/OmniWM

import AppKit

extension WorkspaceSwipePresentation {
private var allPreviews: [WorkspaceSwipePreview] {
Array(previews.values) + (unclaimedPreview.map { [$0] } ?? [])
}

func cancelFlights(onDisplay displayId: CGDirectDisplayID, reason: String) {
for flight in Array(flights.values) where flight.preparation.monitor.displayId == displayId {
cancel(flight, reason: reason, settlingFocus: false)
}
forgetNewestGesture(onDisplay: displayId)
for monitorId in previews.keys where monitorId.displayId == displayId {
previews.removeValue(forKey: monitorId)?.release()
}
}

func windowRemoved(_ token: WindowToken) {
for preview in allPreviews {
preview.remove(token: token)
}
}

func syncAvailability() {
guard let controller, !allPreviews.isEmpty,
!controller.motionPolicy.animationsEnabled || !controller.settings.gestures.workspaceSwipeEnabled
else { return }
cancelAll(reason: "disabled")
for preview in allPreviews {
preview.release()
}
previews.removeAll()
unclaimedPreview = nil
}

func previewSurface(_ controller: WMController, for monitorId: Monitor.ID) -> WorkspaceSwipePreview {
if let preview = previews[monitorId] { return preview }
let preview = unclaimedPreview ?? previewFactory?(controller)
?? WorkspaceSwipePreview(ownedWindowRegistry: controller.ownedWindowRegistry)
unclaimedPreview = nil
previews[monitorId] = preview
return preview
}

func present(_ flight: Flight, at timestamp: TimeInterval) {
flight.progress = flight.motion.progress(at: timestamp)
flight.carried = flight.carry?(timestamp) ?? .zero
if flight.carried == .zero { flight.carry = nil }
previews[flight.preparation.monitor.id]?.update(
sourceOffset: flight.offset(destination: false),
destinationOffset: flight.offset(destination: true)
)
}

func trace(_ action: String, progress: Double = 0) {
guard TrackpadScrollTrace.shared.isActive else { return }
TrackpadScrollTrace.record(.workspacePresentation(
renderer: "preview",
action: action,
progress: progress,
velocity: flight?.motion.velocity(at: mediaTimeProvider()),
target: flight?.motion.target
))
}
}
Loading