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)
}
}
}