feat: pointer cursor preference
App-side chrome: new "Pointer Cursor" toggle (Settings -> Editor, outpost.pointerCursorEnabled, on by default) driving a shared .pointerCursorOnHover() view modifier (push/pop NSCursor.pointingHand, macOS-only, no-op on iOS) wired onto every clickable sidebar/list row: collection tree rows + disclosure chevron, flat collection list, document outline rows, collection-overview tab bar + document/search rows, global search results, command palette rows. In-document link hover: MarkdownEditorConfiguration gets a new pointerCursorOverLinksWhileEditing flag (default true). Read-only mode already showed a pointing hand over links unconditionally; editable mode never did (I-beam only) until now - applyReadOnlyCursor in NativeTextView+CursorRects.swift now applies the same over any .link range while editing too, gated by the flag. Wiki links already carry .link alongside their own custom .wikiLinkID, so they're covered with no extra work. Closes out the last item from the original preferences-wiring scope.
This commit is contained in:
@@ -18,6 +18,7 @@ struct SettingsView: View {
|
||||
@AppStorage("outpost.autocompleteEnabled") private var isAutocompleteEnabled = true
|
||||
@AppStorage("outpost.writingToolsEnabled") private var isWritingToolsEnabled = true
|
||||
@AppStorage("outpost.imagePlaygroundEnabled") private var isImagePlaygroundEnabled = true
|
||||
@AppStorage("outpost.pointerCursorEnabled") private var isPointerCursorEnabled = true
|
||||
@AppStorage("outpost.commandPaletteEnabled") private var isCommandPaletteEnabled = true
|
||||
@AppStorage("outpost.commandPaletteFullWorkspaceSearch") private var isCommandPaletteFullWorkspaceSearch = false
|
||||
@AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false
|
||||
@@ -197,6 +198,16 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: 480, alignment: .leading)
|
||||
|
||||
Divider().frame(maxWidth: 480)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Toggle("Pointer Cursor", isOn: $isPointerCursorEnabled)
|
||||
Text("Show a pointing-hand cursor when hovering sidebar rows and links in a document, instead of the default arrow or I-beam.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: 480, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ private struct DocumentNodeRow: View {
|
||||
.frame(width: 12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -186,6 +187,7 @@ private struct DocumentNodeRow: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 4)
|
||||
|
||||
@@ -34,6 +34,7 @@ struct CollectionListContent: View {
|
||||
CollectionRowView(collection: collection)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ struct CollectionOverviewView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
@@ -152,6 +153,7 @@ struct CollectionOverviewView: View {
|
||||
DocumentRowView(document: document)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,6 +186,7 @@ struct CollectionOverviewView: View {
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ struct CollectionTreeRow: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
.animation(.easeInOut(duration: 0.15), value: isExpanded)
|
||||
.contextMenu { contextMenuContent }
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ struct CommandPaletteView: View {
|
||||
resultRow(result, isSelected: index == selectedIndex)
|
||||
.id(index)
|
||||
.contentShape(Rectangle())
|
||||
.pointerCursorOnHover()
|
||||
.onTapGesture {
|
||||
selectedIndex = index
|
||||
selectCurrent()
|
||||
|
||||
@@ -23,6 +23,7 @@ struct DocumentReaderView: View {
|
||||
@AppStorage("outpost.autocompleteEnabled") private var isAutocompleteEnabled = true
|
||||
@AppStorage("outpost.writingToolsEnabled") private var isWritingToolsEnabled = true
|
||||
@AppStorage("outpost.imagePlaygroundEnabled") private var isImagePlaygroundEnabled = true
|
||||
@AppStorage("outpost.pointerCursorEnabled") private var isPointerCursorEnabled = true
|
||||
|
||||
/// `ImagePlaygroundViewController.isAvailable` gates on both OS version
|
||||
/// (macOS 15.1+) and actual device/region support (Apple Intelligence
|
||||
@@ -557,7 +558,8 @@ struct DocumentReaderView: View {
|
||||
textSubstitution: editorTextSubstitution,
|
||||
textCompletion: editorTextCompletion,
|
||||
writingTools: editorWritingTools,
|
||||
heightBehavior: .fitsContent
|
||||
heightBehavior: .fitsContent,
|
||||
pointerCursorOverLinksWhileEditing: isPointerCursorEnabled
|
||||
),
|
||||
documentId: viewModel.documentId,
|
||||
isEditable: viewModel.isEffectivelyEditable,
|
||||
|
||||
@@ -134,6 +134,7 @@ struct GlobalSearchResultsView: View {
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.pointerCursorOnHover()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
/// Pointing-hand cursor while the mouse is over a clickable sidebar/list row,
|
||||
/// gated by the "Pointer Cursor" setting (Settings → Editor → Pointer Cursor).
|
||||
/// `didPush` tracks whether this instance actually pushed a cursor, so a
|
||||
/// mid-hover toggle of the preference can never leave `NSCursor`'s push/pop
|
||||
/// stack unbalanced — pop only fires for a push this same hover made.
|
||||
private struct PointerCursorOnHover: ViewModifier {
|
||||
@AppStorage("outpost.pointerCursorEnabled") private var isEnabled = true
|
||||
@State private var didPush = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.onHover { hovering in
|
||||
if hovering {
|
||||
guard isEnabled else { return }
|
||||
NSCursor.pointingHand.push()
|
||||
didPush = true
|
||||
} else if didPush {
|
||||
NSCursor.pop()
|
||||
didPush = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func pointerCursorOnHover() -> some View {
|
||||
modifier(PointerCursorOnHover())
|
||||
}
|
||||
}
|
||||
#else
|
||||
extension View {
|
||||
func pointerCursorOnHover() -> some View { self }
|
||||
}
|
||||
#endif
|
||||
Vendored
+11
-1
@@ -94,6 +94,14 @@ public struct MarkdownEditorConfiguration: Sendable {
|
||||
/// so this stays the embedder's explicit decision rather than something the
|
||||
/// engine infers from a color it happens to see.
|
||||
public var cursorFollowsSpanInk: Bool
|
||||
/// Show a pointing-hand cursor over links while editing (not just in
|
||||
/// read-only mode, where it always shows regardless of this flag).
|
||||
///
|
||||
/// On by default. The embedder's "pointer cursor" preference maps
|
||||
/// straight to this — off just means the I-beam stays over links like
|
||||
/// any other text, since a link's edge zone still repositions the caret
|
||||
/// for editing rather than navigating (see `clickedOnLink`).
|
||||
public var pointerCursorOverLinksWhileEditing: Bool
|
||||
|
||||
public init(
|
||||
theme: MarkdownEditorTheme = .default,
|
||||
@@ -123,7 +131,8 @@ public struct MarkdownEditorConfiguration: Sendable {
|
||||
heightBehavior: HeightBehavior = .scrolls,
|
||||
rawSourceMode: Bool = false,
|
||||
extensions: [any MarkdownExtension] = [],
|
||||
cursorFollowsSpanInk: Bool = false
|
||||
cursorFollowsSpanInk: Bool = false,
|
||||
pointerCursorOverLinksWhileEditing: Bool = true
|
||||
) {
|
||||
self.theme = theme
|
||||
self.services = services
|
||||
@@ -153,6 +162,7 @@ public struct MarkdownEditorConfiguration: Sendable {
|
||||
self.rawSourceMode = rawSourceMode
|
||||
self.extensions = extensions
|
||||
self.cursorFollowsSpanInk = cursorFollowsSpanInk
|
||||
self.pointerCursorOverLinksWhileEditing = pointerCursorOverLinksWhileEditing
|
||||
}
|
||||
|
||||
public static let `default` = MarkdownEditorConfiguration()
|
||||
|
||||
+9
-3
@@ -75,11 +75,17 @@ extension NativeTextView {
|
||||
}
|
||||
|
||||
/// In read-only mode, override NSTextView's I-beam: pointing hand over a
|
||||
/// `.link` range, arrow everywhere else.
|
||||
/// `.link` range, arrow everywhere else. In edit mode, only the pointing
|
||||
/// hand part applies (gated on the embedder's preference) — everywhere
|
||||
/// else keeps the I-beam, which `super` already set.
|
||||
private func applyReadOnlyCursor(for event: NSEvent) {
|
||||
guard isSelectable, !isEditable else { return } // edit mode: keep I-beam
|
||||
guard isSelectable else { return }
|
||||
let viewPoint = convert(event.locationInWindow, from: nil)
|
||||
if isOverLink(at: viewPoint) {
|
||||
if isEditable {
|
||||
guard configuration.pointerCursorOverLinksWhileEditing,
|
||||
isOverLink(at: viewPoint) else { return } // keep I-beam
|
||||
NSCursor.pointingHand.set()
|
||||
} else if isOverLink(at: viewPoint) {
|
||||
NSCursor.pointingHand.set()
|
||||
} else {
|
||||
NSCursor.arrow.set()
|
||||
|
||||
Reference in New Issue
Block a user