From 273bdc8b5edf147ab28bfb404653ec1d6f72c124 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 21 Sep 2026 17:52:05 -0700 Subject: [PATCH] Restore header hover scaling and native hand cursors --- apple/aesel/Sources/AeselButtonStyle.swift | 10 ++-- apple/aesel/Sources/AeselScene.swift | 4 ++ apple/aesel/Sources/ContentView.swift | 5 +- .../aesel/Tests/HeaderInteractionChecks.swift | 54 +++++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 apple/aesel/Tests/HeaderInteractionChecks.swift diff --git a/apple/aesel/Sources/AeselButtonStyle.swift b/apple/aesel/Sources/AeselButtonStyle.swift index 4b53242e11..e95f2b5dfd 100644 --- a/apple/aesel/Sources/AeselButtonStyle.swift +++ b/apple/aesel/Sources/AeselButtonStyle.swift @@ -15,11 +15,7 @@ struct AeselButtonPointer: ViewModifier { let enabled: Bool @ViewBuilder func body(content: Content) -> some View { #if os(macOS) - if #available(macOS 15, *) { - content.pointerStyle(enabled ? .link : .default) - } else { - content.background(AeselCursorRegion(enabled: enabled)) - } + content.background(AeselCursorRegion(enabled: enabled)) #else content.hoverEffect(.highlight, isEnabled: enabled) #endif @@ -36,6 +32,10 @@ private struct AeselCursorRegion: NSViewRepresentable { if enabled { addCursorRect(visibleRect, cursor: .pointingHand) } } override func viewDidMoveToWindow() { window?.invalidateCursorRects(for: self) } + override func setFrameSize(_ size: NSSize) { + super.setFrameSize(size) + window?.invalidateCursorRects(for: self) + } } func makeNSView(context: Context) -> CursorView { CursorView() } func updateNSView(_ view: CursorView, context: Context) { diff --git a/apple/aesel/Sources/AeselScene.swift b/apple/aesel/Sources/AeselScene.swift index 24c7cc98f2..3c9275afa8 100644 --- a/apple/aesel/Sources/AeselScene.swift +++ b/apple/aesel/Sources/AeselScene.swift @@ -278,7 +278,9 @@ struct AeselTitle: View { var size: CGFloat = 20 var maximumWidth: CGFloat? = nil var horizontalInset: CGFloat = 12 + var hoverAnchor: UnitPoint = .leading var hoverSound: (() -> Void)? = nil + @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var hovered = false private static func fnv(_ text: String) -> UInt32 { @@ -316,6 +318,8 @@ struct AeselTitle: View { .padding(.horizontal, horizontalInset) .fixedSize() .scaleEffect(restingScale, anchor: .leading) + .scaleEffect(hovered && !reduceMotion ? 1.2 : 1, anchor: hoverAnchor) + .animation(.easeOut(duration: 0.16), value: hovered) .allowsHitTesting(false) } .contentShape(Rectangle()) diff --git a/apple/aesel/Sources/ContentView.swift b/apple/aesel/Sources/ContentView.swift index 55d5d9e3aa..7de43e6cd9 100644 --- a/apple/aesel/Sources/ContentView.swift +++ b/apple/aesel/Sources/ContentView.swift @@ -219,11 +219,12 @@ struct ContentView: View { #endif title(availableWidth: max(0, geometry.size.width - headerReservedWidth)) Spacer(minLength: 8) + .frame(height: 32) + .background { AeselWindowDragArea() } versionLabel } .padding(.horizontal, edgeInset) .frame(height: 32) - .background { AeselWindowDragArea() } } .frame(height: 32) } @@ -311,7 +312,7 @@ struct ContentView: View { /// The piece version opens settings from the fixed title strip. private var versionLabel: some View { Button { openSettings() } label: { - AeselTitle(text: "v\(session.currentRevision)", size: compact ? 11 : 13, horizontalInset: 0, + AeselTitle(text: "v\(session.currentRevision)", size: compact ? 11 : 13, horizontalInset: 0, hoverAnchor: .trailing, hoverSound: { AeselHoverSound.play(project: session.route, revision: session.currentRevision, control: "version") }) } .buttonStyle(AeselButtonStyle()) diff --git a/apple/aesel/Tests/HeaderInteractionChecks.swift b/apple/aesel/Tests/HeaderInteractionChecks.swift new file mode 100644 index 0000000000..7988dceace --- /dev/null +++ b/apple/aesel/Tests/HeaderInteractionChecks.swift @@ -0,0 +1,54 @@ +// Hidden window: no visible demo, browser navigation, or saved-session changes. +import AppKit +import SwiftUI + +@Observable final class Clicks { + var title = 0 + var version = 0 +} +private struct Header: View { + let clicks: Clicks + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 12) { + Color.clear.frame(width: 52) + Button { clicks.title += 1 } label: { + AeselTitle(text: "notebook", size: 16, horizontalInset: 0) + }.accessibilityLabel("Open piece").buttonStyle(AeselButtonStyle()) + Spacer(minLength: 8).frame(height: 32).background { AeselWindowDragArea() } + Button { clicks.version += 1 } label: { + AeselTitle(text: "v0", size: 13, horizontalInset: 0, hoverAnchor: .trailing) + }.accessibilityLabel("Open settings").buttonStyle(AeselButtonStyle()) + }.padding(.horizontal, 14).frame(height: 32) + Color.clear + }.ignoresSafeArea(.container, edges: .top).aeselWindowTitle("Test", paper: .white) + } +} +@main struct Checks { + @MainActor static func main() async { + NSApplication.shared.setActivationPolicy(.prohibited) + let clicks = Clicks() + let host = NSHostingView(rootView: Header(clicks: clicks)) + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 600, height: 400), + styleMask: [.titled, .closable, .fullSizeContentView], backing: .buffered, defer: false) + window.contentView = host + try! await Task.sleep(for: .milliseconds(200)) + func views(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(views) } + let cursors = views(host).filter { String(describing: type(of: $0)) == "CursorView" } + precondition(cursors.count == 2, "Both header buttons need native cursor regions") + let frames = cursors.map { $0.convert($0.bounds, to: host) } + for view in cursors { + precondition(!view.visibleRect.isEmpty) + let root = host.superview! + let point = view.convert(NSPoint(x: view.bounds.midX, y: view.bounds.midY), to: root.superview) + let target = root.hitTest(point) + precondition(target != nil && !(target is AeselWindowDragArea.DragView), "Window drag stole a button hit") + } + let root = host.superview! + let middle = host.convert(NSPoint(x: 300, y: 16), to: root.superview) + precondition(root.hitTest(middle) is AeselWindowDragArea.DragView, "Header gap is not draggable") + try! await Task.sleep(for: .milliseconds(1900)) + precondition(cursors.map { $0.convert($0.bounds, to: host) } == frames, "Letter motion changed button bounds") + print("PASS: native cursor regions, button hit targets, isolated window dragging, stable animated bounds") + } +} -- 2.51.2