diff --git a/Gaze/Services/EyeTracking/CameraSessionManager.swift b/Gaze/Services/EyeTracking/CameraSessionManager.swift index e04dec5..bcb9a5c 100644 --- a/Gaze/Services/EyeTracking/CameraSessionManager.swift +++ b/Gaze/Services/EyeTracking/CameraSessionManager.swift @@ -100,6 +100,13 @@ final class CameraSessionManager: NSObject, ObservableObject { } session.addOutput(output) + if let connection = output.connection(with: .video) { + if connection.isVideoMirroringSupported { + connection.automaticallyAdjustsVideoMirroring = false + connection.isVideoMirrored = true + } + } + self.captureSession = session self.videoOutput = output } diff --git a/Gaze/Views/Components/CameraPreviewView.swift b/Gaze/Views/Components/CameraPreviewView.swift index 73aa7de..c50678b 100644 --- a/Gaze/Views/Components/CameraPreviewView.swift +++ b/Gaze/Views/Components/CameraPreviewView.swift @@ -11,6 +11,20 @@ import AVFoundation struct CameraPreviewView: NSViewRepresentable { let previewLayer: AVCaptureVideoPreviewLayer let borderColor: NSColor + let showsBorder: Bool + let cornerRadius: CGFloat + + init( + previewLayer: AVCaptureVideoPreviewLayer, + borderColor: NSColor, + showsBorder: Bool = true, + cornerRadius: CGFloat = 12 + ) { + self.previewLayer = previewLayer + self.borderColor = borderColor + self.showsBorder = showsBorder + self.cornerRadius = cornerRadius + } func makeNSView(context: Context) -> PreviewContainerView { let view = PreviewContainerView() @@ -22,6 +36,11 @@ struct CameraPreviewView: NSViewRepresentable { previewLayer.frame = view.bounds view.layer?.addSublayer(previewLayer) } + + if let connection = previewLayer.connection, connection.isVideoMirroringSupported { + connection.automaticallyAdjustsVideoMirroring = false + connection.isVideoMirrored = true + } updateBorder(view: view, color: borderColor) @@ -41,14 +60,23 @@ struct CameraPreviewView: NSViewRepresentable { // Same layer, just update frame previewLayer.frame = nsView.bounds } + + if let connection = previewLayer.connection, connection.isVideoMirroringSupported { + connection.automaticallyAdjustsVideoMirroring = false + connection.isVideoMirrored = true + } updateBorder(view: nsView, color: borderColor) } private func updateBorder(view: NSView, color: NSColor) { - view.layer?.borderColor = color.cgColor - view.layer?.borderWidth = 4 - view.layer?.cornerRadius = 12 + if showsBorder { + view.layer?.borderColor = color.cgColor + view.layer?.borderWidth = 4 + } else { + view.layer?.borderWidth = 0 + } + view.layer?.cornerRadius = cornerRadius view.layer?.masksToBounds = true } diff --git a/Gaze/Views/Components/EnforceModeCalibrationOverlayView.swift b/Gaze/Views/Components/EnforceModeCalibrationOverlayView.swift index 7a1d3ec..059ed4d 100644 --- a/Gaze/Views/Components/EnforceModeCalibrationOverlayView.swift +++ b/Gaze/Views/Components/EnforceModeCalibrationOverlayView.swift @@ -16,8 +16,7 @@ struct EnforceModeCalibrationOverlayView: View { var body: some View { ZStack { - Color.black.opacity(0.85) - .ignoresSafeArea() + cameraBackground switch calibrationService.currentStep { case .eyeBox: @@ -31,19 +30,23 @@ struct EnforceModeCalibrationOverlayView: View { } private var eyeBoxStep: some View { - VStack(spacing: 24) { - Text("Adjust Eye Box") - .font(.title2) - .foregroundStyle(.white) + ZStack { + VStack(spacing: 16) { + Text("Adjust Eye Box") + .font(.title2) + .foregroundStyle(.white) - Text( - "Use the sliders to fit the boxes around your eyes. When it looks right, continue." - ) - .font(.callout) - .multilineTextAlignment(.center) - .foregroundStyle(.white.opacity(0.8)) - - eyePreview + Text( + "Use the sliders to fit the boxes around your eyes. When it looks right, continue." + ) + .font(.callout) + .multilineTextAlignment(.center) + .foregroundStyle(.white.opacity(0.8)) + } + .padding(.horizontal, 40) + .padding(.top, 40) + .frame(maxWidth: 520) + .frame(maxWidth: .infinity, alignment: .top) VStack(alignment: .leading, spacing: 12) { Text("Width") @@ -62,24 +65,29 @@ struct EnforceModeCalibrationOverlayView: View { in: 0.01...0.10 ) } - .padding() - .background(.white.opacity(0.1)) + .padding(16) + .background(.black.opacity(0.6)) .clipShape(RoundedRectangle(cornerRadius: 12)) + .frame(maxWidth: 420) + .frame(maxHeight: .infinity, alignment: .center) - HStack(spacing: 12) { - Button("Cancel") { - calibrationService.dismissOverlay() - enforceModeService.stopTestMode() - } - .buttonStyle(.bordered) + VStack { + Spacer() + HStack(spacing: 12) { + Button("Cancel") { + calibrationService.dismissOverlay() + enforceModeService.stopTestMode() + } + .buttonStyle(.bordered) - Button("Continue") { - calibrationService.advance() + Button("Continue") { + calibrationService.advance() + } + .buttonStyle(.borderedProminent) } - .buttonStyle(.borderedProminent) } + .padding(.bottom, 40) } - .padding() } private var targetStep: some View { @@ -99,11 +107,10 @@ struct EnforceModeCalibrationOverlayView: View { } .padding() .background(Color.black.opacity(0.7)) - .frame(maxWidth: .infinity, alignment: .top) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) targetDot - VStack { Spacer() HStack(spacing: 12) { @@ -135,50 +142,56 @@ struct EnforceModeCalibrationOverlayView: View { } } - private var eyePreview: some View { - ZStack { - if let layer = eyeTrackingService.previewLayer { - CameraPreviewView(previewLayer: layer, borderColor: NSColor.systemBlue) - .frame(height: 240) - } - GeometryReader { geometry in - EyeTrackingDebugOverlayView( - debugState: eyeTrackingService.debugState, - viewSize: geometry.size - ) - } - } - .frame(height: 240) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - private var targetDot: some View { GeometryReader { geometry in let target = calibrationService.currentTarget() - Circle() - .fill(Color.blue) - .frame(width: 100, height: 100) - .position( - x: geometry.size.width * target.x, - y: geometry.size.height * target.y - ) - .overlay( - Circle() - .trim(from: 0, to: CGFloat(calibrationService.countdownProgress)) - .stroke(Color.blue.opacity(0.8), lineWidth: 6) - .frame(width: 140, height: 140) - .rotationEffect(.degrees(-90)) - .animation(.linear(duration: 0.02), value: calibrationService.countdownProgress) - ) + let center = CGPoint( + x: geometry.size.width * target.x, + y: geometry.size.height * target.y + ) + + ZStack { + Circle() + .fill(Color.blue) + .frame(width: 120, height: 120) + + Circle() + .trim(from: 0, to: CGFloat(calibrationService.countdownProgress)) + .stroke(Color.blue.opacity(0.8), lineWidth: 8) + .frame(width: 160, height: 160) + .rotationEffect(.degrees(-90)) + .animation(.linear(duration: 0.02), value: calibrationService.countdownProgress) + } + .position(center) } .ignoresSafeArea() } - private var countdownRing: some View { - Circle() - .trim(from: 0, to: CGFloat(calibrationService.countdownProgress)) - .stroke(Color.blue.opacity(0.8), lineWidth: 6) - .frame(width: 120, height: 120) - .rotationEffect(.degrees(-90)) + private var cameraBackground: some View { + ZStack { + if let layer = eyeTrackingService.previewLayer { + CameraPreviewView( + previewLayer: layer, + borderColor: .clear, + showsBorder: false, + cornerRadius: 0 + ) + .opacity(0.5) + } + + if calibrationService.currentStep == .eyeBox { + GeometryReader { geometry in + EyeTrackingDebugOverlayView( + debugState: eyeTrackingService.debugState, + viewSize: geometry.size + ) + .opacity(0.8) + } + } + + Color.black.opacity(0.35) + .ignoresSafeArea() + } + .ignoresSafeArea() } }