Skip to content

Commit 6ddacb9

Browse files
authored
Implement View.onHover modifier (+ HoverExample) (#212)
* 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 * Fixed Default Hover implementation * Added UIKit .onHover + minor consistency improvement | fixed minor Naming fixes in AppKitBackend - tapGestureRecognizer and longPressGestureRecognizer now get removed if their corresponding handler is set to nil. - Added Hoverable Widget - Added hovertarget creation & update functions - added macCalatalyst as compatible for UISlider since it supports it * Added UIKit .onHover + minor consistency improvement | fixed minor Naming fixes in AppKitBackend - tapGestureRecognizer and longPressGestureRecognizer now get removed if their corresponding handler is set to nil. - Added Hoverable Widget - Added hovertarget creation & update functions - added macCalatalyst as compatible for UISlider since it supports it * added gtk4 support * added winUI backend support * post format_and_lint * slight quality improvement in HoverExample * implemented requested changes * implemented requested changes # Conflicts: # Sources/GtkCodeGen/GtkCodeGen.swift * format_and_lint * Comment Style, Pad Action exclusion Name changed added both "GtkPadActionDial" and "PadActionDial" because I couldn't find a definitive answer what its called * Comment Style, Pad Action exclusion Name changed added both "GtkPadActionDial" and "PadActionDial" because I couldn't find a definitive answer what its called # Conflicts: # Sources/GtkCodeGen/GtkCodeGen.swift * requested change on gtk codegen * I'm so sorry
1 parent 38c650e commit 6ddacb9

File tree

22 files changed

+729
-218
lines changed

22 files changed

+729
-218
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.9
1+
// swift-tools-version: 5.10
22

33
import Foundation
44
import PackageDescription
@@ -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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import DefaultBackend
2+
import Foundation
3+
import SwiftCrossUI
4+
5+
#if canImport(SwiftBundlerRuntime)
6+
import SwiftBundlerRuntime
7+
#endif
8+
9+
@main
10+
@HotReloadable
11+
struct HoverExample: App {
12+
var body: some Scene {
13+
WindowGroup("Hover Example") {
14+
#hotReloadable {
15+
VStack(spacing: 0) {
16+
ForEach([Bool](repeating: false, count: 18)) { _ in
17+
HStack(spacing: 0) {
18+
ForEach([Bool](repeating: false, count: 30)) { _ in
19+
CellView()
20+
}
21+
}
22+
}
23+
.background(Color.black)
24+
}
25+
}
26+
}
27+
.defaultSize(width: 900, height: 540)
28+
}
29+
}
30+
31+
struct CellView: View {
32+
@State var timer: Timer?
33+
@State var opacity: Float = 0.0
34+
35+
var body: some View {
36+
Rectangle()
37+
.foregroundColor(Color.blue.opacity(opacity))
38+
.onHover { hovering in
39+
if !hovering {
40+
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
41+
DispatchQueue.main.async {
42+
if opacity >= 0.05 {
43+
opacity -= 0.05
44+
} else {
45+
opacity = 0.0
46+
timer.invalidate()
47+
}
48+
}
49+
}
50+
} else {
51+
opacity = 1.0
52+
timer?.invalidate()
53+
timer = nil
54+
}
55+
}
56+
}
57+
}

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,40 @@ public final class AppKitBackend: AppBackend {
14251425
}
14261426
}
14271427

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 hoverGestureTarget = NSCustomHoverTarget()
1439+
container.addSubview(hoverGestureTarget)
1440+
hoverGestureTarget.leadingAnchor.constraint(equalTo: container.leadingAnchor)
1441+
.isActive = true
1442+
hoverGestureTarget.topAnchor.constraint(equalTo: container.topAnchor)
1443+
.isActive = true
1444+
hoverGestureTarget.trailingAnchor.constraint(equalTo: container.trailingAnchor)
1445+
.isActive = true
1446+
hoverGestureTarget.bottomAnchor.constraint(equalTo: container.bottomAnchor)
1447+
.isActive = true
1448+
hoverGestureTarget.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 hoverGestureTarget = container.subviews[1] as! NSCustomHoverTarget
1459+
hoverGestureTarget.hoverChangesHandler = action
1460+
}
1461+
14281462
final class NSBezierPathView: NSView {
14291463
var path: NSBezierPath!
14301464
var fillColor: NSColor = .clear
@@ -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,51 @@ 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+
setNewTrackingArea()
1766+
} else if hoverChangesHandler == nil, let trackingArea {
1767+
removeTrackingArea(trackingArea)
1768+
self.trackingArea = nil
1769+
}
1770+
}
1771+
}
1772+
1773+
private var trackingArea: NSTrackingArea?
1774+
1775+
override func updateTrackingAreas() {
1776+
super.updateTrackingAreas()
1777+
if let trackingArea {
1778+
self.removeTrackingArea(trackingArea)
1779+
}
1780+
setNewTrackingArea()
1781+
}
1782+
1783+
override func mouseEntered(with event: NSEvent) {
1784+
hoverChangesHandler?(true)
1785+
}
1786+
1787+
override func mouseExited(with event: NSEvent) {
1788+
hoverChangesHandler?(false)
1789+
}
1790+
1791+
private func setNewTrackingArea() {
1792+
let options: NSTrackingArea.Options = [
1793+
.mouseEnteredAndExited,
1794+
.activeInKeyWindow,
1795+
]
1796+
let area = NSTrackingArea(
1797+
rect: self.bounds,
1798+
options: options,
1799+
owner: self,
1800+
userInfo: nil)
1801+
addTrackingArea(area)
1802+
trackingArea = area
1803+
}
1804+
}
1805+
17271806
final class NSCustomMenuItem: NSMenuItem {
17281807
/// This property's only purpose is to keep a strong reference to the wrapped
17291808
/// action so that it sticks around for long enough to be useful.

0 commit comments

Comments
 (0)