@@ -70,6 +70,14 @@ public final class SpotlightWindowController {
7070 }
7171 }
7272 private var navAnchor : NavAnchorState = . none
73+ private struct MeasuredHeightCache {
74+ let text : String
75+ let maxVisibleLines : Int
76+ let chromeAbove : CGFloat
77+ let chromeBelow : CGFloat
78+ let height : CGFloat
79+ }
80+
7381 /// Screen-space `y` of the editor card's top edge. Every non-nav
7482 /// resize (tutorial toggle, editor text growth) keeps this point
7583 /// fixed so the editor never visually jumps. Reset to nil on every
@@ -78,6 +86,8 @@ public final class SpotlightWindowController {
7886 /// what snaps the drifted-during-cycling editor back to its rest
7987 /// position the moment the user starts typing again.
8088 private var editorTopY : CGFloat ?
89+ private var measuredHeightCache : MeasuredHeightCache ?
90+ private var programmaticFrameToIgnore : NSRect ?
8191 private var cancellables : Set < AnyCancellable > = [ ]
8292
8393 /// Layout above the editor card inside the panel (find bar + tutorial
@@ -108,9 +118,24 @@ public final class SpotlightWindowController {
108118 /// Total panel height SwiftUI will render with the current state.
109119 /// Mirrors `SpotlightRootView.extraChromeHeight + editor`.
110120 private var expectedPanelHeight : CGFloat {
121+ let chromeAbove = chromeAboveEditor
122+ let chromeBelow = chromeBelowEditor
123+ if let measuredHeightCache {
124+ if hasMeasuredHeight ( forChromeAbove: chromeAbove, chromeBelow: chromeBelow) {
125+ return measuredHeightCache. height
126+ }
127+ }
111128 let lines = EditorMetrics . lineCount ( in: session. currentText)
112129 let editor = EditorMetrics . panelHeight ( forLines: lines, maxLines: preferences. maxVisibleLines)
113- return editor + chromeAboveEditor + chromeBelowEditor
130+ return editor + chromeAbove + chromeBelow
131+ }
132+
133+ private func hasMeasuredHeight( forChromeAbove chromeAbove: CGFloat , chromeBelow: CGFloat ) -> Bool {
134+ guard let measuredHeightCache else { return false }
135+ return measuredHeightCache. text == session. currentText
136+ && measuredHeightCache. maxVisibleLines == preferences. maxVisibleLines
137+ && measuredHeightCache. chromeAbove == chromeAbove
138+ && measuredHeightCache. chromeBelow == chromeBelow
114139 }
115140
116141 public init (
@@ -249,7 +274,7 @@ public final class SpotlightWindowController {
249274 pinnedTopY = top
250275 }
251276 let x = ( screenFrame. midX - panel. frame. width / 2 ) . rounded ( )
252- panel . setFrame (
277+ setPanelFrame (
253278 NSRect ( x: x, y: top - height, width: panel. frame. width, height: height) ,
254279 display: false
255280 )
@@ -270,12 +295,12 @@ public final class SpotlightWindowController {
270295 panel. isOpaque = false
271296 panel. backgroundColor = . clear
272297 panel. hasShadow = true
273- panel. level = . floating
298+ panel. level = Self . panelLevel
274299 panel. isFloatingPanel = true
275300 panel. hidesOnDeactivate = false
276301 panel. becomesKeyOnlyIfNeeded = false
277302 panel. isMovableByWindowBackground = true
278- panel. collectionBehavior = [ . canJoinAllSpaces , . fullScreenAuxiliary ]
303+ panel. collectionBehavior = Self . panelCollectionBehavior
279304 panel. contentView = NSHostingView (
280305 rootView: SpotlightRootView (
281306 focusTrigger: focusTrigger,
@@ -332,7 +357,10 @@ public final class SpotlightWindowController {
332357 guard dx <= Self . driftCorrectionThreshold, dy <= Self . driftCorrectionThreshold else {
333358 return
334359 }
335- panel. setFrameOrigin ( target)
360+ setPanelFrame (
361+ NSRect ( origin: target, size: panel. frame. size) ,
362+ display: true
363+ )
336364 }
337365
338366 private func setPanelHeight( _ height: CGFloat , animated: Bool ) {
@@ -356,14 +384,42 @@ public final class SpotlightWindowController {
356384 width: current. size. width,
357385 height: height
358386 )
359- panel. setFrame ( newFrame, display: true , animate: animated)
387+ measuredHeightCache = MeasuredHeightCache (
388+ text: session. currentText,
389+ maxVisibleLines: preferences. maxVisibleLines,
390+ chromeAbove: chromeAbove,
391+ chromeBelow: chromeBelowEditor,
392+ height: height
393+ )
394+ setPanelFrame ( newFrame, display: true , animate: animated)
360395 if case . pendingFirstResize = navAnchor {
361396 // The overlay is now on screen. Lock its bottom edge for every
362397 // subsequent cycle.
363398 navAnchor = . bottomPinned( newY)
364399 }
365400 }
366401
402+ private func setPanelFrame( _ frame: NSRect , display: Bool , animate: Bool = false ) {
403+ guard let panel else { return }
404+ programmaticFrameToIgnore = frame
405+ panel. setFrame ( frame, display: display, animate: animate)
406+ }
407+
408+ private func shouldIgnoreProgrammaticMove( _ frame: NSRect ) -> Bool {
409+ guard let expected = programmaticFrameToIgnore else { return false }
410+ guard Self . rect ( frame, isApproximatelyEqualTo: expected) else { return false }
411+ programmaticFrameToIgnore = nil
412+ return true
413+ }
414+
415+ private static func rect( _ lhs: NSRect , isApproximatelyEqualTo rhs: NSRect ) -> Bool {
416+ let tolerance : CGFloat = 0.5
417+ return abs ( lhs. origin. x - rhs. origin. x) <= tolerance
418+ && abs ( lhs. origin. y - rhs. origin. y) <= tolerance
419+ && abs ( lhs. size. width - rhs. size. width) <= tolerance
420+ && abs ( lhs. size. height - rhs. size. height) <= tolerance
421+ }
422+
367423}
368424
369425extension SpotlightWindowController {
@@ -405,6 +461,7 @@ extension SpotlightWindowController {
405461 ) { [ weak self, weak panel] _ in
406462 MainActor . assumeIsolated {
407463 guard let self, let panel else { return }
464+ if self . shouldIgnoreProgrammaticMove ( panel. frame) { return }
408465 let newTop = panel. frame. origin. y + panel. frame. size. height
409466 self . pinnedTopY = newTop
410467 self . editorTopY = newTop - self . chromeAboveEditor
0 commit comments