Initial Commit
This commit is contained in:
commit
9b2f49ea5c
8 changed files with 857 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ArtifactPlatform"
|
||||
BuildableName = "ArtifactPlatform"
|
||||
BlueprintName = "ArtifactPlatform"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ArtifactPlatformTests"
|
||||
BuildableName = "ArtifactPlatformTests"
|
||||
BlueprintName = "ArtifactPlatformTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ArtifactPlatform"
|
||||
BuildableName = "ArtifactPlatform"
|
||||
BlueprintName = "ArtifactPlatform"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
42
Package.swift
Normal file
42
Package.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// swift-tools-version: 6.1
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ArtifactPlatform",
|
||||
platforms: [.macOS(.v14)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "ArtifactPlatform",
|
||||
targets: ["ArtifactPlatform"]
|
||||
),
|
||||
],
|
||||
traits: [
|
||||
.default(enabledTraits: ["GLFW"]),
|
||||
.trait(name: "GLFW", description: "Enables the builtin GLFW backend."),
|
||||
.trait(name: "SDL3", description: "Enables the builtin SDL3 backend."),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../GLFW"),
|
||||
.package(path: "../SDL3"),
|
||||
.package(path: "../ArtifactMath"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.target(
|
||||
name: "ArtifactPlatform",
|
||||
dependencies: [
|
||||
"ArtifactMath",
|
||||
.product(name: "GLFW", package: "GLFW", condition: .when(traits: ["GLFW"])),
|
||||
.product(name: "SDL3", package: "SDL3", condition: .when(traits: ["SDL3"])),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ArtifactPlatformTests",
|
||||
dependencies: ["ArtifactPlatform"],
|
||||
),
|
||||
],
|
||||
)
|
||||
163
Sources/ArtifactPlatform/GLFWWindow.swift
Normal file
163
Sources/ArtifactPlatform/GLFWWindow.swift
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
|
||||
#if GLFW
|
||||
|
||||
import GLFW
|
||||
|
||||
let MIN_STEP_TIME = 0.2
|
||||
|
||||
@MainActor
|
||||
public class GLFWWindow: Window {
|
||||
public static var initialized = false
|
||||
|
||||
public let id: Int = 0
|
||||
public private(set) var width: Int
|
||||
public private(set) var height: Int
|
||||
public private(set) var actions = ActionManager()
|
||||
public var inputState = InputState()
|
||||
|
||||
public let window: OpaquePointer!
|
||||
let clock = ContinuousClock()
|
||||
var previousTime = ContinuousClock.Instant.now
|
||||
var accumulator: Double = 0
|
||||
var running = false
|
||||
var pointerLocked = false
|
||||
|
||||
var tickHandlers: [(Double) -> Void] = []
|
||||
var drawHandlers: [(Double) -> Void] = []
|
||||
var resizeHandlers: [(Int, Int) -> Void] = []
|
||||
var pointerMoveHandlers: [(Double, Double) -> Void] = []
|
||||
var keyDownHandlers: [(Int) -> Void] = []
|
||||
var keyUpHandlers: [(Int) -> Void] = []
|
||||
var scrollHandlers: [(Double, Double) -> Void] = []
|
||||
|
||||
required public convenience init(width: Int, height: Int) {
|
||||
self.init(width: width, height: height, title: "Artifact Engine")
|
||||
}
|
||||
|
||||
required public init(width: Int, height: Int, title: String) {
|
||||
if !GLFWWindow.initialized { glfwInit() }
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
glfwInitHint(GLFW_CLIENT_API, GLFW_NO_API)
|
||||
window = glfwCreateWindow(Int32(width), Int32(height), title, nil, nil)
|
||||
glfwSetWindowUserPointer(window, Unmanaged.passUnretained(self).toOpaque())
|
||||
|
||||
//setPointerLock()
|
||||
|
||||
glfwSetKeyCallback(window) { window, key, scancode, action, mods in
|
||||
let me = Unmanaged<GLFWWindow>.fromOpaque(glfwGetWindowUserPointer(window)).takeUnretainedValue()
|
||||
if key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE {
|
||||
me.close()
|
||||
}
|
||||
for cb in action == GLFW_RELEASE ? me.keyUpHandlers : me.keyDownHandlers {
|
||||
cb(Int(key))
|
||||
}
|
||||
}
|
||||
|
||||
glfwSetFramebufferSizeCallback(window) { window, width, height in
|
||||
let me = Unmanaged<GLFWWindow>.fromOpaque(glfwGetWindowUserPointer(window)).takeUnretainedValue()
|
||||
for cb in me.resizeHandlers {
|
||||
cb(Int(width), Int(height))
|
||||
}
|
||||
}
|
||||
|
||||
glfwSetCursorPosCallback(window) { window, x, y in
|
||||
let me = Unmanaged<GLFWWindow>.fromOpaque(glfwGetWindowUserPointer(window)).takeUnretainedValue()
|
||||
for cb in me.pointerMoveHandlers {
|
||||
cb(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
glfwSetScrollCallback(window) { window, x, y in
|
||||
let me = Unmanaged<GLFWWindow>.fromOpaque(glfwGetWindowUserPointer(window)).takeUnretainedValue()
|
||||
for cb in me.scrollHandlers {
|
||||
cb(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// glfwSetKeyCallback(window) { window, key, scancode, action, mods in
|
||||
// let me = Unmanaged<GLFWWindow>.fromOpaque(glfwGetWindowUserPointer(window)).takeUnretainedValue()
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
public func setTitle(title: String) {
|
||||
glfwSetWindowTitle(window, title)
|
||||
}
|
||||
|
||||
public func setPointerLock() {
|
||||
pointerLocked = true
|
||||
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED)
|
||||
}
|
||||
|
||||
public func close() {
|
||||
glfwSetWindowShouldClose(window, GLFW_TRUE)
|
||||
running = false
|
||||
}
|
||||
|
||||
public func run() {
|
||||
running = true
|
||||
previousTime = clock.now
|
||||
|
||||
while glfwWindowShouldClose(window) == 0 {
|
||||
glfwPollEvents()
|
||||
|
||||
let currentTime = clock.now
|
||||
let frameTime = Double(previousTime.duration(to: currentTime).components.attoseconds) / 1_000_000_000_000_000_000.0
|
||||
previousTime = currentTime
|
||||
accumulator += frameTime
|
||||
|
||||
while accumulator >= MIN_STEP_TIME {
|
||||
tick(dtime: MIN_STEP_TIME)
|
||||
accumulator -= MIN_STEP_TIME
|
||||
}
|
||||
|
||||
let alpha = accumulator / MIN_STEP_TIME
|
||||
for cb in drawHandlers {
|
||||
cb(alpha)
|
||||
}
|
||||
}
|
||||
|
||||
running = false
|
||||
|
||||
glfwDestroyWindow(window)
|
||||
}
|
||||
|
||||
public func tick(dtime: Double) {
|
||||
for handler in tickHandlers {
|
||||
handler(dtime)
|
||||
}
|
||||
}
|
||||
|
||||
public func onTick(_ callback: @escaping (Double) -> Void) {
|
||||
tickHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onDraw(_ callback: @escaping (Double) -> Void) {
|
||||
drawHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onResize(_ callback: @escaping (Int, Int) -> Void) {
|
||||
resizeHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onPointerMove(_ callback: @escaping (Double, Double) -> Void) {
|
||||
pointerMoveHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onKeyDown(_ callback: @escaping (Int) -> Void) {
|
||||
keyDownHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onKeyUp(_ callback: @escaping (Int) -> Void) {
|
||||
keyUpHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onScroll(_ callback: @escaping (Double, Double) -> Void) {
|
||||
scrollHandlers.append(callback)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
140
Sources/ArtifactPlatform/Input.swift
Normal file
140
Sources/ArtifactPlatform/Input.swift
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
|
||||
import Observation
|
||||
import ArtifactMath
|
||||
|
||||
public enum Key: Int, Codable, Hashable, CaseIterable, Sendable {
|
||||
// Letters
|
||||
case a = 0, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
|
||||
|
||||
// Numbers (top row)
|
||||
case digit0, digit1, digit2, digit3, digit4, digit5, digit6, digit7, digit8, digit9
|
||||
|
||||
// Function keys
|
||||
case f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15,
|
||||
f16, f17, f18, f19, f20, f21, f22, f23, f24
|
||||
|
||||
// Modifiers
|
||||
case leftShift, rightShift
|
||||
case leftControl, rightControl
|
||||
case leftAlt, rightAlt
|
||||
case leftMeta, rightMeta // Command/Windows key
|
||||
|
||||
// Navigation
|
||||
case up, down, left, right
|
||||
case home, end, pageUp, pageDown
|
||||
case insert, delete
|
||||
|
||||
// Special
|
||||
case space
|
||||
case tab
|
||||
case enter // Return
|
||||
case escape
|
||||
case backspace
|
||||
|
||||
// Punctuation / Symbols (US layout reference)
|
||||
case grave // ` ~
|
||||
case minus // - _
|
||||
case equal // = +
|
||||
case leftBracket // [ {
|
||||
case rightBracket // ] }
|
||||
case backslash // \ |
|
||||
case semicolon // ; :
|
||||
case quote // ' "
|
||||
case comma // , <
|
||||
case period // . >
|
||||
case slash // / ?
|
||||
|
||||
// Numpad
|
||||
case numpad0, numpad1, numpad2, numpad3, numpad4,
|
||||
numpad5, numpad6, numpad7, numpad8, numpad9
|
||||
case numpadDecimal, numpadDivide, numpadMultiply,
|
||||
numpadMinus, numpadPlus, numpadEnter
|
||||
|
||||
// Other common
|
||||
case capsLock
|
||||
case printScreen
|
||||
case scrollLock
|
||||
case pause
|
||||
case numLock
|
||||
|
||||
// Reserved for backend-specific or future expansion
|
||||
case unknown
|
||||
}
|
||||
|
||||
// MARK: - Mouse
|
||||
|
||||
public enum MouseButton: Int, Codable, Hashable, Sendable {
|
||||
case left = 0
|
||||
case right
|
||||
case middle
|
||||
case extra1 // Side buttons, etc.
|
||||
case extra2
|
||||
case unknown
|
||||
}
|
||||
|
||||
// MARK: - Gamepad / Controller (common across SDL, GLFW, XInput, etc.)
|
||||
|
||||
public enum GamepadButton: Int, Codable, Hashable, Sendable {
|
||||
case a, b, x, y
|
||||
case dpadUp, dpadDown, dpadLeft, dpadRight
|
||||
case leftStick, rightStick // Click/press
|
||||
case leftShoulder, rightShoulder
|
||||
case leftTrigger, rightTrigger // Often analog, but can be digital threshold
|
||||
case start, select, menu // Start / Options / Menu
|
||||
case guide // Home / Xbox button
|
||||
case unknown
|
||||
}
|
||||
|
||||
public enum GamepadAxis: Int, Codable, Hashable, Sendable {
|
||||
case leftStickX, leftStickY
|
||||
case rightStickX, rightStickY
|
||||
case leftTrigger, rightTrigger // Often 0..1 range
|
||||
}
|
||||
|
||||
// MARK: - Unified Input Source (for bindings)
|
||||
|
||||
public enum InputSource: Codable, Hashable {
|
||||
case key(Key)
|
||||
case pointer(PointerState)
|
||||
case mouseMove
|
||||
case mouseWheel
|
||||
case gamepadButton(GamepadButton, device: Int = 0)
|
||||
case gamepadAxis(GamepadAxis, device: Int = 0)
|
||||
|
||||
// Future: .mouseWheel, .touch, etc.
|
||||
}
|
||||
|
||||
@Observable
|
||||
open class InputState {
|
||||
private var nextPointerId = 1
|
||||
public var newPointerId: Int {
|
||||
defer { nextPointerId += 1 }
|
||||
return nextPointerId
|
||||
}
|
||||
|
||||
public var keyboard: [Key: Bool] = [:]
|
||||
public var mouseButtons: Set<MouseButton> = []
|
||||
public var pointers: [Int: PointerState] = [:]
|
||||
public var mousePointer: PointerState? { pointers.values.first { $0.type == .mouse } }
|
||||
public var wheelDelta: Vec2f = .zero
|
||||
public var gamepads: [Int: GamepadState] = [:]
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct PointerState: Codable, Hashable {
|
||||
public typealias ID = Int
|
||||
|
||||
public let id: Int
|
||||
public var position: Vec2f
|
||||
public var delta: Vec2f
|
||||
public var pressure: Float
|
||||
public var type: PointerType
|
||||
}
|
||||
|
||||
public enum PointerType: Codable, Hashable, Sendable { case mouse, pen, touch}
|
||||
|
||||
public struct GamepadState {
|
||||
public var buttons: [GamepadButton: Bool]
|
||||
public var axes: [GamepadAxis: Float]
|
||||
}
|
||||
320
Sources/ArtifactPlatform/SDL3Window.swift
Normal file
320
Sources/ArtifactPlatform/SDL3Window.swift
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
|
||||
#if SDL3
|
||||
|
||||
import SDL3
|
||||
import ArtifactMath
|
||||
|
||||
private let SDLScancodes: [SDL_Scancode: Key] = [
|
||||
SDL_SCANCODE_A: .a,
|
||||
SDL_SCANCODE_B: .b,
|
||||
SDL_SCANCODE_C: .c,
|
||||
SDL_SCANCODE_D: .d,
|
||||
SDL_SCANCODE_E: .e,
|
||||
SDL_SCANCODE_F: .f,
|
||||
SDL_SCANCODE_G: .g,
|
||||
SDL_SCANCODE_H: .h,
|
||||
SDL_SCANCODE_I: .i,
|
||||
SDL_SCANCODE_J: .j,
|
||||
SDL_SCANCODE_K: .k,
|
||||
SDL_SCANCODE_L: .l,
|
||||
SDL_SCANCODE_M: .m,
|
||||
SDL_SCANCODE_N: .n,
|
||||
SDL_SCANCODE_O: .o,
|
||||
SDL_SCANCODE_P: .p,
|
||||
SDL_SCANCODE_Q: .q,
|
||||
SDL_SCANCODE_R: .r,
|
||||
SDL_SCANCODE_S: .s,
|
||||
SDL_SCANCODE_T: .t,
|
||||
SDL_SCANCODE_U: .u,
|
||||
SDL_SCANCODE_V: .v,
|
||||
SDL_SCANCODE_W: .w,
|
||||
SDL_SCANCODE_X: .x,
|
||||
SDL_SCANCODE_Y: .y,
|
||||
SDL_SCANCODE_Z: .z,
|
||||
|
||||
SDL_SCANCODE_0: .digit0,
|
||||
SDL_SCANCODE_1: .digit1,
|
||||
SDL_SCANCODE_2: .digit2,
|
||||
SDL_SCANCODE_3: .digit3,
|
||||
SDL_SCANCODE_4: .digit4,
|
||||
SDL_SCANCODE_5: .digit5,
|
||||
SDL_SCANCODE_6: .digit6,
|
||||
SDL_SCANCODE_7: .digit7,
|
||||
SDL_SCANCODE_8: .digit8,
|
||||
SDL_SCANCODE_9: .digit9,
|
||||
|
||||
SDL_SCANCODE_F1: .f1,
|
||||
SDL_SCANCODE_F2: .f2,
|
||||
SDL_SCANCODE_F3: .f3,
|
||||
SDL_SCANCODE_F4: .f4,
|
||||
SDL_SCANCODE_F5: .f5,
|
||||
SDL_SCANCODE_F6: .f6,
|
||||
SDL_SCANCODE_F7: .f7,
|
||||
SDL_SCANCODE_F8: .f8,
|
||||
SDL_SCANCODE_F9: .f9,
|
||||
SDL_SCANCODE_F10: .f10,
|
||||
SDL_SCANCODE_F11: .f11,
|
||||
SDL_SCANCODE_F12: .f12,
|
||||
SDL_SCANCODE_F13: .f13,
|
||||
SDL_SCANCODE_F14: .f14,
|
||||
SDL_SCANCODE_F15: .f15,
|
||||
SDL_SCANCODE_F16: .f16,
|
||||
SDL_SCANCODE_F17: .f17,
|
||||
SDL_SCANCODE_F18: .f18,
|
||||
SDL_SCANCODE_F19: .f19,
|
||||
SDL_SCANCODE_F20: .f20,
|
||||
SDL_SCANCODE_F21: .f21,
|
||||
SDL_SCANCODE_F22: .f22,
|
||||
SDL_SCANCODE_F23: .f23,
|
||||
SDL_SCANCODE_F24: .f24,
|
||||
|
||||
SDL_SCANCODE_LSHIFT: .leftShift,
|
||||
SDL_SCANCODE_RSHIFT: .rightShift,
|
||||
SDL_SCANCODE_LCTRL: .leftControl,
|
||||
SDL_SCANCODE_RCTRL: .rightControl,
|
||||
SDL_SCANCODE_LALT: .leftAlt,
|
||||
SDL_SCANCODE_RALT: .rightAlt,
|
||||
SDL_SCANCODE_LGUI: .leftMeta,
|
||||
SDL_SCANCODE_RGUI: .rightMeta,
|
||||
|
||||
SDL_SCANCODE_UP: .up,
|
||||
SDL_SCANCODE_DOWN: .down,
|
||||
SDL_SCANCODE_LEFT: .left,
|
||||
SDL_SCANCODE_RIGHT: .right,
|
||||
|
||||
SDL_SCANCODE_HOME: .home,
|
||||
SDL_SCANCODE_END: .end,
|
||||
SDL_SCANCODE_PAGEUP: .pageUp,
|
||||
SDL_SCANCODE_PAGEDOWN: .pageDown,
|
||||
SDL_SCANCODE_INSERT: .insert,
|
||||
SDL_SCANCODE_DELETE: .delete,
|
||||
|
||||
SDL_SCANCODE_SPACE: .space,
|
||||
SDL_SCANCODE_TAB: .tab,
|
||||
SDL_SCANCODE_RETURN: .enter,
|
||||
SDL_SCANCODE_ESCAPE: .escape,
|
||||
SDL_SCANCODE_BACKSPACE: .backspace,
|
||||
|
||||
SDL_SCANCODE_KP_0: .numpad0,
|
||||
SDL_SCANCODE_KP_1: .numpad1,
|
||||
SDL_SCANCODE_KP_2: .numpad2,
|
||||
SDL_SCANCODE_KP_3: .numpad3,
|
||||
SDL_SCANCODE_KP_4: .numpad4,
|
||||
SDL_SCANCODE_KP_5: .numpad5,
|
||||
SDL_SCANCODE_KP_6: .numpad6,
|
||||
SDL_SCANCODE_KP_7: .numpad7,
|
||||
SDL_SCANCODE_KP_8: .numpad8,
|
||||
SDL_SCANCODE_KP_9: .numpad9,
|
||||
]
|
||||
|
||||
private let SDLMouseButtons: [UInt8: MouseButton] = [
|
||||
0: .left,
|
||||
1: .right,
|
||||
2: .middle,
|
||||
3: .extra1,
|
||||
4: .extra2,
|
||||
]
|
||||
|
||||
@MainActor
|
||||
public class SDL3Window: Window {
|
||||
static let __sdl_init__ = SDL_Init(0)
|
||||
|
||||
public let window: OpaquePointer!
|
||||
public let id: Int
|
||||
public var width: Int
|
||||
public var height: Int
|
||||
public var actions = ActionManager()
|
||||
public var inputState = InputState()
|
||||
public var mice: [SDL_MouseID] = []
|
||||
|
||||
let clock = ContinuousClock()
|
||||
var previousTime = ContinuousClock.Instant.now
|
||||
var accumulator: Double = 0
|
||||
var running = false
|
||||
var pointerLocked = false
|
||||
|
||||
var drawHandlers: [(Double) -> Void] = []
|
||||
var tickHandlers: [(Double) -> Void] = []
|
||||
var resizeHandlers: [(Int, Int) -> Void] = []
|
||||
|
||||
public required convenience init(width: Int, height: Int) {
|
||||
self.init(width: width, height: height, title: "Artifact Engine")
|
||||
}
|
||||
|
||||
public required init(width: Int, height: Int, title: String) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
_ = Self.__sdl_init__
|
||||
|
||||
window = SDL_CreateWindow(
|
||||
title,
|
||||
Int32(width),
|
||||
Int32(height),
|
||||
0
|
||||
)
|
||||
id = Int(SDL_GetWindowID(window))
|
||||
}
|
||||
|
||||
public func setTitle(title: String) {
|
||||
SDL_SetWindowTitle(window, title)
|
||||
}
|
||||
|
||||
public func close() {
|
||||
running = false
|
||||
if let window = window {
|
||||
SDL_DestroyWindow(window)
|
||||
}
|
||||
}
|
||||
|
||||
public func tick(dtime: Double) {
|
||||
for handler in tickHandlers {
|
||||
handler(dtime)
|
||||
}
|
||||
}
|
||||
|
||||
public func run() {
|
||||
running = true
|
||||
defer { running = false }
|
||||
previousTime = clock.now
|
||||
|
||||
var event = SDL_Event()
|
||||
|
||||
while running {
|
||||
|
||||
while SDL_PollEvent(&event) {
|
||||
switch SDL_EventType(event.type) {
|
||||
case SDL_EVENT_QUIT:
|
||||
close()
|
||||
return
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
// Update size
|
||||
var w: Int32 = 0, h: Int32 = 0
|
||||
SDL_GetWindowSize(window, &w, &h)
|
||||
self.width = Int(w)
|
||||
self.height = Int(h)
|
||||
for handler in resizeHandlers {
|
||||
handler(Int(w), Int(h))
|
||||
}
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
if let key = SDLScancodes[event.key.scancode] {
|
||||
inputState.keyboard[key] = true
|
||||
actions.dispatchKeyDown(key: key)
|
||||
}
|
||||
case SDL_EVENT_KEY_UP:
|
||||
if let key = SDLScancodes[event.key.scancode] {
|
||||
inputState.keyboard[key] = false
|
||||
actions.dispatchKeyUp(key: key)
|
||||
}
|
||||
case SDL_EVENT_MOUSE_ADDED:
|
||||
if mice.isEmpty {
|
||||
inputState.pointers[0] = PointerState(id: 0, position: .zero, delta: .zero, pressure: 0, type: .mouse)
|
||||
}
|
||||
mice.append(event.mdevice.which)
|
||||
case SDL_EVENT_MOUSE_REMOVED:
|
||||
mice.removeAll { $0 == event.mdevice.which }
|
||||
if mice.isEmpty {
|
||||
inputState.pointers.removeValue(forKey: 0)
|
||||
}
|
||||
case SDL_EVENT_MOUSE_WHEEL:
|
||||
inputState.wheelDelta = [event.wheel.x, event.wheel.y]
|
||||
if let state = inputState.pointers[0] {
|
||||
actions.dispatchMouseWheel(state, delta: inputState.wheelDelta)
|
||||
}
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
let delta = Vec2(event.motion.xrel, event.motion.yrel)
|
||||
let pos = Vec2(event.motion.x, event.motion.y)
|
||||
if inputState.pointers[0] != nil {
|
||||
inputState.pointers[0]!.delta = delta
|
||||
inputState.pointers[0]!.position = pos
|
||||
actions.dispatchPointerMove(inputState.pointers[0]!)
|
||||
}
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
if let button = SDLMouseButtons[event.button.button] {
|
||||
inputState.mouseButtons.insert(button)
|
||||
if var state = inputState.pointers[0] {
|
||||
state.position = [event.button.x, event.button.y]
|
||||
actions.dispatchPointerDown(state)
|
||||
}
|
||||
}
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
if let button = SDLMouseButtons[event.button.button] {
|
||||
inputState.mouseButtons.remove(button)
|
||||
if var state = inputState.pointers[0] {
|
||||
state.position = [event.button.x, event.button.y]
|
||||
actions.dispatchPointerUp(state)
|
||||
}
|
||||
}
|
||||
case SDL_EVENT_FINGER_DOWN:
|
||||
let id = Int(event.tfinger.fingerID)
|
||||
inputState.pointers[id] = PointerState(id: id, position: .zero, delta: .zero, pressure: 0, type: .touch)
|
||||
case SDL_EVENT_FINGER_MOTION:
|
||||
let id = Int(event.tfinger.fingerID)
|
||||
if var pointer = inputState.pointers[id] {
|
||||
pointer.delta = [event.tfinger.dx + Float(width), event.tfinger.dy + Float(height)]
|
||||
pointer.position = [event.tfinger.x + Float(width), event.tfinger.y + Float(height)]
|
||||
}
|
||||
case SDL_EVENT_FINGER_UP:
|
||||
let id = Int(event.tfinger.fingerID)
|
||||
inputState.pointers.removeValue(forKey: id)
|
||||
case SDL_EVENT_PEN_AXIS:
|
||||
let id = Int(event.paxis.which)
|
||||
if var pointer = inputState.pointers[id] {
|
||||
switch event.paxis.axis {
|
||||
case SDL_PEN_AXIS_PRESSURE:
|
||||
pointer.pressure = event.paxis.value
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case SDL_EVENT_PEN_DOWN:
|
||||
let id = Int(event.ptouch.which)
|
||||
inputState.pointers[id] = PointerState(id: id, position: [event.ptouch.x, event.ptouch.y], delta: .zero, pressure: 0, type: .pen)
|
||||
case SDL_EVENT_PEN_MOTION:
|
||||
let id = Int(event.pmotion.which)
|
||||
if var pointer = inputState.pointers[id] {
|
||||
let pos = Vec2(event.pmotion.x, event.pmotion.y)
|
||||
pointer.delta = pos - pointer.position
|
||||
pointer.position = pos
|
||||
}
|
||||
case SDL_EVENT_PEN_UP:
|
||||
let id = Int(event.ptouch.which)
|
||||
inputState.pointers.removeValue(forKey: id)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let currentTime = clock.now
|
||||
let frameTime = Double(previousTime.duration(to: currentTime).components.attoseconds) / 1_000_000_000_000_000_000.0
|
||||
previousTime = currentTime
|
||||
accumulator += frameTime
|
||||
|
||||
while accumulator >= MIN_STEP_TIME {
|
||||
tick(dtime: MIN_STEP_TIME)
|
||||
accumulator -= MIN_STEP_TIME
|
||||
}
|
||||
|
||||
let alpha = accumulator / MIN_STEP_TIME
|
||||
for cb in drawHandlers {
|
||||
cb(frameTime * 1000) // TODO: Handle alpha?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func onTick(_ callback: @escaping (Double) -> Void) {
|
||||
tickHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onDraw(_ callback: @escaping (Double) -> Void) {
|
||||
drawHandlers.append(callback)
|
||||
}
|
||||
|
||||
public func onResize(_ callback: @escaping (Int, Int) -> Void) {
|
||||
resizeHandlers.append(callback)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
93
Sources/ArtifactPlatform/Window.swift
Normal file
93
Sources/ArtifactPlatform/Window.swift
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
import ArtifactMath
|
||||
|
||||
@MainActor
|
||||
public protocol Window {
|
||||
init(width: Int, height: Int)
|
||||
init(width: Int, height: Int, title: String)
|
||||
|
||||
var id: Int { get }
|
||||
|
||||
var width: Int { get }
|
||||
var height: Int { get }
|
||||
var actions: ActionManager { get }
|
||||
var inputState: InputState { get }
|
||||
|
||||
func setTitle(title: String)
|
||||
func close()
|
||||
func run()
|
||||
|
||||
func onTick(_ callback: @escaping (Double) -> Void)
|
||||
func onDraw(_ callback: @escaping (Double) -> Void)
|
||||
func onResize(_ callback: @escaping (Int, Int) -> Void)
|
||||
}
|
||||
|
||||
public class ActionManager {
|
||||
public var activationCallbacks: [InputSource: [() -> Void]] = [:]
|
||||
public var deactivationCallbacks: [InputSource: [() -> Void]] = [:]
|
||||
public var mouseWheelCallbacks: [(PointerState, Vec2f) -> Void] = []
|
||||
public var pointerDownCallbacks: [(PointerState) -> Void] = []
|
||||
public var pointerUpCallbacks: [(PointerState) -> Void] = []
|
||||
public var pointerMoveCallbacks: [(PointerState) -> Void] = []
|
||||
|
||||
public func dispatchMouseWheel(_ pointer: PointerState, delta: Vec2f) {
|
||||
for handler in mouseWheelCallbacks {
|
||||
handler(pointer, delta)
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatchPointerDown(_ pointer: PointerState) {
|
||||
for handler in pointerDownCallbacks {
|
||||
handler(pointer)
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatchPointerMove(_ pointer: PointerState) {
|
||||
for handler in pointerMoveCallbacks {
|
||||
handler(pointer)
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatchPointerUp(_ pointer: PointerState) {
|
||||
for handler in pointerUpCallbacks {
|
||||
handler(pointer)
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatchKeyDown(key: Key) {
|
||||
for handler in activationCallbacks[.key(key)] ?? [] {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatchKeyUp(key: Key) {
|
||||
for handler in deactivationCallbacks[.key(key)] ?? [] {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
|
||||
public func onWheel(_ handler: @escaping (PointerState, Vec2f) -> Void) {
|
||||
mouseWheelCallbacks.append(handler)
|
||||
}
|
||||
|
||||
public func onPointerMove(_ handler: @escaping (PointerState) -> Void) {
|
||||
pointerMoveCallbacks.append(handler)
|
||||
}
|
||||
|
||||
public func onPointerDown(_ handler: @escaping (PointerState) -> Void) {
|
||||
pointerDownCallbacks.append(handler)
|
||||
}
|
||||
|
||||
public func onPointerUp(_ handler: @escaping (PointerState) -> Void) {
|
||||
pointerUpCallbacks.append(handler)
|
||||
}
|
||||
|
||||
public func onKeyDown(_ key: Key, _ handler: @escaping () -> Void) {
|
||||
activationCallbacks[.key(key), default: []].append(handler)
|
||||
}
|
||||
|
||||
public func onKeyUp(_ key: Key, _ handler: @escaping () -> Void) {
|
||||
deactivationCallbacks[.key(key), default: []].append(handler)
|
||||
}
|
||||
}
|
||||
|
||||
12
Tests/ArtifactPlatformTests/ArtifactPlatformTests.swift
Normal file
12
Tests/ArtifactPlatformTests/ArtifactPlatformTests.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import XCTest
|
||||
@testable import ArtifactPlatform
|
||||
|
||||
final class ArtifactPlatformTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// XCTest Documentation
|
||||
// https://developer.apple.com/documentation/xctest
|
||||
|
||||
// Defining Test Cases and Test Methods
|
||||
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue