Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/macro #22

Merged
merged 17 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
26 changes: 18 additions & 8 deletions Example/Example App/StreamDeckLayoutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,28 @@
//

import StreamDeckKit
import StreamDeckSimulator
import SwiftUI

struct StreamDeckLayoutView: View {
var body: some View {
StreamDeckLayout { backgroundContext in
StreamDeckLayout {
StreamDeckKeypadLayout { _ in
NumberDisplayKey()
}
}
.background {
LinearGradient(
gradient: .init(colors: [.teal, .blue]),
startPoint: .topLeading,
startPoint: .topLeading,
endPoint: .bottomTrailing
)
} keyAreaView: { _ in
StreamDeckKeypadLayout { _ in
NumberDisplayKey()
}
}
}
}

struct NumberDisplayKey: StreamDeckView {
@Environment(\.streamDeckViewContext) var context
@StreamDeckView
struct NumberDisplayKey {
@State var isPressed: Bool = false

var emoji: String { emojis[context.index] }
Expand All @@ -42,3 +44,11 @@ struct NumberDisplayKey: StreamDeckView {
}
}
}

#Preview {
StreamDeckSimulator.PreviewView(streamDeck: .regular) {
StreamDeckSession.setUp { _ in
StreamDeckLayoutView()
}
}
}
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
}
20 changes: 17 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "StreamDeckKit",
platforms: [.iOS(.v17)],
platforms: [.iOS(.v17), .macOS(.v10_15)],
products: [
.library(
name: "StreamDeckKit",
Expand All @@ -21,6 +22,10 @@ let package = Package(
url: "https://github.com/pointfreeco/swift-snapshot-testing",
from: "1.12.0"
),
.package(
url: "https://github.com/apple/swift-syntax.git",
from: "509.0.0"
),
],
targets: [
.target(
Expand All @@ -30,18 +35,27 @@ let package = Package(
),
.target(
name: "StreamDeckKit",
dependencies: ["StreamDeckCApi"]
dependencies: ["StreamDeckCApi", "StreamDeckMacro"]
),
.target(
name: "StreamDeckCApi",
linkerSettings: [.linkedFramework("IOKit")]
),
.macro(
name: "StreamDeckMacro",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.testTarget(
name: "StreamDeckSDKTests",
dependencies: [
"StreamDeckKit",
"StreamDeckMacro",
"StreamDeckSimulator",
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ extension DeviceCapabilities {

public var keyAreaBottomSpacing: CGFloat {
guard let screenHeight = screenSize?.height,
let keyAreaHeight = keyAreaRect?.height,
let windowHeight = windowRect?.height
let keyAreaHeight = keyAreaRect?.height
else { return 0 }

guard let windowHeight = windowRect?.height else { // no window area
return screenHeight - (keyAreaTopSpacing + keyAreaHeight)
}

return screenHeight - (keyAreaTopSpacing + keyAreaHeight + windowHeight)
}

Expand Down
25 changes: 4 additions & 21 deletions Sources/StreamDeckKit/Layout/StreamDeckViewContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

/// Provides information about the current device/key context in SwiftUI environments.
/// Provides information about the current context (screen, key-area, key, window, dial) in SwiftUI environments.
///
/// You can access the current context via the environment like this:
/// ```swift
Expand All @@ -30,6 +30,7 @@ public struct StreamDeckViewContext {

/// The Stream Deck device object.
public let device: StreamDeck

private(set) var dirtyMarker: DirtyMarker

/// The size of the current drawing area.
Expand All @@ -41,7 +42,9 @@ public struct StreamDeckViewContext {
///
/// The value will be valid, when the current drawing area represents an input element like a key. Otherwise it will be `-1`.
public private(set) var index: Int

private let onDirty: DirtyHandler?

private let idGenerator = IDGenerator()

public var nextID: UInt64 { idGenerator.next }
Expand All @@ -60,26 +63,6 @@ public struct StreamDeckViewContext {
self.onDirty = onDirty
}

/// Tells StreamDeckLayout that the current drawing area needs to be re-rendered.
///
/// Call this when the layout of your view changes. E.g. due to a change of state.
/// ```swift
/// struct NumberDisplayKey: View {
/// let context: StreamDeckViewContext
/// @State var isPressed: Bool = false
///
/// var body: some View {
/// StreamDeckKeyView { isPressed in
/// self.isPressed = isPressed
/// } content: {
/// isPressed ? Color.orange : Color.clear
/// }
/// .onChange(of: isPressed) {
/// context.updateRequired()
/// }
/// }
/// }
/// ```
@MainActor
public func updateRequired() {
onDirty?(dirtyMarker)
Expand Down
4 changes: 2 additions & 2 deletions Sources/StreamDeckKit/Views/StreamDeckKeyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public struct StreamDeckKeyView<Content: View>: View {

public init(
action: @escaping (Bool) -> Void,
content: @escaping () -> Content
@ViewBuilder content: @escaping () -> Content
) {
self.action = action
self.content = content
}

public init(
action: @escaping () -> Void,
content: @escaping () -> Content
@ViewBuilder content: @escaping () -> Content
) {
self.init(
action: { if $0 { action() } },
Expand Down
50 changes: 26 additions & 24 deletions Sources/StreamDeckKit/Views/StreamDeckLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,53 +13,55 @@ import SwiftUI
///
/// Provide this to the `content` parameter of ``StreamDeckSession/setUp(stateHandler:newDeviceHandler:content:)`` or ``StreamDeck/render(_:)``
/// to draw a layout onto a Stream Deck device.
public struct StreamDeckLayout<BackgroundView: View, KeyAreaView: View, WindowView: View>: View {
public struct StreamDeckLayout<KeyAreaView: View, WindowView: View>: View {
@Environment(\.streamDeckViewContext) var context

@ViewBuilder let background: (StreamDeckViewContext) -> BackgroundView
@ViewBuilder let keyAreaView: (StreamDeckViewContext) -> KeyAreaView
@ViewBuilder let windowView: (StreamDeckViewContext) -> WindowView

@ViewBuilder let keyAreaView: @MainActor () -> KeyAreaView
@ViewBuilder let windowView: @MainActor () -> WindowView

/// Creates a new instance.
/// - Parameters:
/// - background: The background view behind the complete layout. Default is `Color.black`.
/// - keyAreaView: A view to be rendered on the key area of the layout. Use ``StreamDeckKeypadLayout`` to render separate keys.
/// - windowView: A view to be rendered in in a possible window area of the layout.
/// Use ``StreamDeckDialAreaLayout`` to render separate parts of the display. E.g. for each dial on a Stream Deck Plus.
public init(
@ViewBuilder background: @escaping (StreamDeckViewContext) -> BackgroundView = { _ in Color.black },
@ViewBuilder keyAreaView: @escaping (StreamDeckViewContext) -> KeyAreaView,
@ViewBuilder windowView: @escaping (StreamDeckViewContext) -> WindowView = { _ in EmptyView() }
@ViewBuilder keyAreaView: @escaping @MainActor () -> KeyAreaView,
@ViewBuilder windowView: @escaping @MainActor () -> WindowView = { Color.clear }
) {
self.background = background
self.keyAreaView = keyAreaView
self.windowView = windowView
}

public var body: some View {
let caps = context.device.capabilities

ZStack(alignment: .topLeading) {
background(context)
VStack(alignment: .leading, spacing: 0) {
if let keyAreaSize = caps.keyAreaRect?.size {
let keyAreaContext = context.with(
dirtyMarker: .screen,
size: keyAreaSize,
index: -1
)

VStack(alignment: .leading, spacing: 0) {
keyAreaView(context)
keyAreaView()
.frame(width: keyAreaSize.width, height: keyAreaSize.height)
.padding(.top, caps.keyAreaTopSpacing)
.padding(.leading, caps.keyAreaLeadingSpacing)
.padding(.trailing, caps.keyAreaTrailingSpacing)
.padding(.bottom, caps.keyAreaBottomSpacing)
.environment(\.streamDeckViewContext, keyAreaContext)
}

if let windowSize = caps.windowRect?.size {
let windowContext = context.with(
dirtyMarker: .window,
size: windowSize,
index: -1
)
if let windowSize = caps.windowRect?.size {
let windowContext = context.with(
dirtyMarker: .window,
size: windowSize,
index: -1
)

windowView(windowContext)
.frame(width: windowSize.width, height: windowSize.height, alignment: .bottom)
.environment(\.streamDeckViewContext, windowContext)
}
windowView()
.frame(width: windowSize.width, height: windowSize.height, alignment: .bottom)
.environment(\.streamDeckViewContext, windowContext)
}
}
.frame(width: context.size.width, height: context.size.height)
Expand Down
20 changes: 8 additions & 12 deletions Sources/StreamDeckKit/Views/StreamDeckView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import SwiftUI
/// Protocol for views rendered on StreamDeck.
/// This automatically tells StreamDeckLayout that the drawing area of this view needs to be updated on the device.
///
/// - Note: Use this implicitly by applying the ``StreamDeckView()`` macro.
///
/// ```swift
/// struct NumberDisplayKey: StreamDeckView {
/// @Environment(\.streamDeckViewContext) var context
/// @StreamDeckView
/// struct NumberDisplayKey {
/// @State var isPressed: Bool = false
///
/// var body: some View {
/// var streamDeckBody: some View {
/// StreamDeckKeyView { isPressed in
/// self.isPressed = isPressed
/// } content: {
Expand All @@ -32,12 +34,6 @@ public protocol StreamDeckView: View {
@MainActor @ViewBuilder var streamDeckBody: Self.Content { get }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add documentation for streamDeckBody and context properties.

}

extension StreamDeckView {
@MainActor
public var body: some View {
streamDeckBody
.onChange(of: context.nextID) { _, _ in
context.updateRequired()
}
}
}
@attached(extension, conformances: StreamDeckView)
@attached(member, names: named(context), named(body))
public macro StreamDeckView() = #externalMacro(module: "StreamDeckMacro", type: "StreamDeckMacro")
Loading