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
38 changes: 34 additions & 4 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ cursor movement. CLI commands and native app focus changes never warp pointer.

## `[animation]`

Controls scrolling-column focus and managed column-resize animation.
Controls scrolling-column focus and managed layout animations, including width
Comment thread
qeude marked this conversation as resolved.
changes, column/window moves, stacking, and floating transitions.
The macOS Reduce Motion preference also disables these animations, including
workspace transitions and mouse-driven reordering.

Expand All @@ -145,7 +146,8 @@ duration_ms = 125

| Setting | Default | Values/type | Description |
| --- | --- | --- | --- |
| `enabled` | `true` | boolean | Enables visual scrolling and managed resize animation. |
| `enabled` | `true` | boolean | Enables visual scrolling and managed layout animations, including resizing, column/window moves, stacking, and floating transitions. |
| `experimental_window_representations` | `false` | boolean | Experimental horizontal ribbon: animate one-shot window representations and commit native positions behind them. Requires existing Screen Recording access; does not request permission or start a continuous stream. Missing captures, unsupported geometry, and workspace switches use native windows. |
| `duration_ms` | `125` | integer from `0` to `2000` | Animation duration in milliseconds. Vertical workspace transitions use at least 180 ms when the usable viewport covers the physical display; otherwise they switch immediately to prevent reserved-area leaks. `0` disables animation even when `enabled = true`. |

