package com.pixelmintdrop.ui import android.content.Context import android.graphics.Color import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.GridLayout import android.widget.TextView import androidx.cardview.widget.CardView import com.pixelmintdrop.R import com.pixelmintdrop.model.PlayerProgressionManager import android.graphics.drawable.GradientDrawable import android.util.Log /** * UI component for selecting game themes */ class ThemeSelector @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { private val themesGrid: GridLayout private val availableThemesLabel: TextView // Callback when a theme is selected var onThemeSelected: ((String) -> Unit)? = null // Currently selected theme (persisted) private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC // Theme cards map (themeId -> CardView) private val themeCards = mutableMapOf() // Ordered list of theme IDs for navigation private val themeIdList = mutableListOf() // Currently focused theme ID (for gamepad navigation within the selector) private var focusedThemeId: String? = null // Index of the currently focused theme in themeIdList private var focusedIndex: Int = -1 // Flag indicating if the entire selector component has focus from the main menu private var hasComponentFocus: Boolean = false init { // Inflate the layout LayoutInflater.from(context).inflate(R.layout.theme_selector, this, true) // Get references to views themesGrid = findViewById(R.id.themes_grid) availableThemesLabel = findViewById(R.id.available_themes_label) } /** * Update the theme selector with unlocked themes */ fun updateThemes(unlockedThemes: Set, currentTheme: String) { // Clear existing theme cards and ID list themesGrid.removeAllViews() themeCards.clear() themeIdList.clear() // Update selected theme selectedTheme = currentTheme focusedThemeId = currentTheme // Initially focus the selected theme focusedIndex = -1 // Reset index // Get all possible themes and their details, sorted for consistent order val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value } // Add theme cards to the grid and build the ID list allThemes.forEach { (themeId, themeInfo) -> val isUnlocked = unlockedThemes.contains(themeId) val isSelected = themeId == selectedTheme // Only add unlocked themes to the navigable list if (isUnlocked) { themeIdList.add(themeId) } val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected) themeCards[themeId] = themeCard themesGrid.addView(themeCard) // Update focused index if this is the currently selected/focused theme if (isUnlocked && themeId == focusedThemeId) { focusedIndex = themeIdList.indexOf(themeId) } } // Ensure focus index is valid if the previously focused theme is no longer available/unlocked if (focusedIndex == -1 && themeIdList.isNotEmpty()) { focusedIndex = 0 focusedThemeId = themeIdList[0] } // Apply initial focus highlight if the component has focus highlightFocusedCard() } /** * Create a card for a theme */ private fun createThemeCard( themeId: String, themeInfo: ThemeInfo, isUnlocked: Boolean, isSelected: Boolean ): CardView { // Create the card val card = CardView(context).apply { id = View.generateViewId() radius = 12f cardElevation = if (isSelected) 8f else 2f useCompatPadding = true // Set card background color based on theme setCardBackgroundColor(themeInfo.primaryColor) // Set card dimensions val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size) layoutParams = GridLayout.LayoutParams().apply { width = cardSize height = cardSize columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f) rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f) setMargins(8, 8, 8, 8) } // Apply locked/selected state visuals alpha = if (isUnlocked) 1.0f else 0.5f } // Create theme content container val container = FrameLayout(context).apply { layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) } // Create the theme preview val themePreview = View(context).apply { layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) // Set the background color setBackgroundColor(themeInfo.primaryColor) } // Add a label with the theme name val themeLabel = TextView(context).apply { text = themeInfo.displayName setTextColor(themeInfo.textColor) textSize = 14f textAlignment = View.TEXT_ALIGNMENT_CENTER // Position at the bottom of the card layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ).apply { gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL setMargins(4, 4, 4, 8) } } // Add level requirement for locked themes val levelRequirement = TextView(context).apply { text = "Level ${themeInfo.unlockLevel}" setTextColor(Color.WHITE) textSize = 12f textAlignment = View.TEXT_ALIGNMENT_CENTER visibility = if (isUnlocked) View.GONE else View.VISIBLE // Position at the center of the card layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT ).apply { gravity = android.view.Gravity.CENTER } // Make text bold and more visible for better readability typeface = android.graphics.Typeface.DEFAULT_BOLD setShadowLayer(3f, 1f, 1f, Color.BLACK) } // Add a lock icon if the theme is locked val lockOverlay = View(context).apply { layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) // Add lock icon or visual indicator setBackgroundResource(R.drawable.lock_overlay) visibility = if (isUnlocked) View.GONE else View.VISIBLE } // Add all elements to container container.addView(themePreview) container.addView(themeLabel) container.addView(lockOverlay) container.addView(levelRequirement) // Add container to card card.addView(container) // Set up click listener only for unlocked themes if (isUnlocked) { card.setOnClickListener { // Clicking directly selects the theme Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)") focusedThemeId = themeId focusedIndex = themeIdList.indexOf(themeId) confirmSelection() // Directly confirm click selection } } return card } /** * Data class for theme information */ data class ThemeInfo( val displayName: String, val primaryColor: Int, val secondaryColor: Int, val textColor: Int, val unlockLevel: Int ) /** * Get all available themes with their details */ private fun getThemes(): Map { return mapOf( PlayerProgressionManager.THEME_CLASSIC to ThemeInfo( displayName = "Classic", primaryColor = Color.parseColor("#000000"), secondaryColor = Color.parseColor("#1F1F1F"), textColor = Color.WHITE, unlockLevel = 1 ), PlayerProgressionManager.THEME_NEON to ThemeInfo( displayName = "Neon", primaryColor = Color.parseColor("#0D0221"), secondaryColor = Color.parseColor("#650D89"), textColor = Color.parseColor("#FF00FF"), unlockLevel = 5 ), PlayerProgressionManager.THEME_MONOCHROME to ThemeInfo( displayName = "Monochrome", primaryColor = Color.parseColor("#1A1A1A"), secondaryColor = Color.parseColor("#333333"), textColor = Color.LTGRAY, unlockLevel = 10 ), PlayerProgressionManager.THEME_RETRO to ThemeInfo( displayName = "Retro", primaryColor = Color.parseColor("#3F2832"), secondaryColor = Color.parseColor("#087E8B"), textColor = Color.parseColor("#FF5A5F"), unlockLevel = 15 ), PlayerProgressionManager.THEME_MINIMALIST to ThemeInfo( displayName = "Minimalist", primaryColor = Color.parseColor("#FFFFFF"), secondaryColor = Color.parseColor("#F0F0F0"), textColor = Color.BLACK, unlockLevel = 20 ), PlayerProgressionManager.THEME_GALAXY to ThemeInfo( displayName = "Galaxy", primaryColor = Color.parseColor("#0B0C10"), secondaryColor = Color.parseColor("#1F2833"), textColor = Color.parseColor("#66FCF1"), unlockLevel = 25 ) ) } /** * Sets whether the entire component has focus (from the parent menu). * Controls the outer border visibility. */ fun setHasFocus(hasFocus: Boolean) { hasComponentFocus = hasFocus // Update visual state based on component focus highlightFocusedCard() // Re-apply highlights } /** * Moves the internal focus to the next available theme. */ fun focusNextItem() { if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus focusedIndex = (focusedIndex + 1) % themeIdList.size focusedThemeId = themeIdList[focusedIndex] highlightFocusedCard() } /** * Moves the internal focus to the previous available theme. */ fun focusPreviousItem() { if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus focusedIndex = (focusedIndex - 1 + themeIdList.size) % themeIdList.size focusedThemeId = themeIdList[focusedIndex] highlightFocusedCard() } /** * Confirms the currently focused theme as the selected theme. * Triggers the onThemeSelected callback. */ fun confirmSelection() { Log.d("ThemeSelector", "confirmSelection called. Focused theme: $focusedThemeId") if (focusedThemeId == null || focusedThemeId == selectedTheme) { // No change needed if nothing is focused, // or the focused item is already selected return } // Update the selected theme selectedTheme = focusedThemeId!! // Update visual states for all cards highlightFocusedCard() // This will now mark the new theme as selected // Trigger the callback onThemeSelected?.invoke(selectedTheme) } /** * Updates the visual highlight state of the theme cards based on * selection and internal focus. */ private fun highlightFocusedCard() { if (themeCards.isEmpty()) return val focusColor = Color.YELLOW // Color for the focused-but-not-selected item val selectedColor = Color.WHITE // Color for the selected item (might be focused or not) themeCards.forEach { (themeId, card) -> val themeInfo = getThemes()[themeId] ?: return@forEach val isUnlocked = themeIdList.contains(themeId) // Check if it's in the navigable list if (!isUnlocked) { // Keep locked themes visually distinct card.alpha = 0.5f card.cardElevation = 2f card.background = null // Remove any border/background card.setCardBackgroundColor(themeInfo.primaryColor) return@forEach // Skip further styling for locked themes } // Reset unlocked cards first card.alpha = 1.0f card.cardElevation = 4f // Default elevation for unlocked cards card.background = null card.setCardBackgroundColor(themeInfo.primaryColor) card.scaleX = 1.0f card.scaleY = 1.0f val isSelected = (themeId == selectedTheme) val isFocused = (hasComponentFocus && themeId == focusedThemeId) var borderColor = Color.TRANSPARENT var borderWidth = 0 var elevation = 4f var scale = 1.0f if (isSelected) { borderColor = selectedColor borderWidth = 6 // Thick border for selected elevation = 12f // Higher elevation for selected } if (isFocused) { // Focused item gets a distinct border (unless it's also selected) if (!isSelected) { borderColor = focusColor borderWidth = 4 // Slightly thinner border for focused } elevation = 12f // Use high elevation for focus too scale = 1.1f // Scale up the focused item } // Apply scale card.scaleX = scale card.scaleY = scale // Apply border and elevation if (borderWidth > 0) { val gradientDrawable = GradientDrawable().apply { setColor(themeInfo.primaryColor) setStroke(borderWidth, borderColor) cornerRadius = 12f // Keep consistent corner radius } card.background = gradientDrawable } else { card.background = null // Ensure no border if not selected/focused } card.cardElevation = elevation } } }