Skip to content

Commit

Permalink
Add package implementation
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Alexander Jentz <[email protected]>
  • Loading branch information
r-dent and beyama committed Feb 27, 2024
1 parent 94bb29e commit 0805b3e
Show file tree
Hide file tree
Showing 5 changed files with 465 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "509.1.1"
}
}
],
"version" : 2
}
36 changes: 36 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "StreamDeckKitMacros",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
.library(
name: "StreamDeckKitMacros",
targets: ["StreamDeckKitMacros"]
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
],
targets: [
.macro(
name: "StreamDeckView",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.target(name: "StreamDeckKitMacros", dependencies: ["StreamDeckView"]),
.testTarget(
name: "StreamDeckKitMacrosTests",
dependencies: [
"StreamDeckView",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
75 changes: 75 additions & 0 deletions Sources/StreamDeckKitMacros/StreamDeckKitMacros.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// StreamDeckKitMacros.swift
// Created by Alexander Jentz on 16.02.24.
//
// MIT License
//
// Copyright (c) 2023 Corsair Memory Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

import SwiftUI

private var _id: UInt64 = 0

public var _nextID: UInt64 {
if _id == UInt64.max {
_id = 0
}
_id += 1
return _id
}

/// Protocol for views rendered on StreamDeck.
///
/// - Note: Use this implicitly by applying the ``StreamDeckView()`` macro.
public protocol StreamDeckView: View {
/// The type of view representing the streamDeckBody of this view.
associatedtype StreamDeckBody: View
/// The content of the view.
@MainActor @ViewBuilder var streamDeckBody: Self.StreamDeckBody { get }
}

/// Defines and implements conformance of the StreamDeckView protocol.
///
/// This macro adds Stream Deck context information and state tracking. Enabling you to to handle different devices and keys.
///
/// ```swift
/// @StreamDeckView
/// struct NumberDisplayKey {
/// @State var isPressed: Bool = false
///
/// var streamDeckBody: some View {
/// StreamDeckKeyView { isPressed in
/// // Changing state will trigger a re-render on Stream Deck
/// self.isPressed = isPressed
/// } content: {
/// ZStack {
/// isPressed ? Color.orange : Color.clear
/// // Show the current key index
/// Text("\(viewIndex)")
/// }
/// }
/// }
/// }
/// ```
@attached(extension, conformances: StreamDeckView)
@attached(member, names: named(_$streamDeckViewContext), named(body), named(streamDeck), named(viewSize), named(viewIndex))
public macro StreamDeckView() = #externalMacro(module: "StreamDeckView", type: "StreamDeckViewMacro")
180 changes: 180 additions & 0 deletions Sources/StreamDeckView/StreamDeckViewMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//
// StreamDeckViewMacro.swift
// Created by Alexander Jentz in February 2024.
//
// MIT License
//
// Copyright (c) 2023 Corsair Memory Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

enum StreamDeckViewDeclError: CustomStringConvertible, Error {
case onlyStructs
case streamDeckBodyRequired
case bodyMustNotBeImplemented

public var description: String {
switch self {
case .onlyStructs:
"@StreamDeckView can only be used with SwiftUI view structs."
case .streamDeckBodyRequired:
"@StreamDeckView requires the view to implement streamDeckBody."
case .bodyMustNotBeImplemented:
"@StreamDeckView view must not implement `body`"
}
}
}

struct StreamDeckViewMacro: MemberMacro {

static let contextAccessor = "_$streamDeckViewContext"

static func expansion( // swiftlint:disable:this function_body_length
of node: SwiftSyntax.AttributeSyntax,
providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.DeclSyntax] {
guard let identified = declaration.as(StructDeclSyntax.self) else {
throw StreamDeckViewDeclError.onlyStructs
}

let vars = identified.memberBlock.members
.map(\.decl)
.compactMap { $0.as(VariableDeclSyntax.self) }
.compactMap(\.bindings.first?.pattern)
.compactMap { $0.as(IdentifierPatternSyntax.self)?.identifier.text }

guard !vars.contains(where: { $0 == "body" }) else {
throw StreamDeckViewDeclError.bodyMustNotBeImplemented
}

guard vars.contains(where: { $0 == "streamDeckBody" }) else {
throw StreamDeckViewDeclError.streamDeckBodyRequired
}

let context: DeclSyntax =
"""
@Environment(\\.streamDeckViewContext) var \(raw: contextAccessor)
"""

let streamDeck: DeclSyntax =
"""
/// The Stream Deck device object.
var streamDeck: StreamDeck {
\(raw: contextAccessor).device
}
"""

let viewSize: DeclSyntax =
"""
/// The size of the current drawing area.
var viewSize: CGSize {
\(raw: contextAccessor).size
}
"""

let viewIndex: DeclSyntax =
"""
/// The index of this input element if this is a key or dial view otherwise -1.
var viewIndex: Int {
\(raw: contextAccessor).index
}
"""

let body: DeclSyntax =
"""
@MainActor
var body: some View {
if #available(iOS 17, *) {
return streamDeckBody
.onChange(of: StreamDeckKit._nextID) {
\(raw: contextAccessor).updateRequired()
}
} else {
return streamDeckBody
.onChange(of: StreamDeckKit._nextID) { _ in
\(raw: contextAccessor).updateRequired()
}
}
}
"""

return [
context,
streamDeck,
viewSize,
viewIndex,
body
]
}
}

extension StreamDeckViewMacro: ExtensionMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
[try ExtensionDeclSyntax("extension \(type): StreamDeckView {}")]
}
}

extension StreamDeckViewMacro: MemberAttributeMacro {
static func expansion(
of node: SwiftSyntax.AttributeSyntax,
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
providingAttributesFor member: some SwiftSyntax.DeclSyntaxProtocol,
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.AttributeSyntax] {
guard let variableDecl = member.as(VariableDeclSyntax.self),
variableDecl.isStreamDeckBody
else { return [] }

return ["@MainActor", "@ViewBuilder"]
}

}

extension VariableDeclSyntax {
var isStreamDeckBody: Bool {
bindings
.contains(where: { syntax in
syntax
.as(PatternBindingSyntax.self)?
.pattern
.as(IdentifierPatternSyntax.self)?
.identifier.text == "streamDeckBody"
})
}
}

@main
struct StreamDeckMacrosPlugin: CompilerPlugin {
public let providingMacros: [Macro.Type] = [
StreamDeckViewMacro.self
]
}
Loading

0 comments on commit 0805b3e

Please sign in to comment.