Skip to content

Commit 0eff4fc

Browse files
committed
Added .onHover for AppKit Backend
Added: - onHover(_ action: (Bool) -> Void) View Extension - OnHoverModifier TypeSafeView - createHoverTarget(wrapping: Widget) -> Widget to AppBackend Protocol - updateHoverTarget(_: Widget, environment: EnvironmentValues, action: (Bool) -> Void) to AppBackend Protocol - corresponding default implementations - AppKitBackend hover implementation - createHoverTarget implementation - updateHoverTarget implementation - NSCustomHoverTarget (NSView notifying about hovers) - HoverExample Fixed: - AppKitBackend - fixed reference removing for NSClickGestureRecognizer in NSCustomTapGestureTarget
1 parent d9af7bd commit 0eff4fc

File tree

6 files changed

+276
-48
lines changed

6 files changed

+276
-48
lines changed

Examples/Bundler.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,8 @@ version = '0.1.0'
5959
identifier = 'dev.swiftcrossui.WebViewExample'
6060
product = 'WebViewExample'
6161
version = '0.1.0'
62+
63+
[apps.HoverExample]
64+
identifier = 'dev.swiftcrossui.HoverExample'
65+
product = 'HoverExample'
66+
version = '0.1.0'

Examples/Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ let package = Package(
7272
.executableTarget(
7373
name: "WebViewExample",
7474
dependencies: exampleDependencies
75+
),
76+
.executableTarget(
77+
name: "HoverExample",
78+
dependencies: exampleDependencies
7579
)
7680
]
7781
)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import DefaultBackend
2+
import SwiftCrossUI
3+
import Foundation
4+
5+
#if canImport(SwiftBundlerRuntime)
6+
import SwiftBundlerRuntime
7+
#endif
8+
9+
@main
10+
struct HoverExample: App {
11+
var body: some Scene {
12+
WindowGroup("Hover Example") {
13+
VStack(spacing: 0) {
14+
ForEach([Bool](repeating: false, count: 18)) { _ in
15+
HStack(spacing: 0) {
16+
ForEach([Bool](repeating: false, count: 30)) { _ in
17+
CellView()
18+
}
19+
}
20+
}
21+
}
22+
.background(Color.black)
23+
}
24+
.defaultSize(width: 900, height: 540)
25+
}
26+
}
27+
28+
struct CellView: View {
29+
@State var timer: Timer?
30+
@Environment(\.colorScheme) var colorScheme
31+
@State var opacity: Float = 0.0
32+
33+
var body: some View {
34+
Rectangle()
35+
.foregroundColor(Color.blue.opacity(opacity))
36+
.onHover { hovering in
37+
if !hovering {
38+
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
39+
if opacity >= 0.05 {
40+
opacity -= 0.05
41+
} else {
42+
opacity = 0.0
43+
timer.invalidate()
44+
}
45+
}
46+
} else {
47+
opacity = 1.0
48+
timer = nil
49+
}
50+
}
51+
}
52+
}

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,40 @@ public final class AppKitBackend: AppBackend {
14241424
tapGestureTarget.longPressHandler = action
14251425
}
14261426
}
1427+
1428+
public func createHoverTarget(wrapping child: Widget) -> Widget {
1429+
let container = NSView()
1430+
1431+
container.addSubview(child)
1432+
child.leadingAnchor.constraint(equalTo: container.leadingAnchor)
1433+
.isActive = true
1434+
child.topAnchor.constraint(equalTo: container.topAnchor)
1435+
.isActive = true
1436+
child.translatesAutoresizingMaskIntoConstraints = false
1437+
1438+
let tapGestureTarget = NSCustomHoverTarget()
1439+
container.addSubview(tapGestureTarget)
1440+
tapGestureTarget.leadingAnchor.constraint(equalTo: container.leadingAnchor)
1441+
.isActive = true
1442+
tapGestureTarget.topAnchor.constraint(equalTo: container.topAnchor)
1443+
.isActive = true
1444+
tapGestureTarget.trailingAnchor.constraint(equalTo: container.trailingAnchor)
1445+
.isActive = true
1446+
tapGestureTarget.bottomAnchor.constraint(equalTo: container.bottomAnchor)
1447+
.isActive = true
1448+
tapGestureTarget.translatesAutoresizingMaskIntoConstraints = false
1449+
1450+
return container
1451+
}
1452+
1453+
public func updateHoverTarget(
1454+
_ container: Widget,
1455+
environment: EnvironmentValues,
1456+
action: @escaping (Bool) -> Void
1457+
) {
1458+
let tapGestureTarget = container.subviews[1] as! NSCustomHoverTarget
1459+
tapGestureTarget.hoverChangesHandler = action
1460+
}
14271461

