CasinoGames/Baccarat/Baccarat/Views/Game/ActionButtonsView.swift

213 lines
7.9 KiB
Swift

//
// ActionButtonsView.swift
// Baccarat
//
// Action buttons for deal, clear, and new round.
//
import SwiftUI
import CasinoKit
/// Action buttons for deal, clear, and new round.
struct ActionButtonsView: View {
@Bindable var gameState: GameState
let onDeal: () -> Void
let onClear: () -> Void
let onNewRound: () -> Void
// MARK: - Environment
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
/// Whether the current text size is an accessibility size (very large)
private var isAccessibilitySize: Bool {
dynamicTypeSize.isAccessibilitySize
}
// MARK: - Layout Constants
private let buttonFontSize: CGFloat = Design.BaseFontSize.xLarge
private let iconSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
private let statusFontSize: CGFloat = Design.BaseFontSize.medium
private let containerHeight: CGFloat = 70
// MARK: - Body
var body: some View {
ZStack {
// Fixed height container to prevent layout shifts
Color.clear
.frame(height: containerHeight)
// Content changes with animation
Group {
if gameState.currentPhase == .betting {
bettingButtons
} else if gameState.currentPhase == .roundComplete && !gameState.showResultBanner {
newRoundButton
} else if !gameState.showResultBanner {
dealingIndicator
}
}
.animation(.easeInOut(duration: Design.Animation.quick), value: gameState.currentPhase)
.animation(.easeInOut(duration: Design.Animation.quick), value: gameState.showResultBanner)
}
}
// MARK: - Private Views
private var bettingButtons: some View {
VStack(spacing: Design.Spacing.small) {
// Show hint if main bet is below minimum
if gameState.isMainBetBelowMinimum {
Text(String(localized: "Add $\(gameState.amountNeededForMinimum) more to meet minimum"))
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
.foregroundStyle(.orange)
.transition(.opacity.combined(with: .scale(scale: 0.8)))
}
HStack(spacing: Design.Spacing.medium) {
clearButton
dealButton
}
}
.animation(.easeInOut(duration: Design.Animation.standard), value: gameState.isMainBetBelowMinimum)
}
private var dealingIndicator: some View {
HStack(spacing: Design.Spacing.xSmall) {
ProgressView()
.tint(.white)
.scaleEffect(0.8)
Text("Dealing...")
.font(.system(size: statusFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
.lineLimit(1)
.minimumScaleFactor(Design.MinScaleFactor.relaxed)
}
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
}
@ViewBuilder
private var clearButton: some View {
if isAccessibilitySize {
Button("Clear", systemImage: "xmark.circle", action: onClear)
.labelStyle(.iconOnly)
.font(.system(size: iconSize, weight: .semibold))
.foregroundStyle(.white)
.padding(Design.Spacing.medium + Design.Spacing.xxSmall)
.background(
Circle()
.fill(Color.CasinoButton.destructive)
)
.opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
.disabled(gameState.currentBets.isEmpty)
} else {
Button("Clear", systemImage: "xmark.circle", action: onClear)
.labelStyle(.titleAndIcon)
.font(.system(size: buttonFontSize, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, Design.Spacing.xxLarge)
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
.background(
Capsule()
.fill(Color.CasinoButton.destructive)
)
.opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
.disabled(gameState.currentBets.isEmpty)
}
}
@ViewBuilder
private var dealButton: some View {
let buttonBackground = LinearGradient(
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
if isAccessibilitySize {
Button("Deal", systemImage: "play.fill", action: onDeal)
.labelStyle(.iconOnly)
.font(.system(size: iconSize, weight: .bold))
.foregroundStyle(.black)
.padding(Design.Spacing.medium + Design.Spacing.xxSmall)
.background(
Circle()
.fill(buttonBackground)
)
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
.opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
.disabled(!gameState.canDeal)
} else {
Button("Deal", systemImage: "play.fill", action: onDeal)
.labelStyle(.titleAndIcon)
.font(.system(size: buttonFontSize, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xxLarge)
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
.background(
Capsule()
.fill(buttonBackground)
)
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
.opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
.disabled(!gameState.canDeal)
}
}
@ViewBuilder
private var newRoundButton: some View {
let buttonBackground = LinearGradient(
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
if isAccessibilitySize {
Button("New Round", systemImage: "arrow.clockwise", action: onNewRound)
.labelStyle(.iconOnly)
.font(.system(size: iconSize, weight: .bold))
.foregroundStyle(.black)
.padding(Design.Spacing.medium + Design.Spacing.xxSmall)
.background(
Circle()
.fill(buttonBackground)
)
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
} else {
Button("New Round", systemImage: "arrow.clockwise", action: onNewRound)
.labelStyle(.titleAndIcon)
.font(.system(size: buttonFontSize, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xxLarge)
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
.background(
Capsule()
.fill(buttonBackground)
)
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
}
}
}
// MARK: - Previews
#Preview("Betting Phase") {
ZStack {
TableBackgroundView()
VStack {
Spacer()
ActionButtonsView(
gameState: GameState(settings: GameSettings()),
onDeal: {},
onClear: {},
onNewRound: {}
)
.padding()
}
}
}