Path: blob/a-new-beginning/Folium-iOS/Controllers/SettingsControllers/CytrusSettingsController.swift
2 views
//
// CytrusSettingsController.swift
// Folium-iOS
//
// Created by Jarrod Norwell on 26/7/2025.
//
import Cytrus
import Foundation
import SettingsKit
import UIKit
enum CytrusSettingsHeaders : String, CaseIterable {
case core = "Core"
case dataStorage = "Data Storage"
case debugging = "Debugging"
case system = "System"
case systemSaveGame = "System Save Game"
case renderer = "Renderer"
case defaultLayout = "Default Layout"
case customLayout = "Custom Layout"
case audio = "Audio"
case miscellaneous = "Miscellaneous"
case destructive = "Destructive"
var header: SettingHeader {
switch self {
case .core,
.dataStorage,
.debugging,
.system,
.systemSaveGame,
.renderer,
.defaultLayout,
.audio,
.miscellaneous:
SettingHeader(text: rawValue)
case .customLayout:
SettingHeader(text: rawValue,
secondaryText: "Set Default Layout to Custom Layout")
case .destructive:
SettingHeader(text: "")
}
}
static var allHeaders: [SettingHeader] { allCases.map { $0.header } }
}
enum CytrusSettingsItems : String, CaseIterable {
// Destructive
case resetSettings = "cytrus.v1.38.resetSettings"
// Core
case mode = "cytrus.v1.38.mode"
case cpuJIT = "cytrus.v1.38.cpuJIT"
case cpuClockPercentage = "cytrus.v1.38.cpuClockPercentage"
case new3DS = "cytrus.v1.38.new3DS"
case lleApplets = "cytrus.v1.38.lleApplets"
case deterministicAsyncOperations = "cytrus.v1.38.deterministicAsyncOperations"
case enableRequiredOnlineLLEModules = "cytrus.v1.38.enableRequiredOnlineLLEModules"
// Data Storage
case compressCIAInstalls = "cytrus.v1.38.compressCIAInstalls"
// System
case regionValue = "cytrus.v1.38.regionValue"
case pluginLoader = "cytrus.v1.38.pluginLoader"
case allowPluginLoader = "cytrus.v1.38.allowPluginLoader"
case stepsPerHour = "cytrus.v1.38.stepsPerHour"
case applyRegionFreePatch = "cytrus.v1.38.applyRegionFreePatch"
// Renderer
case spirvShaderGeneration = "cytrus.v1.38.spirvShaderGeneration"
case disableSpirvOptimizer = "cytrus.v1.38.disableSpirvOptimizer"
case useAsyncShaderCompilation = "cytrus.v1.38.useAsyncShaderCompilation"
case useAsyncPresentation = "cytrus.v1.38.useAsyncPresentation"
case useHardwareShaders = "cytrus.v1.38.useHardwareShaders"
case useDiskShaderCache = "cytrus.v1.38.useDiskShaderCache"
case useShadersAccurateMul = "cytrus.v1.38.useShadersAccurateMul"
case useNewVSync = "cytrus.v1.38.useNewVSync"
case useShaderJIT = "cytrus.v1.38.useShaderJIT"
case resolutionFactor = "cytrus.v1.38.resolutionFactor"
case textureFilter = "cytrus.v1.38.textureFilter"
case textureSampling = "cytrus.v1.38.textureSampling"
case delayGameRenderThreadUS = "cytrus.v1.38.delayGameRenderThreadUS"
case layoutOption = "cytrus.v1.38.layoutOption"
case customTopX = "cytrus.v1.38.customTopX"
case customTopY = "cytrus.v1.38.customTopY"
case customTopWidth = "cytrus.v1.38.customTopWidth"
case customTopHeight = "cytrus.v1.38.customTopHeight"
case customBottomX = "cytrus.v1.38.customBottomX"
case customBottomY = "cytrus.v1.38.customBottomY"
case customBottomWidth = "cytrus.v1.38.customBottomWidth"
case customBottomHeight = "cytrus.v1.38.customBottomHeight"
case customSecondLayerOpacity = "cytrus.v1.38.customSecondLayerOpacity"
case aspectRatio = "cytrus.v1.38.aspectRatio"
case render3D = "cytrus.v1.38.render3D"
case factor3D = "cytrus.v1.38.factor3D"
case monoRender = "cytrus.v1.38.monoRender"
case filterMode = "cytrus.v1.38.filterMode"
case ppShaderName = "cytrus.v1.38.ppShaderName"
case anaglyphShaderName = "cytrus.v1.38.anaglyphShaderName"
case dumpTextures = "cytrus.v1.38.dumpTextures"
case customTextures = "cytrus.v1.38.customTextures"
case preloadTextures = "cytrus.v1.38.preloadTextures"
case asyncCustomLoading = "cytrus.v1.38.asyncCustomLoading"
case disableRightEyeRender = "cytrus.v1.38.disableRightEyeRender"
// Audio
case audioMuted = "cytrus.v1.38.audioMuted"
case audioEmulation = "cytrus.v1.38.audioEmulation"
case audioStretching = "cytrus.v1.38.audioStretching"
case realtimeAudio = "cytrus.v1.38.realtimeAudio"
case volume = "cytrus.v1.38.volume"
case outputType = "cytrus.v1.38.outputType"
case inputType = "cytrus.v1.38.inputType"
// Miscellaneous
case logLevel = "cytrus.v1.38.logLevel"
case webAPIURL = "cytrus.v1.38.webAPIURL"
case systemLanguage = "cytrus.v1.38.systemLanguage"
case username = "cytrus.v1.38.username"
var title: String {
switch self {
case .resetSettings:
"Reset Settings"
case .mode:
"Emulation Mode"
case .cpuJIT:
"CPU JIT"
case .cpuClockPercentage:
"CPU Clock Percentage"
case .new3DS:
"New 3DS"
case .lleApplets:
"LLE Applets"
case .deterministicAsyncOperations:
"Deterministic Async Operations"
case .enableRequiredOnlineLLEModules:
"Required Online LLE Modules"
case .compressCIAInstalls:
"Compress CIA Installs"
case .regionValue:
"Region Value"
case .pluginLoader:
"Plugin Loader"
case .allowPluginLoader:
"Plugin Loader (Homebrew)"
case .stepsPerHour:
"Steps Per Hour"
case .applyRegionFreePatch:
"Apply Region-Free Patch"
case .spirvShaderGeneration:
"SPIR-V Shader Generation"
case .disableSpirvOptimizer:
"Disable SPIR-V Optimizer"
case .useAsyncShaderCompilation:
"Async Shader Compilation"
case .useAsyncPresentation:
"Async Presentation"
case .useHardwareShaders:
"Hardware Shaders"
case .useDiskShaderCache:
"Disk Shader Cache"
case .useShadersAccurateMul:
"Shaders Accurate Mul"
case .useNewVSync:
"New VSync"
case .useShaderJIT:
"Shader JIT"
case .resolutionFactor:
"Resolution Factor"
case .textureFilter:
"Texture Filter"
case .textureSampling:
"Texture Sampling"
case .delayGameRenderThreadUS:
"Delay Game Render Thread US"
case .layoutOption:
"Layout Option"
case .customTopX:
"Custom Top X"
case .customTopY:
"Custom Top Y"
case .customTopWidth:
"Custom Top Width"
case .customTopHeight:
"Custom Top Height"
case .customBottomX:
"Custom Bottom X"
case .customBottomY:
"Custom Bottom Y"
case .customBottomWidth:
"Custom Bottom Width"
case .customBottomHeight:
"Custom Bottom Height"
case .customSecondLayerOpacity:
"Custom Second Layer Opacity"
case .aspectRatio:
"Aspect Ratio"
case .render3D:
"Render 3D"
case .factor3D:
"Factor 3D"
case .monoRender:
"Mono Render"
case .filterMode:
"Filter Mode"
case .ppShaderName:
"PP Shader Name"
case .anaglyphShaderName:
"Anaglyph Shader Name"
case .dumpTextures:
"Dump Textures"
case .customTextures:
"Custom Textures"
case .preloadTextures:
"Preload Textures"
case .asyncCustomLoading:
"Async Custom Loading"
case .disableRightEyeRender:
"Disable Right Eye Render"
case .audioMuted:
"Audio Muted"
case .audioEmulation:
"Audio Emulation"
case .audioStretching:
"Audio Stretching"
case .realtimeAudio:
"Realtime Audio"
case .volume:
"Volume"
case .outputType:
"Output Type"
case .inputType:
"Input Type"
case .logLevel:
"Log Level"
case .webAPIURL:
"Web API URL"
case .systemLanguage:
"System Language"
case .username:
"Username"
}
}
var secondaryTitle: String? {
if #available(iOS 26, *) {
switch self {
case .cpuJIT,
.useShaderJIT:
"Unavailable on iOS 26"
default:
nil
}
} else {
switch self {
default:
nil
}
}
}
var details: String? {
switch self {
case .resetSettings:
"Resets all settings to their default values"
case .mode:
"Automatically selects the best settings to achieve the selected mode"
case .cpuJIT:
"Enables the use of the Just-In-Time (JIT) compiler for CPU emulation significantly improving performance"
case .cpuClockPercentage:
"Changes the clock frequency of the 3DS CPU\n\nUnderclocking can increase performance at the risk of freezing\n\nOverclocking can fix lag at the risk of freezing"
case .new3DS:
"Changes the system model of the 3DS that Cytrus will try to emulate"
case .lleApplets:
"Enables the use of LLE system applets, if installed"
case .deterministicAsyncOperations:
"Forces deterministic asynchronous operations for debugging, reducing performance"
case .enableRequiredOnlineLLEModules:
"Enables the LLE modules required for online play, if installed"
case .compressCIAInstalls:
"Enables the compression of installed CIA contents, reducing storage usage"
case .regionValue:
"Changes the system region of the 3DS that Cytrus will use when emulating"
case .pluginLoader:
"Enables plugins to be loaded from the SD card"
case .allowPluginLoader:
"Enables plugins to be loaded from the SD card for homebrew apps"
case .stepsPerHour:
"Sets the number of steps reported to the pedometer"
case .applyRegionFreePatch:
"Patches the region of installed applications allowing them to always be displayed on the 3DS home menu"
case .spirvShaderGeneration:
""
case .disableSpirvOptimizer:
""
case .useAsyncShaderCompilation:
""
case .useAsyncPresentation:
""
case .useHardwareShaders:
""
case .useDiskShaderCache:
""
case .useShadersAccurateMul:
""
case .useNewVSync:
""
case .useShaderJIT:
"Enables the use of the Just-In-Time (JIT) compiler for shader emulation significantly improving performance"
case .resolutionFactor:
""
case .textureFilter:
""
case .textureSampling:
""
case .delayGameRenderThreadUS:
""
case .layoutOption:
""
case .customTopX:
""
case .customTopY:
""
case .customTopWidth:
""
case .customTopHeight:
""
case .customBottomX:
""
case .customBottomY:
""
case .customBottomWidth:
""
case .customBottomHeight:
""
case .customSecondLayerOpacity:
""
case .aspectRatio:
""
case .render3D:
""
case .factor3D:
""
case .monoRender:
""
case .filterMode:
""
case .ppShaderName:
""
case .anaglyphShaderName:
""
case .dumpTextures:
""
case .customTextures:
""
case .preloadTextures:
""
case .asyncCustomLoading:
""
case .disableRightEyeRender:
"Disables the rendering of the right eye image significantly increasing performance in some games at the cost of causing flickers in others"
case .audioMuted:
""
case .audioEmulation:
""
case .audioStretching:
"Enables the audio stretching post-processing effect adjusting audio speed to match emulation speed to help prevent audio stutter at the cost of increasing audio latency"
case .realtimeAudio:
"Enables the scaling of audio playback speed to account for drops in emulation speed"
case .volume:
""
case .outputType:
""
case .inputType:
""
case .logLevel:
""
case .webAPIURL:
""
case .systemLanguage:
""
case .username:
""
}
}
var isEnabled: Bool {
if #available(iOS 26, *) {
switch self {
case .cpuJIT,
.useShaderJIT:
false
default:
true
}
} else {
true
}
}
func setting(_ delegate: SettingDelegate? = nil) -> BaseSetting {
switch self {
case .resetSettings:
TapSetting(key: rawValue,
title: title,
details: details,
color: .systemRed,
handler: { controller in
guard let controller: CytrusSettingsController = controller as? CytrusSettingsController else {
return
}
if #available(iOS 26, *) {
UserDefaults.standard.set(false, forKey: "cytrus.v1.38.cpuJIT")
UserDefaults.standard.set(false, forKey: "cytrus.v1.38.useShaderJIT")
} else {
UserDefaults.standard.set(false, forKey: "cytrus.v1.38.cpuJIT")
UserDefaults.standard.set(true, forKey: "cytrus.v1.38.useShaderJIT")
}
let defaults: [String : [String : Any]] = [
"Cytrus" : [
"mode" : 0,
"cpuClockPercentage" : 100,
"new3DS" : true,
"lleApplets" : true,
"deterministicAsyncOperations" : false,
"enableRequiredOnlineLLEModules" : false,
"compressCIAInstalls" : false,
"regionValue" : -1,
"pluginLoader" : false,
"allowPluginLoader" : true,
"stepsPerHour" : 0,
"applyRegionFreePatch" : true,
"spirvShaderGeneration" : true,
"disableSpirvOptimizer" : true,
"useAsyncShaderCompilation" : false,
"useAsyncPresentation" : true,
"useHardwareShaders" : true,
"useDiskShaderCache" : true,
"useShadersAccurateMul" : true,
"useNewVSync" : true,
"resolutionFactor" : 1,
"textureFilter" : 0,
"textureSampling" : 0,
"delayGameRenderThreadUS" : 0,
"layoutOption" : 0,
"customTopX" : 0,
"customTopY" : 0,
"customTopWidth" : 800,
"customTopHeight" : 480,
"customBottomX" : 80,
"customBottomY" : 500,
"customBottomWidth" : 640,
"customBottomHeight" : 480,
"customSecondLayerOpacity" : 100,
"aspectRatio" : 0,
"render3D" : 0,
"factor3D" : 0,
"monoRender" : 0,
"filterMode" : true,
"ppShaderName" : "none (builtin)",
"anaglyphShaderName" : "dubois (builtin)",
"dumpTextures" : false,
"customTextures" : false,
"preloadTextures" : false,
"asyncCustomLoading" : true,
"disableRightEyeRender" : false,
"audioMuted" : false,
"audioEmulation" : 0,
"audioStretching" : true,
"realtimeAudio" : false,
"volume" : 1,
"outputType" : 0,
"inputType" : 0,
"logLevel" : 2,
"webAPIURL" : "http://88.198.47.46:5000",
"systemLanguage" : 1,
"username" : "Cytrus"
]
]
defaults.forEach { core, values in
values.forEach { key, value in
UserDefaults.standard.set(value, forKey: "\(core.lowercased()).v1.38.\(key)")
}
}
controller.populateSettings()
},
delegate: delegate)
case .mode:
SegmentedSetting(key: rawValue,
title: title,
details: details,
values: [
"Speed" : 0,
"Looks" : 1
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: { controller in
guard let controller: CytrusSettingsController = controller as? CytrusSettingsController else {
return
}
switch UserDefaults.standard.integer(forKey: rawValue) {
case 0:
let defaults: [String : [String : Any]] = [
"Cytrus" : [
"cpuClockPercentage" : 30,
"disableRightEyeRender" : true,
"realtimeAudio" : true
]
]
defaults.forEach { core, values in
values.forEach { key, value in
UserDefaults.standard.set(value, forKey: "\(core.lowercased()).v1.38.\(key)")
}
}
case 1:
let defaults: [String : [String : Any]] = [
"Cytrus" : [
"cpuClockPercentage" : 100,
"disableRightEyeRender" : false,
"realtimeAudio" : false
]
]
defaults.forEach { core, values in
values.forEach { key, value in
UserDefaults.standard.set(value, forKey: "\(core.lowercased()).v1.38.\(key)")
}
}
default:
break
}
controller.populateSettings()
},
delegate: delegate)
case .cpuJIT,
.new3DS,
.lleApplets,
.deterministicAsyncOperations,
.enableRequiredOnlineLLEModules,
.compressCIAInstalls,
.pluginLoader,
.allowPluginLoader,
.applyRegionFreePatch,
.spirvShaderGeneration,
.disableSpirvOptimizer,
.useAsyncShaderCompilation,
.useAsyncPresentation,
.useHardwareShaders,
.useDiskShaderCache,
.useShadersAccurateMul,
.useNewVSync,
.useShaderJIT,
.filterMode,
.dumpTextures,
.customTextures,
.preloadTextures,
.asyncCustomLoading,
.disableRightEyeRender,
.audioMuted,
.audioStretching,
.realtimeAudio:
BoolSetting(key: rawValue,
title: title,
details: details,
secondaryTitle: secondaryTitle,
isEnabled: isEnabled,
value: UserDefaults.standard.bool(forKey: rawValue),
delegate: delegate)
case .cpuClockPercentage:
InputNumberSetting(key: rawValue,
title: title,
details: details,
min: 5,
max: 400,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .regionValue:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Automatic" : -1,
"Japan" : 0,
"USA" : 1,
"Europe" : 2,
"Australia" : 3,
"China" : 4,
"Korea" : 5,
"Taiwan" : 6
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .stepsPerHour:
InputNumberSetting(key: rawValue,
title: title,
details: details,
min: 0,
max: 9999,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .resolutionFactor:
StepperSetting(key: rawValue,
title: title,
details: details,
min: 0,
max: 10,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .textureFilter:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"None" : 0,
"Anime4K" : 1,
"Bicubic" : 2,
"ScaleForce" : 3,
"xBRZ" : 4,
"MMPX" : 5
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .textureSampling:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Game Controlled" : 0,
"Nearest Neighbor" : 1,
"Linear" : 2
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .delayGameRenderThreadUS:
InputNumberSetting(key: rawValue,
title: title,
details: details,
min: 0,
max: 16000,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .layoutOption:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Default" : 0,
"Single Screen" : 1,
"Large Screen" : 2,
"Side Screen" : 3,
"Hybrid Screen" : 5,
"Custom Layout" : 6
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .customTopX,
.customTopY,
.customTopWidth,
.customTopHeight,
.customBottomX,
.customBottomY,
.customBottomWidth,
.customBottomHeight:
InputNumberSetting(key: rawValue,
title: title,
details: details,
min: 0,
max: 9999,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .customSecondLayerOpacity:
InputNumberSetting(key: rawValue,
title: title,
details: details,
min: 0,
max: 100,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .aspectRatio:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Default" : 0,
"16:9" : 1,
"4:3" : 2,
"21:9" : 3,
"16:10" : 4,
"Stretch" : 5
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .render3D:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Off" : 0,
"Side by Side" : 1,
"Anaglyph" : 2,
"Interlaced" : 3,
"ReverseInterlaced" : 4,
"CardboardVR" : 5
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .factor3D:
InputNumberSetting(key: rawValue,
title: title,
details: details,
min: 0,
max: 100,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .monoRender:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Left Eye" : 0,
"Right Eye" : 1
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .ppShaderName:
InputStringSetting(key: rawValue,
title: title,
details: details,
placeholder: "None (builtin)",
value: UserDefaults.standard.string(forKey: rawValue),
action: {},
delegate: delegate)
case .anaglyphShaderName:
InputStringSetting(key: rawValue,
title: title,
details: details,
placeholder: "Dubois (builtin)",
value: UserDefaults.standard.string(forKey: rawValue),
action: {},
delegate: delegate)
case .audioEmulation:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"HLE" : 0,
"LLE" : 1,
"LLE (Multithreaded)" : 2
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .volume:
StepperSetting(key: rawValue,
title: title,
details: details,
min: 0,
max: 1,
value: UserDefaults.standard.double(forKey: rawValue),
delegate: delegate)
case .outputType:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Automatic" : 0,
"None" : 1,
"OpenAL" : 3,
// "SDL3" : 5,
"CoreAudio" : 6
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .inputType:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Automatic" : 0,
"None" : 1,
"Static" : 2,
"OpenAL" : 3
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .logLevel:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Trace" : 0,
"Debug" : 1,
"Info" : 2,
"Warning" : 3,
"Error" : 4,
"Critical" : 5
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {},
delegate: delegate)
case .webAPIURL:
InputStringSetting(key: rawValue,
title: title,
details: details,
placeholder: "http(s)://address:port",
value: UserDefaults.standard.string(forKey: rawValue),
action: {
CytrusMultiplayerManager.shared().updateWebAPIURL()
},
delegate: delegate)
case .systemLanguage:
SelectionSetting(key: rawValue,
title: title,
details: details,
values: [
"Japanese" : 0,
"English" : 1,
"French" : 2,
"German" : 3,
"Italian" : 4,
"Spanish" : 5,
"Simplified Chinese" : 6,
"Korean" : 7,
"Dutch" : 8,
"Portuguese" : 9,
"Russian" : 10,
"Traditional Chinese" : 11
],
selectedValue: UserDefaults.standard.value(forKey: rawValue),
action: {
// guard let systemLanguage = UserDefaults.standard.value(forKey: rawValue) as? Int else { return }
// SystemSaveGame.shared.set(systemLanguage)
},
delegate: delegate)
case .username:
InputStringSetting(key: rawValue,
title: title,
details: details,
placeholder: "Cytrus",
value: UserDefaults.standard.string(forKey: rawValue),
action: {
// guard let username = UserDefaults.standard.string(forKey: rawValue) else { return }
// SystemSaveGame.shared.set(username)
},
delegate: delegate)
}
}
static func settings(_ header: CytrusSettingsHeaders) -> [CytrusSettingsItems] {
switch header {
case .core:
[
.mode,
.cpuClockPercentage,
.new3DS,
.lleApplets,
.deterministicAsyncOperations,
.enableRequiredOnlineLLEModules
]
case .dataStorage:
[
.compressCIAInstalls
]
case .debugging:
[
.cpuJIT,
.logLevel
]
case .system:
[
.regionValue,
.pluginLoader,
.allowPluginLoader,
.stepsPerHour,
.applyRegionFreePatch
]
case .systemSaveGame:
[
.systemLanguage,
.username
]
case .renderer:
[
.spirvShaderGeneration,
.disableSpirvOptimizer,
.useAsyncShaderCompilation,
.useAsyncPresentation,
.useHardwareShaders,
.useDiskShaderCache,
.useShadersAccurateMul,
.useNewVSync,
.useShaderJIT,
.aspectRatio,
.resolutionFactor,
.textureFilter,
.textureSampling,
.delayGameRenderThreadUS,
.render3D,
.factor3D,
.monoRender,
.filterMode,
.ppShaderName,
.anaglyphShaderName,
.dumpTextures,
.customTextures,
.preloadTextures,
.asyncCustomLoading,
.disableRightEyeRender
]
case .defaultLayout:
[
.layoutOption
]
case .customLayout:
[
.customTopX,
.customTopY,
.customTopWidth,
.customTopHeight,
.customBottomX,
.customBottomY,
.customBottomWidth,
.customBottomHeight,
.customSecondLayerOpacity,
]
case .audio:
[
.audioMuted,
.audioEmulation,
.audioStretching,
.realtimeAudio,
.volume,
.outputType,
.inputType
]
case .miscellaneous:
[
.webAPIURL
]
case .destructive:
[
.resetSettings
]
}
}
}
class CytrusSettingsController : UIViewController, UICollectionViewDelegate {
var dataSource: UICollectionViewDiffableDataSource<CytrusSettingsHeaders, AnyHashableSendable>! = nil
var snapshot: NSDiffableDataSourceSnapshot<CytrusSettingsHeaders, AnyHashableSendable>! = nil
var cytrus: Cytrus
init(_ cytrus: Cytrus) {
self.cytrus = cytrus
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
if let navigationController {
navigationController.navigationBar.prefersLargeTitles = true
}
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark"), primaryAction: UIAction { _ in
self.dismiss(animated: true)
})
if #available(iOS 26, *) {
navigationItem.title = "Cytrus"
navigationItem.largeTitle = navigationItem.title
navigationItem.subtitle = "Nintendo 3DS (Azahar)"
navigationItem.largeSubtitle = navigationItem.subtitle
} else {
title = "Cytrus"
}
navigationItem.style = .browser
var configuration: UICollectionLayoutListConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
configuration.backgroundColor = .clear
configuration.headerMode = .supplementary
configuration.trailingSwipeActionsConfigurationProvider = { indexPath in
guard let dataSource = self.dataSource, let item: BaseSetting = dataSource.itemIdentifier(for: indexPath) as? BaseSetting else {
return UISwipeActionsConfiguration()
}
let informationContextualAction: UIContextualAction = UIContextualAction(style: .normal, title: nil, handler: { action, view, performed in
let alertController: UIAlertController = UIAlertController(title: item.title, message: item.details, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: .default) { _ in
performed(true)
})
// alertController.preferredAction = alertController.actions.first
self.present(alertController, animated: true)
})
informationContextualAction.image = UIImage(systemName: "info")
return UISwipeActionsConfiguration(actions: [
informationContextualAction
])
}
let collectionView: UICollectionView = UICollectionView(frame: .zero,
collectionViewLayout: UICollectionViewCompositionalLayout.list(using: configuration))
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
view.addSubview(collectionView)
collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
if #available(iOS 26, *) {
let backgroundView: UIVisualEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular))
backgroundView.cornerConfiguration = .corners(radius: .containerConcentric())
collectionView.backgroundColor = .clear
collectionView.backgroundView = backgroundView
view.backgroundColor = .clear
} else {
view.backgroundColor = .systemBackground
}
let selectionCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SelectionSetting> { cell, indexPath, itemIdentifier in
if #available(iOS 26, *) {
var backgroundConfiguration: UIBackgroundConfiguration = .clear()
let effect: UIGlassEffect = UIGlassEffect(style: .regular)
let visualEffectView: UIVisualEffectView = UIVisualEffectView(effect: effect)
visualEffectView.cornerConfiguration = .corners(topLeftRadius: .fixed(cell.effectiveRadius(corner: .topLeft)),
topRightRadius: .fixed(cell.effectiveRadius(corner: .topRight)),
bottomLeftRadius: .fixed(cell.effectiveRadius(corner: .bottomLeft)),
bottomRightRadius: .fixed(cell.effectiveRadius(corner: .bottomRight)))
backgroundConfiguration.customView = visualEffectView
cell.backgroundConfiguration = backgroundConfiguration
}
var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = itemIdentifier.title
cell.contentConfiguration = contentConfiguration
let children: [UIMenuElement] = switch itemIdentifier.values {
case let stringInt as [String : Int]:
stringInt.reduce(into: [UIAction](), { partialResult, element in
var state: UIMenuElement.State = .off
if let selectedValue = itemIdentifier.selectedValue as? Int {
state = element.value == selectedValue ? .on : .off
}
partialResult.append(.init(title: element.key, state: state, handler: { _ in
UserDefaults.standard.set(element.value, forKey: itemIdentifier.key)
if let delegate = itemIdentifier.delegate {
delegate.didChangeSetting(at: indexPath)
}
}))
})
case let stringString as [String : String]:
stringString.reduce(into: [UIAction](), { partialResult, element in
var state: UIMenuElement.State = .off
if let selectedValue = itemIdentifier.selectedValue as? String {
state = element.value == selectedValue ? .on : .off
}
partialResult.append(.init(title: element.key, state: state, handler: { _ in
UserDefaults.standard.set(element.value, forKey: itemIdentifier.key)
if let delegate = itemIdentifier.delegate {
delegate.didChangeSetting(at: indexPath)
}
}))
})
default:
[]
}
var title = "Automatic"
if let selectedValue = itemIdentifier.selectedValue {
switch selectedValue {
case let intValue as Int:
if let values = itemIdentifier.values as? [String : Int] {
title = values.first(where: { $0.value == intValue })?.key ?? title
}
case let stringValue as String:
if let values = itemIdentifier.values as? [String : String] {
title = values.first(where: { $0.value == stringValue })?.key ?? title
}
default:
break
}
}
cell.accessories = [
UICellAccessory.label(text: title),
UICellAccessory.popUpMenu(UIMenu(children: children.sorted(by: { $0.title < $1.title })))
]
}
let blankSettingsCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, BlankSetting> { cell, indexPath, itemIdentifier in
cell.contentConfiguration = UIListContentConfiguration.cell()
}
let headerCellRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
var contentConfiguration = UIListContentConfiguration.extraProminentInsetGroupedHeader()
contentConfiguration.text = self.snapshot.sectionIdentifiers[indexPath.section].header.text
contentConfiguration.secondaryText = self.snapshot.sectionIdentifiers[indexPath.section].header.secondaryText
contentConfiguration.secondaryTextProperties.color = .secondaryLabel
supplementaryView.contentConfiguration = contentConfiguration
}
let boolCell: UICollectionView.CellRegistration<UICollectionViewListCell, BoolSetting> = CellManager.Settings.boolCell
let inputNumberCell: UICollectionView.CellRegistration<UICollectionViewListCell, InputNumberSetting> = CellManager.Settings.inputNumberCell
let inputStringCell: UICollectionView.CellRegistration<UICollectionViewListCell, InputStringSetting> = CellManager.Settings.inputStringCell
let segmentedCell: UICollectionView.CellRegistration<UICollectionViewListCell, SegmentedSetting> = CellManager.Settings.segmentedCell(self)
let stepperCell: UICollectionView.CellRegistration<UICollectionViewListCell, StepperSetting> = CellManager.Settings.stepperCell
let tapCell: UICollectionView.CellRegistration<UICollectionViewListCell, TapSetting> = CellManager.Settings.tapCell
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case let blankSetting as BlankSetting:
collectionView.dequeueConfiguredReusableCell(using: blankSettingsCellRegistration, for: indexPath, item: blankSetting)
case let boolSetting as BoolSetting:
collectionView.dequeueConfiguredReusableCell(using: boolCell, for: indexPath, item: boolSetting)
case let inputNumberSetting as InputNumberSetting:
collectionView.dequeueConfiguredReusableCell(using: inputNumberCell, for: indexPath, item: inputNumberSetting)
case let inputStringSetting as InputStringSetting:
collectionView.dequeueConfiguredReusableCell(using: inputStringCell, for: indexPath, item: inputStringSetting)
case let segmentedSetting as SegmentedSetting:
collectionView.dequeueConfiguredReusableCell(using: segmentedCell, for: indexPath, item: segmentedSetting)
case let stepperSetting as StepperSetting:
collectionView.dequeueConfiguredReusableCell(using: stepperCell, for: indexPath, item: stepperSetting)
case let selectionSetting as SelectionSetting:
collectionView.dequeueConfiguredReusableCell(using: selectionCellRegistration, for: indexPath, item: selectionSetting)
case let tapSetting as TapSetting:
collectionView.dequeueConfiguredReusableCell(using: tapCell, for: indexPath, item: tapSetting)
default:
nil
}
}
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
collectionView.dequeueConfiguredReusableSupplementary(using: headerCellRegistration, for: indexPath)
}
populateSettings()
}
func populateSettings() {
snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections(CytrusSettingsHeaders.allCases)
snapshot.sectionIdentifiers.forEach { header in
snapshot.appendItems(CytrusSettingsItems.settings(header).map { item in
item.setting(self)
}, toSection: header)
}
Task {
await dataSource.apply(snapshot)
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
switch dataSource.itemIdentifier(for: indexPath) {
case let inputSetting as InputNumberSetting:
let alertController = UIAlertController(title: inputSetting.title,
message: "Min: \(Int(inputSetting.min)), Max: \(Int(inputSetting.max))",
preferredStyle: .alert)
alertController.addTextField {
$0.keyboardType = .numberPad
}
alertController.addAction(.init(title: "Cancel", style: .cancel))
alertController.addAction(.init(title: "Save", style: .default, handler: { _ in
guard let textFields = alertController.textFields, let textField = textFields.first, let value = textField.text as? NSString else {
return
}
UserDefaults.standard.set(value.doubleValue, forKey: inputSetting.key)
if let delegate = inputSetting.delegate {
delegate.didChangeSetting(at: indexPath)
}
}))
present(alertController, animated: true)
case let inputSetting as InputStringSetting:
let alertController = UIAlertController(title: inputSetting.title,
message: inputSetting.details,
preferredStyle: .alert)
alertController.addTextField {
$0.placeholder = inputSetting.placeholder
}
alertController.addAction(.init(title: "Cancel", style: .cancel))
alertController.addAction(.init(title: "Save", style: .default, handler: { _ in
guard let textFields = alertController.textFields, let textField = textFields.first, let value = textField.text else {
return
}
UserDefaults.standard.set(value, forKey: inputSetting.key)
if let delegate = inputSetting.delegate {
inputSetting.action()
delegate.didChangeSetting(at: indexPath)
}
}))
present(alertController, animated: true)
case let tapSetting as TapSetting:
tapSetting.handler(self)
default:
break
}
}
}
extension CytrusSettingsController : SettingDelegate {
func didChangeSetting(at indexPath: IndexPath) {
cytrus.updateSettings()
guard let sectionIdentifier = dataSource.sectionIdentifier(for: indexPath.section) else {
return
}
var snapshot = dataSource.snapshot()
let item = snapshot.itemIdentifiers(inSection: sectionIdentifier)[indexPath.item]
switch item {
case let boolSetting as BoolSetting:
boolSetting.value = UserDefaults.standard.bool(forKey: boolSetting.key)
case let inputNumberSetting as InputNumberSetting:
inputNumberSetting.value = UserDefaults.standard.double(forKey: inputNumberSetting.key)
case let inputStringSetting as InputStringSetting:
inputStringSetting.value = UserDefaults.standard.string(forKey: inputStringSetting.key)
case let segmentedSetting as SegmentedSetting:
segmentedSetting.selectedValue = UserDefaults.standard.value(forKey: segmentedSetting.key)
case let stepperSetting as StepperSetting:
stepperSetting.value = UserDefaults.standard.double(forKey: stepperSetting.key)
case let selectionSetting as SelectionSetting:
selectionSetting.selectedValue = UserDefaults.standard.value(forKey: selectionSetting.key)
default:
break
}
snapshot.reloadItems([item])
Task {
await dataSource.apply(snapshot, animatingDifferences: false)
}
}
}