## `[overview]`
Expand All @@ -156,13 +158,15 @@ Controls the Overview scale and optional pixels inside window cards.
[overview]
zoom = 0.5
window_previews = false
experimental_surface_transitions = false
window_corner_radius = 12
```

| Setting | Default | Values/type | Description |
| --- | --- | --- | --- |
| `zoom` | `0.5` | number from `0` to `0.75` | Scales workspaces and windows. Lower values show more of the neighboring workspaces. |
| `window_previews` | `false` | boolean | Captures a card-sized still image when a window first becomes visible in the current Overview session. |
| `experimental_surface_transitions` | `false` | boolean | Requires `window_previews = true`. Prepares public one-shot ScreenCaptureKit samples for native-size-to-Overview zoom transitions. Normal ribbon navigation remains native. |
| `window_corner_radius` | `12` | number from `0` to `64` | Rounds window cards and their borders in the Overview. |

With the default `false`, Defi performs no Screen Recording permission check,
Expand All @@ -172,8 +176,34 @@ image. Denial, revocation, protected content, and capture errors leave the
icon-and-title cards fully usable and do not trigger repeated prompts in the
same daemon session.

Previews are memory-only, contain no audio or cursor, and use at most 32 MiB
between Overview sessions. Defi validates the window and process identity before
The surface transition experiment prepares one-shot screenshots of the active
workspace's visible windows and up to two nearby offscreen windows at native
resolution. While the Overview is closed, preparation runs at most once per
second, and only one batch may run at a time. It never starts persistent capture
streams. Opening freezes one fixed screenshot set through the zoom and the
Overview session; these are snapshots, not live video. Native ribbon navigation
keeps the ordinary native animation backend.

The retained NV12 buffers use public AVFoundation video layers without video
encoding or per-frame CGImage conversion. NV12 preserves luma resolution but
subsamples chroma. A conservative 256 MiB screenshot/renderer overlap estimate
bounds the selected window set; this is not a bound on total process or
WindowServer RAM. Inactive cards use one-shot captures in the existing 32 MiB
compact-preview cache. No pixels are written to disk and no frame history is
retained. Public screenshot capture may still involve macOS permission or
transient capture UI; this option does not suppress system privacy indicators.

When permission, geometry, readiness, snapshot age or the budget prevents a
complete transition, the normal Overview opens instead. Changing the Overview
projection returns to ordinary card previews. A cold start still needs screenshot
preparation. Escape can zoom back when native frames and Overview offsets are
unchanged; selecting a different window/workspace uses the existing native path.
Snapshot freshness during typing or animated content, shadows, widgets and
native fullscreen windows remain limits of this experiment.

Ordinary remembered previews are memory-only, contain no audio or cursor, and
use at most 32 MiB between Overview sessions. The surface experiment has the
additional budgets described above. Defi validates the window and process identity before
reuse, requests a fresh image immediately, and removes the remembered image if
that capture fails. Remembered previews are limited to 512 pixels on their longest
edge and retained until macOS signals memory pressure or their window identity
Expand Down
3 changes: 3 additions & 0 deletions Sources/DefiConfig/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ public struct Config: Equatable, Sendable {
guard overview.zoom.isFinite, (0...0.75).contains(overview.zoom) else {
throw ConfigError.invalidValue("overview.zoom")
}
guard !overview.experimentalSurfaceTransitions || overview.windowPreviews else {
throw ConfigError.invalidValue("overview.experimental_surface_transitions requires window_previews")
}
guard overview.windowCornerRadius.isFinite,
(0...64).contains(overview.windowCornerRadius)
else {
Expand Down
15 changes: 14 additions & 1 deletion Sources/DefiConfig/ConfigModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,43 +137,53 @@ public func parseBorderColor(_ value: String) -> UInt32? {
public struct AnimationConfig: Codable, Equatable, Sendable {
public var enabled: Bool
public var durationMS: Int
public var experimentalWindowRepresentations: Bool

public init(enabled: Bool = true, durationMS: Int = 125) {
public init(enabled: Bool = true, durationMS: Int = 125,
experimentalWindowRepresentations: Bool = false) {
self.enabled = enabled
self.durationMS = durationMS
self.experimentalWindowRepresentations = experimentalWindowRepresentations
}

enum CodingKeys: String, CodingKey {
case enabled
case durationMS = "duration_ms"
case experimentalWindowRepresentations = "experimental_window_representations"
}

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let defaults = AnimationConfig()
enabled = try values.decodeIfPresent(Bool.self, forKey: .enabled) ?? defaults.enabled
durationMS = try values.decodeIfPresent(Int.self, forKey: .durationMS) ?? defaults.durationMS
experimentalWindowRepresentations = try values.decodeIfPresent(
Bool.self, forKey: .experimentalWindowRepresentations) ?? defaults.experimentalWindowRepresentations
}
}

public struct OverviewConfig: Codable, Equatable, Sendable {
public var zoom: Double
public var windowPreviews: Bool
public var experimentalSurfaceTransitions: Bool
public var windowCornerRadius: Double

public init(
zoom: Double = 0.5,
windowPreviews: Bool = false,
experimentalSurfaceTransitions: Bool = false,
windowCornerRadius: Double = 12
) {
self.zoom = zoom
self.windowPreviews = windowPreviews
self.experimentalSurfaceTransitions = experimentalSurfaceTransitions
self.windowCornerRadius = windowCornerRadius
}

enum CodingKeys: String, CodingKey {
case zoom
case windowPreviews = "window_previews"
case experimentalSurfaceTransitions = "experimental_surface_transitions"
case windowCornerRadius = "window_corner_radius"
}

Expand All @@ -183,6 +193,9 @@ public struct OverviewConfig: Codable, Equatable, Sendable {
zoom = try values.decodeIfPresent(Double.self, forKey: .zoom) ?? defaults.zoom
windowPreviews =
try values.decodeIfPresent(Bool.self, forKey: .windowPreviews) ?? defaults.windowPreviews
experimentalSurfaceTransitions = try values.decodeIfPresent(
Bool.self, forKey: .experimentalSurfaceTransitions
) ?? defaults.experimentalSurfaceTransitions
windowCornerRadius =
try values.decodeIfPresent(Double.self, forKey: .windowCornerRadius) ?? defaults.windowCornerRadius
}
Expand Down
32 changes: 26 additions & 6 deletions Sources/DefiCore/Overview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ public func interpolateOverviewViewport(
)
}

public func overviewProjectionReordersExistingCards(
from: OverviewProjection, to: OverviewProjection
) -> Bool {
guard from.monitorID == to.monitorID else { return false }
let previous = Dictionary(uniqueKeysWithValues: from.workspaces.flatMap { workspace in
workspace.windows.map { ($0.windowID, (workspace.workspaceID, $0.layer,
$0.frame.x - workspace.frame.x, $0.frame.y - workspace.frame.y)) }
})
return to.workspaces.contains { workspace in
workspace.windows.contains { card in
guard let old = previous[card.windowID] else { return false }
return old.0 != workspace.workspaceID || old.1 != card.layer
Comment thread
qeude marked this conversation as resolved.
|| (card.layer == .floating && (
abs(old.2 - (card.frame.x - workspace.frame.x)) >= 0.5
|| abs(old.3 - (card.frame.y - workspace.frame.y)) >= 0.5))
}
}
}

public func overviewProjectionResizesExistingCards(
from: OverviewProjection, to: OverviewProjection
) -> Bool {
Expand Down Expand Up @@ -304,7 +323,8 @@ public func projectOverview(
viewport: OverviewViewport,
layout: LayoutSettings,
zoom: Double = 0.5,
workspaceGap: Double = 28
workspaceGap: Double = 28,
includeOffscreenContent: Bool = false
) -> OverviewProjection {
guard let monitor = snapshot.monitors.first(where: { $0.id == monitorID }),
let monitorFrame = snapshot.monitorFrames[monitorID],
Expand Down Expand Up @@ -343,7 +363,7 @@ public func projectOverview(
width: workspaceWidth,
height: workspaceHeight
)
guard workspaceFrame.intersects(projectionBounds) else { continue }
guard includeOffscreenContent || workspaceFrame.intersects(projectionBounds) else { continue }

var workspace = originalWorkspace
workspace.scrollOffset = viewport.horizontalOffsets[workspace.id]
Expand Down Expand Up @@ -390,13 +410,13 @@ public func projectOverview(
)
if projectedFrame.x + projectedFrame.width <= workspaceFrame.x {
hiddenTiledWindowCountBefore += 1
continue
if !includeOffscreenContent { continue }
}
if projectedFrame.x >= workspaceFrame.x + workspaceFrame.width {
hiddenTiledWindowCountAfter += 1
continue
if !includeOffscreenContent { continue }
}
guard projectedFrame.intersects(workspaceFrame) else { continue }
guard includeOffscreenContent || projectedFrame.intersects(workspaceFrame) else { continue }
let isFullscreen = snapshot.nativeFullscreenWindowIDs.contains(windowID)
projectedWindows.append(
OverviewWindowProjection(
Expand Down Expand Up @@ -425,7 +445,7 @@ public func projectOverview(
width: localFrame.width * contentScale,
height: localFrame.height * contentScale
)
guard projectedFrame.intersects(workspaceFrame) else { continue }
guard includeOffscreenContent || projectedFrame.intersects(workspaceFrame) else { continue }
let isFullscreen = snapshot.nativeFullscreenWindowIDs.contains(windowID)
projectedWindows.append(
OverviewWindowProjection(
Expand Down
4 changes: 2 additions & 2 deletions Sources/DefiDaemon/DaemonAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ extension Daemon {
return true
}

func dispatchManagedResizeAnimation(
func dispatchManagedLayoutAnimation(
monitorIDs: Set<MonitorID>? = nil,
skipping skippedWindowIDs: Set<WindowID> = [],
forcingFloatingFrameWritesFor forcedFloatingWindowIDs: Set<WindowID> = [],
Expand All @@ -329,7 +329,7 @@ extension Daemon {
skipping: skippedWindowIDs,
forcingFloatingFrameWritesFor: forcedFloatingWindowIDs,
commandPerformance: commandPerformance,
source: "command-resize-animation"
source: "command-layout-animation"
)
needsDesktopSync = true
return true
Expand Down
47 changes: 40 additions & 7 deletions Sources/DefiDaemon/DaemonCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,39 @@ extension Daemon {
ribbonPrototype: overviewRibbonPrototypeRequested(in: rawCommand)
)
}
if overviewState.isOpen, command.editsSelectedLayout, monitorIndex == nil {
let originatingSession = overviewToggleState.snapshot().sessionGeneration
deferredResponse?.deferResponse()
DispatchQueue.main.async { [weak self] in
guard let self else { deferredResponse?.fail("daemon unavailable"); return }
if let controller = overviewController,
overviewToggleState.isCurrentSession(originatingSession),
controller.sessionGeneration == originatingSession {
let accepted = controller.applyLayoutCommand(command) {
[weak self] command, windowID, appID, monitorID, workspaceID, generation in
NavigationActor.enqueue { [weak self] in
let operation = {
guard let self else { return CommandResponse.failure("daemon unavailable") }
guard self.overviewToggleState.isCurrentSession(generation) else {
return CommandResponse.failure("overview session changed; command was not applied")
}
return self.editLayoutFromOverview(command, intent: OverviewWindowIntent(
windowID: windowID, expectedAppID: appID,
sourceMonitorID: monitorID, sourceWorkspaceID: workspaceID))
}
if let deferredResponse { deferredResponse.perform(operation) }
else { _ = operation() }
}
}
if !accepted {
deferredResponse?.fail("overview selection cannot apply this layout command")
}
} else {
deferredResponse?.fail("overview session changed; command was not applied")
}
}
return .success("queued for overview selection")
}
let commandMonitorID: MonitorID?
if let monitorIndex {
guard let monitorID = monitorID(atAppKitIndex: monitorIndex) else {
Expand Down Expand Up @@ -577,21 +610,21 @@ extension Daemon {
let switchesWorkspace = command.activatesWorkspace
let mutatesWorkspaceWindows = command.movesWindowBetweenWorkspaces
let movesAcrossMonitors = command.movesWindowsAcrossMonitors
let resizesManagedLayout = command.resizesManagedLayout
let animatesManagedLayout = command.animatesManagedLayout
let speculativeRibbonNavigation = isSpeculativeRibbonNavigation(command)
if switchesWorkspace || mutatesWorkspaceWindows || movesAcrossMonitors
|| resizesManagedLayout || speculativeRibbonNavigation
|| animatesManagedLayout || speculativeRibbonNavigation
{
rearmPointerFocusTransition()
}
if switchesWorkspace || mutatesWorkspaceWindows || movesAcrossMonitors
|| resizesManagedLayout
|| animatesManagedLayout
|| speculativeRibbonNavigation
{
preemptMouseGesture()
}
let animatedManagedResize =
resizesManagedLayout
let animatedManagedLayout =
animatesManagedLayout
&& animationsEnabled
&& config.animation.durationMS > 0
let previousWorkspaceID = commandMonitorID.flatMap { monitorID in
Expand Down Expand Up @@ -757,8 +790,8 @@ extension Daemon {
inFlightAnimations: inFlightAnimationMonitorIDs
)
let dispatchedAnimation =
animatedManagedResize
? dispatchManagedResizeAnimation(
animatedManagedLayout
? dispatchManagedLayoutAnimation(
monitorIDs: affectedMonitorIDs,
forcingFloatingFrameWritesFor: movedFloatingWindowIDs,
commandPerformance: commandPerformance
Expand Down
9 changes: 9 additions & 0 deletions Sources/DefiDaemon/DaemonConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ extension Daemon {
)
let requiresLayout = state.applyConfiguration(nextConfig)
config = nextConfig
platform.experimentalSurfaceRibbonEnabled = nextConfig.animation.experimentalWindowRepresentations
Task { @MainActor in
DefiSettingsRuntimeStatus.shared.updateConfiguration(nextConfig)
}
Expand Down Expand Up @@ -294,6 +295,14 @@ extension Daemon {
self?.publishOverviewState()
}
},
overviewCancelHandler: { [weak self] in
Comment thread
qeude marked this conversation as resolved.
NavigationActor.enqueue { [weak self] in self?.handleCheatsheetInput(.dismiss) }
DispatchQueue.main.async { [weak self] in
self?.cheatsheetController?.close()
self?.overviewController?.close()
self?.publishOverviewState()
}
},
cheatsheetHandler: { [weak self] input in
guard let self, self.hotKeyGeneration.withLock({ $0 == generation }) else { return }
self.handleCheatsheetInput(input)
Expand Down
3 changes: 3 additions & 0 deletions Sources/DefiDaemon/DaemonDesktop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ extension Daemon {
guard !restorationInFlight, !shouldShutdown else { return }
let layoutStartedAt = ProcessInfo.processInfo.systemUptime
var assignments: [FrameAssignment] = []
var ribbonFrames: [FrameAssignment] = []
var borderAssignments: [FrameAssignment] = []
var nativeFullscreenPlaceholderAssignments: [FrameAssignment] = []
var hiddenWindowIDs = Set<WindowID>()
Expand Down Expand Up @@ -147,6 +148,7 @@ extension Daemon {
)
let sizedFrames = layout.map(preserveIntrinsicSize)
if workspace.id == monitor.activeWorkspace && !overviewParksWindows {
ribbonFrames.append(contentsOf: sizedFrames)
let strip = continuousStripFramesForActiveWorkspace(
sizedFrames,
viewport: viewport,
Expand Down Expand Up @@ -302,6 +304,7 @@ extension Daemon {
platform.stageWindowBorderSelection(selectedWindowID)
platform.apply(
platformAssignments,
ribbonFrames: ribbonFrames,
hiddenWindowIDs: hiddenWindowIDs,
skipping: skipped,
asynchronousPositionTimeoutSeconds: positionTimeoutSeconds,
Expand Down
Loading
Loading