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.
/// 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)

View File

@ -15,15 +15,11 @@ 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) {
VStack(spacing: Design.Spacing.small) {
// Primary actions
HStack(spacing: Design.Spacing.medium) {
switch state.currentPhase {
@ -41,7 +37,7 @@ struct ActionButtonsView: View {
}
.animation(.spring(duration: Design.Animation.quick), value: state.currentPhase)
}
}
.frame(minHeight: containerHeight)
.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
@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)

View File

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

View File

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

View File

@ -76,3 +76,6 @@
// - CloudSyncManager
// - 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
}
}
}