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
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ extension HoverCardPanel {
let generation = Self.morphGeneration
let mask = CALayer()
mask.backgroundColor = NSColor.black.cgColor
mask.cornerRadius = PopupStyle.standard.cornerRadius
mask.cornerRadius = Self.cornerRadius
mask.cornerCurve = .continuous
mask.anchorPoint = corner
CATransaction.begin()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum HoverCardPlacement: Sendable {

/// The one hover card window of the app: a borderless, non-activating,
/// click-through child window holding a glass card (opaque under Reduce
/// Transparency, `OverlaySurfaceView`) in the popup style (`PopupStyle`). The coordinator owns
/// Transparency, `OverlaySurfaceView`) in the popup style (`PopupStyle`), shaped like the other
/// floating glass panels (`cornerRadius`). The coordinator owns
/// the only instance and swaps the card's body (a tab card or a workspace
/// card view, each reused) into it.
///
Expand Down Expand Up @@ -48,6 +49,9 @@ final class HoverCardPanel: NSPanel {
static var entryOffset: CGFloat { motion.entryOffset }
/// The entrance and exit every hover card plays (the update card's too).
static let motion = HoverCardMotion.standard
/// The card's continuous radius: the floating glass panels' (the palette, dialogs), not a
/// menu's 8 pt (Leo 2026-10-20: the hover card in iOS's glass style). The shadow follows it.
static var cornerRadius: CGFloat { Metrics.panelCornerRadius }
/// The window's clear band around the card: the shadow band plus the slide's room.
static var windowMargin: CGFloat { PopupStyle.standard.shadowMargin + travel }
private(set) weak var body: NSView?
Expand Down Expand Up @@ -78,10 +82,16 @@ final class HoverCardPanel: NSPanel {
private weak var adoptedScope: ThemeScope?

init() {
glass = Glass.makeOverlayPanel(cornerRadius: PopupStyle.standard.cornerRadius)
// `PopupHostView` resets an overlay card to the popup radius; the host's radius below sets it back.
glass = Glass.makeOverlayPanel(cornerRadius: Self.cornerRadius)
// The card is its clear window's whole content: before macOS 26 its blur samples what is
// behind the window (the sidebar, a terminal), as its Liquid Glass refracts it.
glass.samplesBehindWindow = true
super.init(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: true)
Self.liveInstances += 1
adoptPopupStyle(card: glass).travelMargin = Self.travel
let host = adoptPopupStyle(card: glass)
host.travelMargin = Self.travel
host.cornerRadius = Self.cornerRadius
ignoresMouseEvents = true
isReleasedWhenClosed = false
// A no-activate test run is never active; its cards must still show.
Expand Down Expand Up @@ -174,6 +184,8 @@ final class HoverCardPanel: NSPanel {
}
self.anchor = anchor
self.placement = placement
// The density setting may have changed the floating panels' radius since the last card.
host?.cornerRadius = Self.cornerRadius
place()
if let previous {
// A visible card: it slides from where it is drawn to its new place, and resizes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ public final class HoverCardGalleryHost: NSView, ThemeResponsive {
public init(body: NSView, recolor: @escaping () -> Void) {
self.recolor = recolor
super.init(frame: .zero)
let glass = Glass.makeOverlayPanel(cornerRadius: PopupStyle.standard.cornerRadius, interactive: false)
// The real card's shape (`HoverCardPanel.cornerRadius`), so the gallery shows what a hover shows.
let glass = Glass.makeOverlayPanel(cornerRadius: HoverCardPanel.cornerRadius, interactive: false)
body.translatesAutoresizingMaskIntoConstraints = false
glass.contentView.addSubview(body)
addSubview(glass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ public final class OverlaySurfaceView: NSView {
didSet { if cornerRadius != oldValue { applyShape() } }
}

/// The vibrancy fallback blurs what is behind the window instead of the window's own
/// content: set it for a surface that is the whole content of a clear window of its own
/// (a hover card), where a blur within the window has nothing under it. Liquid Glass and
/// the solid surface ignore it.
public var samplesBehindWindow = false {
didSet { if samplesBehindWindow != oldValue { (materialView as? NSVisualEffectView)?.blendingMode = blending } }
}

private var blending: NSVisualEffectView.BlendingMode { samplesBehindWindow ? .behindWindow : .withinWindow }

/// `material` pins one material (tests, previews); nil follows
/// `reduceTransparency` on this Mac.
/// `tone` picks the theme tint (default) or the shared neutral glass with a rim.
Expand Down Expand Up @@ -130,7 +140,7 @@ public final class OverlaySurfaceView: NSView {
case .vibrancy:
let effect = NSVisualEffectView()
effect.material = .popover
effect.blendingMode = .withinWindow
effect.blendingMode = blending
effect.state = .active
effect.wantsLayer = true
// The Ghostty-derived tint sits on the blur, under the content.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ public import AppKit
/// the icon picker, page info, the palette's actions menu): the web's popup
/// surface (webviews/src/ui/popupSurface.css, #18729) in AppKit. Small-radius
/// rects, compact rows and one subtle shadow (`elevation`); each surface keeps its own
/// material and theme colors. `PopupStyleTests` holds the two in step.
/// material and theme colors. `PopupStyleTests` holds the two in step. Hover cards
/// keep the shadow and rows but take the floating glass panels' larger radius
/// (`HoverCardPanel.cornerRadius`).
public struct PopupStyle: Sendable {
/// The style every native popover uses.
public static let standard = PopupStyle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import Testing
#expect(panel.glass.materialOverride == nil)
#expect(panel.glass.material == OverlayMaterial.select(liquidGlassAvailable: Glass.isLiquidGlassAvailable,
reduceTransparency: ReduceTransparency.shared.isEnabled))
#expect(panel.glass.cornerRadius == PopupStyle.standard.cornerRadius)
#expect(panel.glass.cornerRadius == HoverCardPanel.cornerRadius)
}

/// On each material the surface takes the scope's own tokens: the overlay glass tint (the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import AppKit
@testable import CmuxNextDesign
import Testing

/// Leo 2026-10-20: the hover card should look like iOS's refracting glass. It is the app's one
/// overlay glass (`OverlaySurfaceView`: themed Liquid Glass with its own refracting rim, vibrancy
/// before macOS 26, solid under Reduce Transparency), shaped like the other floating glass panels
/// rather than a menu, and before macOS 26 it blurs what is under it.
@MainActor @Suite(.serialized) struct HoverCardGlassTests {
/// The floating glass panels' continuous radius (the palette, dialogs), not the menus'
/// 8 pt, and the shadow follows the same shape.
@Test func theCardTakesTheFloatingGlassPanelRadius() throws {
let panel = HoverCardPanel()
#expect(panel.glass.cornerRadius == Metrics.panelCornerRadius)
let host = try #require(panel.contentView as? PopupHostView)
#expect(host.cornerRadius == Metrics.panelCornerRadius, "the shadow does not follow the card's corners")
}

/// The card is the whole content of its own clear window, so a blur within that window has
/// nothing under it: the fallback blurs what is behind the window (the sidebar, the
/// terminal), as its Liquid Glass refracts it.
@Test func theFallbackGlassBlursWhatIsBehindTheCardsWindow() throws {
let panel = HoverCardPanel()
panel.glass.materialOverride = .vibrancy
defer { panel.glass.materialOverride = nil }
let effect = try #require(panel.glass.subviews.compactMap { $0 as? NSVisualEffectView }.first)
#expect(effect.blendingMode == .behindWindow)
#expect(effect.state == .active)
}

/// An overlay inside a window (a find bar over a pane) still blurs that window's content.
@Test func anOverlayInsideAWindowStillBlursWithinIt() throws {
let surface = OverlaySurfaceView(material: .vibrancy, interactive: true)
let effect = try #require(surface.subviews.compactMap { $0 as? NSVisualEffectView }.first)
#expect(effect.blendingMode == .withinWindow)
}
}