Skip to content

Commit c5a9995

Browse files
committed
Add GraphProxy and .graphOverlay(alignment:content:)
1 parent d57627c commit c5a9995

File tree

10 files changed

+119
-36
lines changed

10 files changed

+119
-36
lines changed

Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,16 @@ struct MermaidVisualization: View {
9999
} emittingNewNodesWithStates: { id in
100100
KineticState(position: getInitialPosition(id: id, r: 100))
101101
}
102-
.onNodeTapped {
103-
tappedNode = $0
104-
}
102+
.graphOverlay(content: { proxy in
103+
Rectangle().fill(.clear).contentShape(Rectangle())
104+
.onTapGesture { value in
105+
if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) {
106+
guard let nodeID = nodeID as? String else { return }
107+
print(nodeID)
108+
tappedNode = nodeID
109+
}
110+
}
111+
})
105112
.ignoresSafeArea()
106113
#if !os(visionOS)
107114
.inspector(isPresented: .constant(true)) {
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
@usableFromInline
2-
struct AnyGraphContent<NodeID: Hashable>: GraphContent {
1+
2+
public struct AnyGraphContent<NodeID: Hashable>: GraphContent {
33

44
@usableFromInline
55
let storage: any GraphContent<NodeID>
66

77
@inlinable
8-
init(_ storage: any GraphContent<NodeID>) {
8+
public init(_ storage: any GraphContent<NodeID>) {
99
self.storage = storage
1010
}
1111

1212
@inlinable
13-
func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext<NodeID>) {
13+
public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext<NodeID>) {
1414
storage._attachToGraphRenderingContext(&context)
1515
}
1616

17-
}
17+
}

Sources/Grape/Contents/GraphContent.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,3 @@ public protocol GraphContent<NodeID> {
77
@inlinable
88
func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext<NodeID>)
99
}
10-
11-
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import SwiftUI
2+
3+
public struct GraphProxy {
4+
5+
@usableFromInline
6+
let storage: (any _AnyGraphProxyProtocol)?
7+
8+
@inlinable
9+
init(_ storage: some _AnyGraphProxyProtocol) {
10+
self.storage = storage
11+
}
12+
13+
@inlinable
14+
public init() {
15+
self.storage = nil
16+
}
17+
}
18+
19+
extension GraphProxy: _AnyGraphProxyProtocol {
20+
public func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable? {
21+
storage?.locateNode(at: locationInViewportCoordinate)
22+
}
23+
}
24+
25+
@usableFromInline
26+
struct GraphProxyKey: PreferenceKey {
27+
@inlinable
28+
static func reduce(value: inout GraphProxy, nextValue: () -> GraphProxy) {
29+
value = nextValue()
30+
}
31+
32+
@inlinable
33+
static var defaultValue: GraphProxy {
34+
get {
35+
.init()
36+
}
37+
}
38+
}
39+
40+
41+
42+
extension View {
43+
@inlinable
44+
public func graphOverlay<V>(
45+
alignment: Alignment = .center,
46+
@ViewBuilder content: @escaping (GraphProxy) -> V
47+
) -> some View where V: View {
48+
self.overlayPreferenceValue(GraphProxyKey.self, content)
49+
}
50+
}

Sources/Grape/Views/ForceDirectedGraph+Gesture.swift

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,17 @@ import SwiftUI
7979
@inlinable
8080
static var minimumDragDistance: CGFloat { 3.0 }
8181
}
82-
@MainActor
83-
extension ForceDirectedGraph {
84-
@inlinable
85-
internal func onTapGesture(
86-
_ location: CGPoint
87-
) {
88-
guard let action = self.model._onNodeTapped else { return }
89-
let nodeID = self.model.findNode(at: location)
90-
action(nodeID)
91-
}
92-
}
82+
// @MainActor
83+
// extension ForceDirectedGraph {
84+
// @inlinable
85+
// internal func onTapGesture(
86+
// _ location: CGPoint
87+
// ) {
88+
// guard let action = self.model._onNodeTapped else { return }
89+
// let nodeID = self.model.findNode(at: location)
90+
// action(nodeID)
91+
// }
92+
// }
9393
#endif
9494

9595
#if os(iOS) || os(macOS)
@@ -192,14 +192,23 @@ extension ForceDirectedGraph {
192192
}
193193

194194
@inlinable
195+
@available(*, deprecated, message: "Use `graphOverlay` instead")
195196
public func onNodeTapped(
196197
perform action: @escaping (NodeID?) -> Void
197-
) -> Self {
198-
self.model._onNodeTapped = action
199-
return self
198+
) -> some View {
199+
self.graphOverlay { proxy in
200+
Rectangle().fill(.clear).contentShape(Rectangle())
201+
.onTapGesture { value in
202+
if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) {
203+
guard let nodeID = nodeID as? NodeID else { return }
204+
action(nodeID)
205+
}
206+
}
207+
}
200208
}
201209

