Skip to content

Commit

Permalink
Merge pull request #63 from li3zhen1/GraphProxy
Browse files Browse the repository at this point in the history
Add `GraphProxy` and `.graphOverlay(alignment:content:)`
  • Loading branch information
li3zhen1 authored Dec 29, 2024
2 parents d57627c + c5a9995 commit 17c30e4
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,16 @@ struct MermaidVisualization: View {
} emittingNewNodesWithStates: { id in
KineticState(position: getInitialPosition(id: id, r: 100))
}
.onNodeTapped {
tappedNode = $0
}
.graphOverlay(content: { proxy in
Rectangle().fill(.clear).contentShape(Rectangle())
.onTapGesture { value in
if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) {
guard let nodeID = nodeID as? String else { return }
print(nodeID)
tappedNode = nodeID
}
}
})
.ignoresSafeArea()
#if !os(visionOS)
.inspector(isPresented: .constant(true)) {
Expand Down
10 changes: 5 additions & 5 deletions Sources/Grape/Contents/AnyGraphContent.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
@usableFromInline
struct AnyGraphContent<NodeID: Hashable>: GraphContent {

public struct AnyGraphContent<NodeID: Hashable>: GraphContent {

@usableFromInline
let storage: any GraphContent<NodeID>

@inlinable
init(_ storage: any GraphContent<NodeID>) {
public init(_ storage: any GraphContent<NodeID>) {
self.storage = storage
}

@inlinable
func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext<NodeID>) {
public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext<NodeID>) {
storage._attachToGraphRenderingContext(&context)
}

}
}
2 changes: 0 additions & 2 deletions Sources/Grape/Contents/GraphContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ public protocol GraphContent<NodeID> {
@inlinable
func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext<NodeID>)
}


50 changes: 50 additions & 0 deletions Sources/Grape/Modifiers/GraphProxy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import SwiftUI

public struct GraphProxy {

@usableFromInline
let storage: (any _AnyGraphProxyProtocol)?

@inlinable
init(_ storage: some _AnyGraphProxyProtocol) {
self.storage = storage
}

@inlinable
public init() {
self.storage = nil
}
}

extension GraphProxy: _AnyGraphProxyProtocol {
public func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable? {
storage?.locateNode(at: locationInViewportCoordinate)
}
}

@usableFromInline
struct GraphProxyKey: PreferenceKey {
@inlinable
static func reduce(value: inout GraphProxy, nextValue: () -> GraphProxy) {
value = nextValue()
}

@inlinable
static var defaultValue: GraphProxy {
get {
.init()
}
}
}



extension View {
@inlinable
public func graphOverlay<V>(
alignment: Alignment = .center,
@ViewBuilder content: @escaping (GraphProxy) -> V
) -> some View where V: View {
self.overlayPreferenceValue(GraphProxyKey.self, content)
}
}
39 changes: 25 additions & 14 deletions Sources/Grape/Views/ForceDirectedGraph+Gesture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,17 @@ import SwiftUI
@inlinable
static var minimumDragDistance: CGFloat { 3.0 }
}
@MainActor
extension ForceDirectedGraph {
@inlinable
internal func onTapGesture(
_ location: CGPoint
) {
guard let action = self.model._onNodeTapped else { return }
let nodeID = self.model.findNode(at: location)
action(nodeID)
}
}
// @MainActor
// extension ForceDirectedGraph {
// @inlinable
// internal func onTapGesture(
// _ location: CGPoint
// ) {
// guard let action = self.model._onNodeTapped else { return }
// let nodeID = self.model.findNode(at: location)
// action(nodeID)
// }
// }
#endif

#if os(iOS) || os(macOS)
Expand Down Expand Up @@ -192,14 +192,23 @@ extension ForceDirectedGraph {
}

@inlinable
@available(*, deprecated, message: "Use `graphOverlay` instead")
public func onNodeTapped(
perform action: @escaping (NodeID?) -> Void
) -> Self {
self.model._onNodeTapped = action
return self
) -> some View {
self.graphOverlay { proxy in
Rectangle().fill(.clear).contentShape(Rectangle())
.onTapGesture { value in
if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) {
guard let nodeID = nodeID as? NodeID else { return }
action(nodeID)
}
}
}
}

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

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

