Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Examples/Bundler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ version = '0.1.0'
identifier = 'dev.swiftcrossui.WebViewExample'
product = 'WebViewExample'
version = '0.1.0'

[apps.HoverExample]
identifier = 'dev.swiftcrossui.HoverExample'
product = 'HoverExample'
version = '0.1.0'
6 changes: 5 additions & 1 deletion Examples/Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 5.10

import Foundation
import PackageDescription
Expand Down Expand Up @@ -72,6 +72,10 @@ let package = Package(
.executableTarget(
name: "WebViewExample",
dependencies: exampleDependencies
),
.executableTarget(
name: "HoverExample",
dependencies: exampleDependencies
)
]
)
54 changes: 54 additions & 0 deletions Examples/Sources/HoverExample/HoverApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import DefaultBackend
import Foundation
import SwiftCrossUI

#if canImport(SwiftBundlerRuntime)
import SwiftBundlerRuntime
#endif

@main
struct HoverExample: App {
var body: some Scene {
WindowGroup("Hover Example") {
VStack(spacing: 0) {
ForEach([Bool](repeating: false, count: 18)) { _ in
HStack(spacing: 0) {
ForEach([Bool](repeating: false, count: 30)) { _ in
CellView()
}
}
}
}
.background(Color.black)
}
.defaultSize(width: 900, height: 540)
}
}

struct CellView: View {
@State var timer: Timer?
@State var opacity: Float = 0.0

var body: some View {
Rectangle()
.foregroundColor(Color.blue.opacity(opacity))
.onHover { hovering in
if !hovering {
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
DispatchQueue.main.async {
if opacity >= 0.05 {
opacity -= 0.05
} else {
opacity = 0.0
timer.invalidate()
}
}
}
} else {
opacity = 1.0
timer?.invalidate()
timer = nil
}
}
}
}
92 changes: 90 additions & 2 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,40 @@ public final class AppKitBackend: AppBackend {
}
}

public func createHoverTarget(wrapping child: Widget) -> Widget {
let container = NSView()

container.addSubview(child)
child.leadingAnchor.constraint(equalTo: container.leadingAnchor)
.isActive = true
child.topAnchor.constraint(equalTo: container.topAnchor)
.isActive = true
child.translatesAutoresizingMaskIntoConstraints = false

let hoverGestureTarget = NSCustomHoverTarget()
container.addSubview(hoverGestureTarget)
hoverGestureTarget.leadingAnchor.constraint(equalTo: container.leadingAnchor)
.isActive = true
hoverGestureTarget.topAnchor.constraint(equalTo: container.topAnchor)
.isActive = true
hoverGestureTarget.trailingAnchor.constraint(equalTo: container.trailingAnchor)
.isActive = true
hoverGestureTarget.bottomAnchor.constraint(equalTo: container.bottomAnchor)
.isActive = true
hoverGestureTarget.translatesAutoresizingMaskIntoConstraints = false

return container
}

public func updateHoverTarget(
_ container: Widget,
environment: EnvironmentValues,
action: @escaping (Bool) -> Void
) {
let hoverGestureTarget = container.subviews[1] as! NSCustomHoverTarget
hoverGestureTarget.hoverChangesHandler = action
}

final class NSBezierPathView: NSView {
var path: NSBezierPath!
var fillColor: NSColor = .clear
Expand Down Expand Up @@ -1663,7 +1697,7 @@ final class NSCustomTapGestureTarget: NSView {
leftClickRecognizer = gestureRecognizer
} else if leftClickHandler == nil, let leftClickRecognizer {
removeGestureRecognizer(leftClickRecognizer)
self.leftClickHandler = nil
self.leftClickRecognizer = nil
}
}
}
Expand All @@ -1678,7 +1712,7 @@ final class NSCustomTapGestureTarget: NSView {
rightClickRecognizer = gestureRecognizer
} else if rightClickHandler == nil, let rightClickRecognizer {
removeGestureRecognizer(rightClickRecognizer)
self.rightClickHandler = nil
self.rightClickRecognizer = nil
}
}
}
Expand Down Expand Up @@ -1724,6 +1758,60 @@ final class NSCustomTapGestureTarget: NSView {
}
}

final class NSCustomHoverTarget: NSView {
var hoverChangesHandler: ((Bool) -> Void)? {
didSet {
if hoverChangesHandler != nil && trackingArea == nil {
let options: NSTrackingArea.Options = [
.mouseEnteredAndExited,
.activeInKeyWindow,
]
let area = NSTrackingArea(
rect: self.bounds,
options: options,
owner: self,
userInfo: nil)
addTrackingArea(area)
trackingArea = area
} else if hoverChangesHandler == nil, let trackingArea {
// should be impossible at the moment of implementation
// keeping it to be save in case of later changes
removeTrackingArea(trackingArea)
self.trackingArea = nil
}
}
}

private var trackingArea: NSTrackingArea?

override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea = trackingArea {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [
.mouseEnteredAndExited,
.activeInKeyWindow,
]

trackingArea = NSTrackingArea(
rect: self.bounds,
options: options,
owner: self,
userInfo: nil)
self.addTrackingArea(trackingArea!)
}

override func mouseEntered(with event: NSEvent) {
hoverChangesHandler?(true)
}

override func mouseExited(with event: NSEvent) {
// Mouse exited the view's bounds
hoverChangesHandler?(false)
}
}

final class NSCustomMenuItem: NSMenuItem {
/// This property's only purpose is to keep a strong reference to the wrapped
/// action so that it sticks around for long enough to be useful.
Expand Down
Loading
Loading