202210
@inlinable
211+
@available(*, deprecated, message: "Use `graphOverlay` instead")
203212
public func onNodeDragChanged(
204213
perform action: @escaping (NodeID, CGPoint) -> Void
205214
) -> Self {
@@ -208,6 +217,7 @@ extension ForceDirectedGraph {
208217
}
209218

210219
@inlinable
220+
@available(*, deprecated, message: "Use `graphOverlay` instead")
211221
public func onNodeDragEnded(
212222
shouldBeFixed action: @escaping (NodeID, CGPoint) -> Bool
213223
) -> Self {
@@ -216,6 +226,7 @@ extension ForceDirectedGraph {
216226
}
217227

218228
@inlinable
229+
@available(*, deprecated, message: "Use `graphOverlay` instead")
219230
public func onGraphMagnified(
220231
perform action: @escaping () -> Void
221232
) -> Self {

Sources/Grape/Views/ForceDirectedGraph+View.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ extension ForceDirectedGraph: View {
1313
// canvas
1414
// }
1515
canvas
16+
.preference(key: GraphProxyKey.self, value: .init(model))
1617
.onChange(
1718
of: self._graphRenderingContextShadow,
1819
initial: false // Don't trigger on initial value, keep `changeMessage` as "N/A"
@@ -77,7 +78,7 @@ extension ForceDirectedGraph: View {
7778
.onChanged(onDragChange)
7879
.onEnded(onDragEnd)
7980
)
80-
.onTapGesture(count: 1, perform: onTapGesture)
81+
// .onTapGesture(count: 1, perform: onTapGesture)
8182
#endif
8283

8384
#if os(iOS) || os(macOS)

Sources/Grape/Views/ForceDirectedGraph.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ where NodeID == Content.NodeID {
3737

3838
// @State
3939
@inlinable
40-
internal var model: ForceDirectedGraphModel<Content>
40+
internal var model: ForceDirectedGraphModel<Content.NodeID>
4141
{
4242
@storageRestrictions(initializes: _model)
4343
init(initialValue) {
@@ -48,7 +48,7 @@ where NodeID == Content.NodeID {
4848
}
4949

5050
@usableFromInline
51-
internal var _model: State<ForceDirectedGraphModel<Content>>
51+
internal var _model: State<ForceDirectedGraphModel<Content.NodeID>>
5252

5353
/// The default force to be applied to the graph
5454
///

Sources/Grape/Views/ForceDirectedGraphModel.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,32 @@ import Foundation
33
import Observation
44
import SwiftUI
55

6+
7+
@MainActor
8+
public protocol _AnyGraphProxyProtocol {
9+
@inlinable
10+
func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable?
11+
}
12+
13+
extension ForceDirectedGraphModel: _AnyGraphProxyProtocol {
14+
public func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable? {
15+
if let nodeID = findNode(at: locationInViewportCoordinate) {
16+
return AnyHashable(nodeID)
17+
} else {
18+
return nil
19+
}
20+
}
21+
}
622
@MainActor
7-
public final class ForceDirectedGraphModel<Content: GraphContent> {
23+
public final class ForceDirectedGraphModel<NodeID: Hashable> {
824

925
@usableFromInline
1026
internal struct ObsoleteState {
1127
@usableFromInline
1228
var cgSize: CGSize
1329
}
1430

15-
public typealias NodeID = Content.NodeID
31+
// public typealias NodeID = Content.NodeID
1632

1733
@usableFromInline
1834
var graphRenderingContext: _GraphRenderingContext<NodeID>
@@ -128,8 +144,8 @@ public final class ForceDirectedGraphModel<Content: GraphContent> {
128144
@usableFromInline
129145
var _onNodeDragEnded: ((NodeID, CGPoint) -> Bool)? = nil
130146

131-
@usableFromInline
132-
var _onNodeTapped: ((NodeID?) -> Void)? = nil
147+
// @usableFromInline
148+
// var _onNodeTapped: ((NodeID?) -> Void)? = nil
133149

134150
@usableFromInline
135151
var _onViewportTransformChanged: ((ViewportTransform, Bool) -> Void)? = nil

Sources/Grape/Views/RenderOperation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import SwiftUI
22

33
@usableFromInline
4-
enum PathOrSymbolSize {
4+
enum PathOrSymbolSize: Equatable {
55
case path(Path)
66
case symbolSize(CGSize)
77
}
@@ -60,7 +60,7 @@ extension RenderOperation.Node: Equatable {
6060
@inlinable
6161
internal static func == (lhs: Self, rhs: Self) -> Bool {
6262
let fillEq = lhs.fill == nil && rhs.fill == nil
63-
let pathEq = lhs.pathOrSymbolSize == nil && rhs.pathOrSymbolSize == nil
63+
let pathEq = lhs.pathOrSymbolSize == rhs.pathOrSymbolSize
6464
return lhs.mark == rhs.mark
6565
&& fillEq
6666
&& lhs.stroke == rhs.stroke

Tests/GrapeTests/ContentBuilderTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ final class ContentBuilderTests: XCTestCase {
2929

3030
NodeMark(id: 3)
3131
NodeMark(id: 4)
32-
AnyGraphContent(
33-
NodeMark(id: 5)
34-
)
32+
// AnyGraphContent(
33+
// NodeMark(id: 5)
34+
// )
3535
}
3636
}
3737

0 commit comments

Comments
 (0)