Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
3aa1ed77ae
commit
e1655ce20c
@ -26,17 +26,29 @@ struct CardStackView: View {
|
||||
/// Animation offset for dealing cards (direction cards fly in from).
|
||||
let dealOffset: CGPoint
|
||||
|
||||
/// Minimum number of card slots to reserve space for.
|
||||
/// This prevents width changes during dealing by pre-allocating placeholder space.
|
||||
let minimumCardSlots: Int
|
||||
|
||||
/// Number of additional placeholders needed to reach minimum slots.
|
||||
private var placeholdersNeeded: Int {
|
||||
max(0, minimumCardSlots - cards.count)
|
||||
}
|
||||
|
||||
/// Scaled animation duration based on dealing speed.
|
||||
private var animationDuration: Double {
|
||||
Design.Animation.springDuration * dealingSpeed
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
||||
if cards.isEmpty {
|
||||
CardPlaceholderView(width: cardWidth)
|
||||
HStack(spacing: cards.isEmpty && placeholdersNeeded > 0 ? Design.Spacing.small : cardSpacing) {
|
||||
if cards.isEmpty && placeholdersNeeded > 0 {
|
||||
// Show placeholders when empty
|
||||
ForEach(0..<placeholdersNeeded, id: \.self) { _ in
|
||||
CardPlaceholderView(width: cardWidth)
|
||||
}
|
||||
} else {
|
||||
// Show actual cards
|
||||
ForEach(cards.indices, id: \.self) { index in
|
||||
let faceUp = isFaceUp(index)
|
||||
CardView(
|
||||
@ -61,6 +73,13 @@ struct CardStackView: View {
|
||||
: .identity
|
||||
)
|
||||
}
|
||||
|
||||
// Show trailing placeholders to reserve space for upcoming cards
|
||||
ForEach(0..<placeholdersNeeded, id: \.self) { index in
|
||||
CardPlaceholderView(width: cardWidth)
|
||||
.opacity(Design.Opacity.medium)
|
||||
.zIndex(Double(cards.count + index))
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(
|
||||
@ -96,18 +115,21 @@ extension CardStackView {
|
||||
dealOffset: CGPoint(
|
||||
x: Design.DealAnimation.dealerOffsetX,
|
||||
y: Design.DealAnimation.dealerOffsetY
|
||||
)
|
||||
),
|
||||
minimumCardSlots: 2
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a card stack for the player (all cards face up).
|
||||
/// - Parameter minimumCardSlots: Minimum slots to reserve (default 2 for initial deal).
|
||||
static func player(
|
||||
cards: [Card],
|
||||
cardWidth: CGFloat,
|
||||
cardSpacing: CGFloat,
|
||||
showAnimations: Bool,
|
||||
dealingSpeed: Double,
|
||||
showCardCount: Bool
|
||||
showCardCount: Bool,
|
||||
minimumCardSlots: Int = 2
|
||||
) -> CardStackView {
|
||||
CardStackView(
|
||||
cards: cards,
|
||||
@ -120,7 +142,8 @@ extension CardStackView {
|
||||
dealOffset: CGPoint(
|
||||
x: Design.DealAnimation.playerOffsetX,
|
||||
y: Design.DealAnimation.playerOffsetY
|
||||
)
|
||||
),
|
||||
minimumCardSlots: minimumCardSlots
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -138,7 +161,8 @@ extension CardStackView {
|
||||
dealingSpeed: 1.0,
|
||||
showCardCount: false,
|
||||
isFaceUp: { _ in true },
|
||||
dealOffset: .zero
|
||||
dealOffset: .zero,
|
||||
minimumCardSlots: 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,6 @@ struct DealerHandView: View {
|
||||
.animation(nil, value: showHoleCard)
|
||||
|
||||
// Cards with result badge overlay
|
||||
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
||||
CardStackView.dealer(
|
||||
cards: hand.cards,
|
||||
showHoleCard: showHoleCard,
|
||||
@ -58,13 +57,6 @@ struct DealerHandView: View {
|
||||
dealingSpeed: dealingSpeed,
|
||||
showCardCount: showCardCount
|
||||
)
|
||||
|
||||
// Show placeholder for second card in European mode (no hole card)
|
||||
if hand.cards.count == 1 && !showHoleCard {
|
||||
CardPlaceholderView(width: cardWidth)
|
||||
.opacity(Design.Opacity.medium)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
// Result badge - centered on cards
|
||||
if let result = hand.cards.count >= 2 && showHoleCard ? handResultText : nil {
|
||||
|
||||
@ -29,14 +29,22 @@ struct PlayerHandsContainer: View {
|
||||
/// Whether the hint toast should be visible.
|
||||
let showHintToast: Bool
|
||||
|
||||
/// Total card count across all hands - used to trigger scroll when hitting
|
||||
/// Tracks the currently scrolled-to hand ID for smooth positioning
|
||||
@State private var scrolledHandID: UUID?
|
||||
|
||||
/// The active hand's ID, used for scroll position binding
|
||||
private var activeHandID: UUID? {
|
||||
guard activeHandIndex < hands.count else { return nil }
|
||||
return hands[activeHandIndex].id
|
||||
}
|
||||
|
||||
/// Total card count across all hands - used for animation
|
||||
private var totalCardCount: Int {
|
||||
hands.reduce(0) { $0 + $1.cards.count }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
/// The hands content HStack
|
||||
private var handsContent: some View {
|
||||
HStack(spacing: Design.Spacing.large) {
|
||||
// Display hands in reverse order (right to left play order)
|
||||
// Visual order: Hand 3, Hand 2, Hand 1 (left to right)
|
||||
@ -64,34 +72,45 @@ struct PlayerHandsContainer: View {
|
||||
}
|
||||
}
|
||||
.animation(.spring(duration: Design.Animation.springDuration), value: hands.count)
|
||||
// Animate card count changes so width growth is smooth
|
||||
.animation(.spring(duration: Design.Animation.springDuration), value: totalCardCount)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
handsContent
|
||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||
// Ensure minimum width to fill viewport, centering smaller content via layout
|
||||
// This avoids scroll anchor re-centering which doesn't animate
|
||||
.containerRelativeFrame(.horizontal, alignment: .center)
|
||||
.scrollTargetLayout()
|
||||
}
|
||||
.scrollClipDisabled()
|
||||
.scrollBounceBehavior(.always)
|
||||
.defaultScrollAnchor(.center)
|
||||
.onChange(of: activeHandIndex) { _, _ in
|
||||
scrollToActiveHand(proxy: proxy)
|
||||
.scrollPosition(id: $scrolledHandID, anchor: .center)
|
||||
.scrollTargetBehavior(.viewAligned)
|
||||
.onChange(of: activeHandID) { _, newID in
|
||||
// Animate scroll when switching between split hands
|
||||
if let newID {
|
||||
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
|
||||
scrolledHandID = newID
|
||||
}
|
||||
}
|
||||
.onChange(of: totalCardCount) { _, _ in
|
||||
scrollToActiveHand(proxy: proxy)
|
||||
}
|
||||
.onChange(of: hands.count) { _, _ in
|
||||
scrollToActiveHand(proxy: proxy)
|
||||
// Re-center when hands are added (split)
|
||||
if let activeID = activeHandID {
|
||||
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
|
||||
scrolledHandID = activeID
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
scrollToActiveHand(proxy: proxy)
|
||||
}
|
||||
// Set initial position without animation
|
||||
scrolledHandID = activeHandID
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private func scrollToActiveHand(proxy: ScrollViewProxy) {
|
||||
guard activeHandIndex < hands.count else { return }
|
||||
let activeHandId = hands[activeHandIndex].id
|
||||
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
|
||||
proxy.scrollTo(activeHandId, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
Loading…
Reference in New Issue
Block a user