Skip to content

Commit a63b5e6

Browse files
committed
Show pointer cursor on clickable controls
1 parent ccac34e commit a63b5e6

7 files changed

Lines changed: 72 additions & 0 deletions

Sources/NoturcodeApp/DisplayCoordinator.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,11 @@ private final class AnnouncementHostingView: NSHostingView<AnyView> {
552552

553553
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
554554

555+
override func resetCursorRects() {
556+
super.resetCursorRects()
557+
addCursorRect(bounds, cursor: .pointingHand)
558+
}
559+
555560
@objc private func handleClick() {
556561
onClick?()
557562
}

Sources/NoturcodeApp/NotchSurfaceView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ private struct AdaptiveDockHeader: View {
408408
)
409409
}
410410
.buttonStyle(.plain)
411+
.clickableCursor()
411412
.accessibilityLabel("Show \(count) more connected sessions, \(overflowState.displayName.lowercased())")
412413
}
413414
}

Sources/NoturcodeApp/SelectionQuestionController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ private struct SelectionQuestionView: View {
9595
.background(.white.opacity(0.07), in: Circle())
9696
}
9797
.buttonStyle(.plain)
98+
.clickableCursor()
9899
.keyboardShortcut(.cancelAction)
99100
}
100101

@@ -142,6 +143,7 @@ private struct SelectionQuestionView: View {
142143
.background(.white.opacity(question.isEmpty ? 0.05 : 0.12), in: Circle())
143144
}
144145
.buttonStyle(.plain)
146+
.clickableCursor()
145147
.disabled(question.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isLoading)
146148
}
147149
.padding(.horizontal, 10)

Sources/NoturcodeApp/SessionViews.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,31 @@ import SwiftUI
33
import NoturcodeCore
44
import UniformTypeIdentifiers
55

