Skip to content

Instantly share code, notes, and snippets.

@jarrodnorwell
Created October 18, 2023 08:19
Show Gist options
  • Save jarrodnorwell/9ffe674e68c5b3b7c5b355fbbb67bcc3 to your computer and use it in GitHub Desktop.
Save jarrodnorwell/9ffe674e68c5b3b7c5b355fbbb67bcc3 to your computer and use it in GitHub Desktop.
LMVirtualControllerView is a custom on-screen (virtual) controller replacement for GCVirualController used in emuThreeDS/Limón
//
// ABXYButton.swift
// Limon
//
// Created by Jarrod Norwell on 10/18/23.
//
import Foundation
import UIKit
protocol ABXYButtonDelegate {
func touchDown(_ buttonType: ABXYButton.ButtonType)
func touchUpInside(_ buttonType: ABXYButton.ButtonType)
}
class ABXYButton : UIView {
enum ButtonType : String {
case a = "a", b = "b", x = "x", y = "y"
var color: UIColor {
switch self {
case .a:
.systemRed
case .b:
.systemYellow
case .x:
.systemBlue
case .y:
.systemGreen
}
}
var systemName: String {
"\(rawValue).circle.fill"
}
var letter: String {
rawValue
}
}
var buttonType: ButtonType
var letterImageView: UIImageView!
init(_ buttonType: ButtonType) {
self.buttonType = buttonType
super.init(frame: .zero)
letterImageView = .init(image: .init(systemName: buttonType.systemName)?
.applyingSymbolConfiguration(.init(pointSize: 40, weight: .regular, scale: .large))?
.applyingSymbolConfiguration(.init(paletteColors: [
.white, buttonType.color
]))
)
letterImageView.translatesAutoresizingMaskIntoConstraints = false
letterImageView.contentMode = .scaleAspectFill
addSubview(letterImageView)
addConstraints([
letterImageView.topAnchor.constraint(equalTo: topAnchor),
letterImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
letterImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
letterImageView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//
// BumperTriggerButton.swift
// Limon
//
// Created by Jarrod Norwell on 10/18/23.
//
import Foundation
import UIKit
protocol BumperTriggerButtonDelegate {
func touchDown(_ buttonType: BumperTriggerButton.ButtonType)
func touchUpInside(_ buttonType: BumperTriggerButton.ButtonType)
}
class BumperTriggerButton : UIView {
enum ButtonType : String {
case l = "l.button.roundedbottom.horizontal", zl = "zl.button.roundedtop.horizontal", r = "r.button.roundedbottom.horizontal", zr = "zr.button.roundedtop.horizontal"
var color: UIColor {
.systemGray
}
var systemName: String {
"\(rawValue).fill"
}
var letter: String {
rawValue
}
}
var buttonType: ButtonType
var letterImageView: UIImageView!
init(_ buttonType: ButtonType) {
self.buttonType = buttonType
super.init(frame: .zero)
letterImageView = .init(image: .init(systemName: buttonType.systemName)?
.applyingSymbolConfiguration(.init(pointSize: 40, weight: .regular, scale: .large))?
.applyingSymbolConfiguration(.init(paletteColors: [
.white, buttonType.color
]))
)
letterImageView.translatesAutoresizingMaskIntoConstraints = false
letterImageView.contentMode = .scaleAspectFill
addSubview(letterImageView)
addConstraints([
letterImageView.topAnchor.constraint(equalTo: topAnchor),
letterImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
letterImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
letterImageView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//
// LDURButton.swift
// Limon
//
// Created by Jarrod Norwell on 10/18/23.
//
import Foundation
import UIKit
protocol LDURButtonDelegate {
func touchDown(_ buttonType: LDURButton.ButtonType)
func touchUpInside(_ buttonType: LDURButton.ButtonType)
}
class LDURButton : UIView {
enum ButtonType : String {
case left = "arrowtriangle.left", down = "arrowtriangle.down", up = "arrowtriangle.up", right = "arrowtriangle.right"
var color: UIColor {
.systemGray
}
var systemName: String {
"\(rawValue).circle.fill"
}
var letter: String {
rawValue
}
}
var buttonType: ButtonType
var letterImageView: UIImageView!
init(_ buttonType: ButtonType) {
self.buttonType = buttonType
super.init(frame: .zero)
letterImageView = .init(image: .init(systemName: buttonType.systemName)?
.applyingSymbolConfiguration(.init(pointSize: 40, weight: .regular, scale: .large))?
.applyingSymbolConfiguration(.init(paletteColors: [
.white, buttonType.color
]))
)
letterImageView.translatesAutoresizingMaskIntoConstraints = false
letterImageView.contentMode = .scaleAspectFill
addSubview(letterImageView)
addConstraints([
letterImageView.topAnchor.constraint(equalTo: topAnchor),
letterImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
letterImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
letterImageView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//
// LMVirtualControllerView.swift
// Limon
//
// Created by Jarrod Norwell on 10/9/23.
//
import Foundation
import UIKit
class LMVirtualControllerView : UIView {
var abxyButtonDelegate: ABXYButtonDelegate?
var ldurButtonDelegate: LDURButtonDelegate?
var selectStartButtonDelegate: SelectStartButtonDelegate?
var bumperTriggerButtonDelegate: BumperTriggerButtonDelegate?
var aButton, bButton, xButton, yButton: ABXYButton!
var leftButton, downButton, upButton, rightButton: LDURButton!
var selectButton, startButton: SelectStartButton!
var lButton, zlButton, rButton, zrButton: BumperTriggerButton!
enum State {
case activated, deactivated
}
var state: State = .activated
func addAButton() {
aButton = .init(.a)
aButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(aButton)
addConstraints([
aButton.widthAnchor.constraint(equalToConstant: 50),
aButton.heightAnchor.constraint(equalToConstant: 50),
aButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -88),
aButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20)
])
}
func addBButton() {
bButton = .init(.b)
bButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(bButton)
addConstraints([
bButton.widthAnchor.constraint(equalToConstant: 50),
bButton.heightAnchor.constraint(equalToConstant: 50),
bButton.topAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.bottomAnchor),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor)
])
}
func addXButton() {
xButton = .init(.x)
xButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(xButton)
addConstraints([
xButton.widthAnchor.constraint(equalToConstant: 50),
xButton.heightAnchor.constraint(equalToConstant: 50),
xButton.bottomAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.topAnchor),
xButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor)
])
}
func addYButton() {
yButton = .init(.y)
yButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(yButton)
addConstraints([
yButton.widthAnchor.constraint(equalToConstant: 50),
yButton.heightAnchor.constraint(equalToConstant: 50),
yButton.topAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.topAnchor),
yButton.trailingAnchor.constraint(equalTo: xButton.safeAreaLayoutGuide.leadingAnchor)
])
}
func addLeftButton() {
leftButton = .init(.left)
leftButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(leftButton)
addConstraints([
leftButton.widthAnchor.constraint(equalToConstant: 50),
leftButton.heightAnchor.constraint(equalToConstant: 50),
leftButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -88),
leftButton.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20)
])
}
func addDownButton() {
downButton = .init(.down)
downButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(downButton)
addConstraints([
downButton.widthAnchor.constraint(equalToConstant: 50),
downButton.heightAnchor.constraint(equalToConstant: 50),
downButton.topAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.bottomAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor)
])
}
func addUpButton() {
upButton = .init(.up)
upButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(upButton)
addConstraints([
upButton.widthAnchor.constraint(equalToConstant: 50),
upButton.heightAnchor.constraint(equalToConstant: 50),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor)
])
}
func addRightButton() {
rightButton = .init(.right)
rightButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(rightButton)
addConstraints([
rightButton.widthAnchor.constraint(equalToConstant: 50),
rightButton.heightAnchor.constraint(equalToConstant: 50),
rightButton.topAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
rightButton.leadingAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.trailingAnchor)
])
}
func addSelectButton() {
selectButton = .init(.select)
selectButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(selectButton)
addConstraints([
selectButton.widthAnchor.constraint(equalToConstant: 40),
selectButton.heightAnchor.constraint(equalToConstant: 40),
selectButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20),
selectButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor, constant: -10)
])
}
func addStartButton() {
startButton = .init(.start)
startButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(startButton)
addConstraints([
startButton.widthAnchor.constraint(equalToConstant: 40),
startButton.heightAnchor.constraint(equalToConstant: 40),
startButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20),
startButton.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor, constant: 10)
])
}
func addLButton() {
lButton = .init(.l)
lButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(lButton)
addConstraints([
lButton.widthAnchor.constraint(equalToConstant: 56),
lButton.heightAnchor.constraint(equalToConstant: 48),
lButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor, constant: -20),
lButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.leadingAnchor)
])
}
func addZLButton() {
zlButton = .init(.zl)
zlButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(zlButton)
addConstraints([
zlButton.widthAnchor.constraint(equalToConstant: 56),
zlButton.heightAnchor.constraint(equalToConstant: 48),
zlButton.topAnchor.constraint(equalTo: lButton.safeAreaLayoutGuide.topAnchor),
zlButton.leadingAnchor.constraint(equalTo: lButton.safeAreaLayoutGuide.trailingAnchor, constant: 20)
])
}
func addRButton() {
rButton = .init(.r)
rButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(rButton)
addConstraints([
rButton.widthAnchor.constraint(equalToConstant: 56),
rButton.heightAnchor.constraint(equalToConstant: 48),
rButton.bottomAnchor.constraint(equalTo: xButton.safeAreaLayoutGuide.topAnchor, constant: -20),
rButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.trailingAnchor)
])
}
func addZRButton() {
zrButton = .init(.zr)
zrButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(zrButton)
addConstraints([
zrButton.widthAnchor.constraint(equalToConstant: 56),
zrButton.heightAnchor.constraint(equalToConstant: 48),
zrButton.topAnchor.constraint(equalTo: rButton.safeAreaLayoutGuide.topAnchor),
zrButton.trailingAnchor.constraint(equalTo: rButton.safeAreaLayoutGuide.leadingAnchor, constant: -20)
])
}
func hide() {
state = .deactivated
aButton.letterImageView.addSymbolEffect(.disappear)
bButton.letterImageView.addSymbolEffect(.disappear)
xButton.letterImageView.addSymbolEffect(.disappear)
yButton.letterImageView.addSymbolEffect(.disappear)
leftButton.letterImageView.addSymbolEffect(.disappear)
downButton.letterImageView.addSymbolEffect(.disappear)
upButton.letterImageView.addSymbolEffect(.disappear)
rightButton.letterImageView.addSymbolEffect(.disappear)
selectButton.letterImageView.addSymbolEffect(.disappear)
startButton.letterImageView.addSymbolEffect(.disappear)
lButton.letterImageView.addSymbolEffect(.disappear)
zlButton.letterImageView.addSymbolEffect(.disappear)
rButton.letterImageView.addSymbolEffect(.disappear)
zrButton.letterImageView.addSymbolEffect(.disappear)
}
func show() {
state = .activated
aButton.letterImageView.addSymbolEffect(.appear)
bButton.letterImageView.addSymbolEffect(.appear)
xButton.letterImageView.addSymbolEffect(.appear)
yButton.letterImageView.addSymbolEffect(.appear)
leftButton.letterImageView.addSymbolEffect(.appear)
downButton.letterImageView.addSymbolEffect(.appear)
upButton.letterImageView.addSymbolEffect(.appear)
rightButton.letterImageView.addSymbolEffect(.appear)
selectButton.letterImageView.addSymbolEffect(.appear)
startButton.letterImageView.addSymbolEffect(.appear)
lButton.letterImageView.addSymbolEffect(.appear)
zlButton.letterImageView.addSymbolEffect(.appear)
rButton.letterImageView.addSymbolEffect(.appear)
zrButton.letterImageView.addSymbolEffect(.appear)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
state == .activated ? super.hitTest(point, with: event) == self ? nil : super.hitTest(point, with: event) : nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard state == .activated, let abxyButtonDelegate, let ldurButtonDelegate, let selectStartButtonDelegate, let bumperTriggerButtonDelegate, let touch = touches.first else {
return
}
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
switch touch.view {
case aButton:
abxyButtonDelegate.touchDown(.a)
case bButton:
abxyButtonDelegate.touchDown(.b)
case xButton:
abxyButtonDelegate.touchDown(.x)
case yButton:
abxyButtonDelegate.touchDown(.y)
case leftButton:
ldurButtonDelegate.touchDown(.left)
case downButton:
ldurButtonDelegate.touchDown(.down)
case upButton:
ldurButtonDelegate.touchDown(.up)
case rightButton:
ldurButtonDelegate.touchDown(.right)
case selectButton:
selectStartButtonDelegate.touchDown(.select)
case startButton:
selectStartButtonDelegate.touchDown(.start)
case lButton:
bumperTriggerButtonDelegate.touchDown(.l)
case zlButton:
bumperTriggerButtonDelegate.touchDown(.zl)
case rButton:
bumperTriggerButtonDelegate.touchDown(.r)
case zrButton:
bumperTriggerButtonDelegate.touchDown(.zr)
default:
break
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard state == .activated, let abxyButtonDelegate, let ldurButtonDelegate, let selectStartButtonDelegate, let bumperTriggerButtonDelegate, let touch = touches.first else {
return
}
switch touch.view {
case aButton:
abxyButtonDelegate.touchUpInside(.a)
case bButton:
abxyButtonDelegate.touchUpInside(.b)
case xButton:
abxyButtonDelegate.touchUpInside(.x)
case yButton:
abxyButtonDelegate.touchUpInside(.y)
case leftButton:
ldurButtonDelegate.touchUpInside(.left)
case downButton:
ldurButtonDelegate.touchUpInside(.down)
case upButton:
ldurButtonDelegate.touchUpInside(.up)
case rightButton:
ldurButtonDelegate.touchUpInside(.right)
case selectButton:
selectStartButtonDelegate.touchUpInside(.select)
case startButton:
selectStartButtonDelegate.touchUpInside(.start)
case lButton:
bumperTriggerButtonDelegate.touchUpInside(.l)
case zlButton:
bumperTriggerButtonDelegate.touchUpInside(.zl)
case rButton:
bumperTriggerButtonDelegate.touchUpInside(.r)
case zrButton:
bumperTriggerButtonDelegate.touchUpInside(.zr)
default:
break
}
}
}
//
// SelectStartButton.swift
// Limon
//
// Created by Jarrod Norwell on 10/18/23.
//
import Foundation
import UIKit
protocol SelectStartButtonDelegate {
func touchDown(_ buttonType: SelectStartButton.ButtonType)
func touchUpInside(_ buttonType: SelectStartButton.ButtonType)
}
class SelectStartButton : UIView {
enum ButtonType : String {
case select = "minus", start = "plus"
var color: UIColor {
.systemGray
}
var systemName: String {
"\(rawValue).circle.fill"
}
var letter: String {
rawValue
}
}
var buttonType: ButtonType
var letterImageView: UIImageView!
init(_ buttonType: ButtonType) {
self.buttonType = buttonType
super.init(frame: .zero)
letterImageView = .init(image: .init(systemName: buttonType.systemName)?
.applyingSymbolConfiguration(.init(pointSize: 30, weight: .regular, scale: .medium))?
.applyingSymbolConfiguration(.init(paletteColors: [
.white, buttonType.color
]))
)
letterImageView.translatesAutoresizingMaskIntoConstraints = false
letterImageView.contentMode = .scaleAspectFill
addSubview(letterImageView)
addConstraints([
letterImageView.topAnchor.constraint(equalTo: topAnchor),
letterImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
letterImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
letterImageView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment