299 lines
9.2 KiB
Swift
299 lines
9.2 KiB
Swift
//
|
|
// TopBarView.swift
|
|
// CasinoKit
|
|
//
|
|
// A reusable top bar for casino games showing balance and toolbar buttons.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// A button configuration for the top bar toolbar.
|
|
public struct TopBarButton: Identifiable {
|
|
public let id = UUID()
|
|
|
|
/// The SF Symbol icon name.
|
|
public let icon: String
|
|
|
|
/// The accessibility label for VoiceOver.
|
|
public let accessibilityLabel: String
|
|
|
|
/// The action to perform when tapped.
|
|
public let action: () -> Void
|
|
|
|
/// Creates a toolbar button configuration.
|
|
/// - Parameters:
|
|
/// - icon: SF Symbol name for the button icon.
|
|
/// - accessibilityLabel: VoiceOver label for the button.
|
|
/// - action: Closure to execute when tapped.
|
|
public init(icon: String, accessibilityLabel: String, action: @escaping () -> Void) {
|
|
self.icon = icon
|
|
self.accessibilityLabel = accessibilityLabel
|
|
self.action = action
|
|
}
|
|
}
|
|
|
|
/// Optional Sherpa tags for TopBarView elements.
|
|
/// Use this to enable walkthrough highlighting of individual top bar elements.
|
|
public struct TopBarSherpaTags<Tags: SherpaTags> {
|
|
public let balance: Tags?
|
|
public let cardsRemaining: Tags?
|
|
public let stats: Tags?
|
|
public let rules: Tags?
|
|
public let settings: Tags?
|
|
|
|
public init(
|
|
balance: Tags? = nil,
|
|
cardsRemaining: Tags? = nil,
|
|
stats: Tags? = nil,
|
|
rules: Tags? = nil,
|
|
settings: Tags? = nil
|
|
) {
|
|
self.balance = balance
|
|
self.cardsRemaining = cardsRemaining
|
|
self.stats = stats
|
|
self.rules = rules
|
|
self.settings = settings
|
|
}
|
|
}
|
|
|
|
/// A top bar showing balance and customizable toolbar buttons.
|
|
public struct TopBarView<Tags: SherpaTags>: View {
|
|
/// The current balance to display.
|
|
public let balance: Int
|
|
|
|
/// Optional secondary info (e.g., cards remaining).
|
|
public let secondaryInfo: String?
|
|
|
|
/// Icon for secondary info.
|
|
public let secondaryIcon: String?
|
|
|
|
/// Custom buttons to display at the front of the toolbar (before stats/help/settings).
|
|
public let leadingButtons: [TopBarButton]
|
|
|
|
/// Action when settings is tapped.
|
|
public let onSettings: (() -> Void)?
|
|
|
|
/// Action when help/rules is tapped.
|
|
public let onHelp: (() -> Void)?
|
|
|
|
/// Action when stats is tapped.
|
|
public let onStats: (() -> Void)?
|
|
|
|
/// Optional Sherpa tags for walkthrough highlighting.
|
|
public let sherpaTags: TopBarSherpaTags<Tags>?
|
|
|
|
// MARK: - Font Sizes (fixed for top bar constraints)
|
|
|
|
private let balanceFontSize: CGFloat = 24
|
|
private let dollarFontSize: CGFloat = 14
|
|
private let secondaryFontSize: CGFloat = 14
|
|
private let iconSize: CGFloat = 20
|
|
|
|
/// Creates a top bar.
|
|
/// - Parameters:
|
|
/// - balance: The current balance.
|
|
/// - secondaryInfo: Optional secondary info text.
|
|
/// - secondaryIcon: Optional SF Symbol for secondary info.
|
|
/// - leadingButtons: Custom buttons to add at the front of the toolbar.
|
|
/// - onSettings: Settings button action.
|
|
/// - onHelp: Help button action.
|
|
/// - onStats: Stats button action.
|
|
/// - sherpaTags: Optional Sherpa tags for walkthrough highlighting.
|
|
public init(
|
|
balance: Int,
|
|
secondaryInfo: String? = nil,
|
|
secondaryIcon: String? = nil,
|
|
leadingButtons: [TopBarButton] = [],
|
|
onSettings: (() -> Void)? = nil,
|
|
onHelp: (() -> Void)? = nil,
|
|
onStats: (() -> Void)? = nil,
|
|
sherpaTags: TopBarSherpaTags<Tags>? = nil
|
|
) {
|
|
self.balance = balance
|
|
self.secondaryInfo = secondaryInfo
|
|
self.secondaryIcon = secondaryIcon
|
|
self.leadingButtons = leadingButtons
|
|
self.onSettings = onSettings
|
|
self.onHelp = onHelp
|
|
self.onStats = onStats
|
|
self.sherpaTags = sherpaTags
|
|
}
|
|
|
|
public var body: some View {
|
|
HStack {
|
|
// Balance display
|
|
balanceView
|
|
|
|
Spacer()
|
|
|
|
// Secondary info (centered)
|
|
if let info = secondaryInfo {
|
|
secondaryInfoView(info: info)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Toolbar buttons
|
|
toolbarButtonsView
|
|
}
|
|
.padding(.horizontal, CasinoDesign.Spacing.large)
|
|
.padding(.vertical, CasinoDesign.Spacing.small)
|
|
}
|
|
|
|
// MARK: - Private Views
|
|
|
|
@ViewBuilder
|
|
private var balanceView: some View {
|
|
let view = HStack(spacing: CasinoDesign.Spacing.xxSmall) {
|
|
Text("$")
|
|
.font(.system(size: dollarFontSize, weight: .bold))
|
|
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
|
|
|
Text(balance.formatted())
|
|
.font(.system(size: balanceFontSize, weight: .bold, design: .rounded))
|
|
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
|
.contentTransition(.numericText())
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(String(localized: "Balance", bundle: .module))
|
|
.accessibilityValue("$\(balance.formatted())")
|
|
|
|
if let tag = sherpaTags?.balance {
|
|
view.sherpaTag(tag)
|
|
} else {
|
|
view
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func secondaryInfoView(info: String) -> some View {
|
|
let view = HStack(spacing: CasinoDesign.Spacing.xSmall) {
|
|
if let icon = secondaryIcon {
|
|
Image(systemName: icon)
|
|
}
|
|
Text(info)
|
|
}
|
|
.font(.system(size: secondaryFontSize))
|
|
.foregroundStyle(Color.CasinoTopBar.secondaryText)
|
|
|
|
if let tag = sherpaTags?.cardsRemaining {
|
|
view.sherpaTag(tag)
|
|
} else {
|
|
view
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var toolbarButtonsView: some View {
|
|
HStack(spacing: CasinoDesign.Spacing.medium) {
|
|
// Custom leading buttons (game-specific)
|
|
ForEach(leadingButtons) { button in
|
|
ToolbarButton(icon: button.icon, action: button.action)
|
|
.accessibilityLabel(button.accessibilityLabel)
|
|
}
|
|
|
|
if let onStats = onStats {
|
|
let statsButton = ToolbarButton(icon: "chart.bar.fill", action: onStats)
|
|
.accessibilityLabel(String(localized: "Statistics", bundle: .module))
|
|
|
|
if let tag = sherpaTags?.stats {
|
|
statsButton.sherpaTag(tag)
|
|
} else {
|
|
statsButton
|
|
}
|
|
}
|
|
|
|
if let onHelp = onHelp {
|
|
let helpButton = ToolbarButton(icon: "info.circle", action: onHelp)
|
|
.accessibilityLabel(String(localized: "Rules", bundle: .module))
|
|
|
|
if let tag = sherpaTags?.rules {
|
|
helpButton.sherpaTag(tag)
|
|
} else {
|
|
helpButton
|
|
}
|
|
}
|
|
|
|
if let onSettings = onSettings {
|
|
let settingsButton = ToolbarButton(icon: "gearshape.fill", action: onSettings)
|
|
.accessibilityLabel(String(localized: "Settings", bundle: .module))
|
|
|
|
if let tag = sherpaTags?.settings {
|
|
settingsButton.sherpaTag(tag)
|
|
} else {
|
|
settingsButton
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convenience initializer for TopBarView without Sherpa tags.
|
|
public extension TopBarView where Tags == NoSherpaTags {
|
|
/// Creates a top bar without Sherpa walkthrough support.
|
|
init(
|
|
balance: Int,
|
|
secondaryInfo: String? = nil,
|
|
secondaryIcon: String? = nil,
|
|
leadingButtons: [TopBarButton] = [],
|
|
onSettings: (() -> Void)? = nil,
|
|
onHelp: (() -> Void)? = nil,
|
|
onStats: (() -> Void)? = nil
|
|
) {
|
|
self.balance = balance
|
|
self.secondaryInfo = secondaryInfo
|
|
self.secondaryIcon = secondaryIcon
|
|
self.leadingButtons = leadingButtons
|
|
self.onSettings = onSettings
|
|
self.onHelp = onHelp
|
|
self.onStats = onStats
|
|
self.sherpaTags = nil
|
|
}
|
|
}
|
|
|
|
/// A placeholder SherpaTags type for when no walkthrough is needed.
|
|
public enum NoSherpaTags: SherpaTags {
|
|
public func makeCallout() -> Callout {
|
|
.text("")
|
|
}
|
|
}
|
|
|
|
/// A single toolbar button.
|
|
struct ToolbarButton: View {
|
|
let icon: String
|
|
let action: () -> Void
|
|
|
|
private let iconSize: CGFloat = 20
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: iconSize))
|
|
.foregroundStyle(Color.CasinoTopBar.iconButton)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ZStack {
|
|
Color.CasinoTable.felt.ignoresSafeArea()
|
|
|
|
VStack {
|
|
TopBarView<NoSherpaTags>(
|
|
balance: 10_500,
|
|
secondaryInfo: "411",
|
|
secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill",
|
|
leadingButtons: [
|
|
TopBarButton(icon: "arrow.counterclockwise", accessibilityLabel: "Reset") {}
|
|
],
|
|
onSettings: {},
|
|
onHelp: {},
|
|
onStats: {}
|
|
)
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
|