Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
folium-app
GitHub Repository: folium-app/Folium
Path: blob/a-new-beginning/Folium-iOS/Classes/ControllerView/ControllerButton.swift
2 views
//
//  ControllerButton.swift
//  Folium-iOS
//
//  Created by Jarrod Norwell on 9/6/2024.
//

import Foundation
import GameController
import UIKit

protocol ControllerButtonDelegate {
    func touchBegan(with type: Button.`Type`, playerIndex: GCControllerPlayerIndex)
    func touchEnded(with type: Button.`Type`, playerIndex: GCControllerPlayerIndex)
    func touchMoved(with type: Button.`Type`, playerIndex: GCControllerPlayerIndex)
    
    func touchBegan(with type: Button.`Type`, playerIndex: GCControllerPlayerIndex) async
    func touchEnded(with type: Button.`Type`, playerIndex: GCControllerPlayerIndex) async
    func touchMoved(with type: Button.`Type`, playerIndex: GCControllerPlayerIndex) async
}

enum ButtonClass : String {
    case blurredButton = "blurredButton"
    case borderedButton = "borderedButton"
    case defaultButton = "defaultButton"
}

class ControllerButton : UIView {
    var button: Button
    var skin: Skin
    var delegate: ControllerButtonDelegate? = nil
    
    init(button: Button, skin: Skin, delegate: ControllerButtonDelegate? = nil) {
        self.button = button
        self.skin = skin
        self.delegate = delegate
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc func touchUpInside() {
        guard let delegate else {
            return
        }
        
        if [.grape, .guava].contains(skin.core) {
            delegate.touchEnded(with: button.type, playerIndex: .index1)
        } else {
            Task {
                await delegate.touchEnded(with: button.type, playerIndex: .index1)
            }
        }
    }
    
    @objc func touchDown() {
        guard let delegate else {
            return
        }
        
        if let vibrateOnTap = button.vibrateOnTap, vibrateOnTap {
            UIImpactFeedbackGenerator().impactOccurred()
        }
        
        if [.grape, .guava].contains(skin.core) {
            delegate.touchBegan(with: button.type, playerIndex: .index1)
        } else {
            Task {
                await delegate.touchBegan(with: button.type, playerIndex: .index1)
            }
        }
    }
}

class BlurredButton : ControllerButton {
    fileprivate var visualEffectView: UIVisualEffectView? = nil
    fileprivate var imageView: UIImageView? = nil
    fileprivate var label: UILabel? = nil
    
    override init(button: Button, skin: Skin, delegate: (any ControllerButtonDelegate)? = nil) {
        super.init(button: button, skin: skin, delegate: delegate)
        
        if #available(iOS 26, *) {
            var configuration: UIButton.Configuration = .glass()
            configuration.buttonSize = .medium
            configuration.cornerStyle = .capsule
            if [.l, .r, .zl, .zr].contains(button.type) || skin.core.nintendo && [.a, .b, .x, .y].contains(button.type) {
                configuration.attributedTitle = .init(button.letter(for: skin.core) ?? "", attributes: .init([
                    .font : UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title2)
                        .pointSize, weight: .medium)
                ]))
            } else {
                configuration.image = if [.minus, .home, .plus, .settings, .loadState, .saveState].contains(button.type) {
                    button.image(for: skin.core)?
                        .applyingSymbolConfiguration(.init(scale: .small))?
                        .applyingSymbolConfiguration(.init(weight: .bold))
                } else {
                    button.image(for: skin.core)?
                        .applyingSymbolConfiguration(.init(scale: .medium))?
                        .applyingSymbolConfiguration(.init(weight: .bold))
                }
            }
            
            let button: UIButton = .init(configuration: configuration)
            button.translatesAutoresizingMaskIntoConstraints = false
            button.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
            button.addTarget(self, action: #selector(touchDown), for: .touchDown)
            addSubview(button)
            
            button.topAnchor.constraint(equalTo: topAnchor).isActive = true
            button.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
            button.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
            button.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        } else {
            let effect: UIBlurEffect = .init(style: .systemMaterial)
            
            var configuration: UIButton.Configuration = .plain()
            configuration.baseForegroundColor = .label
            configuration.buttonSize = .medium
            configuration.cornerStyle = .capsule
            if [.l, .r, .zl, .zr].contains(button.type) || skin.core.nintendo && [.a, .b, .x, .y].contains(button.type) {
                configuration.attributedTitle = .init(button.letter(for: skin.core) ?? "", attributes: .init([
                    .font : UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title2)
                        .pointSize, weight: .medium)
                ]))
            } else {
                configuration.image = if [.minus, .home, .plus, .settings, .loadState, .saveState].contains(button.type) {
                    button.image(for: skin.core)?
                        .applyingSymbolConfiguration(.init(scale: .small))?
                        .applyingSymbolConfiguration(.init(weight: .bold))
                } else {
                    button.image(for: skin.core)?
                        .applyingSymbolConfiguration(.init(scale: .medium))?
                        .applyingSymbolConfiguration(.init(weight: .bold))
                }
            }
            configuration.background.visualEffect = effect
            
            let button: UIButton = .init(configuration: configuration)
            button.translatesAutoresizingMaskIntoConstraints = false
            button.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
            button.addTarget(self, action: #selector(touchDown), for: .touchDown)
            button.layer.shadowColor = UIColor.black.cgColor
            button.layer.shadowOpacity = 1 / 5
            button.layer.shadowRadius = 20
            button.layer.shadowOffset = .init(width: 0, height: 10)
            addSubview(button)
            
            button.topAnchor.constraint(equalTo: topAnchor).isActive = true
            button.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
            button.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
            button.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        }
    }
    
    @MainActor required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class BorderedButton : ControllerButton {
    override init(button: Button, skin: Skin, delegate: (any ControllerButtonDelegate)? = nil) {
        super.init(button: button, skin: skin, delegate: delegate)
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 2
    }
    
    @MainActor required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        switch button.type {
        case .l, .zl, .r, .zr:
            layer.cornerRadius = frame.height / 3
        default:
            layer.cornerRadius = frame.height / 2
        }
    }
}

class DefaultButton : ControllerButton {
    fileprivate var imageView: UIImageView? = nil
    
    override init(button: Button, skin: Skin, delegate: (any ControllerButtonDelegate)? = nil) {
        super.init(button: button, skin: skin, delegate: delegate)
        
        imageView = .init()
        guard let imageView else {
            return
        }
        
        if let debugging = skin.debugging, debugging {
            imageView.backgroundColor = .systemRed.withAlphaComponent(1 / 3)
        }
        
        if let backgroundImageName = button.backgroundImageName, let url = skin.url {
            imageView.image = .init(contentsOfFile: url
                .appendingPathComponent("buttons", conformingTo: .folder)
                .appendingPathComponent(backgroundImageName, conformingTo: .fileURL)
                .path
            )
        } else {
            if let transparent = button.transparent, transparent {
                imageView.image = nil
            } else {
                imageView.image = button.image(for: skin.core)?
                    .applyingSymbolConfiguration(.init(paletteColors: [.black, .white]))
            }
        }
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFill
        addSubview(imageView)
        
        imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
    }
    
    @MainActor required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}