Skip to content

Commit 2294750

Browse files
evowhalemobrava
andauthored
Start the panel slide after the window is on screen (#24)
The reveal animation ran on the content inside a fixed panel frame, but it was started in the same turn as `orderFrontRegardless()`. Ordering a window in is not instant: the window server needs a composited frame first. Measured on a real launch, that gap is 47-62ms (six samples, mean 53ms). The slide is a 250ms easeOut, so those ~50ms are 20% of the duration and, on that curve, about 36% of the distance. The panel therefore materialised already a third of the way up and read as a pop rather than a slide. `hidePanel` was unaffected because its window is already on screen when it animates, which is why closing looked right and opening did not. Push the parked first frame out with `displayIfNeeded()` + a CATransaction flush, then start the slide on the next main actor turn so the whole curve happens on screen. Also drop the window shadow before ordering in, so the first composited frame never carries a shadow around empty space. Costs ~50ms before the content starts moving. Total hotkey-to-open is about 310ms, still well below the 168ms build cost this path had before #18. Co-authored-by: mobrava <82764703+mobrava@users.noreply.github.com>
1 parent c3871e5 commit 2294750

1 file changed

Lines changed: 27 additions & 14 deletions

File tree

Clipbara/Panel/PanelController.swift

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,42 @@ final class PanelController {
111111
// end up on the wrong screen.
112112
contentHost?.frame.origin.y = -endFrame.height
113113
panel?.alphaValue = 1
114-
panel?.orderFrontRegardless()
115-
panel?.makeKey()
116-
panel?.makeFirstResponder(nil)
117114

118115
// The window shadow is derived from the content alpha. While the
119116
// content is only partly inside the frame the shadow would outline
120117
// empty space, so drop it for the duration of the slide.
121118
panel?.hasShadow = false
122119

123-
NSAnimationContext.runAnimationGroup({ [contentHost] context in
124-
context.duration = 0.25
125-
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
126-
if let contentHost {
120+
panel?.orderFrontRegardless()
121+
panel?.makeKey()
122+
panel?.makeFirstResponder(nil)
123+
124+
// Ordering a window in is not instant: the window server needs a
125+
// composited frame before anything reaches the screen. Starting the
126+
// slide in this same turn meant the easeOut curve was already most of
127+
// the way through by the time the panel actually appeared, so the
128+
// content popped in instead of riding up. Push the parked first frame
129+
// out now, then start the slide on the next main actor turn so the
130+
// whole curve happens on screen. `hidePanel` never had this problem
131+
// because its window is already visible when it animates.
132+
panel?.contentView?.displayIfNeeded()
133+
CATransaction.flush()
134+
135+
Task { @MainActor [weak self] in
136+
guard let self, let contentHost = self.contentHost else { return }
137+
NSAnimationContext.runAnimationGroup({ context in
138+
context.duration = 0.25
139+
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
127140
var target = contentHost.frame
128141
target.origin.y = 0
129142
contentHost.animator().frame = target
130-
}
131-
}, completionHandler: { [weak self] in
132-
Task { @MainActor in
133-
self?.panel?.hasShadow = true
134-
self?.panel?.invalidateShadow()
135-
}
136-
})
143+
}, completionHandler: {
144+
Task { @MainActor [weak self] in
145+
self?.panel?.hasShadow = true
146+
self?.panel?.invalidateShadow()
147+
}
148+
})
149+
}
137150

138151
isVisible = true
139152
appState.markPanelPresented()

0 commit comments

Comments
 (0)