@inlinable
@available(*, deprecated, message: "Use `graphOverlay` instead")
public func onGraphMagnified(
perform action: @escaping () -> Void
) -> Self {
Expand Down
3 changes: 2 additions & 1 deletion Sources/Grape/Views/ForceDirectedGraph+View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extension ForceDirectedGraph: View {
// canvas
// }
canvas
.preference(key: GraphProxyKey.self, value: .init(model))
.onChange(
of: self._graphRenderingContextShadow,
initial: false // Don't trigger on initial value, keep `changeMessage` as "N/A"
Expand Down Expand Up @@ -77,7 +78,7 @@ extension ForceDirectedGraph: View {
.onChanged(onDragChange)
.onEnded(onDragEnd)
)
.onTapGesture(count: 1, perform: onTapGesture)
// .onTapGesture(count: 1, perform: onTapGesture)
#endif

#if os(iOS) || os(macOS)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Grape/Views/ForceDirectedGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ where NodeID == Content.NodeID {

// @State
@inlinable
internal var model: ForceDirectedGraphModel<Content>
internal var model: ForceDirectedGraphModel<Content.NodeID>
{
@storageRestrictions(initializes: _model)
init(initialValue) {
Expand All @@ -48,7 +48,7 @@ where NodeID == Content.NodeID {
}

@usableFromInline
internal var _model: State<ForceDirectedGraphModel<Content>>
internal var _model: State<ForceDirectedGraphModel<Content.NodeID>>

/// The default force to be applied to the graph
///
Expand Down
24 changes: 20 additions & 4 deletions Sources/Grape/Views/ForceDirectedGraphModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,32 @@ import Foundation
import Observation
import SwiftUI


@MainActor
public protocol _AnyGraphProxyProtocol {
@inlinable
func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable?
}

extension ForceDirectedGraphModel: _AnyGraphProxyProtocol {
public func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable? {
if let nodeID = findNode(at: locationInViewportCoordinate) {
return AnyHashable(nodeID)
} else {
return nil
}
}
}
@MainActor
public final class ForceDirectedGraphModel<Content: GraphContent> {
public final class ForceDirectedGraphModel<NodeID: Hashable> {

@usableFromInline
internal struct ObsoleteState {
@usableFromInline
var cgSize: CGSize
}

public typealias NodeID = Content.NodeID
// public typealias NodeID = Content.NodeID

@usableFromInline
var graphRenderingContext: _GraphRenderingContext<NodeID>
Expand Down Expand Up @@ -128,8 +144,8 @@ public final class ForceDirectedGraphModel<Content: GraphContent> {
@usableFromInline
var _onNodeDragEnded: ((NodeID, CGPoint) -> Bool)? = nil

@usableFromInline
var _onNodeTapped: ((NodeID?) -> Void)? = nil
// @usableFromInline
// var _onNodeTapped: ((NodeID?) -> Void)? = nil

@usableFromInline
var _onViewportTransformChanged: ((ViewportTransform, Bool) -> Void)? = nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/Grape/Views/RenderOperation.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

@usableFromInline
enum PathOrSymbolSize {
enum PathOrSymbolSize: Equatable {
case path(Path)
case symbolSize(CGSize)
}
Expand Down Expand Up @@ -60,7 +60,7 @@ extension RenderOperation.Node: Equatable {
@inlinable
internal static func == (lhs: Self, rhs: Self) -> Bool {
let fillEq = lhs.fill == nil && rhs.fill == nil
let pathEq = lhs.pathOrSymbolSize == nil && rhs.pathOrSymbolSize == nil
let pathEq = lhs.pathOrSymbolSize == rhs.pathOrSymbolSize
return lhs.mark == rhs.mark
&& fillEq
&& lhs.stroke == rhs.stroke
Expand Down
6 changes: 3 additions & 3 deletions Tests/GrapeTests/ContentBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ final class ContentBuilderTests: XCTestCase {

NodeMark(id: 3)
NodeMark(id: 4)
AnyGraphContent(
NodeMark(id: 5)
)
// AnyGraphContent(
// NodeMark(id: 5)
// )
}
}

Expand Down

0 comments on commit 17c30e4

Please sign in to comment.