11import AppKit
22import QuartzCore
3+ import SwiftUI
34
45// Idle: tiny pill. Active: expanded wave container.
56private let kIdleW : CGFloat = 44
@@ -221,6 +222,8 @@ final class OverlayPanel: NSPanel {
221222 private var baseCenterX : CGFloat = 0
222223 private var shortcutLabel : String = " "
223224 private var tooltipTimer : Timer ?
225+ private var answerHost : NSHostingView < AnswerCardView > ?
226+ private( set) var isShowingAnswer = false
224227
225228 var onMouseDown : ( ( ) -> Void ) ? {
226229 get { drawView. onMouseDown }
@@ -272,6 +275,9 @@ final class OverlayPanel: NSPanel {
272275 }
273276
274277 func updateStatus( _ status: AppStatus ) {
278+ // Don't overwrite the card while it's showing
279+ if isShowingAnswer { return }
280+
275281 drawView. status = status
276282
277283 if !isVisible {
@@ -297,7 +303,9 @@ final class OverlayPanel: NSPanel {
297303 let targetFrame = NSRect ( x: baseCenterX - w / 2 , y: baseY, width: w, height: h)
298304
299305 DispatchQueue . main. async { [ weak self] in
300- self ? . setFrame ( targetFrame, display: true , animate: true )
306+ // Re-check — the answer card may have taken over in the meantime
307+ guard let self, !self . isShowingAnswer else { return }
308+ self . setFrame ( targetFrame, display: true , animate: true )
301309 }
302310 }
303311
@@ -329,6 +337,61 @@ final class OverlayPanel: NSPanel {
329337 drawView. isAIMode = enabled
330338 }
331339
340+ // MARK: - Answer card morph
341+
342+ func showAnswer( _ text: String , onCopy: @escaping ( ) -> Void , onClose: @escaping ( ) -> Void ) {
343+ // Dismiss tooltip if visible
344+ tooltipTimer? . invalidate ( )
345+ tooltipTimer = nil
346+ tooltipPanel. hide ( )
347+
348+ // Make sure baseCenterX/baseY are current
349+ if baseCenterX == 0 { positionOnScreen ( ) }
350+
351+ let card = AnswerCardView ( text: text, onCopy: onCopy, onClose: onClose)
352+ let host = NSHostingView ( rootView: card)
353+
354+ // Force SwiftUI to lay out and compute an honest intrinsic size
355+ host. layoutSubtreeIfNeeded ( )
356+ let fitSize = host. fittingSize
357+ let cardW : CGFloat = max ( 360 , fitSize. width)
358+ let cardH : CGFloat = min ( 320 , max ( 90 , fitSize. height) )
359+ host. frame = NSRect ( x: 0 , y: 0 , width: cardW, height: cardH)
360+
361+ contentView = host
362+ answerHost = host
363+ isShowingAnswer = true
364+ ignoresMouseEvents = false
365+
366+ // Anchor bottom-center; grow upward from pill's base Y
367+ let targetFrame = NSRect (
368+ x: baseCenterX - cardW / 2 ,
369+ y: baseY,
370+ width: cardW,
371+ height: cardH
372+ )
373+ if !isVisible { orderFrontRegardless ( ) }
374+ setFrame ( targetFrame, display: true , animate: true )
375+ }
376+
377+ func hideAnswer( ) {
378+ guard isShowingAnswer else { return }
379+ isShowingAnswer = false
380+ answerHost = nil
381+
382+ drawView. frame = NSRect ( x: 0 , y: 0 , width: kIdleW, height: kIdleH)
383+ drawView. alphaValue = 1
384+ contentView = drawView
385+
386+ let targetFrame = NSRect (
387+ x: baseCenterX - kIdleW / 2 ,
388+ y: baseY,
389+ width: kIdleW,
390+ height: kIdleH
391+ )
392+ setFrame ( targetFrame, display: true )
393+ }
394+
332395 @objc private func screenDidChange( ) { positionOnScreen ( ) }
333396
334397 private func positionOnScreen( ) {
@@ -341,6 +404,14 @@ final class OverlayPanel: NSPanel {
341404 ? visibleFrame. minY + 8
342405 : screenFrame. minY + 12
343406
407+ if isShowingAnswer {
408+ // Re-center the card at new baseCenterX, keep its current size
409+ let w = frame. width
410+ let h = frame. height
411+ setFrame ( NSRect ( x: baseCenterX - w / 2 , y: baseY, width: w, height: h) , display: true )
412+ return
413+ }
414+
344415 let w = frame. width > kIdleW ? kActiveW : kIdleW
345416 let h = frame. height > kIdleH ? kActiveH : kIdleH
346417 setFrame ( NSRect ( x: baseCenterX - w / 2 , y: baseY, width: w, height: h) , display: true )
0 commit comments