6+
private final class PointingHandCursorView: NSView {
7+
override func resetCursorRects() {
8+
super.resetCursorRects()
9+
addCursorRect(bounds, cursor: .pointingHand)
10+
}
11+
12+
override func hitTest(_ point: NSPoint) -> NSView? { nil }
13+
}
14+
15+
private struct PointingHandCursorRegion: NSViewRepresentable {
16+
func makeNSView(context: Context) -> PointingHandCursorView {
17+
PointingHandCursorView(frame: .zero)
18+
}
19+
20+
func updateNSView(_ nsView: PointingHandCursorView, context: Context) {
21+
nsView.window?.invalidateCursorRects(for: nsView)
22+
}
23+
}
24+
25+
extension View {
26+
func clickableCursor() -> some View {
27+
background(PointingHandCursorRegion())
28+
}
29+
}
30+
631
struct NoturcodeBrandMark: View {
732
let size: CGFloat
833

@@ -614,6 +639,7 @@ private struct SessionRow: View {
614639
.contentShape(Circle())
615640
}
616641
.buttonStyle(.plain)
642+
.clickableCursor()
617643
.help("Disconnect from Noturcode — keeps the terminal and agent running")
618644
.accessibilityLabel("Disconnect from Noturcode")
619645
.accessibilityHint("Removes this card without stopping the terminal session")
@@ -631,6 +657,7 @@ private struct SessionRow: View {
631657
.contentShape(Capsule())
632658
}
633659
.buttonStyle(.plain)
660+
.clickableCursor()
634661
.accessibilityIdentifier("view-terminal-\(session.id)")
635662
}
636663
.font(.system(size: 10.5, weight: .regular))
@@ -662,6 +689,7 @@ private struct SessionRow: View {
662689
.onTapGesture(perform: onSelect)
663690
.frame(maxWidth: .infinity)
664691
.onHover(perform: onHover)
692+
.clickableCursor()
665693
.animation(reduceMotion ? nil : .snappy(duration: 0.18, extraBounce: 0), value: isHovered)
666694
.animation(reduceMotion ? nil : .easeOut(duration: 0.08), value: isPressed)
667695
.animation(reduceMotion ? nil : .easeOut(duration: 0.16), value: isUnreadFinished)
@@ -695,6 +723,7 @@ private struct ConversationSidebarToggle: View {
695723
}
696724
}
697725
.buttonStyle(ConversationControlButtonStyle(reduceMotion: reduceMotion))
726+
.clickableCursor()
698727
.onHover { hovering in
699728
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.10)) {
700729
isHovered = hovering
@@ -879,6 +908,7 @@ struct TerminalViewportContent: View {
879908
.contentShape(Rectangle())
880909
}
881910
.buttonStyle(.plain)
911+
.clickableCursor()
882912
.help(isSidebarVisible ? "Hide sidebar" : "Show sidebar")
883913
.accessibilityLabel(isSidebarVisible ? "Hide sidebar" : "Show sidebar")
884914
.accessibilityIdentifier("toggle-chat-sidebar")
@@ -895,6 +925,7 @@ struct TerminalViewportContent: View {
895925
.background(.white.opacity(0.055), in: Capsule())
896926
}
897927
.buttonStyle(.plain)
928+
.clickableCursor()
898929
.help("Compact this \(session.key.source.displayName) session")
899930
.accessibilityIdentifier("compact-session")
900931
}
@@ -907,6 +938,7 @@ struct TerminalViewportContent: View {
907938
.background(.white.opacity(0.08), in: Circle())
908939
}
909940
.buttonStyle(.plain)
941+
.clickableCursor()
910942
.accessibilityLabel("Close session chat")
911943
.accessibilityIdentifier("close-terminal-preview")
912944
}
@@ -1069,6 +1101,7 @@ struct TerminalViewportContent: View {
10691101
.background(.regularMaterial, in: Circle())
10701102
}
10711103
.buttonStyle(.plain)
1104+
.clickableCursor()
10721105
.padding(8)
10731106
.help("Jump to latest message")
10741107
.accessibilityIdentifier("chat-jump-to-latest")
@@ -1125,6 +1158,7 @@ struct TerminalViewportContent: View {
11251158
.background(.white.opacity(0.035), in: RoundedRectangle(cornerRadius: 9, style: .continuous))
11261159
}
11271160
.buttonStyle(.plain)
1161+
.clickableCursor()
11281162
.accessibilityIdentifier("return-to-session-chat")
11291163
}
11301164
}
@@ -1236,6 +1270,7 @@ struct TerminalViewportContent: View {
12361270
.contentShape(Rectangle())
12371271
}
12381272
.buttonStyle(.plain)
1273+
.clickableCursor()
12391274
.accessibilityLabel("Show workflow sidebar")
12401275
.accessibilityIdentifier("show-chat-sidebar-rail")
12411276
.transition(.move(edge: .trailing).combined(with: .opacity))
@@ -1366,6 +1401,7 @@ struct TerminalViewportContent: View {
13661401
.contentShape(Rectangle())
13671402
}
13681403
.buttonStyle(.plain)
1404+
.clickableCursor()
13691405
.help("Hide sidebar")
13701406
.accessibilityLabel("Close workflow sidebar")
13711407
.accessibilityIdentifier("close-chat-sidebar")
@@ -1436,6 +1472,7 @@ struct TerminalViewportContent: View {
14361472
.background(.white.opacity(selectedWorkflowNodeID == node.id ? 0.09 : 0.035), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
14371473
}
14381474
.buttonStyle(.plain)
1475+
.clickableCursor()
14391476
.accessibilityLabel("Open \(node.title) workflow details")
14401477
.accessibilityIdentifier("workflow-node-\(node.id)")
14411478
}
@@ -1510,6 +1547,7 @@ struct TerminalViewportContent: View {
15101547
.foregroundStyle(.white, .black.opacity(0.72))
15111548
}
15121549
.buttonStyle(.plain)
1550+
.clickableCursor()
15131551
.offset(x: 4, y: -4)
15141552
.accessibilityLabel("Remove \(attachment.url.lastPathComponent)")
15151553
}
@@ -1550,13 +1588,15 @@ struct TerminalViewportContent: View {
15501588
.background(canSend ? Color.white.opacity(0.92) : .white.opacity(0.07), in: Circle())
15511589
}
15521590
.buttonStyle(.plain)
1591+
.clickableCursor()
15531592
.keyboardShortcut(.return, modifiers: .command)
15541593
.disabled(!canSend)
15551594
.help("Send to this session (Return or ⌘↩)")
15561595
.accessibilityLabel("Send prompt to \(session.name)")
15571596
.accessibilityIdentifier("send-prompt")
15581597
}
15591598
.buttonStyle(.plain)
1599+
.clickableCursor()
15601600
.foregroundStyle(.white.opacity(0.46))
15611601
.padding(.horizontal, 5)
15621602
.padding(.vertical, 3)
@@ -1778,6 +1818,7 @@ private struct PromptTimelineMarker: View {
17781818
.contentShape(Rectangle())
17791819
}
17801820
.buttonStyle(.plain)
1821+
.clickableCursor()
17811822
.onHover { hovering in
17821823
if hovering {
17831824
previewDismissalTask?.cancel()
@@ -1986,6 +2027,7 @@ private struct ToolBatchRow: View {
19862027
.contentShape(Rectangle())
19872028
}
19882029
.buttonStyle(.plain)
2030+
.clickableCursor()
19892031
.accessibilityLabel("\(isExpanded ? "Collapse" : "Expand") tool batch: \(summary)")
19902032
.accessibilityIdentifier("chat-tool-batch")
19912033

@@ -2021,6 +2063,7 @@ private struct ToolBatchRow: View {
20212063
.contentShape(Rectangle())
20222064
}
20232065
.buttonStyle(.plain)
2066+
.clickableCursor()
20242067
.accessibilityLabel("Show tool detail: \(entry.title ?? "Tool")")
20252068
.accessibilityIdentifier("chat-tool-call")
20262069
}
@@ -2058,6 +2101,7 @@ private struct ToolDetailInspector: View {
20582101
.background(.white.opacity(0.07), in: Circle())
20592102
}
20602103
.buttonStyle(.plain)
2104+
.clickableCursor()
20612105
.accessibilityLabel("Close tool details")
20622106
}
20632107
.foregroundStyle(.white.opacity(0.68))
@@ -2202,6 +2246,7 @@ private struct ChatTranscriptRow: View {
22022246
.contentShape(Rectangle())
22032247
}
22042248
.buttonStyle(.plain)
2249+
.clickableCursor()
22052250
.accessibilityLabel("Show \(entry.title ?? "tool") details")
22062251
.accessibilityIdentifier("chat-tool-call")
22072252
.background(.white.opacity(isHovered ? 0.065 : 0.035), in: RoundedRectangle(cornerRadius: 9, style: .continuous))
@@ -2238,6 +2283,7 @@ private struct ChatTranscriptRow: View {
22382283
.background(.white.opacity(0.05), in: Capsule())
22392284
}
22402285
.buttonStyle(.plain)
2286+
.clickableCursor()
22412287
.help("Preview \(url.path)")
22422288
.accessibilityLabel("Preview \(url.lastPathComponent)")
22432289
.accessibilityIdentifier("chat-file-reference")

