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

This commit is contained in:
Matt Bruce 2025-12-22 12:10:31 -06:00
parent ec484d8574
commit 740ef6a73b
7 changed files with 132 additions and 46 deletions

View File

@ -14,6 +14,10 @@ import CasinoKit
/// Design constants for the Blackjack app. /// Design constants for the Blackjack app.
/// Shared constants are imported from CasinoDesign; game-specific values are defined here. /// Shared constants are imported from CasinoDesign; game-specific values are defined here.
enum Design { enum Design {
// MARK: - Debug
/// Set to true to show layout debug borders on views
static let showDebugBorders = false
// MARK: - Shared Constants (from CasinoKit) // MARK: - Shared Constants (from CasinoKit)
@ -39,12 +43,13 @@ enum Design {
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap
// Player hands container height (accommodates larger cards) // Player hands container height (accommodates larger cards + labels)
static let playerHandsHeight: CGFloat = 180 * handScale // 270pt at 1.5x // Reduced from 180 to fit content more snugly
static let playerHandsHeight: CGFloat = 160 * handScale // 240pt at 1.5x
// Hand label font sizes (scaled) // Hand label font sizes (scaled)
static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale 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 static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale
// Hint font size (scaled to match hands) // Hint font size (scaled to match hands)

View File

@ -15,15 +15,11 @@ struct ActionButtonsView: View {
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large @ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large @ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large
// Fixed height to prevent layout shifts // Scaled container height - base 60pt, scales with accessibility
private let containerHeight: CGFloat = 120 @ScaledMetric(relativeTo: .body) private var containerHeight: CGFloat = 60
var body: some View { var body: some View {
ZStack { VStack(spacing: Design.Spacing.small) {
Color.clear
.frame(height: containerHeight)
VStack(spacing: Design.Spacing.medium) {
// Primary actions // Primary actions
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
switch state.currentPhase { switch state.currentPhase {
@ -41,7 +37,7 @@ struct ActionButtonsView: View {
} }
.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) .padding(.horizontal, Design.Spacing.large)
} }

View File

@ -65,6 +65,9 @@ struct GameTableView: View {
} }
} }
// Use global debug flag from Design constants
private var showDebugBorders: Bool { Design.showDebugBorders }
// MARK: - Main Game View // MARK: - Main Game View
@ViewBuilder @ViewBuilder
@ -85,6 +88,7 @@ struct GameTableView: View {
onStats: { showStats = true } onStats: { showStats = true }
) )
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
// Card count display (when enabled) // Card count display (when enabled)
if settings.showCardCount { if settings.showCardCount {
@ -93,6 +97,7 @@ struct GameTableView: View {
trueCount: state.engine.trueCount trueCount: state.engine.trueCount
) )
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
.debugBorder(showDebugBorders, color: .mint, label: "CardCount")
} }
// Reshuffle notification // Reshuffle notification
@ -102,17 +107,18 @@ struct GameTableView: View {
.transition(.move(edge: .top).combined(with: .opacity)) .transition(.move(edge: .top).combined(with: .opacity))
} }
// Table layout // Table layout - fills available space
BlackjackTableView( BlackjackTableView(
state: state, state: state,
onPlaceBet: { placeBet(state: state) } onPlaceBet: { placeBet(state: state) }
) )
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
Spacer()
// Chip selector - only shown during betting phase // Chip selector - only shown during betting phase
if state.currentPhase == .betting { if state.currentPhase == .betting {
Spacer()
.debugBorder(showDebugBorders, color: .yellow, label: "ChipSpacer")
ChipSelectorView( ChipSelectorView(
selectedChip: $selectedChip, selectedChip: $selectedChip,
balance: state.balance, balance: state.balance,
@ -120,14 +126,15 @@ struct GameTableView: View {
maxBet: state.settings.maxBet maxBet: state.settings.maxBet
) )
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
.padding(.bottom, Design.Spacing.small)
.transition(.opacity.combined(with: .move(edge: .bottom))) .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) ActionButtonsView(state: state)
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
.padding(.bottom, Design.Spacing.medium) .padding(.bottom, Design.Spacing.small)
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)

View File

@ -26,8 +26,14 @@ struct BlackjackTableView: View {
private let cardWidth: CGFloat = Design.Size.cardWidth private let cardWidth: CGFloat = Design.Size.cardWidth
private let cardSpacing: CGFloat = Design.Size.cardOverlap 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 { var body: some View {
VStack(spacing: Design.Spacing.large) { VStack(spacing: Design.Spacing.small) {
// Dealer area // Dealer area
DealerHandView( DealerHandView(
hand: state.dealerHand, hand: state.dealerHand,
@ -36,8 +42,11 @@ struct BlackjackTableView: View {
cardWidth: cardWidth, cardWidth: cardWidth,
cardSpacing: cardSpacing 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 // Player hands area - only show when there are cards dealt
if state.playerHands.first?.cards.isEmpty == false { if state.playerHands.first?.cards.isEmpty == false {
@ -50,10 +59,14 @@ struct BlackjackTableView: View {
cardSpacing: cardSpacing cardSpacing: cardSpacing
) )
.transition(.opacity) .transition(.opacity)
.debugBorder(showDebugBorders, color: .green, label: "Player")
} }
// Betting zone (when betting) // Betting zone (when betting)
if state.currentPhase == .betting { if state.currentPhase == .betting {
Spacer()
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer2")
BettingZoneView( BettingZoneView(
betAmount: state.currentBet, betAmount: state.currentBet,
minBet: state.settings.minBet, minBet: state.settings.minBet,
@ -61,22 +74,29 @@ struct BlackjackTableView: View {
onTap: onPlaceBet onTap: onPlaceBet
) )
.transition(.scale.combined(with: .opacity)) .transition(.scale.combined(with: .opacity))
.debugBorder(showDebugBorders, color: .blue, label: "BetZone")
// Betting hint based on count (only when card counting enabled) // Betting hint based on count (only when card counting enabled)
if showCardCount, let bettingHint = bettingHint { if showCardCount, let bettingHint = bettingHint {
BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount) BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount)
.transition(.opacity) .transition(.opacity)
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
} }
} } else {
// Fixed-height hint area to prevent layout shifts during player turn
// Hint (when enabled and player turn) ZStack {
if state.settings.showHints && isPlayerTurn, let hint = currentHint { if state.settings.showHints && isPlayerTurn, let hint = currentHint {
HintView(hint: hint) HintView(hint: hint)
.transition(.opacity) .transition(.opacity)
} }
} }
.frame(height: hintAreaHeight)
.debugBorder(showDebugBorders, color: .orange, label: "HintArea")
}
}
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
.debugBorder(showDebugBorders, color: .white, label: "TableView")
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase) .animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
} }

View File

@ -19,6 +19,11 @@ struct PlayerHandsView: View {
let cardWidth: CGFloat let cardWidth: CGFloat
let cardSpacing: 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 { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
ScrollViewReader { proxy in ScrollViewReader { proxy in
@ -40,24 +45,35 @@ struct PlayerHandsView: View {
.id(index) .id(index)
} }
} }
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.large)
.frame(minWidth: geometry.size.width) .frame(minWidth: geometry.size.width)
} }
.scrollClipDisabled() .scrollClipDisabled()
.scrollBounceBehavior(.basedOnSize)
.onChange(of: activeHandIndex) { _, newIndex in .onChange(of: activeHandIndex) { _, newIndex in
withAnimation(.easeInOut(duration: Design.Animation.quick)) { scrollToHand(proxy: proxy, index: newIndex)
proxy.scrollTo(newIndex, anchor: .center)
} }
.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 { .onAppear {
if hands.count > 1 { scrollToHand(proxy: proxy, index: activeHandIndex)
proxy.scrollTo(activeHandIndex, anchor: .center)
}
} }
} }
} }
.frame(height: Design.Size.playerHandsHeight) .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 // MARK: - Single Player Hand

View File

@ -76,3 +76,6 @@
// - CloudSyncManager // - CloudSyncManager
// - PersistableGameData (protocol) // - PersistableGameData (protocol)
// MARK: - Debug
// - debugBorder(_:color:label:) View modifier

View File

@ -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
}
}
}