diff --git a/Baccarat/Baccarat/Theme/DesignConstants.swift b/Baccarat/Baccarat/Theme/DesignConstants.swift index 7580337..6608404 100644 --- a/Baccarat/Baccarat/Theme/DesignConstants.swift +++ b/Baccarat/Baccarat/Theme/DesignConstants.swift @@ -14,7 +14,7 @@ import CasinoKit enum Design { /// Set to true to show layout debug borders on views - static let showDebugBorders = false + static let showDebugBorders = true // MARK: - Shared Constants (from CasinoKit) diff --git a/Baccarat/Baccarat/Views/Game/GameTableView.swift b/Baccarat/Baccarat/Views/Game/GameTableView.swift index 1f6d7cd..aabdb12 100644 --- a/Baccarat/Baccarat/Views/Game/GameTableView.swift +++ b/Baccarat/Baccarat/Views/Game/GameTableView.swift @@ -115,22 +115,20 @@ struct GameTableView: View { @ViewBuilder private func mainContent(geometry: GeometryProxy) -> some View { - let screenWidth = geometry.size.width - let screenHeight = geometry.size.height // Use geometry to detect landscape on iPad (width > height and large screen) - let isLandscapeLayout = isLargeScreen && screenWidth > screenHeight + let isLandscapeLayout = isLargeScreen && geometry.size.width > geometry.size.height if isLandscapeLayout { // Landscape iPad: RoadMap on left, game content on right - landscapeLayout(screenWidth: screenWidth) + landscapeLayout } else { // Portrait or iPhone: vertical stack with RoadMap inline - portraitLayout(screenWidth: screenWidth) + portraitLayout } } /// Landscape layout with TopBar spanning full width, RoadMap grid on left below TopBar - private func landscapeLayout(screenWidth: CGFloat) -> some View { + private var landscapeLayout: some View { VStack(spacing: 0) { // Top bar spans full width TopBarView( @@ -195,10 +193,10 @@ struct GameTableView: View { bankerValue: state.bankerHandValue, playerIsWinner: playerIsWinner, bankerIsWinner: bankerIsWinner, - isTie: isTie, - screenWidth: screenWidth + isTie: isTie ) .frame(maxWidth: maxContentWidth) + .padding(.horizontal, Design.Spacing.medium) .debugBorder(showDebugBorders, color: .red, label: "CardsArea") Spacer(minLength: 0) @@ -250,7 +248,7 @@ struct GameTableView: View { } /// Portrait layout with RoadMap inline - private func portraitLayout(screenWidth: CGFloat) -> some View { + private var portraitLayout: some View { VStack(spacing: 0) { // Top bar with balance and info (from CasinoKit) TopBarView( @@ -277,10 +275,10 @@ struct GameTableView: View { bankerValue: state.bankerHandValue, playerIsWinner: playerIsWinner, bankerIsWinner: bankerIsWinner, - isTie: isTie, - screenWidth: screenWidth + isTie: isTie ) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) + .padding(.horizontal, Design.Spacing.medium) .debugBorder(showDebugBorders, color: .red, label: "CardsArea") Spacer(minLength: minSpacerHeight) @@ -290,7 +288,7 @@ struct GameTableView: View { if settings.showHistory && !state.roundHistory.isEmpty { RoadMapView(results: state.recentResults) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) - .padding(.horizontal) + .padding(.horizontal, Design.Spacing.medium) .debugBorder(showDebugBorders, color: .orange, label: "RoadMap") } diff --git a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift index de2434c..24311e1 100644 --- a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift +++ b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift @@ -19,8 +19,6 @@ struct CardsDisplayArea: View { let playerIsWinner: Bool let bankerIsWinner: Bool let isTie: Bool - /// Screen width for responsive card sizing - var screenWidth: CGFloat = 400 // MARK: - Environment @@ -51,16 +49,6 @@ struct CardsDisplayArea: View { isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large } - /// Horizontal padding inside the container - private var containerPaddingH: CGFloat { - isLargeScreen ? Design.Spacing.xLarge : Design.Spacing.medium - } - - /// Outer horizontal padding - private var outerPaddingH: CGFloat { - isLargeScreen ? Design.Spacing.large : Design.Spacing.small - } - // MARK: - Accessibility private var playerHandDescription: String { @@ -107,15 +95,14 @@ struct CardsDisplayArea: View { bankerHandSection .debugBorder(showDebugBorders, color: .red, label: "Banker") } + .frame(maxWidth: .infinity) .padding(.top, Design.Spacing.medium) .padding(.bottom, Design.Spacing.large) - .padding(.horizontal, containerPaddingH) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) .fill(Color.black.opacity(Design.Opacity.quarter)) .accessibilityHidden(true) ) - .padding(.horizontal, outerPaddingH) .debugBorder(showDebugBorders, color: .mint, label: "HandsContainer") } @@ -139,10 +126,10 @@ struct CardsDisplayArea: View { CompactHandView( cards: playerCards, cardsFaceUp: playerCardsFaceUp, - isWinner: playerIsWinner, - screenWidth: screenWidth + isWinner: playerIsWinner ) } + .frame(maxWidth: .infinity) .accessibilityElement(children: .ignore) .accessibilityLabel(String(localized: "Player hand")) .accessibilityValue(playerHandDescription + (playerIsWinner ? ", " + String(localized: "Winner") : "")) @@ -166,10 +153,10 @@ struct CardsDisplayArea: View { CompactHandView( cards: bankerCards, cardsFaceUp: bankerCardsFaceUp, - isWinner: bankerIsWinner, - screenWidth: screenWidth + isWinner: bankerIsWinner ) } + .frame(maxWidth: .infinity) .accessibilityElement(children: .ignore) .accessibilityLabel(String(localized: "Banker hand")) .accessibilityValue(bankerHandDescription + (bankerIsWinner ? ", " + String(localized: "Winner") : "")) diff --git a/Baccarat/Baccarat/Views/Table/CompactHandView.swift b/Baccarat/Baccarat/Views/Table/CompactHandView.swift index cc305cc..0b87eeb 100644 --- a/Baccarat/Baccarat/Views/Table/CompactHandView.swift +++ b/Baccarat/Baccarat/Views/Table/CompactHandView.swift @@ -13,13 +13,25 @@ struct CompactHandView: View { let cards: [Card] let cardsFaceUp: [Bool] let isWinner: Bool - /// Screen width passed from parent for responsive sizing - var screenWidth: CGFloat = 400 // MARK: - Environment @Environment(\.horizontalSizeClass) private var horizontalSizeClass + // MARK: - Constants + + /// Overlap ratio relative to card width (negative = overlap) + private let overlapRatio: CGFloat = -0.45 + + /// Maximum number of cards in baccarat hand + private let maxCards: Int = 3 + + /// Padding around cards + private let containerPadding: CGFloat = Design.Spacing.xSmall + + /// Placeholder spacing when no cards + private let placeholderSpacing: CGFloat = Design.Spacing.small + // MARK: - Computed Properties /// Whether we're on a large screen (iPad) @@ -32,55 +44,60 @@ struct CompactHandView: View { isLargeScreen ? 14 : 10 } - /// Card width - larger on iPad - private var cardWidth: CGFloat { - isLargeScreen ? Design.Size.cardWidthTableiPad : Design.Size.cardWidthTable + /// Calculate card width from available container width + /// Formula: containerWidth = cardWidth + (cardWidth + overlap) * 2 + padding + /// Where overlap = cardWidth * overlapRatio + /// Solving: cardWidth = (containerWidth - 2*padding) / (1 + 2*(1 + overlapRatio)) + private func cardWidth(for containerWidth: CGFloat) -> CGFloat { + let availableWidth = containerWidth - containerPadding * 2 + let divisor = 1 + CGFloat(maxCards - 1) * (1 + overlapRatio) + return availableWidth / divisor + } + + /// Card overlap based on card width + private func cardOverlap(for cardWidth: CGFloat) -> CGFloat { + cardWidth * overlapRatio } /// Card height based on aspect ratio - private var cardHeight: CGFloat { + private func cardHeight(for cardWidth: CGFloat) -> CGFloat { cardWidth * CasinoDesign.Size.cardAspectRatio } - /// Card overlap - larger on iPad - private var cardOverlap: CGFloat { - isLargeScreen ? Design.Size.cardOverlapiPad : Design.Size.cardOverlap - } - - private let placeholderSpacing: CGFloat = Design.Spacing.small - - /// Fixed container width to prevent resizing during deal - private var fixedContainerWidth: CGFloat { - // Max 3 cards: first card full width + 2 more with overlap - let cardsWidth = cardWidth + (cardWidth + cardOverlap) * 2 - return cardsWidth + Design.Spacing.xSmall * 2 - } - - /// Fixed container height - private var fixedContainerHeight: CGFloat { - cardHeight + Design.Spacing.xSmall * 2 - } - // MARK: - Body var body: some View { - ZStack { - // Fixed-size container - Color.clear - .frame(width: fixedContainerWidth, height: fixedContainerHeight) + GeometryReader { geometry in + let width = cardWidth(for: geometry.size.width) + let overlap = cardOverlap(for: width) + let height = cardHeight(for: width) - // Cards content centered in fixed container - cardsContent - } - .background(winnerBorder) - .overlay(alignment: .bottom) { - winBadge + cardsContent(cardWidth: width, cardOverlap: overlap) + .frame(width: geometry.size.width, height: height + containerPadding * 2) + .background(winnerBorder) + .overlay(alignment: .bottom) { + winBadge + } } + .aspectRatio(contentWidth / contentHeight, contentMode: .fit) + } + + /// Aspect ratio helper - use base card dimensions for consistent sizing + private var contentWidth: CGFloat { + // Base card width for ratio calculation + let baseCardWidth: CGFloat = 45 + let baseOverlap = baseCardWidth * overlapRatio + return baseCardWidth + (baseCardWidth + baseOverlap) * CGFloat(maxCards - 1) + containerPadding * 2 + } + + private var contentHeight: CGFloat { + let baseCardWidth: CGFloat = 45 + return baseCardWidth * CasinoDesign.Size.cardAspectRatio + containerPadding * 2 } // MARK: - Private Views - private var cardsContent: some View { + private func cardsContent(cardWidth: CGFloat, cardOverlap: CGFloat) -> some View { HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) { if cards.isEmpty { // Placeholders - no overlap, just side by side @@ -99,7 +116,6 @@ struct CompactHandView: View { } } } - .frame(width: fixedContainerWidth, height: fixedContainerHeight) .animation(nil, value: cards.count) // Prevent size animation during dealing }