Sources/NoturcodeApp/StatusWindowController.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ private struct StatusOverviewView: View {
160160
.background(.white.opacity(0.055), in: RoundedRectangle(cornerRadius: 7))
161161
}
162162
.buttonStyle(.plain)
163+
.clickableCursor()
163164
.help("Clear session search")
164165
}
165166
.padding(.horizontal, 14)
@@ -263,6 +264,7 @@ private struct StatusOverviewView: View {
263264
.contentShape(Rectangle())
264265
}
265266
.buttonStyle(.plain)
267+
.clickableCursor()
266268
.foregroundStyle(.white.opacity(selected ? 0.94 : 0.72))
267269
.accessibilityIdentifier("desktop-session-row")
268270
}
@@ -396,6 +398,7 @@ private struct DesktopHeaderControl: View {
396398
}
397399
}
398400
.buttonStyle(ShellPressButtonStyle(reduceMotion: reduceMotion))
401+
.clickableCursor()
399402
.onHover { hovering in
400403
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.10)) {
401404
isHovered = hovering

Sources/NoturcodeApp/TerminalViewportWindowController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ private struct FilePreviewWindowView: View {
285285
.background(.white.opacity(0.08), in: Circle())
286286
}
287287
.buttonStyle(.plain)
288+
.clickableCursor()
288289
.accessibilityLabel("Close file preview")
289290
.accessibilityIdentifier("close-file-preview")
290291
}

Tests/NoturcodeCoreTests/NoturcodeCoreTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,10 +1258,24 @@ final class NoturcodeCoreTests: XCTestCase {
12581258

12591259
XCTAssertTrue(row.contains(".fixedSize(horizontal: true, vertical: false)"))
12601260
XCTAssertTrue(row.contains("if session.key.source != .codex"))
1261+
XCTAssertTrue(row.contains(".clickableCursor()"))
12611262
XCTAssertFalse(row.contains("if let tokens = session.tokens"))
12621263
XCTAssertFalse(row.contains("DurationFormatting.tokens"))
12631264
}
12641265

1266+
func testAnnouncementUsesNativePointingHandCursor() throws {
1267+
let repository = URL(fileURLWithPath: #filePath)
1268+
.deletingLastPathComponent()
1269+
.deletingLastPathComponent()
1270+
.deletingLastPathComponent()
1271+
let source = try String(contentsOf: repository.appendingPathComponent("Sources/NoturcodeApp/DisplayCoordinator.swift"))
1272+
let hostingStart = try XCTUnwrap(source.range(of: "private final class AnnouncementHostingView"))
1273+
let hosting = String(source[hostingStart.lowerBound...])
1274+
1275+
XCTAssertTrue(hosting.contains("override func resetCursorRects()"))
1276+
XCTAssertTrue(hosting.contains("addCursorRect(bounds, cursor: .pointingHand)"))
1277+
}
1278+
12651279
func testExpandedNotchShowsRotatingQuoteAndKeepsEverySessionInList() throws {
12661280
let repository = URL(fileURLWithPath: #filePath)
12671281
.deletingLastPathComponent()

0 commit comments

Comments
 (0)