14281462
final class NSBezierPathView: NSView {
14291463
var path: NSBezierPath!
@@ -1663,7 +1697,7 @@ final class NSCustomTapGestureTarget: NSView {
16631697
leftClickRecognizer = gestureRecognizer
16641698
} else if leftClickHandler == nil, let leftClickRecognizer {
16651699
removeGestureRecognizer(leftClickRecognizer)
1666-
self.leftClickHandler = nil
1700+
self.leftClickRecognizer = nil
16671701
}
16681702
}
16691703
}
@@ -1678,7 +1712,7 @@ final class NSCustomTapGestureTarget: NSView {
16781712
rightClickRecognizer = gestureRecognizer
16791713
} else if rightClickHandler == nil, let rightClickRecognizer {
16801714
removeGestureRecognizer(rightClickRecognizer)
1681-
self.rightClickHandler = nil
1715+
self.rightClickRecognizer = nil
16821716
}
16831717
}
16841718
}
@@ -1724,6 +1758,56 @@ final class NSCustomTapGestureTarget: NSView {
17241758
}
17251759
}
17261760

1761+
final class NSCustomHoverTarget: NSView {
1762+
var hoverChangesHandler: ((Bool) -> Void)? {
1763+
didSet {
1764+
if hoverChangesHandler != nil && trackingArea == nil {
1765+
let options: NSTrackingArea.Options = [
1766+
.mouseEnteredAndExited,
1767+
.activeInKeyWindow
1768+
]
1769+
let area = NSTrackingArea(rect: self.bounds,
1770+
options: options,
1771+
owner: self,
1772+
userInfo: nil)
1773+
addTrackingArea(area)
1774+
trackingArea = area
1775+
} else if hoverChangesHandler == nil, let trackingArea {
1776+
removeTrackingArea(trackingArea)
1777+
self.trackingArea = nil
1778+
}
1779+
}
1780+
}
1781+
1782+
private var trackingArea: NSTrackingArea?
1783+
1784+
override func updateTrackingAreas() {
1785+
super.updateTrackingAreas()
1786+
if let trackingArea = trackingArea {
1787+
self.removeTrackingArea(trackingArea)
1788+
}
1789+
let options: NSTrackingArea.Options = [
1790+
.mouseEnteredAndExited,
1791+
.activeInKeyWindow
1792+
]
1793+
1794+
trackingArea = NSTrackingArea(rect: self.bounds,
1795+
options: options,
1796+
owner: self,
1797+
userInfo: nil)
1798+
self.addTrackingArea(trackingArea!)
1799+
}
1800+
1801+
override func mouseEntered(with event: NSEvent) {
1802+
hoverChangesHandler?(true)
1803+
}
1804+
1805+
override func mouseExited(with event: NSEvent) {
1806+
// Mouse exited the view's bounds
1807+
hoverChangesHandler?(false)
1808+
}
1809+
}
1810+
17271811
final class NSCustomMenuItem: NSMenuItem {
17281812
/// This property's only purpose is to keep a strong reference to the wrapped
17291813
/// action so that it sticks around for long enough to be useful.

0 commit comments

Comments
 (0)