Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-22 23:03:28 -06:00
parent 7baab0428f
commit 21fc2d6f5b
4 changed files with 69 additions and 68 deletions

View File

@ -14,7 +14,7 @@ import CasinoKit
enum Design { enum Design {
/// Set to true to show layout debug borders on views /// Set to true to show layout debug borders on views
static let showDebugBorders = false static let showDebugBorders = true
// MARK: - Shared Constants (from CasinoKit) // MARK: - Shared Constants (from CasinoKit)

View File

@ -115,22 +115,20 @@ struct GameTableView: View {
@ViewBuilder @ViewBuilder
private func mainContent(geometry: GeometryProxy) -> some View { 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) // 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 { if isLandscapeLayout {
// Landscape iPad: RoadMap on left, game content on right // Landscape iPad: RoadMap on left, game content on right
landscapeLayout(screenWidth: screenWidth) landscapeLayout
} else { } else {
// Portrait or iPhone: vertical stack with RoadMap inline // 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 /// 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) { VStack(spacing: 0) {
// Top bar spans full width // Top bar spans full width
TopBarView( TopBarView(
@ -195,10 +193,10 @@ struct GameTableView: View {
bankerValue: state.bankerHandValue, bankerValue: state.bankerHandValue,
playerIsWinner: playerIsWinner, playerIsWinner: playerIsWinner,
bankerIsWinner: bankerIsWinner, bankerIsWinner: bankerIsWinner,
isTie: isTie, isTie: isTie
screenWidth: screenWidth
) )
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
.padding(.horizontal, Design.Spacing.medium)
.debugBorder(showDebugBorders, color: .red, label: "CardsArea") .debugBorder(showDebugBorders, color: .red, label: "CardsArea")
Spacer(minLength: 0) Spacer(minLength: 0)
@ -250,7 +248,7 @@ struct GameTableView: View {
} }
/// Portrait layout with RoadMap inline /// Portrait layout with RoadMap inline
private func portraitLayout(screenWidth: CGFloat) -> some View { private var portraitLayout: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Top bar with balance and info (from CasinoKit) // Top bar with balance and info (from CasinoKit)
TopBarView( TopBarView(
@ -277,10 +275,10 @@ struct GameTableView: View {
bankerValue: state.bankerHandValue, bankerValue: state.bankerHandValue,
playerIsWinner: playerIsWinner, playerIsWinner: playerIsWinner,
bankerIsWinner: bankerIsWinner, bankerIsWinner: bankerIsWinner,
isTie: isTie, isTie: isTie
screenWidth: screenWidth
) )
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
.padding(.horizontal, Design.Spacing.medium)
.debugBorder(showDebugBorders, color: .red, label: "CardsArea") .debugBorder(showDebugBorders, color: .red, label: "CardsArea")
Spacer(minLength: minSpacerHeight) Spacer(minLength: minSpacerHeight)
@ -290,7 +288,7 @@ struct GameTableView: View {
if settings.showHistory && !state.roundHistory.isEmpty { if settings.showHistory && !state.roundHistory.isEmpty {
RoadMapView(results: state.recentResults) RoadMapView(results: state.recentResults)
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
.padding(.horizontal) .padding(.horizontal, Design.Spacing.medium)
.debugBorder(showDebugBorders, color: .orange, label: "RoadMap") .debugBorder(showDebugBorders, color: .orange, label: "RoadMap")
} }

View File

@ -19,8 +19,6 @@ struct CardsDisplayArea: View {
let playerIsWinner: Bool let playerIsWinner: Bool
let bankerIsWinner: Bool let bankerIsWinner: Bool
let isTie: Bool let isTie: Bool
/// Screen width for responsive card sizing
var screenWidth: CGFloat = 400
// MARK: - Environment // MARK: - Environment
@ -51,16 +49,6 @@ struct CardsDisplayArea: View {
isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large 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 // MARK: - Accessibility
private var playerHandDescription: String { private var playerHandDescription: String {
@ -107,15 +95,14 @@ struct CardsDisplayArea: View {
bankerHandSection bankerHandSection
.debugBorder(showDebugBorders, color: .red, label: "Banker") .debugBorder(showDebugBorders, color: .red, label: "Banker")
} }
.frame(maxWidth: .infinity)
.padding(.top, Design.Spacing.medium) .padding(.top, Design.Spacing.medium)
.padding(.bottom, Design.Spacing.large) .padding(.bottom, Design.Spacing.large)
.padding(.horizontal, containerPaddingH)
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.black.opacity(Design.Opacity.quarter)) .fill(Color.black.opacity(Design.Opacity.quarter))
.accessibilityHidden(true) .accessibilityHidden(true)
) )
.padding(.horizontal, outerPaddingH)
.debugBorder(showDebugBorders, color: .mint, label: "HandsContainer") .debugBorder(showDebugBorders, color: .mint, label: "HandsContainer")
} }
@ -139,10 +126,10 @@ struct CardsDisplayArea: View {
CompactHandView( CompactHandView(
cards: playerCards, cards: playerCards,
cardsFaceUp: playerCardsFaceUp, cardsFaceUp: playerCardsFaceUp,
isWinner: playerIsWinner, isWinner: playerIsWinner
screenWidth: screenWidth
) )
} }
.frame(maxWidth: .infinity)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Player hand")) .accessibilityLabel(String(localized: "Player hand"))
.accessibilityValue(playerHandDescription + (playerIsWinner ? ", " + String(localized: "Winner") : "")) .accessibilityValue(playerHandDescription + (playerIsWinner ? ", " + String(localized: "Winner") : ""))
@ -166,10 +153,10 @@ struct CardsDisplayArea: View {
CompactHandView( CompactHandView(
cards: bankerCards, cards: bankerCards,
cardsFaceUp: bankerCardsFaceUp, cardsFaceUp: bankerCardsFaceUp,
isWinner: bankerIsWinner, isWinner: bankerIsWinner
screenWidth: screenWidth
) )
} }
.frame(maxWidth: .infinity)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Banker hand")) .accessibilityLabel(String(localized: "Banker hand"))
.accessibilityValue(bankerHandDescription + (bankerIsWinner ? ", " + String(localized: "Winner") : "")) .accessibilityValue(bankerHandDescription + (bankerIsWinner ? ", " + String(localized: "Winner") : ""))

View File

@ -13,13 +13,25 @@ struct CompactHandView: View {
let cards: [Card] let cards: [Card]
let cardsFaceUp: [Bool] let cardsFaceUp: [Bool]
let isWinner: Bool let isWinner: Bool
/// Screen width passed from parent for responsive sizing
var screenWidth: CGFloat = 400
// MARK: - Environment // MARK: - Environment
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @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 // MARK: - Computed Properties
/// Whether we're on a large screen (iPad) /// Whether we're on a large screen (iPad)
@ -32,55 +44,60 @@ struct CompactHandView: View {
isLargeScreen ? 14 : 10 isLargeScreen ? 14 : 10
} }
/// Card width - larger on iPad /// Calculate card width from available container width
private var cardWidth: CGFloat { /// Formula: containerWidth = cardWidth + (cardWidth + overlap) * 2 + padding
isLargeScreen ? Design.Size.cardWidthTableiPad : Design.Size.cardWidthTable /// 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 /// Card height based on aspect ratio
private var cardHeight: CGFloat { private func cardHeight(for cardWidth: CGFloat) -> CGFloat {
cardWidth * CasinoDesign.Size.cardAspectRatio 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 // MARK: - Body
var body: some View { var body: some View {
ZStack { GeometryReader { geometry in
// Fixed-size container let width = cardWidth(for: geometry.size.width)
Color.clear let overlap = cardOverlap(for: width)
.frame(width: fixedContainerWidth, height: fixedContainerHeight) let height = cardHeight(for: width)
// Cards content centered in fixed container cardsContent(cardWidth: width, cardOverlap: overlap)
cardsContent .frame(width: geometry.size.width, height: height + containerPadding * 2)
} .background(winnerBorder)
.background(winnerBorder) .overlay(alignment: .bottom) {
.overlay(alignment: .bottom) { winBadge
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 // MARK: - Private Views
private var cardsContent: some View { private func cardsContent(cardWidth: CGFloat, cardOverlap: CGFloat) -> some View {
HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) { HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) {
if cards.isEmpty { if cards.isEmpty {
// Placeholders - no overlap, just side by side // 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 .animation(nil, value: cards.count) // Prevent size animation during dealing
} }