diff --git a/Sources/GateEngine/ECS/2D Specific/Physics/Collision2DSystem.swift b/Sources/GateEngine/ECS/2D Specific/Physics/Collision2DSystem.swift index 4829d928..15b3961f 100644 --- a/Sources/GateEngine/ECS/2D Specific/Physics/Collision2DSystem.swift +++ b/Sources/GateEngine/ECS/2D Specific/Physics/Collision2DSystem.swift @@ -7,14 +7,14 @@ public final class Collision2DSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - for entity in game.entities { + for entity in context.entities { guard entity.hasComponent(Collision2DComponent.self) else { continue } guard entity.hasComponent(Transform2Component.self) else { continue } entity[Collision2DComponent.self].updateColliders(entity.transform2) } guard - let quadtreeEntity = game.entities.first(where: { + let quadtreeEntity = context.entities.first(where: { $0.hasComponent(QuadtreeComponent.self) }) else { @@ -22,7 +22,7 @@ public final class Collision2DSystem: System { } let quadtree = quadtreeEntity[QuadtreeComponent.self].quadtree! - for entity in game.entities { + for entity in context.entities { guard entity.hasComponent(Collision2DComponent.self) else { continue } if let transformComponent = entity.component(ofType: Transform2Component.self) { let object = entity[Collision2DComponent.self] @@ -69,9 +69,3 @@ public final class Collision2DSystem: System { public override class var phase: System.Phase { .simulation } public override class func sortOrder() -> SystemSortOrder? { .collision2DSystem } } - -@MainActor extension Game { - public var collision2DSystem: Collision2DSystem { - return self.system(ofType: Collision2DSystem.self) - } -} diff --git a/Sources/GateEngine/ECS/2D Specific/Physics/Physics2DSystem.swift b/Sources/GateEngine/ECS/2D Specific/Physics/Physics2DSystem.swift index c5831d0c..3e2e6797 100755 --- a/Sources/GateEngine/ECS/2D Specific/Physics/Physics2DSystem.swift +++ b/Sources/GateEngine/ECS/2D Specific/Physics/Physics2DSystem.swift @@ -12,7 +12,7 @@ public final class Physics2DSystem: System { // Skip Physics if we don't have at least 20 fps guard deltaTime < 1 / 20 else { return } - for entity in game.entities { + for entity in context.entities { guard let physicsComponent = entity.component(ofType: Physics2DComponent.self) else { continue } diff --git a/Sources/GateEngine/ECS/2D Specific/Sprite/SpriteSystem.swift b/Sources/GateEngine/ECS/2D Specific/Sprite/SpriteSystem.swift index 14f384e0..c3e777e8 100644 --- a/Sources/GateEngine/ECS/2D Specific/Sprite/SpriteSystem.swift +++ b/Sources/GateEngine/ECS/2D Specific/Sprite/SpriteSystem.swift @@ -7,7 +7,7 @@ public final class SpriteSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - for entity in game.entities { + for entity in context.entities { if let spriteComponent = entity.component(ofType: SpriteComponent.self) { if spriteComponent.moveToNextAnimationIfNeeded { spriteComponent.moveToNextAnimationIfNeeded = false diff --git a/Sources/GateEngine/ECS/2D Specific/TileMap/TileMapSystem.swift b/Sources/GateEngine/ECS/2D Specific/TileMap/TileMapSystem.swift index 1c5a82b9..6b17400f 100644 --- a/Sources/GateEngine/ECS/2D Specific/TileMap/TileMapSystem.swift +++ b/Sources/GateEngine/ECS/2D Specific/TileMap/TileMapSystem.swift @@ -7,7 +7,7 @@ public final class TileMapSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - for entity in game.entities { + for entity in context.entities { if let component = entity.component(ofType: TileMapComponent.self) { if component.needsSetup { self.setup(component) diff --git a/Sources/GateEngine/ECS/3D Specific/Billboard/BillboardSystem.swift b/Sources/GateEngine/ECS/3D Specific/Billboard/BillboardSystem.swift index 0deb9505..3e2af34e 100644 --- a/Sources/GateEngine/ECS/3D Specific/Billboard/BillboardSystem.swift +++ b/Sources/GateEngine/ECS/3D Specific/Billboard/BillboardSystem.swift @@ -7,10 +7,10 @@ public final class BillboardSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - guard let camera = game.cameraEntity else {return} + guard let camera = context.cameraEntity else {return} let cameraTransform = camera.transform3 - for entity in game.entities { + for entity in context.entities { guard let billboardComponent = entity.component(ofType: BillboardComponent.self) else {continue} guard let transformComponent = entity.component(ofType: Transform3Component.self) else {continue} diff --git a/Sources/GateEngine/ECS/3D Specific/CameraComponent.swift b/Sources/GateEngine/ECS/3D Specific/CameraComponent.swift index 1f9b2ae9..ca29c967 100755 --- a/Sources/GateEngine/ECS/3D Specific/CameraComponent.swift +++ b/Sources/GateEngine/ECS/3D Specific/CameraComponent.swift @@ -19,7 +19,7 @@ public final class CameraComponent: Component { public static let componentID: ComponentID = ComponentID() } -@MainActor extension Game { +@MainActor extension ECSContext { public var cameraEntity: Entity? { return self.entities.first(where: { return $0.component(ofType: CameraComponent.self)?.isActive == true diff --git a/Sources/GateEngine/ECS/3D Specific/ObjectAnimation/ObjectAnimation3DSystem.swift b/Sources/GateEngine/ECS/3D Specific/ObjectAnimation/ObjectAnimation3DSystem.swift index 0b5bd4fd..f01dd2be 100644 --- a/Sources/GateEngine/ECS/3D Specific/ObjectAnimation/ObjectAnimation3DSystem.swift +++ b/Sources/GateEngine/ECS/3D Specific/ObjectAnimation/ObjectAnimation3DSystem.swift @@ -7,7 +7,7 @@ public final class ObjectAnimation3DSystem: System { var checkedIDs: Set = [] - func getFarAway(from entities: ContiguousArray) -> Entity? { + func getFarAway(from entities: Set) -> Entity? { func filter(_ entity: Entity) -> Bool { if let objectAnimation = entity.component(ofType: ObjectAnimation3DComponent.self) { return objectAnimation.disabled == false && objectAnimation.deltaAccumulator > 0 @@ -30,7 +30,7 @@ public final class ObjectAnimation3DSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { func shouldAccumulate(entity: Entity) -> Bool { guard - let cameraTransform = game.cameraEntity?.component(ofType: Transform3Component.self) + let cameraTransform = context.cameraEntity?.component(ofType: Transform3Component.self) else { return false } @@ -64,11 +64,11 @@ public final class ObjectAnimation3DSystem: System { } } - let slowEntity = getFarAway(from: game.entities) + let slowEntity = getFarAway(from: context.entities) if let entity = slowEntity { updateAnimation(for: entity) } - for entity in game.entities { + for entity in context.entities { guard entity != slowEntity else { continue } if let component = entity.component(ofType: ObjectAnimation3DComponent.self), component.disabled == false diff --git a/Sources/GateEngine/ECS/3D Specific/Physics/Collision3DSystem.swift b/Sources/GateEngine/ECS/3D Specific/Physics/Collision3DSystem.swift index b1dae687..c8d5bac5 100755 --- a/Sources/GateEngine/ECS/3D Specific/Physics/Collision3DSystem.swift +++ b/Sources/GateEngine/ECS/3D Specific/Physics/Collision3DSystem.swift @@ -7,7 +7,7 @@ public final class Collision3DSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - let staticEntities = game.entities.filter({ + let staticEntities = context.entities.filter({ guard let collisionComponenet = $0.component(ofType: Collision3DComponent.self) else {return false} if case .static = collisionComponenet.kind { return true @@ -17,7 +17,7 @@ public final class Collision3DSystem: System { for entity in staticEntities { entity.collision3DComponent.updateColliders(entity.transform3) } - let dynamicEntities = game.entities.filter({ + let dynamicEntities = context.entities.filter({ guard let collisionComponenet = $0.component(ofType: Collision3DComponent.self) else {return false} if case .dynamic(_) = collisionComponenet.kind { return true @@ -526,9 +526,8 @@ extension Collision3DSystem { } extension Collision3DSystem { - @_transparent private var octrees: [OctreeComponent] { - return game.entities.compactMap({ $0.component(ofType: OctreeComponent.self) }) + return context.entities.compactMap({ $0.component(ofType: OctreeComponent.self) }) } @inline(__always) @@ -594,7 +593,7 @@ extension Collision3DSystem { ) -> [Entity] { var entities: [Entity] = [] - for entity in game.entities { + for entity in context.entities { if let collisionComponent = entity.component(ofType: Collision3DComponent.self), filter?(entity) ?? true @@ -616,7 +615,7 @@ extension Collision3DSystem { ) -> [Entity] { var entities: [Entity] = [] - for entity in game.entities { + for entity in context.entities { if let collisionComponent = entity.component(ofType: Collision3DComponent.self), filter?(entity) ?? true, @@ -694,7 +693,7 @@ extension Collision3DSystem { } } -@MainActor extension Game { +@MainActor extension ECSContext { @_transparent public var collision3DSystem: Collision3DSystem { return self.system(ofType: Collision3DSystem.self) diff --git a/Sources/GateEngine/ECS/3D Specific/Physics/Physics3DSystem.swift b/Sources/GateEngine/ECS/3D Specific/Physics/Physics3DSystem.swift index fa68f879..aa55c600 100755 --- a/Sources/GateEngine/ECS/3D Specific/Physics/Physics3DSystem.swift +++ b/Sources/GateEngine/ECS/3D Specific/Physics/Physics3DSystem.swift @@ -12,7 +12,7 @@ public final class Physics3DSystem: System { // Skip Physics if we don't have at least 20 fps guard deltaTime < 1 / 20 else { return } - for entity in game.entities { + for entity in context.entities { var deltaTime = deltaTime if let scale = entity.component(ofType: TimeScaleComponent.self)?.scale { deltaTime *= scale diff --git a/Sources/GateEngine/ECS/3D Specific/Rig/Rig3DSystem.swift b/Sources/GateEngine/ECS/3D Specific/Rig/Rig3DSystem.swift index b63b7520..6e2b4d99 100755 --- a/Sources/GateEngine/ECS/3D Specific/Rig/Rig3DSystem.swift +++ b/Sources/GateEngine/ECS/3D Specific/Rig/Rig3DSystem.swift @@ -10,7 +10,7 @@ public final class RigSystem {} public final class Rig3DSystem: System { var checkedIDs: Set = [] - func getFarAway(from entities: ContiguousArray) -> Entity? { + func getFarAway(from entities: Set) -> Entity? { func filter(_ entity: Entity) -> Bool { if let rig = entity.component(ofType: Rig3DComponent.self) { return rig.disabled == false && rig.deltaAccumulator > 0 @@ -33,7 +33,7 @@ public final class Rig3DSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { func shouldAccumulate(entity: Entity) -> Bool { guard - let cameraTransform = game.cameraEntity?.component(ofType: Transform3Component.self) + let cameraTransform = context.cameraEntity?.component(ofType: Transform3Component.self) else { return false } @@ -81,11 +81,11 @@ public final class Rig3DSystem: System { } } - let slowEntity = getFarAway(from: game.entities) + let slowEntity = getFarAway(from: context.entities) if let entity = slowEntity { updateAnimation(for: entity) } - for entity in game.entities { + for entity in context.entities { guard entity != slowEntity else { continue } if let component = entity.component(ofType: Rig3DComponent.self), component.disabled == false @@ -98,7 +98,7 @@ public final class Rig3DSystem: System { } } - for entity in game.entities { + for entity in context.entities { if let rigAttachmentComponent = entity.component(ofType: RigAttachmentComponent.self) { updateRigAttachmentTransform( game, @@ -127,12 +127,12 @@ public final class Rig3DSystem: System { rigAttachmentComponent: RigAttachmentComponent ) { guard - let parent = game.entities.first(where: { + let parent = context.entities.first(where: { $0.id == rigAttachmentComponent.parentEntityID }) else { //If the parent is gone trash the attachment - game.removeEntity(entity) + context.removeEntity(entity) return } guard let parentTransform = parent.component(ofType: Transform3Component.self) else { diff --git a/Sources/GateEngine/ECS/Base/ECSContext.swift b/Sources/GateEngine/ECS/Base/ECSContext.swift index 50bb1d21..9c5120c0 100644 --- a/Sources/GateEngine/ECS/Base/ECSContext.swift +++ b/Sources/GateEngine/ECS/Base/ECSContext.swift @@ -5,86 +5,84 @@ * http://stregasgate.com */ -@MainActor extension Game { +@MainActor internal extension Game { @_transparent - public var entities: ContiguousArray { + var entities: ContiguousArray { return ecs.sortedEntities() } @_transparent - public func insertEntity(_ entity: Entity) { + func insertEntity(_ entity: Entity) { ecs.insertEntity(entity) } @_transparent - public func removeEntity(_ entity: Entity) { + func removeEntity(_ entity: Entity) { ecs.removeEntity(entity) } @_transparent @discardableResult - public func removeEntity(named name: String) -> Entity? { + func removeEntity(named name: String) -> Entity? { return ecs.removeEntity(named: name) } @_transparent @discardableResult - public func removeEntity(where block: (Entity) -> (Bool)) -> Entity? { + func removeEntity(where block: (Entity) -> (Bool)) -> Entity? { return ecs.removeEntity(where: block) } @_transparent - public func entity(named name: String) -> Entity? { + func entity(named name: String) -> Entity? { return ecs.entity(named: name) } @_transparent - public func entity(withID id: ObjectIdentifier) -> Entity? { + func entity(withID id: ObjectIdentifier) -> Entity? { return ecs.entity(withID: id) } @_transparent - public func firstEntity(withComponent component: any Component.Type) -> Entity? { + func firstEntity(withComponent component: any Component.Type) -> Entity? { return ecs.firstEntity(withComponent: component) } @_transparent - public func system(ofType systemType: T.Type) -> T { + func system(ofType systemType: T.Type) -> T { return ecs.system(ofType: systemType) as! T } @_transparent - public func hasSystem(ofType systemType: T.Type) -> Bool { + func hasSystem(ofType systemType: T.Type) -> Bool { return ecs.hasSystem(ofType: systemType) } @_transparent - public func system(ofType systemType: T.Type) -> T { + func system(ofType systemType: T.Type) -> T { return ecs.system(ofType: systemType) as! T } @_transparent - public func insertSystem(_ newSystem: System) { + func insertSystem(_ newSystem: System) { ecs.insertSystem(newSystem) } @_transparent - public func insertSystem(_ newSystem: RenderingSystem) { + func insertSystem(_ newSystem: RenderingSystem) { ecs.insertSystem(newSystem) } @_transparent @discardableResult - public func insertSystem(_ system: T.Type) -> T { + func insertSystem(_ system: T.Type) -> T { return ecs.insertSystem(system) as! T } @_transparent @discardableResult - public func insertSystem(_ system: T.Type) -> T { + func insertSystem(_ system: T.Type) -> T { return ecs.insertSystem(system) as! T } @_transparent - public func removeSystem(_ system: System) { + func removeSystem(_ system: System) { ecs.removeSystem(system) } @_transparent - public func removeSystem(_ system: RenderingSystem) { + func removeSystem(_ system: RenderingSystem) { ecs.removeSystem(system) } @_transparent @discardableResult - public func removeSystem(_ system: T.Type) -> T? { + func removeSystem(_ system: T.Type) -> T? { return ecs.removeSystem(system) as? T } @_transparent @discardableResult - public func removeSystem(_ system: T.Type) -> T? { + func removeSystem(_ system: T.Type) -> T? { return ecs.removeSystem(system) as? T } -} -@MainActor extension Game { @_transparent func system(ofType systemType: T.Type) -> T { return ecs.system(ofType: systemType) as! T @@ -268,13 +266,13 @@ extension ECSContext { // } for system in self.systems { self.performance?.beginStatForSystem(system) - await system.willUpdate(context: self, input: input, withTimePassed: deltaTime) + await system.willUpdate(input: input, withTimePassed: deltaTime) self.performance?.endCurrentStatistic() } for system in self.platformSystems { // guard type(of: system).phase == .postDeferred else { continue } self.performance?.beginStatForSystem(system) - await system.willUpdate(context: self, input: input, withTimePassed: deltaTime) + await system.willUpdate(input: input, withTimePassed: deltaTime) self.performance?.endCurrentStatistic() } @@ -318,7 +316,7 @@ extension ECSContext { for system in self.renderingSystems { self.performance?.beginStatForSystem(system) - system.willRender(context: self, into: view, withTimePassed: deltaTime) + system.willRender(into: view, withTimePassed: deltaTime) self.performance?.endCurrentStatistic() } @@ -330,8 +328,7 @@ extension ECSContext { } //MARK: Entity Management -extension ECSContext { - @usableFromInline +public extension ECSContext { func insertEntity(_ entity: Entity) { self.entities.insert(entity) @@ -345,13 +342,11 @@ extension ECSContext { } } } - @usableFromInline func removeEntity(_ entity: Entity) { if let entity = self.entities.remove(entity) { self._removedEntities.append(entity) } } - @usableFromInline func removeEntity(named name: String) -> Entity? { if let entity = self.removeEntity(where: { $0.name == name }) { self._removedEntities.append(entity) @@ -359,7 +354,6 @@ extension ECSContext { } return nil } - @usableFromInline func removeEntity(where block: (Entity) -> (Bool)) -> Entity? { if let removed = self.entities.first(where: block) { self._removedEntities.append(removed) @@ -368,15 +362,15 @@ extension ECSContext { } return nil } - @usableFromInline @_transparent + @_transparent func entity(named name: String) -> Entity? { return entities.first(where: { $0.name == name }) } - @usableFromInline @_transparent + @_transparent func entity(withID id: ObjectIdentifier) -> Entity? { return entities.first(where: { $0.id == id }) } - @usableFromInline @_transparent + @_transparent func firstEntity(withComponent type: any Component.Type) -> Entity? { return entities.first(where: { $0.hasComponent(type) }) } @@ -384,10 +378,10 @@ extension ECSContext { //MARK: System Management public extension ECSContext { - func system(ofType systemType: System.Type) -> System { + func system(ofType systemType: T.Type) -> T { for system in _systems { - if type(of: system) == systemType { - return system + if system is T { + return unsafeDowncast(system, to: systemType) } } return insertSystem(systemType) @@ -400,18 +394,18 @@ public extension ECSContext { } return false } - func system(ofType systemType: RenderingSystem.Type) -> RenderingSystem { + func system(ofType systemType: T.Type) -> T { for system in _renderingSystems { - if type(of: system) == systemType { - return system + if system is T { + return unsafeDowncast(system, to: systemType) } } return insertSystem(systemType) } - internal func system(ofType systemType: PlatformSystem.Type) -> PlatformSystem { + internal func system(ofType systemType: T.Type) -> T { for system in _platformSystems { - if type(of: system) == systemType { - return system + if system is T { + return unsafeDowncast(system, to: systemType) } } return insertSystem(systemType) @@ -445,36 +439,39 @@ public extension ECSContext { platformSystemsNeedSorting = true } @discardableResult - func insertSystem(_ system: System.Type) -> System { - let system = system.init() + func insertSystem(_ system: T.Type) -> T { + let system = system.init(context: self) self.insertSystem(system) return system } @discardableResult - func insertSystem(_ system: RenderingSystem.Type) -> RenderingSystem { - let system = system.init() + func insertSystem(_ system: T.Type) -> T { + let system = system.init(context: self) self.insertSystem(system) return system } @inline(__always) @discardableResult - internal func insertSystem(_ system: PlatformSystem.Type) -> PlatformSystem { - let system = system.init() + internal func insertSystem(_ system: T.Type) -> T { + let system = system.init(context: self) self.insertSystem(system) return system } func removeSystem(_ system: System) { if let index = self._systems.firstIndex(where: { $0 === system }) { - self._systems.remove(at: index).teardown(context: self) + let system = self._systems.remove(at: index) + system.teardown(context: self) } } func removeSystem(_ system: RenderingSystem) { if let index = self._renderingSystems.firstIndex(where: { $0 === system }) { - self._renderingSystems.remove(at: index).teardown(context: self) + let system = self._renderingSystems.remove(at: index) + system.teardown(context: self) } } internal func removeSystem(_ system: PlatformSystem) { if let index = self._platformSystems.firstIndex(where: { $0 === system }) { - self._platformSystems.remove(at: index).teardown(context: self) + let system = self._platformSystems.remove(at: index) + system.teardown(context: self) } } @discardableResult diff --git a/Sources/GateEngine/ECS/Base/PlatformSystem.swift b/Sources/GateEngine/ECS/Base/PlatformSystem.swift index 9bd01d02..4f523f2a 100644 --- a/Sources/GateEngine/ECS/Base/PlatformSystem.swift +++ b/Sources/GateEngine/ECS/Base/PlatformSystem.swift @@ -12,9 +12,13 @@ import GameMath private var didSetup = false private(set) lazy var backgroundTask = BackgroundTask(system: self) - required init() {} + required init(context: ECSContext) { + self.context = context + } + + public let context: ECSContext - internal final func willUpdate(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { + internal final func willUpdate(input: HID, withTimePassed deltaTime: Float) async { if didSetup == false { didSetup = true await setup(context: context, input: input) diff --git a/Sources/GateEngine/ECS/Base/RenderingSystem.swift b/Sources/GateEngine/ECS/Base/RenderingSystem.swift index 84078ff4..d2564dad 100644 --- a/Sources/GateEngine/ECS/Base/RenderingSystem.swift +++ b/Sources/GateEngine/ECS/Base/RenderingSystem.swift @@ -13,12 +13,14 @@ public final var game: Game { return Game.shared } + + public let context: ECSContext - required public init() { - + required public init(context: ECSContext) { + self.context = context } - internal final func willRender(context: ECSContext, into view: GameView, withTimePassed deltaTime: Float) { + internal final func willRender(into view: GameView, withTimePassed deltaTime: Float) { if didSetup == false { didSetup = true setup(context: context) @@ -87,10 +89,10 @@ } extension RenderingSystem: Hashable { - public static func == (lhs: RenderingSystem, rhs: RenderingSystem) -> Bool { + nonisolated public static func ==(lhs: RenderingSystem, rhs: RenderingSystem) -> Bool { return type(of: lhs) == type(of: rhs) } - public func hash(into hasher: inout Hasher) { + nonisolated public func hash(into hasher: inout Hasher) { hasher.combine("\(type(of: self))") } } diff --git a/Sources/GateEngine/ECS/Base/System.swift b/Sources/GateEngine/ECS/Base/System.swift index 02de5d3e..0924bd9d 100755 --- a/Sources/GateEngine/ECS/Base/System.swift +++ b/Sources/GateEngine/ECS/Base/System.swift @@ -11,14 +11,18 @@ import GameMath private var didSetup = false public private(set) lazy var backgroundTask = BackgroundTask(system: self) - required public init() {} + required public init(context: ECSContext) { + self.context = context + } @inlinable @inline(__always) public var game: Game { return Game.shared } + + public let context: ECSContext - internal final func willUpdate(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { + internal final func willUpdate(input: HID, withTimePassed deltaTime: Float) async { if didSetup == false { didSetup = true await setup(for: context, input: input) diff --git a/Sources/GateEngine/ECS/FinalizeSimulation/FinalizeSimulationSystem.swift b/Sources/GateEngine/ECS/FinalizeSimulation/FinalizeSimulationSystem.swift index 821a9027..038db1a4 100755 --- a/Sources/GateEngine/ECS/FinalizeSimulation/FinalizeSimulationSystem.swift +++ b/Sources/GateEngine/ECS/FinalizeSimulation/FinalizeSimulationSystem.swift @@ -9,18 +9,18 @@ import Foundation public final class FinalizeSimulation: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - for entity in game.entities { + for entity in context.entities { if let timedDeathComponent = entity.component(ofType: TimedDeathComponent.self) { timedDeathComponent.timeRemaining -= deltaTime if timedDeathComponent.timeRemaining < 0 { - game.removeEntity(entity) + context.removeEntity(entity) continue } } if let relationshipComponent = entity.component(ofType: ParentRelationshipComponent.self) { if let parentID = relationshipComponent.parent { - if let parent = game.entity(withID: parentID) { + if let parent = context.entity(withID: parentID) { if let transform = relationshipComponent.relativeTransform { if relationshipComponent.options.contains(.relativePosition) { entity.transform3.position = parent.transform3.position + transform.position @@ -43,7 +43,7 @@ public final class FinalizeSimulation: System { } } }else{ - game.removeEntity(entity) + context.removeEntity(entity) continue } } @@ -57,7 +57,7 @@ public final class FinalizeSimulation: System { var maxQuantities: [Int:Int] = [:] var quantities: [Int:Int] = [:] var quantityEntities: [Int:[Entity]] = [:] - for entity in game.entities { + for entity in context.entities { guard let maxQuantityComponent = entity.component(ofType: MaxQuantityComponent.self) else {continue} maxQuantities[maxQuantityComponent.quantityMatchID] = min(maxQuantityComponent.maxQuantity, maxQuantities[maxQuantityComponent.quantityMatchID] ?? .max) @@ -77,7 +77,7 @@ public final class FinalizeSimulation: System { entities.sort(by: {$0[MaxQuantityComponent.self] < $1[MaxQuantityComponent.self]}) for entity in entities[max...] { - game.removeEntity(entity) + context.removeEntity(entity) } } } diff --git a/Sources/GateEngine/ECS/PerformanceRenderingSystem.swift b/Sources/GateEngine/ECS/PerformanceRenderingSystem.swift index ef638056..afd9f88b 100644 --- a/Sources/GateEngine/ECS/PerformanceRenderingSystem.swift +++ b/Sources/GateEngine/ECS/PerformanceRenderingSystem.swift @@ -12,7 +12,7 @@ public final class PerformanceRenderingSystem: RenderingSystem { lazy var text: Text = Text(string: "Accumulating Statistics...\nLoading Resources: \(game.resourceManager.currentlyLoading)", pointSize: 20, color: .green) func rebuildText() -> Bool { - let performance = game.ecs.performance! + let performance = context.performance! let systemsFrameTime = performance.systemsFrameTime let renderingSystemsFrameTime = performance.renderingSystemsFrameTime let totalSystemTime = systemsFrameTime + renderingSystemsFrameTime @@ -43,8 +43,8 @@ public final class PerformanceRenderingSystem: RenderingSystem { string += ", " } } - if game.entities.isEmpty == false { - string += "\(game.entities.count) Entities" + if context.entities.isEmpty == false { + string += "\(context.entities.count) Entities" loadedThingsCount += 1 } loadedThingsLine() @@ -116,6 +116,5 @@ public final class PerformanceRenderingSystem: RenderingSystem { // renderTarget.insert(canvas) } - required public init() {} public override class func sortOrder() -> RenderingSystemSortOrder? { .performance } } diff --git a/Sources/GateEngine/ECS/PlatformSystems/AudioSystem.swift b/Sources/GateEngine/ECS/PlatformSystems/AudioSystem.swift index 39844b74..53d09f56 100644 --- a/Sources/GateEngine/ECS/PlatformSystems/AudioSystem.swift +++ b/Sources/GateEngine/ECS/PlatformSystems/AudioSystem.swift @@ -36,7 +36,7 @@ internal final class AudioSystem: PlatformSystem { } override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - updateSounds(game: Game.shared, withTimePassed: deltaTime) + updateSounds(context: context, withTimePassed: deltaTime) updateMusic(withTimePassed: deltaTime) } @@ -227,12 +227,12 @@ extension AudioSystem { // MARK: - Sounds extension AudioSystem { @inline(__always) - func updateSounds(game: Game, withTimePassed deltaTime: Float) { - updateListener(game: game) + func updateSounds(context: ECSContext, withTimePassed deltaTime: Float) { + updateListener(context: context) for index in soundsPlaying.indices.reversed() { let sound = soundsPlaying[index] - sound.update(deltaTime) + sound.update(deltaTime, context: context) var remove = sound.isDone if case .failed(_) = sound.buffer.state { @@ -407,7 +407,7 @@ extension AudioSystem { } @inline(__always) - @MainActor func update(_ deltaTime: Float) { + @MainActor func update(_ deltaTime: Float, context: ECSContext) { guard buffer.state == .ready else { return } if accumulatedTime == 0 { Task(priority: .medium) { @@ -417,7 +417,7 @@ extension AudioSystem { } if let position = entity?.position3 { source.setPosition(position) - } else if let camera = Game.shared.cameraEntity { + } else if let camera = context.cameraEntity { source.setPosition(camera.position3.moved(0.001, toward: camera.rotation.forward)) } else { source.setPosition(Position3(0, 0, -0.001)) @@ -442,14 +442,14 @@ extension AudioSystem { } @inline(__always) - func updateListener(game: Game) { + func updateListener(context: ECSContext) { var entity: Entity? = nil if let listenerID { - entity = game.entity(withID: listenerID) + entity = context.entity(withID: listenerID) } if entity == nil { listenerID = nil - entity = game.cameraEntity + entity = context.cameraEntity } if let entity { let position = entity.position3 diff --git a/Sources/GateEngine/ECS/PlatformSystems/DeferredDelaySystem.swift b/Sources/GateEngine/ECS/PlatformSystems/DeferredDelaySystem.swift index fc021169..dc99904f 100644 --- a/Sources/GateEngine/ECS/PlatformSystems/DeferredDelaySystem.swift +++ b/Sources/GateEngine/ECS/PlatformSystems/DeferredDelaySystem.swift @@ -51,16 +51,34 @@ internal final class DeferredDelaySystem: PlatformSystem { override class func sortOrder() -> PlatformSystemSortOrder? { .deferredSystem } } +extension ECSContext { + @inline(__always) + public func `defer`(_ closure: @escaping DeferredClosure) { + let _system = self.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) + system.append(deferredClosure: closure) + } + + @inline(__always) + public func delay(_ duration: Float, completion: @escaping ()->()) { + let _system = self.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) + system.append(delayDuration: duration, closure: completion) + } +} + extension System { @inline(__always) public func `defer`(_ closure: @escaping DeferredClosure) { - let system = Game.shared.system(ofType: DeferredDelaySystem.self) + let _system = context.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) system.append(deferredClosure: closure) } @inline(__always) public func delay(_ duration: Float, completion: @escaping ()->()) { - let system = Game.shared.system(ofType: DeferredDelaySystem.self) + let _system = context.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) system.append(delayDuration: duration, closure: completion) } } @@ -68,13 +86,15 @@ extension System { extension PlatformSystem { @_transparent func `defer`(_ closure: @escaping DeferredClosure) { - let system = Game.shared.system(ofType: DeferredDelaySystem.self) + let _system = context.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) system.append(deferredClosure: closure) } @_transparent func delay(_ duration: Float, completion: @escaping ()->()) { - let system = Game.shared.system(ofType: DeferredDelaySystem.self) + let _system = context.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) system.append(delayDuration: duration, closure: completion) } } @@ -82,13 +102,15 @@ extension PlatformSystem { @MainActor extension Game { @inline(__always) public func `defer`(_ closure: @escaping DeferredClosure) { - let system = self.system(ofType: DeferredDelaySystem.self) + let _system = self.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) system.append(deferredClosure: closure) } @inline(__always) public func delay(_ duration: Float, _ closure: @escaping DeferredClosure) { - let system = self.system(ofType: DeferredDelaySystem.self) + let _system = self.system(ofType: DeferredDelaySystem.self) + let system = unsafeDowncast(_system, to: DeferredDelaySystem.self) system.append(delayDuration: duration, closure: closure) } } diff --git a/Sources/GateEngine/ECS/StandardRenderingSystem.swift b/Sources/GateEngine/ECS/StandardRenderingSystem.swift index 27e7d206..03855788 100644 --- a/Sources/GateEngine/ECS/StandardRenderingSystem.swift +++ b/Sources/GateEngine/ECS/StandardRenderingSystem.swift @@ -11,13 +11,10 @@ public final class StandardRenderingSystem: RenderingSystem { internal var verticalResolution: Float? = nil internal lazy var renderTarget: RenderTarget = RenderTarget() - public init(verticalResolution: UInt) { + public convenience init(verticalResolution: UInt, context: ECSContext) { + self.init(context: context) self.verticalResolution = Float(verticalResolution) } - - required public init() { - - } public override func render(context: ECSContext, into view: GameView, withTimePassed deltaTime: Float) { if let verticalResolution = verticalResolution { @@ -27,8 +24,8 @@ public final class StandardRenderingSystem: RenderingSystem { } do { // 3D - if let camera = Camera(game.cameraEntity) { - let sorted3DEntities = game.entities.filter({ + if let camera = Camera(context.cameraEntity) { + let sorted3DEntities = context.entities.filter({ return $0.hasComponent(Transform3Component.self) && $0.hasComponent(MaterialComponent.self) }).sorted { entity1, entity2 in @@ -93,7 +90,7 @@ public final class StandardRenderingSystem: RenderingSystem { interfaceScale: verticalResolution == nil ? view.interfaceScale : 1 ) - for entity in game.entities { + for entity in context.entities { if let transform = entity.component(ofType: Transform3Component.self) { if let spriteComponent = entity.component(ofType: SpriteComponent.self) { if let sprite = spriteComponent.sprite() { diff --git a/Sources/GateEngine/ECS/StateMachine/StateMachine.swift b/Sources/GateEngine/ECS/StateMachine/StateMachine.swift index 90f2e674..4226edec 100755 --- a/Sources/GateEngine/ECS/StateMachine/StateMachine.swift +++ b/Sources/GateEngine/ECS/StateMachine/StateMachine.swift @@ -12,17 +12,17 @@ public final class StateMachine { self.currentState = initialState.init() } - @MainActor internal func updateState(for entity: Entity, game: Game, input: HID, deltaTime: Float) { - currentState.update(for: entity, inGame: game, input: input, withTimePassed: deltaTime) - guard currentState.canMoveToNextState(for: entity, game: game, input: input) else {return} + @MainActor internal func updateState(for entity: Entity, context: ECSContext, input: HID, deltaTime: Float) { + currentState.update(for: entity, inContext: context, input: input, withTimePassed: deltaTime) + guard currentState.canMoveToNextState(for: entity, context: context, input: input) else {return} - for state in currentState.possibleNextStates(for: entity, game: game, input: input) { - if state.canBecomeCurrentState(for: entity, from: currentState, game: game, input: input) { - currentState.willMoveToNextState(for: entity, nextState: state, game: game, input: input) + for state in currentState.possibleNextStates(for: entity, context: context, input: input) { + if state.canBecomeCurrentState(for: entity, from: currentState, context: context, input: input) { + currentState.willMoveToNextState(for: entity, nextState: state, context: context, input: input) let previousState = currentState currentState = state.init() - currentState.apply(to: entity, previousState: previousState, game: game, input: input) - currentState.update(for: entity, inGame: game, input: input, withTimePassed: deltaTime) + currentState.apply(to: entity, previousState: previousState, context: context, input: input) + currentState.update(for: entity, inContext: context, input: input, withTimePassed: deltaTime) return } } @@ -32,27 +32,27 @@ public final class StateMachine { @MainActor public protocol State: AnyObject { nonisolated init() - func apply(to entity: Entity, previousState: some State, game: Game, input: HID) - func update(for entity: Entity, inGame game: Game, input: HID, withTimePassed deltaTime: Float) + func apply(to entity: Entity, previousState: some State, context: ECSContext, input: HID) + func update(for entity: Entity, inContext context: ECSContext, input: HID, withTimePassed deltaTime: Float) - func canMoveToNextState(for entity: Entity, game: Game, input: HID) -> Bool - func possibleNextStates(for entity: Entity, game: Game, input: HID) -> [any State.Type] + func canMoveToNextState(for entity: Entity, context: ECSContext, input: HID) -> Bool + func possibleNextStates(for entity: Entity, context: ECSContext, input: HID) -> [any State.Type] - func willMoveToNextState(for entity: Entity, nextState: any State.Type, game: Game, input: HID) + func willMoveToNextState(for entity: Entity, nextState: any State.Type, context: ECSContext, input: HID) - static func canBecomeCurrentState(for entity: Entity, from currentState: some State, game: Game, input: HID) -> Bool + static func canBecomeCurrentState(for entity: Entity, from currentState: some State, context: ECSContext, input: HID) -> Bool } public extension State { - func canMoveToNextState(for entity: Entity, game: Game, input: HID) -> Bool { + func canMoveToNextState(for entity: Entity, context: ECSContext, input: HID) -> Bool { return true } - func willMoveToNextState(for entity: Entity, nextState: any State.Type, game: Game, input: HID) { + func willMoveToNextState(for entity: Entity, nextState: any State.Type, context: ECSContext, input: HID) { } - static func canBecomeCurrentState(for entity: Entity, from currentState: some State, game: Game, input: HID) -> Bool { + static func canBecomeCurrentState(for entity: Entity, from currentState: some State, context: ECSContext, input: HID) -> Bool { return true } } diff --git a/Sources/GateEngine/ECS/StateMachine/StateMachineComponent.swift b/Sources/GateEngine/ECS/StateMachine/StateMachineComponent.swift index c47f403a..114305c9 100644 --- a/Sources/GateEngine/ECS/StateMachine/StateMachineComponent.swift +++ b/Sources/GateEngine/ECS/StateMachine/StateMachineComponent.swift @@ -16,9 +16,9 @@ public final class StateMachineComponent: Component { final class NoState: State { init() { } - func apply(to entity: Entity, previousState: some State, game: Game, input: HID) { } - func update(for entity: Entity, inGame game: Game, input: HID, withTimePassed deltaTime: Float) { } - func possibleNextStates(for entity: Entity, game: Game, input: HID) -> [any State.Type] { + func apply(to entity: Entity, previousState: some State, context: ECSContext, input: HID) { } + func update(for entity: Entity, inContext context: ECSContext, input: HID, withTimePassed deltaTime: Float) { } + func possibleNextStates(for entity: Entity, context: ECSContext, input: HID) -> [any State.Type] { return [] } } diff --git a/Sources/GateEngine/ECS/StateMachine/StateMachineSystem.swift b/Sources/GateEngine/ECS/StateMachine/StateMachineSystem.swift index 61e279d8..b04f664b 100755 --- a/Sources/GateEngine/ECS/StateMachine/StateMachineSystem.swift +++ b/Sources/GateEngine/ECS/StateMachine/StateMachineSystem.swift @@ -7,19 +7,19 @@ public final class StateMachineSystem: System { public override func update(context: ECSContext, input: HID, withTimePassed deltaTime: Float) async { - for entity in game.entities { + for entity in context.entities { guard let stateMachineComponent = entity.component(ofType: StateMachineComponent.self) else { continue } if stateMachineComponent.shouldApplyInitialState { - applyInitialStateIfNeeded(for: stateMachineComponent, of: entity, inGame: game, input: input) + applyInitialStateIfNeeded(for: stateMachineComponent, of: entity, inContext: context, input: input) } - stateMachineComponent.stateMachine.updateState(for: entity, game: game, input: input, deltaTime: deltaTime) + stateMachineComponent.stateMachine.updateState(for: entity, context: context, input: input, deltaTime: deltaTime) } } - func applyInitialStateIfNeeded(for component: StateMachineComponent, of entity: Entity, inGame game: Game, input: HID) { - component.stateMachine.currentState.apply(to: entity, previousState: component.stateMachine.currentState, game: game, input: input) + func applyInitialStateIfNeeded(for component: StateMachineComponent, of entity: Entity, inContext context: ECSContext, input: HID) { + component.stateMachine.currentState.apply(to: entity, previousState: component.stateMachine.currentState, context: context, input: input) component.shouldApplyInitialState = false } @@ -31,7 +31,7 @@ public final class StateMachineSystem: System { stateMachineComponent.stateMachine.currentState.willMoveToNextState( for: entity, nextState: StateMachineComponent.NoState.self, - game: game, + context: context, input: input ) } diff --git a/Sources/GateEngine/Game.swift b/Sources/GateEngine/Game.swift index 2b46e9d1..bd785112 100644 --- a/Sources/GateEngine/Game.swift +++ b/Sources/GateEngine/Game.swift @@ -53,7 +53,7 @@ public final class Game { @MainActor @usableFromInline let renderer: Renderer! @MainActor public private(set) lazy var windowManager: WindowManager = WindowManager(self) - @MainActor @usableFromInline private(set) lazy var ecs: ECSContext = ECSContext() + @MainActor internal private(set) lazy var ecs: ECSContext = ECSContext() @MainActor public private(set) lazy var hid: HID = HID() public private(set) lazy var resourceManager: ResourceManager = { return ResourceManager(game: self) @@ -152,20 +152,6 @@ public final class Game { self.windowManager.drawWindows() completion() } - if await self.ecs.shouldRenderAfterUpdate( - withTimePassed: Float(deltaTime) - ) { - // Add a high priority Task so we can jump the line if other Tasks were started - Task(priority: .high) { @MainActor in - self.windowManager.drawWindows() - completion() - } - } else { - #if GATEENGINE_DEBUG_RENDERING - Log.warn("Frame Dropped", "DeltaTime:", deltaTime) - #endif - completion() - } } } #else diff --git a/Sources/GateEngine/System/HID/Mouse/Mouse.swift b/Sources/GateEngine/System/HID/Mouse/Mouse.swift index 84c45914..18824755 100644 --- a/Sources/GateEngine/System/HID/Mouse/Mouse.swift +++ b/Sources/GateEngine/System/HID/Mouse/Mouse.swift @@ -12,6 +12,23 @@ import GameMath public var window: Window? { return _window } + + public enum Style { + case arrow + case resizeHorizontal + case resizeVertical + case iBeam + case handPointing + case handOpen + case handClosed + case crosshair + } + + public var style: Style = .arrow { + didSet { + Game.shared.platform.setCursorStyle(style) + } + } public enum Mode { /// Regular cursor behavior diff --git a/Sources/GateEngine/System/Platforms/Apple/AppKit/AppKitPlatform.swift b/Sources/GateEngine/System/Platforms/Apple/AppKit/AppKitPlatform.swift index 6fa82300..bd65d50c 100644 --- a/Sources/GateEngine/System/Platforms/Apple/AppKit/AppKitPlatform.swift +++ b/Sources/GateEngine/System/Platforms/Apple/AppKit/AppKitPlatform.swift @@ -16,6 +16,35 @@ public final class AppKitPlatform: InternalPlatform { init(delegate: any GameDelegate) { self.staticResourceLocations = Self.getStaticSearchPaths(delegate: delegate) } + + func setCursorStyle(_ style: Mouse.Style) { + switch style { + case .arrow: + NSCursor.arrow.set() + case .resizeHorizontal: + if #available(macOS 15.0, *) { + NSCursor.columnResize.set() + } else { + NSCursor.resizeLeftRight.set() + } + case .resizeVertical: + if #available(macOS 15.0, *) { + NSCursor.rowResize.set() + } else { + NSCursor.resizeUpDown.set() + } + case .iBeam: + NSCursor.iBeam.set() + case .handPointing: + NSCursor.pointingHand.set() + case .handOpen: + NSCursor.openHand.set() + case .handClosed: + NSCursor.closedHand.set() + case .crosshair: + NSCursor.crosshair.set() + } + } public var supportsMultipleWindows: Bool { return true diff --git a/Sources/GateEngine/System/Platforms/Platform.swift b/Sources/GateEngine/System/Platforms/Platform.swift index 389d5b28..e394e6f6 100644 --- a/Sources/GateEngine/System/Platforms/Platform.swift +++ b/Sources/GateEngine/System/Platforms/Platform.swift @@ -29,6 +29,8 @@ extension Platform { internal protocol InternalPlatform: AnyObject, Platform { var staticResourceLocations: [URL] { get } + + func setCursorStyle(_ style: Mouse.Style) func systemTime() -> Double @MainActor func main() diff --git a/Sources/GateEngine/System/WindowManager.swift b/Sources/GateEngine/System/WindowManager.swift index e10a70fc..2eb73d52 100644 --- a/Sources/GateEngine/System/WindowManager.swift +++ b/Sources/GateEngine/System/WindowManager.swift @@ -108,7 +108,7 @@ import GameMath } game.attributes.remove(.renderingIsPermitted) self.windowsThatRequestedDraw.removeAll(keepingCapacity: true) - Game.shared.ecs.performance?.endFrame() +// Game.shared.ecs.performance?.endFrame() } } diff --git a/Sources/GateEngine/UI/Layout.swift b/Sources/GateEngine/UI/Layout.swift index a949f11a..62b9ef81 100644 --- a/Sources/GateEngine/UI/Layout.swift +++ b/Sources/GateEngine/UI/Layout.swift @@ -246,8 +246,8 @@ public struct Layout { // Determine the local trailing offset between the view and the target coordinate spaces func relativeResolvedTrailing(forTarget targetView: View) -> Float? { if view.superView === targetView {// Target is the supreview - if let targetHeight = resolveWidth(for: targetView) { - return targetHeight.value + if let targetWidth = resolveWidth(for: targetView) { + return targetWidth.value } }else if targetView.superView === view {// Target is a subview if let targetX = resolveX(for: targetView) { @@ -268,6 +268,27 @@ public struct Layout { return nil } + // Determine the local trailing offset between the view and the target coordinate spaces + func relativeResolvedLeading(forTarget targetView: View) -> Float? { + if view.superView === targetView {// Target is the supreview + if let targetWidth = resolveWidth(for: targetView) { + return targetWidth.value + } + }else if targetView.superView === view {// Target is a subview + if let targetX = resolveX(for: targetView) { + return targetX.value + } + }else if view.superView === targetView.superView {// Target is a sibling + if let targetX = resolveX(for: targetView) { + return targetX.value + } + }else{ + //TODO: The view is in another coordinate space + fatalError("Layout cannot yet constrain views between coordinate spaces.") + } + return nil + } + var computed: Value.Computed? = nil for hSize in view.layoutConstraints.horizontalSizes { @@ -296,6 +317,10 @@ public struct Layout { if let targetTrailing = relativeResolvedTrailing(forTarget: targetView) { computed = Value.Computed(value: targetTrailing - sourceX.value + trailing.constant) } + }else if trailing.target === targetView.leadingAnchor { + if let targetLeading = relativeResolvedLeading(forTarget: targetView) { + computed = Value.Computed(value: targetLeading - sourceX.value + trailing.constant) + } } } } diff --git a/Sources/GateEngine/UI/ScrollView.swift b/Sources/GateEngine/UI/ScrollView.swift index 99ed67a3..a18eb538 100644 --- a/Sources/GateEngine/UI/ScrollView.swift +++ b/Sources/GateEngine/UI/ScrollView.swift @@ -5,7 +5,24 @@ * http://stregasgate.com */ +import GameMath + +public extension ScrollView { + struct ScrollDirection: OptionSet, Sendable { + public let rawValue: UInt8 + + public static let horizontal: Self = Self(rawValue: 1 << 1) + public static let vertical: Self = Self(rawValue: 1 << 2) + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + } +} + open class ScrollView: View { + public var scrollDirection: ScrollDirection = [.vertical] + public var offset: Position2 = .zero { didSet { if offset != oldValue { @@ -13,7 +30,7 @@ open class ScrollView: View { } } } - var animationDuration: Float = 0.01 + var animationDuration: Float = 0.25 var animationAccumulator: Float = 0 var destinationOffset: Position2 = .zero @@ -48,9 +65,12 @@ open class ScrollView: View { open override func scrolled(_ delta: Position2, isPlatformGeneratedMomentum isMomentum: Bool) { super.scrolled(delta, isPlatformGeneratedMomentum: isMomentum) if isMomentum == false { - destinationOffset += delta - destinationOffset.x = .maximum(0, destinationOffset.x) - destinationOffset.y = .maximum(0, destinationOffset.y) + if scrollDirection.contains(.horizontal) { + destinationOffset.x = delta.x + } + if scrollDirection.contains(.vertical) { + destinationOffset.y = delta.y + } } } diff --git a/Sources/GateEngine/UI/SplitViewController.swift b/Sources/GateEngine/UI/SplitViewController.swift new file mode 100644 index 00000000..b2e49e49 --- /dev/null +++ b/Sources/GateEngine/UI/SplitViewController.swift @@ -0,0 +1,163 @@ +/* + * Copyright © 2023-2024 Dustin Collins (Strega's Gate) + * All Rights Reserved. + * + * http://stregasgate.com + */ + +import Foundation + +final class SplitViewDividerControl: Control { + override func canBeHit() -> Bool { + return true + } + + let divider = View() + + var isDragging: Bool = false + + override init() { + super.init() + divider.backgroundColor = .red + self.addSubview(divider) + } + + override func cursorButtonDown(button: MouseButton, mouse: Mouse) { + super.cursorButtonDown(button: button, mouse: mouse) + + if button == .button1 { + self.isDragging = true + self.sendActions(forEvent: .changed) + } + } + + override func cursorButtonUp(button: MouseButton, mouse: Mouse) { + super.cursorButtonUp(button: button, mouse: mouse) + if button == .button1 { + self.isDragging = false + self.sendActions(forEvent: .changed) + } + } + + override func cursorEntered(_ cursor: Mouse) { + super.cursorEntered(cursor) + cursor.style = .resizeHorizontal + cursorFixTimer?.invalidate() + } + + var cursorFixTimer: Timer? = nil + + override func cursorMoved(_ cursor: Mouse) { + super.cursorMoved(cursor) + cursorFixTimer?.invalidate() + } + + override func cursorExited(_ cursor: Mouse) { + super.cursorExited(cursor) + if isDragging { + cursorFixTimer = .scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { timer in + Task { @MainActor in + cursor.style = .arrow + } + }) + }else{ + cursor.style = .arrow + } + } + + override func updateLayoutConstraints() { + super.updateLayoutConstraints() + + self.divider.layoutConstraints.removeAllConstraints() + self.divider.widthAnchor.constrain(to: 1) + divider.topAnchor.constrain(to: self.topAnchor) + divider.bottomAnchor.constrain(to: self.bottomAnchor) + divider.centerXAnchor.constrain(to: self.centerXAnchor) + + self.widthAnchor.constrain(to: 8) + } +} + +public final class SplitView: View { + var dividerXOffset: Float = 230 { + didSet { + self.setNeedsUpdateConstraints() + } + } + + let dividerControl: SplitViewDividerControl = SplitViewDividerControl() + func addSidebarView(_ sidebarView: View, contentView: View) { + for subview in subviews { + subview.removeFromSuperview() + } + self.addSubview(sidebarView) + self.addSubview(contentView) + self.addSubview(dividerControl) + } + + public override init() { + super.init() + + } + + public override func canBeHit() -> Bool { + return true + } + + public override func cursorMoved(_ cursor: Mouse) { + super.cursorMoved(cursor) + if dividerControl.isDragging { + if cursor.button(.button1).isPressed { + if let new = cursor.loactionInView(self)?.x { + dividerXOffset = new + } + } + } + } + + public override func updateLayoutConstraints() { + super.updateLayoutConstraints() + + dividerControl.layoutConstraints.removeAllConstraints() + dividerControl.topAnchor.constrain(to: self.topAnchor) + dividerControl.bottomAnchor.constrain(to: self.bottomAnchor) + dividerControl.widthAnchor.constrain(to: 8) + dividerControl.leadingAnchor.constrain(dividerXOffset - 4, from: self.leadingAnchor, priority: .high) + + self.subviews[0].layoutConstraints.removeAllConstraints() + self.subviews[0].topAnchor.constrain(to: self.topAnchor) + self.subviews[0].leadingAnchor.constrain(to: self.leadingAnchor) + self.subviews[0].bottomAnchor.constrain(to: self.bottomAnchor) + self.subviews[0].widthAnchor.constrain(to: dividerXOffset) + + self.subviews[1].layoutConstraints.removeAllConstraints() + self.subviews[1].topAnchor.constrain(to: self.topAnchor) + self.subviews[1].leadingAnchor.constrain(dividerXOffset + 1, from: self.leadingAnchor) + self.subviews[1].bottomAnchor.constrain(to: self.bottomAnchor) + self.subviews[1].trailingAnchor.constrain(to: self.trailingAnchor) + } +} + +open class SplitViewController: ViewController { + @usableFromInline + internal let context = ECSContext() + + final public override func loadView() { + self.view = SplitView() + } + + open override func viewDidLoad() { + super.viewDidLoad() + self.splitView.addSidebarView(children[0].view, contentView: children[1].view) + } + + var splitView: SplitView { + return self.view as! SplitView + } + + public init(sideBar: ViewController, content: ViewController) { + super.init() + self.addChildViewController(sideBar) + self.addChildViewController(content) + } +} diff --git a/Sources/GateEngine/UI/TableView.swift b/Sources/GateEngine/UI/TableView.swift new file mode 100644 index 00000000..a9476ed4 --- /dev/null +++ b/Sources/GateEngine/UI/TableView.swift @@ -0,0 +1,78 @@ +/* + * Copyright © 2023-2024 Dustin Collins (Strega's Gate) + * All Rights Reserved. + * + * http://stregasgate.com + */ + +open class TableView: View { + public typealias SampleFilter = Material.Channel.SampleFilter + + internal var material = Material() + public var sampleFilter: SampleFilter { + get { + return material.channel(0) { channel in + return channel.sampleFilter + } + } + set { + material.channel(0) { channel in + channel.sampleFilter = newValue + } + } + } + + private var subRect: Rect? = nil + + public init(path: String, meta: (textureSize: Size2, subRect: Rect)? = nil, sampleFilter: SampleFilter = .linear) { + self.subRect = meta?.subRect + self.material.channel(0) { channel in + channel.texture = Texture(path: path, sizeHint: meta?.textureSize, mipMapping: .none) + channel.sampleFilter = sampleFilter + if let subRect = meta?.subRect { + + channel.setSubRect(subRect) + } + } + super.init() + } + + open override func updateLayoutConstraints() { + let size = contentSize() + self.layoutConstraints.removeAllHorizontalSizeConstraints() + self.layoutConstraints.removeAllVerticalSizeConstraints() + self.widthAnchor.constrain(to: size.width) + self.heightAnchor.constrain(to: size.height) + } + + public override func contentSize() -> Size2 { + if let subRect { + return subRect.size + } + if let texture = material.channels[0].texture, texture.sizeIsAvailable { + return texture.size + } + return super.contentSize() + } + + override func draw(_ rect: Rect, into canvas: inout UICanvas) { + super.draw(rect, into: &canvas) + + canvas.insert( + DrawCommand( + resource: .geometry(.rectOriginTopLeft), + transforms: [ + Transform3( + position: Position3(rect.x, rect.y, 0), + scale: Size3(rect.width, rect.height, 1) + ) + ], + material: material, + vsh: .standard, + fsh: .textureSample, + flags: .userInterface + ) + ) + } +} + diff --git a/Sources/GateEngine/UI/TextField.swift b/Sources/GateEngine/UI/TextField.swift index 172e0327..e210c3df 100644 --- a/Sources/GateEngine/UI/TextField.swift +++ b/Sources/GateEngine/UI/TextField.swift @@ -132,6 +132,11 @@ public final class TextField: View { } } } + + public override func didLayout() { + super.didLayout() + self.paragraphWidth = self.frame.size.width * (self.window?.interfaceScale ?? 1) + } public init( text: String, @@ -144,7 +149,7 @@ public final class TextField: View { self.needsUpdateTexture = true self.text = text self.font = font - self.fontSize = fontSize + self.fontSize = fontSize * 2 self.style = style self.material = Material(color: textColor) super.init()