diff --git a/Blackjack/Theme/DesignConstants.swift b/Blackjack/Theme/DesignConstants.swift index 68a9ae8..235b83c 100644 --- a/Blackjack/Theme/DesignConstants.swift +++ b/Blackjack/Theme/DesignConstants.swift @@ -14,6 +14,10 @@ import CasinoKit /// Design constants for the Blackjack app. /// Shared constants are imported from CasinoDesign; game-specific values are defined here. enum Design { + // MARK: - Debug + + /// Set to true to show layout debug borders on views + static let showDebugBorders = false // MARK: - Shared Constants (from CasinoKit) @@ -39,12 +43,13 @@ enum Design { static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap - // Player hands container height (accommodates larger cards) - static let playerHandsHeight: CGFloat = 180 * handScale // 270pt at 1.5x + // Player hands container height (accommodates larger cards + labels) + // Reduced from 180 to fit content more snugly + static let playerHandsHeight: CGFloat = 160 * handScale // 240pt at 1.5x // Hand label font sizes (scaled) static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale - static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale + static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale // Same as label static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale // Hint font size (scaled to match hands) diff --git a/Blackjack/Views/Game/ActionButtonsView.swift b/Blackjack/Views/Game/ActionButtonsView.swift index 6fdbb94..6fdcb1c 100644 --- a/Blackjack/Views/Game/ActionButtonsView.swift +++ b/Blackjack/Views/Game/ActionButtonsView.swift @@ -15,33 +15,29 @@ struct ActionButtonsView: View { @ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large @ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large - // Fixed height to prevent layout shifts - private let containerHeight: CGFloat = 120 + // Scaled container height - base 60pt, scales with accessibility + @ScaledMetric(relativeTo: .body) private var containerHeight: CGFloat = 60 var body: some View { - ZStack { - Color.clear - .frame(height: containerHeight) - - VStack(spacing: Design.Spacing.medium) { - // Primary actions - HStack(spacing: Design.Spacing.medium) { - switch state.currentPhase { - case .betting: - bettingButtons - case .playerTurn: - playerTurnButtons - case .roundComplete: - // Empty - handled by result banner - EmptyView() - default: - // Dealing, dealer turn - show nothing - EmptyView() - } + VStack(spacing: Design.Spacing.small) { + // Primary actions + HStack(spacing: Design.Spacing.medium) { + switch state.currentPhase { + case .betting: + bettingButtons + case .playerTurn: + playerTurnButtons + case .roundComplete: + // Empty - handled by result banner + EmptyView() + default: + // Dealing, dealer turn - show nothing + EmptyView() } - .animation(.spring(duration: Design.Animation.quick), value: state.currentPhase) } + .animation(.spring(duration: Design.Animation.quick), value: state.currentPhase) } + .frame(minHeight: containerHeight) .padding(.horizontal, Design.Spacing.large) } diff --git a/Blackjack/Views/Game/GameTableView.swift b/Blackjack/Views/Game/GameTableView.swift index 412d7bc..8b9ac32 100644 --- a/Blackjack/Views/Game/GameTableView.swift +++ b/Blackjack/Views/Game/GameTableView.swift @@ -65,6 +65,9 @@ struct GameTableView: View { } } + // Use global debug flag from Design constants + private var showDebugBorders: Bool { Design.showDebugBorders } + // MARK: - Main Game View @ViewBuilder @@ -85,6 +88,7 @@ struct GameTableView: View { onStats: { showStats = true } ) .frame(maxWidth: maxContentWidth) + .debugBorder(showDebugBorders, color: .cyan, label: "TopBar") // Card count display (when enabled) if settings.showCardCount { @@ -93,6 +97,7 @@ struct GameTableView: View { trueCount: state.engine.trueCount ) .frame(maxWidth: maxContentWidth) + .debugBorder(showDebugBorders, color: .mint, label: "CardCount") } // Reshuffle notification @@ -102,17 +107,18 @@ struct GameTableView: View { .transition(.move(edge: .top).combined(with: .opacity)) } - // Table layout + // Table layout - fills available space BlackjackTableView( state: state, onPlaceBet: { placeBet(state: state) } ) .frame(maxWidth: maxContentWidth) - Spacer() - // Chip selector - only shown during betting phase if state.currentPhase == .betting { + Spacer() + .debugBorder(showDebugBorders, color: .yellow, label: "ChipSpacer") + ChipSelectorView( selectedChip: $selectedChip, balance: state.balance, @@ -120,14 +126,15 @@ struct GameTableView: View { maxBet: state.settings.maxBet ) .frame(maxWidth: maxContentWidth) - .padding(.bottom, Design.Spacing.small) .transition(.opacity.combined(with: .move(edge: .bottom))) + .debugBorder(showDebugBorders, color: .pink, label: "ChipSelector") } - // Action buttons + // Action buttons - minimal spacing during player turn ActionButtonsView(state: state) .frame(maxWidth: maxContentWidth) - .padding(.bottom, Design.Spacing.medium) + .padding(.bottom, Design.Spacing.small) + .debugBorder(showDebugBorders, color: .blue, label: "ActionBtns") } .frame(maxWidth: .infinity) diff --git a/Blackjack/Views/Table/BlackjackTableView.swift b/Blackjack/Views/Table/BlackjackTableView.swift index 4aa21a3..f4276e4 100644 --- a/Blackjack/Views/Table/BlackjackTableView.swift +++ b/Blackjack/Views/Table/BlackjackTableView.swift @@ -26,8 +26,14 @@ struct BlackjackTableView: View { private let cardWidth: CGFloat = Design.Size.cardWidth private let cardSpacing: CGFloat = Design.Size.cardOverlap + /// Fixed height for the hint area to prevent layout shifts + private let hintAreaHeight: CGFloat = 44 + + // Use global debug flag from Design constants + private var showDebugBorders: Bool { Design.showDebugBorders } + var body: some View { - VStack(spacing: Design.Spacing.large) { + VStack(spacing: Design.Spacing.small) { // Dealer area DealerHandView( hand: state.dealerHand, @@ -36,8 +42,11 @@ struct BlackjackTableView: View { cardWidth: cardWidth, cardSpacing: cardSpacing ) + .debugBorder(showDebugBorders, color: .red, label: "Dealer") - Spacer() + // Flexible space between dealer and player (minimum 60pt) + Spacer(minLength: 60) + .debugBorder(showDebugBorders, color: .yellow, label: "Spacer") // Player hands area - only show when there are cards dealt if state.playerHands.first?.cards.isEmpty == false { @@ -50,10 +59,14 @@ struct BlackjackTableView: View { cardSpacing: cardSpacing ) .transition(.opacity) + .debugBorder(showDebugBorders, color: .green, label: "Player") } // Betting zone (when betting) if state.currentPhase == .betting { + Spacer() + .debugBorder(showDebugBorders, color: .yellow, label: "Spacer2") + BettingZoneView( betAmount: state.currentBet, minBet: state.settings.minBet, @@ -61,22 +74,29 @@ struct BlackjackTableView: View { onTap: onPlaceBet ) .transition(.scale.combined(with: .opacity)) + .debugBorder(showDebugBorders, color: .blue, label: "BetZone") // Betting hint based on count (only when card counting enabled) if showCardCount, let bettingHint = bettingHint { BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount) .transition(.opacity) + .debugBorder(showDebugBorders, color: .purple, label: "BetHint") } - } - - // Hint (when enabled and player turn) - if state.settings.showHints && isPlayerTurn, let hint = currentHint { - HintView(hint: hint) - .transition(.opacity) + } else { + // Fixed-height hint area to prevent layout shifts during player turn + ZStack { + if state.settings.showHints && isPlayerTurn, let hint = currentHint { + HintView(hint: hint) + .transition(.opacity) + } + } + .frame(height: hintAreaHeight) + .debugBorder(showDebugBorders, color: .orange, label: "HintArea") } } .padding(.horizontal, Design.Spacing.large) .padding(.vertical, Design.Spacing.medium) + .debugBorder(showDebugBorders, color: .white, label: "TableView") .animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase) } diff --git a/Blackjack/Views/Table/PlayerHandView.swift b/Blackjack/Views/Table/PlayerHandView.swift index dfb9b1f..812bba9 100644 --- a/Blackjack/Views/Table/PlayerHandView.swift +++ b/Blackjack/Views/Table/PlayerHandView.swift @@ -19,6 +19,11 @@ struct PlayerHandsView: View { let cardWidth: CGFloat let cardSpacing: CGFloat + /// Total card count across all hands - used to trigger scroll when hitting + private var totalCardCount: Int { + hands.reduce(0) { $0 + $1.cards.count } + } + var body: some View { GeometryReader { geometry in ScrollViewReader { proxy in @@ -40,24 +45,35 @@ struct PlayerHandsView: View { .id(index) } } - .padding(.horizontal, Design.Spacing.medium) + .padding(.horizontal, Design.Spacing.large) .frame(minWidth: geometry.size.width) } .scrollClipDisabled() + .scrollBounceBehavior(.basedOnSize) .onChange(of: activeHandIndex) { _, newIndex in - withAnimation(.easeInOut(duration: Design.Animation.quick)) { - proxy.scrollTo(newIndex, anchor: .center) - } + scrollToHand(proxy: proxy, index: newIndex) + } + .onChange(of: totalCardCount) { _, _ in + // Scroll to active hand when cards are added (hit) + scrollToHand(proxy: proxy, index: activeHandIndex) + } + .onChange(of: hands.count) { _, _ in + // Scroll to active hand when split occurs + scrollToHand(proxy: proxy, index: activeHandIndex) } .onAppear { - if hands.count > 1 { - proxy.scrollTo(activeHandIndex, anchor: .center) - } + scrollToHand(proxy: proxy, index: activeHandIndex) } } } .frame(height: Design.Size.playerHandsHeight) } + + private func scrollToHand(proxy: ScrollViewProxy, index: Int) { + withAnimation(.easeInOut(duration: Design.Animation.quick)) { + proxy.scrollTo(index, anchor: .center) + } + } } // MARK: - Single Player Hand diff --git a/CasinoKit/Sources/CasinoKit/Exports.swift b/CasinoKit/Sources/CasinoKit/Exports.swift index 2d93f9d..65f3f12 100644 --- a/CasinoKit/Sources/CasinoKit/Exports.swift +++ b/CasinoKit/Sources/CasinoKit/Exports.swift @@ -76,3 +76,6 @@ // - CloudSyncManager // - PersistableGameData (protocol) +// MARK: - Debug +// - debugBorder(_:color:label:) View modifier + diff --git a/CasinoKit/Sources/CasinoKit/Views/Debug/DebugBorderModifier.swift b/CasinoKit/Sources/CasinoKit/Views/Debug/DebugBorderModifier.swift new file mode 100644 index 0000000..e97cac0 --- /dev/null +++ b/CasinoKit/Sources/CasinoKit/Views/Debug/DebugBorderModifier.swift @@ -0,0 +1,39 @@ +// +// DebugBorderModifier.swift +// CasinoKit +// +// Debug view modifier for visualizing layout boundaries. +// + +import SwiftUI + +// MARK: - Debug Border Modifier + +public extension View { + /// Adds a colored border and label to a view for debugging layout. + /// - Parameters: + /// - show: Whether to show the debug border. + /// - color: The color of the border and label. + /// - label: The label text to display in the corner. + /// - Returns: The view with an optional debug border overlay. + @ViewBuilder + func debugBorder(_ show: Bool, color: Color, label: String) -> some View { + if show { + self + .overlay( + RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.xSmall) + .strokeBorder(color, lineWidth: CasinoDesign.LineWidth.thin) + ) + .overlay(alignment: .topLeading) { + Text(label) + .font(.system(size: 8, weight: .bold)) + .foregroundStyle(color) + .padding(2) + .background(Color.black.opacity(CasinoDesign.Opacity.strong)) + } + } else { + self + } + } +} +