From b66f62afb062da36841df220cb2015dc50fb0a3e Mon Sep 17 00:00:00 2001 From: "tattn (Tatsuya Tanaka)" Date: Mon, 16 Oct 2023 14:14:20 +0900 Subject: [PATCH] Open source --- .../VCamBridge/FilterParameterPreset.swift | 2 +- app/xcode/Sources/VCamBridge/LensFlare.swift | 34 +++++ .../UniBridge+ExternalStateBinding.swift | 37 ++++- .../VCamUI/UIComponent/ColorEditField.swift | 8 ++ .../UIComponent/UniFloatEditField.swift | 33 +++++ .../VCamUI/UIComponent/VCamSection.swift | 33 ++--- .../VCamUI/UIComponent/ValueEditField.swift | 40 +++++- .../Sources/VCamUI/VCamContentView.swift | 42 ++++++ .../Sources/VCamUI/VCamDisplayView.swift | 132 ++++++++++++++++++ 9 files changed, 334 insertions(+), 27 deletions(-) create mode 100644 app/xcode/Sources/VCamBridge/LensFlare.swift create mode 100644 app/xcode/Sources/VCamUI/UIComponent/UniFloatEditField.swift create mode 100644 app/xcode/Sources/VCamUI/VCamContentView.swift create mode 100644 app/xcode/Sources/VCamUI/VCamDisplayView.swift diff --git a/app/xcode/Sources/VCamBridge/FilterParameterPreset.swift b/app/xcode/Sources/VCamBridge/FilterParameterPreset.swift index 12b815e..84a0ec2 100644 --- a/app/xcode/Sources/VCamBridge/FilterParameterPreset.swift +++ b/app/xcode/Sources/VCamBridge/FilterParameterPreset.swift @@ -38,7 +38,7 @@ extension FilterParameterPreset { private let currentFilterParameterPresetId = UUID() public extension ExternalState { - static var currentFilterParameterPreset: ExternalState { + static var currentDisplayParameterPreset: ExternalState { .init(id: currentFilterParameterPresetId) { FilterParameterPreset(string: UniBridge.shared.currentDisplayParameter.wrappedValue) } set: { diff --git a/app/xcode/Sources/VCamBridge/LensFlare.swift b/app/xcode/Sources/VCamBridge/LensFlare.swift new file mode 100644 index 0000000..ae69689 --- /dev/null +++ b/app/xcode/Sources/VCamBridge/LensFlare.swift @@ -0,0 +1,34 @@ +// +// LensFlare.swift +// +// +// Created by Tatsuya Tanaka on 2022/03/22. +// + +import Foundation +import VCamLocalization + +public enum LensFlare: Int32, CaseIterable, Identifiable, CustomStringConvertible { + case none, type1, type2, type3, type4 + + public var id: Self { self } + + public static func initOrNone(_ value: Int32) -> Self { + .init(rawValue: value) ?? .none + } + + public var description: String { + switch self { + case .none: + return L10n.none.text + case .type1: + return L10n.typeNo("1").text + case .type2: + return L10n.typeNo("2").text + case .type3: + return L10n.typeNo("3").text + case .type4: + return L10n.typeNo("4").text + } + } +} diff --git a/app/xcode/Sources/VCamBridge/UniBridge+ExternalStateBinding.swift b/app/xcode/Sources/VCamBridge/UniBridge+ExternalStateBinding.swift index b868f9c..4793f52 100644 --- a/app/xcode/Sources/VCamBridge/UniBridge+ExternalStateBinding.swift +++ b/app/xcode/Sources/VCamBridge/UniBridge+ExternalStateBinding.swift @@ -7,13 +7,42 @@ import Foundation -private let baseUUID = UUID() +private let baseUUID = UUID().uuid + +private func uuid(_ keyPath: WritableKeyPath, typeValue: Int32) -> UUID { + var uuid = baseUUID + uuid[keyPath: keyPath] = UInt8(typeValue) + return UUID(uuid: uuid) +} public extension ExternalStateBinding { init(_ type: UniBridge.BoolType) where Value == Bool { - var uuid = baseUUID.uuid - uuid.0 = UInt8(type.rawValue) let mapper = UniBridge.shared.boolMapper - self.init(id: UUID(uuid: uuid), get: { mapper.get(type) }, set: mapper.set(type)) + self.init(id: uuid(\.0, typeValue: type.rawValue), get: { mapper.get(type) }, set: mapper.set(type)) + } + + init(_ type: UniBridge.FloatType) where Value == CGFloat { + let mapper = UniBridge.shared.floatMapper + self.init(id: uuid(\.1, typeValue: type.rawValue), get: { mapper.get(type) }, set: mapper.set(type)) + } + + init(_ type: UniBridge.IntType) where Value == Int32 { + let mapper = UniBridge.shared.intMapper + self.init(id: uuid(\.2, typeValue: type.rawValue), get: { mapper.get(type) }, set: mapper.set(type)) + } + + init(_ type: UniBridge.StringType) where Value == String { + let mapper = UniBridge.shared.stringMapper + self.init(id: uuid(\.3, typeValue: type.rawValue), get: { mapper.get(type) }, set: mapper.set(type)) + } + + init(_ type: UniBridge.ArrayType, as: Array.Type) where Value == Array { + let mapper = UniBridge.shared.arrayMapper + self.init(id: uuid(\.4, typeValue: type.rawValue), get: { mapper.binding(type, size: type.arraySize).wrappedValue }, set: mapper.set(type)) + } + + init(_ type: UniBridge.StructType, as: Value.Type = Value.self) where Value: ValueBindingStructType { + let mapper = UniBridge.shared.structMapper + self.init(id: uuid(\.5, typeValue: type.rawValue), get: { mapper.binding(type).wrappedValue }, set: { mapper.binding(type).wrappedValue = $0 }) } } diff --git a/app/xcode/Sources/VCamUI/UIComponent/ColorEditField.swift b/app/xcode/Sources/VCamUI/UIComponent/ColorEditField.swift index 6f83c70..52ab0c6 100644 --- a/app/xcode/Sources/VCamUI/UIComponent/ColorEditField.swift +++ b/app/xcode/Sources/VCamUI/UIComponent/ColorEditField.swift @@ -20,7 +20,15 @@ public struct ColorEditField: View { public var body: some View { ColorPicker(selection: $value) { Text(label, bundle: .localize).bold() +#if !DEBUG .offset(x: 0, y: 16) // Workaround for release build +#endif } } } + +extension ColorEditField: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.value == rhs.value && lhs.label == rhs.label + } +} diff --git a/app/xcode/Sources/VCamUI/UIComponent/UniFloatEditField.swift b/app/xcode/Sources/VCamUI/UIComponent/UniFloatEditField.swift new file mode 100644 index 0000000..247924e --- /dev/null +++ b/app/xcode/Sources/VCamUI/UIComponent/UniFloatEditField.swift @@ -0,0 +1,33 @@ +// +// UniFloatEditField.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/22. +// + +import SwiftUI +import VCamBridge + +public struct UniFloatEditField: View { + public init(_ label: LocalizedStringKey, type: UniBridge.FloatType, format: String = "%.1f", range: ClosedRange) { + self.label = label + _value = ExternalStateBinding(type) + self.format = format + self.range = range + } + + private let label: LocalizedStringKey + @ExternalStateBinding private var value: CGFloat + private let format: String + private let range: ClosedRange + + public var body: some View { + ValueEditField(label, value: $value, format: format, type: .slider(range)) + } +} + +extension UniFloatEditField: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.value == rhs.value && lhs.range == rhs.range && lhs.label == rhs.label + } +} diff --git a/app/xcode/Sources/VCamUI/UIComponent/VCamSection.swift b/app/xcode/Sources/VCamUI/UIComponent/VCamSection.swift index 367c67d..8e39f36 100644 --- a/app/xcode/Sources/VCamUI/UIComponent/VCamSection.swift +++ b/app/xcode/Sources/VCamUI/UIComponent/VCamSection.swift @@ -18,28 +18,21 @@ public struct VCamSection: View { @State private var isExpanded = false public var body: some View { - // switch to DisclosureGroup? - VStack(alignment: .leading, spacing: 0) { - HStack { - Image(systemName: isExpanded ? "chevron.down" : "chevron.right") - .foregroundColor(.secondary) - Text(title, bundle: .localize).bold() - Spacer() + DisclosureGroup.init(isExpanded: $isExpanded) { + VStack(alignment: .leading, spacing: 0) { + content } - .contentShape(Rectangle()) - .onTapGesture { - withAnimation { - isExpanded.toggle() + .padding(.top, 8) + .padding(.leading) + } label: { + Text(title, bundle: .localize) + .bold() + .contentShape(Rectangle()) + .onTapGesture { + withAnimation { + isExpanded.toggle() + } } - } - if isExpanded { - VStack(alignment: .leading, spacing: 0) { - content - } - .padding(.top, 8).padding(.leading) - } else { - Color.clear.frame(height: 8) - } } } } diff --git a/app/xcode/Sources/VCamUI/UIComponent/ValueEditField.swift b/app/xcode/Sources/VCamUI/UIComponent/ValueEditField.swift index 0c90183..d031468 100644 --- a/app/xcode/Sources/VCamUI/UIComponent/ValueEditField.swift +++ b/app/xcode/Sources/VCamUI/UIComponent/ValueEditField.swift @@ -22,15 +22,30 @@ public struct ValueEditField: View { let format: String let type: EditType + @State private var valueText = "" + @State private var debounceTask: Task? + public var body: some View { HStack(spacing: 2) { HStack(spacing: 2) { Text(label, bundle: .localize).bold() if !valueHidden { - Text("[\(.init(format: format, value))]") + Text("[\(valueText)]") + .lineLimit(1) .font(.caption2) .fontWeight(.thin) .foregroundColor(.secondary) + .onChange(of: value) { newValue in + debounceTask?.cancel() + debounceTask = Task { + do { + try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) + } catch { + return + } + valueText = .init(format: format, newValue) + } + } } } .layoutPriority(1) @@ -43,10 +58,31 @@ public struct ValueEditField: View { .textFieldStyle(.roundedBorder) } } + .onAppear { + valueText = .init(format: format, value) + } } - public enum EditType { + public enum EditType: Equatable { case slider(ClosedRange, onEditingChanged: (Bool) -> Void = { _ in }) case stepper + + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.slider(lrange, _), .slider(rrange, _)): lrange == rrange + case (.stepper, .stepper): true + case (.slider, .stepper), (.stepper, .slider): false + } + } + } +} + +extension ValueEditField: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.value == rhs.value && + lhs.valueHidden == rhs.valueHidden && + lhs.format == rhs.format && + lhs.type == rhs.type && + lhs.label == rhs.label } } diff --git a/app/xcode/Sources/VCamUI/VCamContentView.swift b/app/xcode/Sources/VCamUI/VCamContentView.swift new file mode 100644 index 0000000..d27ebc5 --- /dev/null +++ b/app/xcode/Sources/VCamUI/VCamContentView.swift @@ -0,0 +1,42 @@ +// +// VCamContentView.swift +// +// +// Created by Tatsuya Tanaka on 2022/04/09. +// + +import Foundation +import SwiftUI + +public struct VCamContentView: View { + public init() {} + + @EnvironmentObject var state: VCamUIState + + public var body: some View { + content() + .frame(maxWidth: .infinity) + .padding(8) + .background(.thinMaterial) + } + + @ViewBuilder + func content() -> some View { + switch state.currentMenu { + case .main: + VCamMainView() + case .screenEffect: + VCamDisplayView() + case .recording: + VCamRecordingView() + } + } +} + +struct VCamUI_Preview: PreviewProvider { + static var previews: some View { + return VCamContentView() + .frame(width: 500, height: 300) + .background(Color.white) + } +} diff --git a/app/xcode/Sources/VCamUI/VCamDisplayView.swift b/app/xcode/Sources/VCamUI/VCamDisplayView.swift new file mode 100644 index 0000000..aebf43b --- /dev/null +++ b/app/xcode/Sources/VCamUI/VCamDisplayView.swift @@ -0,0 +1,132 @@ +// +// VCamDisplayView.swift +// +// +// Created by Tatsuya Tanaka on 2022/02/22. +// + +import SwiftUI +import VCamEntity +import VCamBridge + +public struct VCamDisplayView: View { + public init() {} + + @ExternalStateBinding(.currentDisplayParameter) private var currentDisplayParameter + @ExternalStateBinding(.usePostEffect) private var usePostEffect + @ExternalStateBinding(.currentDisplayParameterPreset) private var preset + + @UniAction(.saveDisplayParameter) private var saveDisplayParameter + @UniAction(.addDisplayParameter) private var addDisplayParameter + @UniAction(.deleteDisplayParameter) private var deleteDisplayParameter + + @ObservedObject private var windowManager = WindowManager.shared + + public var body: some View { + VStack { + HStack { + GroupBox { + Toggle(isOn: $usePostEffect) { + Text(L10n.enable.key, bundle: .localize) + } + } + GroupBox { + HStack { + SelectEditField(L10n.preset.key, value: $preset) + TextField("", text: $preset.description) + .frame(width: 120) + Button { + saveDisplayParameter() + } label: { + Text(L10n.save.key, bundle: .module) + } + Button { + addDisplayParameter() + } label: { + Image(systemName: "plus") + } + Button { + deleteDisplayParameter() + } label: { + Image(systemName: "trash") + .foregroundColor(.red) + } + } + } + .disabled(!usePostEffect) + .opacity(usePostEffect ? 1 : 0.5) + } + + VCamDisplayParameterView() + .disabled(!usePostEffect) + .opacity(usePostEffect ? 1 : 0.5) + } + } +} + +private struct VCamDisplayParameterView: View { + @ExternalStateBinding(.environmentLightColor) private var environmentLightColor: Color + @ExternalStateBinding(.colorFilter) private var colorFilter: Color + @ExternalStateBinding(.bloomColor) private var bloomColor: Color + @ExternalStateBinding(.lensFlare) private var lensFlare + @ExternalStateBinding(.vignetteColor) private var vignetteColor: Color + + @ObservedObject private var windowManager = WindowManager.shared + + var body: some View { + let minHeight = windowManager.size.height * 0.4 + ScrollView { + GroupBox { + HStack { + VStack(alignment: .leading) { + Form { + UniFloatEditField(L10n.ambientLightIntensity.key, type: .light, range: 0...2) + ColorEditField(L10n.ambientLightColor.key, value: $environmentLightColor) + UniFloatEditField(L10n.cameraExposure.key, type: .postExposure, range: -2...6) + ColorEditField(L10n.colorFilter.key, value: $colorFilter) + UniFloatEditField(L10n.saturation.key, type: .saturation, format: "%.0f", range: -100...100) + UniFloatEditField(L10n.hueShift.key, type: .hueShift, format: "%.0f", range: -180...180) + UniFloatEditField(L10n.contrast.key, type: .contrast, format: "%.0f", range: -100...100) + Spacer() + } + } + VStack(alignment: .leading) { + VCamSection(L10n.whiteBalance.key) { + Form { + UniFloatEditField(L10n.colorTemperature.key, type: .whiteBalanceTemperature, format: "%.0f", range: -100...100) + UniFloatEditField(L10n.tint.key, type: .whiteBalanceTint, format: "%.0f", range: -100...100) + } + } + VCamSection(L10n.bloom.key) { + Form { + UniFloatEditField(L10n.intensity.key, type: .bloomIntensity, range: 0...60) + UniFloatEditField(L10n.thresholdScreenEffect.key, type: .bloomThreshold, range: 0...2) + UniFloatEditField(L10n.softKnee.key, type: .bloomSoftKnee, range: 0...1) + UniFloatEditField(L10n.diffusion.key, type: .bloomDiffusion, range: 1...10) + UniFloatEditField(L10n.anamorphicRatio.key, type: .bloomAnamorphicRatio, range: -1...1) + ColorEditField(L10n.color.key, value: $bloomColor) + SelectEditField(L10n.lensFlare.key, value: $lensFlare.map(get: LensFlare.initOrNone, set: { $0.rawValue })) + UniFloatEditField(L10n.lensFlareIntensity.key, type: .bloomLensFlareIntensity, range: 0...50) + } + } + VCamSection(L10n.vignette.key) { + Form { + UniFloatEditField(L10n.intensity.key, type: .vignetteIntensity, range: 0...1) + ColorEditField(L10n.color.key, value: $vignetteColor) + UniFloatEditField(L10n.smoothness.key, type: .vignetteSmoothness, range: 0...1) + UniFloatEditField(L10n.roundness.key, type: .vignetteRoundness, range: 0...1) + } + } + Spacer() + } + } + } + } + .frame(minHeight: minHeight) + .layoutPriority(1) + } +} + +#Preview { + VCamDisplayView() +}