diff --git a/app/xcode/Sources/VCamBridge/ExternalStateBinding.swift b/app/xcode/Sources/VCamBridge/ExternalStateBinding.swift index bdc1abc..9ab4699 100644 --- a/app/xcode/Sources/VCamBridge/ExternalStateBinding.swift +++ b/app/xcode/Sources/VCamBridge/ExternalStateBinding.swift @@ -72,4 +72,8 @@ private var reloaders: [UUID: Reloader] = [:] wrappedValue[keyPath: keyPath] = newValue.wrappedValue } } + + public static func constant(_ value: Value) -> Self { + .init(id: UUID(uuidString: "2546C91C-12FB-4BC2-A7C0-F1A7DA7318C0")!, get: { value }, set: { _ in }) + } } diff --git a/app/xcode/Sources/VCamBridge/UniBridge.swift b/app/xcode/Sources/VCamBridge/UniBridge.swift index 1837e79..3cb260c 100644 --- a/app/xcode/Sources/VCamBridge/UniBridge.swift +++ b/app/xcode/Sources/VCamBridge/UniBridge.swift @@ -2,7 +2,11 @@ import SwiftUI public final class UniBridge { public static let shared = UniBridge() + #if DEBUG + public init() {} + #else private init() {} + #endif public enum IntType: Int32 { case lensFlare = 0 case facialExpression = 1 diff --git a/app/xcode/Sources/VCamUI/RootContentView.swift b/app/xcode/Sources/VCamUI/RootContentView.swift index 630cf65..0ab7b99 100644 --- a/app/xcode/Sources/VCamUI/RootContentView.swift +++ b/app/xcode/Sources/VCamUI/RootContentView.swift @@ -6,21 +6,18 @@ // import SwiftUI +import VCamBridge -public struct RootContentView: View { - public init(vcamUI: VCamUI, menuBottomView: MenuBottomView, toolbar: Toolbar, unityView: NSView, interactable: Bool) { - self.vcamUI = vcamUI +public struct RootContentView: View { + public init(menuBottomView: MenuBottomView, unityView: NSView, interactable: ExternalStateBinding = .init(.interactable)) { self.menuBottomView = menuBottomView - self.toolbar = toolbar self.unityView = unityView - self.interactable = interactable + self._interactable = interactable } - let vcamUI: VCamUI let menuBottomView: MenuBottomView - let toolbar: Toolbar let unityView: NSView - let interactable: Bool + @ExternalStateBinding(.interactable) private var interactable public var body: some View { if interactable { @@ -36,7 +33,7 @@ public struct RootContentView VSplitView { HStack(alignment: .bottom, spacing: 0) { - toolbar + VCamMainToolbar() UnityView(unityView: unityView) .equatable() .frame(maxWidth: .infinity) @@ -44,7 +41,7 @@ public struct RootContentView .frame(maxWidth: .infinity) .layoutPriority(1) - vcamUI + VCamContentView() .onTapGesture { unityView.window?.makeFirstResponder(nil) NotificationCenter.default.post(name: .unfocusObject, object: nil) @@ -88,11 +85,9 @@ private struct UnityContainerView: NSViewRepresentable { struct RootContentView_Previews: PreviewProvider { static var previews: some View { RootContentView( - vcamUI: Color.red, menuBottomView: Color.blue, - toolbar: Color.yellow, unityView: NSView(), - interactable: true + interactable: .constant(true) ) } } diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift index e2d72c5..b4a77cb 100644 --- a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift @@ -9,17 +9,7 @@ import SwiftUI import VCamEntity public struct VCamMainToolbar: View { - public init(photoPicker: VCamMainToolbarBackgroundColorPicker, emojiPicker: VCamMainToolbarEmojiPicker, motionPicker: VCamMainToolbarMotionPicker, blendShapePicker: VCamMainToolbarBlendShapePicker) { - self.photoPicker = photoPicker - self.emojiPicker = emojiPicker - self.motionPicker = motionPicker - self.blendShapePicker = blendShapePicker - } - - let photoPicker: VCamMainToolbarBackgroundColorPicker - let emojiPicker: VCamMainToolbarEmojiPicker - let motionPicker: VCamMainToolbarMotionPicker - let blendShapePicker: VCamMainToolbarBlendShapePicker + public init() {} @State private var isPhotoPopover = false @State private var isEmojiPickerPopover = false @@ -38,7 +28,7 @@ public struct VCamMainToolbar: View { } .popover(isPresented: $isPhotoPopover) { VCamPopoverContainer(L10n.background.key) { - photoPicker + VCamMainToolbarBackgroundColorPicker() } .environment(\.locale, locale) } @@ -62,7 +52,7 @@ public struct VCamMainToolbar: View { } } } content: { - emojiPicker + VCamMainToolbarEmojiPicker() } .environment(\.locale, locale) .frame(width: 240) @@ -75,7 +65,7 @@ public struct VCamMainToolbar: View { } .popover(isPresented: $isMotionPickerPopover) { VCamPopoverContainerWithWindow(L10n.motion.key) { - motionPicker + VCamMainToolbarMotionPicker() } .environment(\.locale, locale) .frame(width: 240) @@ -88,7 +78,7 @@ public struct VCamMainToolbar: View { } .popover(isPresented: $isBlendShapePickerPopover) { VCamPopoverContainerWithWindow(L10n.facialExpression.key) { - blendShapePicker + VCamMainToolbarBlendShapePicker() } .environment(\.locale, locale) .frame(width: 280, height: 150) @@ -115,13 +105,6 @@ public struct VCamMainToolbar: View { } } -struct VCamMainToolbar_Previews: PreviewProvider { - static var previews: some View { - VCamMainToolbar( - photoPicker: VCamMainToolbarBackgroundColorPicker(backgroundColor: .constant(.red)), - emojiPicker: VCamMainToolbarEmojiPicker(), - motionPicker: VCamMainToolbarMotionPicker(motionHello: {}, motionBye: { .constant(false) }, motionJump: {}, motionYear: {}, motionWhat: {}, motionWin: {}, motionNod: { .constant(false) }, motionShakeHead: { .constant(false) }, motionShakeBody: { .constant(false) }, motionRun: { .constant(false) }), - blendShapePicker: VCamMainToolbarBlendShapePicker(blendShapes: [], selectedBlendShape: { .constant(nil) }) - ) - } +#Preview { + VCamMainToolbar() } diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBackgroundColorPicker.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBackgroundColorPicker.swift index 84bb885..62d3a7c 100644 --- a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBackgroundColorPicker.swift +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBackgroundColorPicker.swift @@ -7,13 +7,16 @@ import SwiftUI import VCamUIFoundation +import VCamBridge public struct VCamMainToolbarBackgroundColorPicker: View { - public init(backgroundColor: Binding) { - self._backgroundColor = backgroundColor + public init( + backgroundColor: ExternalStateBinding = .init(.backgroundColor) + ) { + _backgroundColor = backgroundColor } - @Binding var backgroundColor: Color + @ExternalStateBinding(.backgroundColor) private var backgroundColor: Color public var body: some View { GroupBox { @@ -29,8 +32,6 @@ public struct VCamMainToolbarBackgroundColorPicker: View { } } -struct VCamMainToolbarPhotoPicker_Previews: PreviewProvider { - static var previews: some View { - VCamMainToolbarBackgroundColorPicker(backgroundColor: .constant(.red)) - } +#Preview { + VCamMainToolbarBackgroundColorPicker(backgroundColor: .constant(.red)) } diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift index 20ce290..47507d2 100644 --- a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift @@ -6,25 +6,24 @@ // import SwiftUI +import VCamBridge public struct VCamMainToolbarBlendShapePicker: View { - public init(blendShapes: [String], selectedBlendShape: () -> Binding) { + public init(blendShapes: [String] = UniBridge.cachedBlendShapes) { self.blendShapes = blendShapes - self._selectedBlendShape = selectedBlendShape() } let blendShapes: [String] - @Binding var selectedBlendShape: String? - @UniReload private var reload: Void + @ExternalStateBinding(.currentBlendShape) private var currentBlendShape public var body: some View { ScrollView(.vertical, showsIndicators: true) { GroupBox { LazyVGrid(columns: [GridItem(.adaptive(minimum: 60))]) { ForEach(blendShapes) { blendShape in - HoverToggle(text: blendShape, isOn: $selectedBlendShape.map( + HoverToggle(text: blendShape, isOn: $currentBlendShape.map( get: { blendShape == $0 }, - set: { $0 ? blendShape : nil } + set: { $0 ? blendShape : "" } )) } } @@ -73,8 +72,6 @@ extension VCamMainToolbarBlendShapePicker: MacWindow { } } -struct VCamMainToolbarBlendShapePicker_Previews: PreviewProvider { - static var previews: some View { - VCamMainToolbarBlendShapePicker(blendShapes: ["natural", "joy"], selectedBlendShape: { .constant("joy") }) - } +#Preview { + VCamMainToolbarBlendShapePicker(blendShapes: ["natural", "joy"]) } diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift index 32c0efa..9283d77 100644 --- a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift @@ -6,31 +6,21 @@ // import SwiftUI +import VCamBridge public struct VCamMainToolbarMotionPicker: View { - public init(motionHello: @escaping () -> Void, motionBye: @escaping () -> Binding, motionJump: @escaping () -> Void, motionYear: @escaping () -> Void, motionWhat: @escaping () -> Void, motionWin: @escaping () -> Void, motionNod: @escaping () -> Binding, motionShakeHead: @escaping () -> Binding, motionShakeBody: @escaping () -> Binding, motionRun: @escaping () -> Binding) { - self.motionHello = motionHello - self.motionBye = motionBye - self.motionJump = motionJump - self.motionYear = motionYear - self.motionWhat = motionWhat - self.motionWin = motionWin - self.motionNod = motionNod - self.motionShakeHead = motionShakeHead - self.motionShakeBody = motionShakeBody - self.motionRun = motionRun - } + public init() {} - let motionHello: () -> Void - let motionBye: () -> Binding - let motionJump: () -> Void - let motionYear: () -> Void - let motionWhat: () -> Void - let motionWin: () -> Void - let motionNod: () -> Binding - let motionShakeHead: () -> Binding - let motionShakeBody: () -> Binding - let motionRun: () -> Binding + @UniAction(.motionHello) var motionHello + @ExternalStateBinding(.motionBye) var motionBye + @UniAction(.motionJump) var motionJump + @UniAction(.motionYear) var motionYear + @UniAction(.motionWhat) var motionWhat + @UniAction(.motionWin) var motionWin + @ExternalStateBinding(.motionNod) var motionNod + @ExternalStateBinding(.motionShakeHead) var motionShakeHead + @ExternalStateBinding(.motionShakeBody) var motionShakeBody + @ExternalStateBinding(.motionRun) var motionRun @Environment(\.nsWindow) var nsWindow @UniReload private var reload: Void @@ -38,17 +28,17 @@ public struct VCamMainToolbarMotionPicker: View { public var body: some View { GroupBox { LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))], spacing: 2) { - button(key: L10n.hi.key, action: motionHello) - toggle(key: L10n.bye.key, isOn: motionBye()) - button(key: L10n.jump.key, action: motionJump) - button(key: L10n.cheer.key, action: motionYear) - button(key: L10n.what.key, action: motionWhat) + button(key: L10n.hi.key, action: { motionHello() }) + toggle(key: L10n.bye.key, isOn: $motionBye.workaround()) + button(key: L10n.jump.key, action: { motionJump() }) + button(key: L10n.cheer.key, action: { motionYear() }) + button(key: L10n.what.key, action: { motionWhat() }) Group { - button(key: L10n.pose.key, action: motionWin) - toggle(key: L10n.nod.key, isOn: motionNod()) - toggle(key: L10n.no.key, isOn: motionShakeHead()) - toggle(key: L10n.shudder.key, isOn: motionShakeBody()) - toggle(key: L10n.run.key, isOn: motionRun()) + button(key: L10n.pose.key, action: { motionWin() }) + toggle(key: L10n.nod.key, isOn: $motionNod.workaround()) + toggle(key: L10n.no.key, isOn: $motionShakeHead.workaround()) + toggle(key: L10n.shudder.key, isOn: $motionShakeBody.workaround()) + toggle(key: L10n.run.key, isOn: $motionRun.workaround()) } } } @@ -100,9 +90,20 @@ extension VCamMainToolbarMotionPicker: MacWindow { } } -struct VCamMainToolbarMotionPicker_Previews: PreviewProvider { - static var previews: some View { - VCamMainToolbarMotionPicker(motionHello: {}, motionBye: { .constant(false) }, motionJump: {}, motionYear: {}, motionWhat: {}, motionWin: {}, motionNod: { .constant(false) }, motionShakeHead: { .constant(false) }, motionShakeBody: { .constant(false) }, motionRun: { .constant(false) }) - .frame(width: 240) +private extension Binding { + // The state updates with a slight delay, so wait a bit before refreshing the UI + func workaround() -> Self { + map(get: { $0 }, set: { + Task { + try? await Task.sleep(nanoseconds: NSEC_PER_MSEC * 300) + UniReload.Reloader.shared.objectWillChange.send() + } + return $0 + }) } } + +#Preview { + VCamMainToolbarMotionPicker() + .frame(width: 240) +}