diff --git a/Package.resolved b/Package.resolved index 24aa8df33..e2c3213d6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -53,6 +53,15 @@ "revision" : "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", "version" : "1.9.0" } + }, + { + "identity" : "swiftsdl2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ctreffs/SwiftSDL2.git", + "state" : { + "revision" : "30a2886bd68e43fc19ba29b63ffe230ac0e4db7a", + "version" : "1.4.1" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index c062c573c..114f21cd5 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,10 @@ let package = Package( name: "TokamakStaticHTMLDemo", targets: ["TokamakStaticHTMLDemo"] ), + .library( + name: "TokamakSDL2", + targets: ["TokamakSDL2"] + ), .library( name: "TokamakGTK", targets: ["TokamakGTK"] @@ -65,6 +69,10 @@ let package = Package( url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.9.0" ), + .package( + url: "https://github.com/ctreffs/SwiftSDL2.git", + from: "1.4.1" + ), ], targets: [ // Targets are the basic building blocks of a package. A target can define @@ -85,6 +93,7 @@ let package = Package( dependencies: [ .target(name: "TokamakDOM", condition: .when(platforms: [.wasi])), .target(name: "TokamakGTK", condition: .when(platforms: [.linux])), + .target(name: "TokamakSDL2", condition: .when(platforms: [.linux, .windows, .android])), ] ), .systemLibrary( @@ -119,6 +128,20 @@ let package = Package( ), ] ), + .target( + name: "TokamakSDL2", + dependencies: [ + "TokamakCore", + .product( + name: "OpenCombineShim", + package: "OpenCombine" + ), + .product( + name: "SDL", + package: "SwiftSDL2" + ), + ] + ), .executableTarget( name: "TokamakGTKDemo", dependencies: ["TokamakGTK"], diff --git a/Sources/TokamakSDL2/App/App.swift b/Sources/TokamakSDL2/App/App.swift new file mode 100644 index 000000000..b698df4a6 --- /dev/null +++ b/Sources/TokamakSDL2/App/App.swift @@ -0,0 +1,39 @@ +// Copyright 2020-2021 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Szymon on 2/10/23. +// + +import OpenCombineShim +import SDL +import TokamakCore + +public extension App { + static func _launch(_ app: Self, with configuration: _AppConfiguration) { + _ = Unmanaged.passRetained(SDLRenderer(app, configuration.rootEnvironment)) + } + + static func _setTitle(_ title: String) { + guard let window = SDLRenderer.shared?.window else { return } + SDL_SetWindowTitle(window, title) + } + + var _phasePublisher: AnyPublisher { + CurrentValueSubject(.active).eraseToAnyPublisher() + } + + var _colorSchemePublisher: AnyPublisher { + CurrentValueSubject(.light).eraseToAnyPublisher() + } +} diff --git a/Sources/TokamakSDL2/Core.swift b/Sources/TokamakSDL2/Core.swift new file mode 100644 index 000000000..2d44cabca --- /dev/null +++ b/Sources/TokamakSDL2/Core.swift @@ -0,0 +1,256 @@ +// Copyright 2020-2021 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Szymon on 30/09/23. +// + +import TokamakCore + +// MARK: Environment & State + +public typealias DynamicProperty = TokamakCore.DynamicProperty + +public typealias Environment = TokamakCore.Environment +public typealias EnvironmentKey = TokamakCore.EnvironmentKey +public typealias EnvironmentObject = TokamakCore.EnvironmentObject +public typealias EnvironmentValues = TokamakCore.EnvironmentValues + +public typealias PreferenceKey = TokamakCore.PreferenceKey + +public typealias Binding = TokamakCore.Binding +public typealias ObservableObject = TokamakCore.ObservableObject +public typealias ObservedObject = TokamakCore.ObservedObject +public typealias Published = TokamakCore.Published +public typealias State = TokamakCore.State +public typealias StateObject = TokamakCore.StateObject + +// MARK: Modifiers & Styles + +public typealias ViewModifier = TokamakCore.ViewModifier +public typealias ModifiedContent = TokamakCore.ModifiedContent + +public typealias DefaultTextFieldStyle = TokamakCore.DefaultTextFieldStyle +public typealias PlainTextFieldStyle = TokamakCore.PlainTextFieldStyle +public typealias RoundedBorderTextFieldStyle = TokamakCore.RoundedBorderTextFieldStyle +public typealias SquareBorderTextFieldStyle = TokamakCore.SquareBorderTextFieldStyle + +public typealias DefaultListStyle = TokamakCore.DefaultListStyle +public typealias PlainListStyle = TokamakCore.PlainListStyle +public typealias InsetListStyle = TokamakCore.InsetListStyle +public typealias GroupedListStyle = TokamakCore.GroupedListStyle +public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle +public typealias SidebarListStyle = TokamakCore.SidebarListStyle + +public typealias DefaultPickerStyle = TokamakCore.DefaultPickerStyle +public typealias PopUpButtonPickerStyle = TokamakCore.PopUpButtonPickerStyle +public typealias RadioGroupPickerStyle = TokamakCore.RadioGroupPickerStyle +public typealias SegmentedPickerStyle = TokamakCore.SegmentedPickerStyle +public typealias WheelPickerStyle = TokamakCore.WheelPickerStyle + +public typealias ToggleStyle = TokamakCore.ToggleStyle +public typealias ToggleStyleConfiguration = TokamakCore.ToggleStyleConfiguration + +public typealias ButtonStyle = TokamakCore.ButtonStyle +public typealias ButtonStyleConfiguration = TokamakCore.ButtonStyleConfiguration +public typealias DefaultButtonStyle = TokamakCore.DefaultButtonStyle +public typealias PlainButtonStyle = TokamakCore.PlainButtonStyle +public typealias BorderedButtonStyle = TokamakCore.BorderedButtonStyle +public typealias BorderedProminentButtonStyle = TokamakCore.BorderedProminentButtonStyle +public typealias BorderlessButtonStyle = TokamakCore.BorderlessButtonStyle +public typealias LinkButtonStyle = TokamakCore.LinkButtonStyle + +public typealias ControlGroupStyle = TokamakCore.ControlGroupStyle +public typealias AutomaticControlGroupStyle = TokamakCore.AutomaticControlGroupStyle +public typealias NavigationControlGroupStyle = TokamakCore.NavigationControlGroupStyle + +public typealias TextFieldStyle = TokamakCore.TextFieldStyle + +public typealias FillStyle = TokamakCore.FillStyle +public typealias ShapeStyle = TokamakCore.ShapeStyle +public typealias StrokeStyle = TokamakCore.StrokeStyle + +public typealias ColorScheme = TokamakCore.ColorScheme + +// MARK: Shapes + +public typealias Shape = TokamakCore.Shape + +public typealias Capsule = TokamakCore.Capsule +public typealias Circle = TokamakCore.Circle +public typealias Ellipse = TokamakCore.Ellipse +public typealias Path = TokamakCore.Path +public typealias Rectangle = TokamakCore.Rectangle +public typealias RoundedRectangle = TokamakCore.RoundedRectangle +public typealias ContainerRelativeShape = TokamakCore.ContainerRelativeShape + +// MARK: Shape Styles + +public typealias HierarchicalShapeStyle = TokamakCore.HierarchicalShapeStyle + +public typealias ForegroundStyle = TokamakCore.ForegroundStyle +public typealias BackgroundStyle = TokamakCore.BackgroundStyle + +public typealias Material = TokamakCore.Material + +public typealias Gradient = TokamakCore.Gradient +public typealias LinearGradient = TokamakCore.LinearGradient +public typealias RadialGradient = TokamakCore.RadialGradient +public typealias EllipticalGradient = TokamakCore.EllipticalGradient +public typealias AngularGradient = TokamakCore.AngularGradient + +// MARK: Primitive values + +public typealias Color = TokamakCore.Color +public typealias Font = TokamakCore.Font + +#if !canImport(CoreGraphics) +public typealias CGAffineTransform = TokamakCore.CGAffineTransform +#endif + +public typealias Angle = TokamakCore.Angle +public typealias Axis = TokamakCore.Axis +public typealias UnitPoint = TokamakCore.UnitPoint + +public typealias Edge = TokamakCore.Edge + +public typealias Prominence = TokamakCore.Prominence + +public typealias GraphicsContext = TokamakCore.GraphicsContext + +public typealias TimelineSchedule = TokamakCore.TimelineSchedule +public typealias TimelineScheduleMode = TokamakCore.TimelineScheduleMode +public typealias AnimationTimelineSchedule = TokamakCore.AnimationTimelineSchedule +public typealias EveryMinuteTimelineSchedule = TokamakCore.EveryMinuteTimelineSchedule +public typealias ExplicitTimelineSchedule = TokamakCore.ExplicitTimelineSchedule +public typealias PeriodicTimelineSchedule = TokamakCore.PeriodicTimelineSchedule + +public typealias HorizontalAlignment = TokamakCore.HorizontalAlignment +public typealias VerticalAlignment = TokamakCore.VerticalAlignment + +// MARK: Views + +public typealias Alignment = TokamakCore.Alignment +public typealias Button = TokamakCore.Button +public typealias Canvas = TokamakCore.Canvas +public typealias ControlGroup = TokamakCore.ControlGroup +public typealias ControlSize = TokamakCore.ControlSize +public typealias DatePicker = TokamakCore.DatePicker +public typealias DisclosureGroup = TokamakCore.DisclosureGroup +public typealias Divider = TokamakCore.Divider +public typealias ForEach = TokamakCore.ForEach +public typealias GeometryReader = TokamakCore.GeometryReader +public typealias GridItem = TokamakCore.GridItem +public typealias Group = TokamakCore.Group +public typealias HStack = TokamakCore.HStack +public typealias Image = TokamakCore.Image +public typealias LazyHGrid = TokamakCore.LazyHGrid +public typealias LazyVGrid = TokamakCore.LazyVGrid +public typealias Link = TokamakCore.Link +public typealias List = TokamakCore.List +public typealias NavigationLink = TokamakCore.NavigationLink +public typealias NavigationView = TokamakCore.NavigationView +public typealias OutlineGroup = TokamakCore.OutlineGroup +public typealias Picker = TokamakCore.Picker +public typealias ProgressView = TokamakCore.ProgressView +public typealias ScrollView = TokamakCore.ScrollView +public typealias Section = TokamakCore.Section +public typealias SecureField = TokamakCore.SecureField +public typealias Slider = TokamakCore.Slider +public typealias Spacer = TokamakCore.Spacer +public typealias Text = TokamakCore.Text +public typealias TextEditor = TokamakCore.TextEditor +public typealias TextField = TokamakCore.TextField +public typealias TimelineView = TokamakCore.TimelineView +public typealias Toggle = TokamakCore.Toggle +public typealias VStack = TokamakCore.VStack +public typealias ZStack = TokamakCore.ZStack + +// MARK: Special Views + +public typealias View = TokamakCore.View +public typealias AnyView = TokamakCore.AnyView +public typealias EmptyView = TokamakCore.EmptyView + +// MARK: Layout + +public typealias Layout = TokamakCore.Layout +public typealias AnyLayout = TokamakCore.AnyLayout +public typealias LayoutProperties = TokamakCore.LayoutProperties +public typealias LayoutSubviews = TokamakCore.LayoutSubviews +public typealias LayoutSubview = TokamakCore.LayoutSubview +public typealias LayoutValueKey = TokamakCore.LayoutValueKey +public typealias ProposedViewSize = TokamakCore.ProposedViewSize +public typealias ViewSpacing = TokamakCore.ViewSpacing + +// MARK: Toolbars + +public typealias ToolbarItem = TokamakCore.ToolbarItem +public typealias ToolbarItemGroup = TokamakCore.ToolbarItemGroup +public typealias ToolbarItemPlacement = TokamakCore.ToolbarItemPlacement +public typealias ToolbarContentBuilder = TokamakCore.ToolbarContentBuilder + +// MARK: Text + +public typealias TextAlignment = TokamakCore.TextAlignment + +// MARK: App & Scene + +public typealias App = TokamakCore.App +public typealias _AppConfiguration = TokamakCore._AppConfiguration +public typealias Scene = TokamakCore.Scene +public typealias WindowGroup = TokamakCore.WindowGroup +public typealias ScenePhase = TokamakCore.ScenePhase +public typealias AppStorage = TokamakCore.AppStorage +public typealias SceneStorage = TokamakCore.SceneStorage + +// MARK: Misc + +public typealias ViewBuilder = TokamakCore.ViewBuilder + +// MARK: Animation + +public typealias Animation = TokamakCore.Animation +public typealias Transaction = TokamakCore.Transaction + +public typealias Animatable = TokamakCore.Animatable +public typealias AnimatablePair = TokamakCore.AnimatablePair +public typealias EmptyAnimatableData = TokamakCore.EmptyAnimatableData + +public typealias AnimatableModifier = TokamakCore.AnimatableModifier + +public typealias AnyTransition = TokamakCore.AnyTransition + +public func withTransaction( + _ transaction: Transaction, + _ body: () throws -> Result +) rethrows -> Result { + try TokamakCore.withTransaction(transaction, body) +} + +public func withAnimation( + _ animation: Animation? = .default, + _ body: () throws -> Result +) rethrows -> Result { + try TokamakCore.withAnimation(animation, body) +} + +// FIXME: I would put this inside TokamakCore, but for +// some reason it doesn't get exported with the typealias +public extension Text { + static func + (lhs: Self, rhs: Self) -> Self { + _concatenating(lhs: lhs, rhs: rhs) + } +} + +public typealias PreviewProvider = TokamakCore.PreviewProvider diff --git a/Sources/TokamakSDL2/SDLRenderer.swift b/Sources/TokamakSDL2/SDLRenderer.swift new file mode 100644 index 000000000..92da537b1 --- /dev/null +++ b/Sources/TokamakSDL2/SDLRenderer.swift @@ -0,0 +1,118 @@ +import Dispatch +import Foundation +import SDL +@_spi(TokamakCore) import TokamakCore + +extension EnvironmentValues { + /// Returns default settings for the GTK environment + static var defaultEnvironment: Self { + var environment = EnvironmentValues() + environment[_ColorSchemeKey] = .light + // environment._defaultAppStorage = LocalStorage.standard + // _DefaultSceneStorageProvider.default = SessionStorage.standard + + return environment + } +} + +final class SDLRenderer: Renderer { + static var shared: SDLRenderer? + private(set) var reconciler: StackReconciler? + private(set) var window: OpaquePointer? + private var renderer: OpaquePointer? + + init(_ app: A, _ environment: EnvironmentValues) { + window = SDL_CreateWindow( + "SDL Tokamak Renderer", + Int32(SDL_WINDOWPOS_CENTERED_MASK), + Int32(SDL_WINDOWPOS_CENTERED_MASK), + 800, + 600, + UInt32(SDL_WINDOW_SHOWN.rawValue) + ) + + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED.rawValue) + + if let window { + reconciler = StackReconciler( + app: app, + target: SDLTarget(window: window), + environment: .defaultEnvironment.merging(environment), + renderer: self, + scheduler: { next in + DispatchQueue.main.async { + next() + SDL_ShowWindow(window) + } + } + ) + } + + SDLRenderer.shared = self + } + + func mountTarget( + before sibling: SDLTarget?, + to parent: SDLTarget, + with host: MountedHost + ) -> TargetType? { + guard let anyTarget = mapAnyView( + host.view, + transform: { (target: AnySDL) in target } + ) else { + if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil { + return parent + } + + return nil + } + + let target: OpaquePointer? + switch parent.storage { + case let .application(app): + target = app + case let .renderer(view): + target = view + if let view { + // Present the view content to the window + SDL_RenderPresent(view) + } + } + + guard let target else { return nil } + return SDLTarget(host.view, target) + } + + func update( + target: SDLTarget, + with host: MountedHost + ) { + guard let view = mapAnyView(host.view, transform: { (target: AnySDL) in target }) + else { return } + + view.update(target: target) + } + + func unmount( + target: SDLTarget, + from parent: SDLTarget, + with task: UnmountHostTask + ) { + defer { task.finish() } + guard mapAnyView(task.host.view, transform: { (target: AnySDL) in target }) != nil + else { return } + target.destroy() + } + + public func isPrimitiveView(_ type: Any.Type) -> Bool { + type is SDLPrimitive.Type + } + + public func primitiveBody(for view: Any) -> AnyView? { + (view as? SDLPrimitive)?.renderedBody + } +} + +protocol SDLPrimitive { + var renderedBody: AnyView { get } +} diff --git a/Sources/TokamakSDL2/SDLTarget.swift b/Sources/TokamakSDL2/SDLTarget.swift new file mode 100644 index 000000000..39b0337e6 --- /dev/null +++ b/Sources/TokamakSDL2/SDLTarget.swift @@ -0,0 +1,93 @@ +import Foundation +import SDL +@_spi(TokamakCore) import TokamakCore + +protocol AnySDL { + var expand: Bool { get } + func new(_ application: OpaquePointer) -> SDLTarget + func update(target: SDLTarget) +} + +extension AnySDL { + var expand: Bool { false } +} + +struct SDLView: View, AnySDL, ParentView { + let build: (OpaquePointer) -> SDLTarget + let update: (SDLTarget) -> () + let content: Content + let expand: Bool + + init( + build: @escaping (OpaquePointer) -> SDLTarget, + update: @escaping (SDLTarget) -> () = { _ in }, + expand: Bool = false, + @ViewBuilder content: () -> Content + ) { + self.build = build + self.expand = expand + self.content = content() + self.update = update + } + + func new(_ application: OpaquePointer) -> SDLTarget { + build(application) + } + + func update(target: SDLTarget) { + if case .renderer = target.storage { + update(target) + } + } + + var body: Never { + neverBody("SDLView") + } + + var children: [AnyView] { + [AnyView(content)] + } +} + +extension SDLView where Content == EmptyView { + init( + build: @escaping (OpaquePointer) -> SDLTarget, + expand: Bool = false + ) { + self.init(build: build, expand: expand) { EmptyView() } + } +} + +final class SDLTarget: Target { + enum Storage { + case application(OpaquePointer?) + case renderer(OpaquePointer?) + } + + let storage: Storage + var view: AnyView + + init(_ view: V, _ ref: OpaquePointer) { + storage = .renderer(ref) + self.view = AnyView(view) + } + + init(renderer ref: OpaquePointer) { + storage = .renderer(ref) + view = AnyView(EmptyView()) + } + + init(window ref: OpaquePointer) { + storage = .application(ref) + view = AnyView(EmptyView()) + } + + func destroy() { + switch storage { + case .application: + fatalError("Attempt to destroy root Application.") + case let .renderer(target): + SDL_DestroyRenderer(target) + } + } +} diff --git a/Sources/TokamakSDL2/Scenes/WindowGroup.swift b/Sources/TokamakSDL2/Scenes/WindowGroup.swift new file mode 100644 index 000000000..466986dd3 --- /dev/null +++ b/Sources/TokamakSDL2/Scenes/WindowGroup.swift @@ -0,0 +1,7 @@ +import TokamakCore + +extension WindowGroup: SceneDeferredToRenderer { + public var deferredBody: AnyView { + AnyView(content) + } +} diff --git a/Sources/TokamakShim/TokamakShim.swift b/Sources/TokamakShim/TokamakShim.swift index 33a10ca67..c82f492ee 100644 --- a/Sources/TokamakShim/TokamakShim.swift +++ b/Sources/TokamakShim/TokamakShim.swift @@ -17,5 +17,9 @@ #elseif os(WASI) @_exported import TokamakDOM #elseif os(Linux) -@_exported import TokamakGTK +@_exported import TokamakSDL2 +#elseif os(Windows) +@_exported import TokamakSDL2 +#elseif os(Android) +@_exported import TokamakSDL2 #endif