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/LatestControllerThumbstick.swift
2 views
//
//  LatestControllerThumbstick.swift
//  Folium-iOS
//
//  Created by Jarrod Norwell on 23/10/2025.
//

import ColourKit
import Foundation
import GameController
import UIKit

enum LatestThumbstick : Int {
    case left, right
}

protocol LatestControllerThumbstickDelegate {
    func touchBegan(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex)
    func touchEnded(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex)
    func touchMoved(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex)
    
    func touchBegan(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async
    func touchEnded(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async
    func touchMoved(with thumbstick: LatestThumbstick, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async
}

class LatestControllerThumbstick : UIView {
    var centerXConstraint: NSLayoutConstraint? = nil,
        centerYConstraint: NSLayoutConstraint? = nil,
        widthConstraint: NSLayoutConstraint? = nil,
        heightConstraint: NSLayoutConstraint? = nil
    
    private var stickView: UIView? = nil
    
    var thumbstick: LatestThumbstick
    var delegate: LatestControllerThumbstickDelegate? = nil
    init(_ thumbstick: LatestThumbstick, _ delegate: LatestControllerThumbstickDelegate? = nil, _ multiplier: CGFloat = 1 / 3) {
        self.thumbstick = thumbstick
        self.delegate = delegate
        super.init(frame: .zero)
        
        stickView = if #available(iOS 26, *) {
            UIVisualEffectView(effect: UIGlassEffect(style: .regular))
        } else {
            UIView()
        }
        guard let stickView else {
            return
        }
        stickView.translatesAutoresizingMaskIntoConstraints = false
        if #unavailable(iOS 26) {
            stickView.backgroundColor = .label
            stickView.layer.cornerCurve = .continuous
            
            stickView.layer.shadowColor = UIColour.black.cgColor
            stickView.layer.shadowOpacity = 1 / 5
            stickView.layer.shadowRadius = 20
            stickView.layer.shadowOffset = .init(width: 0, height: 10)
        }
        addSubview(stickView)
        
        centerXConstraint = stickView.centerXAnchor.constraint(equalTo: centerXAnchor)
        guard let centerXConstraint else {
            return
        }
        centerXConstraint.isActive = true
        
        centerYConstraint = stickView.centerYAnchor.constraint(equalTo: centerYAnchor)
        guard let centerYConstraint else {
            return
        }
        centerYConstraint.isActive = true
        
        widthConstraint = stickView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multiplier, constant: -8)
        guard let widthConstraint else {
            return
        }
        widthConstraint.isActive = true
        
        heightConstraint = stickView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: multiplier, constant: -8)
        guard let heightConstraint else {
            return
        }
        heightConstraint.isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if let stickView {
            if #available(iOS 26, *) {
                stickView.cornerConfiguration = .capsule()
            } else {
                stickView.layer.cornerRadius = stickView.frame.height / 2
            }
        }
    }
    
    private func position(in view: UIView, with location: CGPoint) -> (x: Float, y: Float) {
        let radius: CGFloat = .init(view.frame.width / 2)
        let x: Float = .init((location.x - radius) / radius)
        let y: Float = .init((location.y - radius) / radius)
        return (x, -y)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        guard let delegate, let touch = touches.first else {
            return
        }
        
        let location: CGPoint = touch.location(in: self)
        let position: (x: Float, y: Float) = position(in: self, with: location)
        
        delegate.touchBegan(with: thumbstick, position: position, playerIndex: .index1)
        Task {
            await delegate.touchBegan(with: thumbstick, position: position, playerIndex: .index1)
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        guard let delegate,
              let centerXConstraint,
              let centerYConstraint else {
            return
        }
        
        centerXConstraint.constant = 0
        centerYConstraint.constant = 0
        UIView.animate(withDuration: 1 / 3) {
            self.layoutIfNeeded()
        }
        
        let position: (x: Float, y: Float) = (0, 0)
        
        delegate.touchEnded(with: thumbstick, position: position, playerIndex: .index1)
        Task {
            await delegate.touchEnded(with: thumbstick, position: position, playerIndex: .index1)
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
        guard let delegate,
              let centerXConstraint,
              let centerYConstraint,
              let touch = touches.first else {
            return
        }
        
        let halfHeight: CGFloat = frame.height / 2
        let halfWidth: CGFloat = frame.width / 2
        
        let touchLocation: CGPoint = touch.location(in: self)
        
        var dx: CGFloat = touchLocation.x - halfWidth
        var dy: CGFloat = touchLocation.y - halfHeight

        let maximumRadius: CGFloat = min(halfWidth, halfHeight) * 0.9
        
        let distance: CGFloat = sqrt(dx * dx + dy * dy)
        if distance > maximumRadius {
            let scale: CGFloat = maximumRadius / distance
            dx *= scale
            dy *= scale
        }
        
        centerXConstraint.constant = dx
        centerYConstraint.constant = dy
        layoutIfNeeded()
        
        let normalizedX: Float = max(-1, min(1, .init(dx / maximumRadius)))
        let normalizedY: Float = max(-1, min(1, .init(-dy / maximumRadius)))
        
        let position: (x: Float, y: Float) = (normalizedX, normalizedY)

        delegate.touchMoved(with: thumbstick, position: position, playerIndex: .index1)
        Task {
            await delegate.touchMoved(with: thumbstick, position: position, playerIndex: .index1)
        }
    }
}