diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..867ed8e88 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "swift.path": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin", + "swift.swiftEnvironmentVariables": { + "DEVELOPER_DIR": "/Applications/Xcode.app/Contents/Developer" + } +} \ No newline at end of file diff --git a/Example/ShaftExample/.gitignore b/Example/ShaftExample/.gitignore new file mode 100644 index 000000000..b7f13992f --- /dev/null +++ b/Example/ShaftExample/.gitignore @@ -0,0 +1 @@ +.build \ No newline at end of file diff --git a/Example/ShaftExample/Package.resolved b/Example/ShaftExample/Package.resolved new file mode 100644 index 000000000..80138361e --- /dev/null +++ b/Example/ShaftExample/Package.resolved @@ -0,0 +1,159 @@ +{ + "originHash" : "d4e2dd09edcc5a35a8f033b498c3430ea20e4f83d6815bc2ab70e7ce60a326c4", + "pins" : [ + { + "identity" : "darwinprivateframeworks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", + "state" : { + "branch" : "main", + "revision" : "392e3b27e14a11bc4713f7a746d59ceb0076c85f" + } + }, + { + "identity" : "openattributegraph", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/OpenAttributeGraph", + "state" : { + "branch" : "main", + "revision" : "b8ee96828f38cd221c67c252a6488d99fef04468" + } + }, + { + "identity" : "opencoregraphics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/OpenCoreGraphics", + "state" : { + "branch" : "main", + "revision" : "cd89c292c4ed4c25d9468a12d9490cc18304ff37" + } + }, + { + "identity" : "openobservation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/OpenObservation", + "state" : { + "branch" : "main", + "revision" : "814dbe008056db6007bfc3d27fe585837f30e9ed" + } + }, + { + "identity" : "openrenderbox", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/OpenRenderBox", + "state" : { + "branch" : "main", + "revision" : "ebed504f2785edfe500ebd0552a1bcf6d37071ba" + } + }, + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Rainbow", + "state" : { + "revision" : "16da5c62dd737258c6df2e8c430f8a3202f655a7", + "version" : "4.2.0" + } + }, + { + "identity" : "shaft", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/Shaft", + "state" : { + "branch" : "main", + "revision" : "ea52999447248fcb91096392dc95e2f1afece8ef" + } + }, + { + "identity" : "splash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/Splash", + "state" : { + "branch" : "master", + "revision" : "ed08785980b61de9b98306434410ce7fc10572ea" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark.git", + "state" : { + "branch" : "gfm", + "revision" : "924936d0427cb25a61169739a7660230bffa6ea6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/swift-collections", + "state" : { + "revision" : "52a1f698d5faa632df0e1219b1bbffa07cf65260", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-markdown", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-markdown.git", + "state" : { + "branch" : "main", + "revision" : "b2135f426fca19029430fbf26564e953b2d0f3d3" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, + { + "identity" : "swiftmath", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftMath", + "state" : { + "revision" : "29039462bcd88b9469041f2678b892d0dd7a4c6f", + "version" : "3.4.0" + } + }, + { + "identity" : "swiftreload", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftReload.git", + "state" : { + "revision" : "e0b67c14779b880c475de7c3c5e4778b23cf90fa", + "version" : "0.0.1" + } + }, + { + "identity" : "swiftsdl3", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftSDL3", + "state" : { + "revision" : "64fb16e7b2546cc33aefdad2304f909e08a5e54e", + "version" : "0.1.6" + } + }, + { + "identity" : "symbollocator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/SymbolLocator.git", + "state" : { + "revision" : "546053c03f282df1a8270853da6692e1b078be09", + "version" : "0.2.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", + "version" : "5.3.1" + } + } + ], + "version" : 3 +} diff --git a/Example/ShaftExample/Package.swift b/Example/ShaftExample/Package.swift new file mode 100644 index 000000000..20b9fa32e --- /dev/null +++ b/Example/ShaftExample/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "ShaftExample", + platforms: [ + .macOS(.v15), + .iOS(.v18), + ], + products: [ + .executable(name: "ShaftExample", targets: ["ShaftExample"]), + ], + dependencies: [ + .package(path: "../../"), // OpenSwiftUI + ], + targets: [ + .executableTarget( + name: "ShaftExample", + dependencies: [ + .product(name: "OpenSwiftUI", package: "OpenSwiftUI"), + .product(name: "OpenSwiftUIShaftBackend", package: "OpenSwiftUI"), + ], + swiftSettings: [ + .interoperabilityMode(.Cxx), + ] + ), + ], + cxxLanguageStandard: .cxx17, +) + diff --git a/Example/ShaftExample/Sources/ShaftExample/main.swift b/Example/ShaftExample/Sources/ShaftExample/main.swift new file mode 100644 index 000000000..4ad2f6b7f --- /dev/null +++ b/Example/ShaftExample/Sources/ShaftExample/main.swift @@ -0,0 +1,30 @@ +// +// main.swift +// ShaftExample +// +// Example application demonstrating OpenSwiftUI rendering via Shaft +// + +import Foundation +import OpenSwiftUI +import OpenSwiftUIShaftBackend + +// Define a simple OpenSwiftUI view +struct ContentView: View { + var body: some View { + VStack { + Color.red + .frame(width: 100, height: 60) + Spacer() + Color.blue + .frame(width: 100, height: 60) + .rotationEffect(.degrees(45)) + Spacer() + Color.green + .frame(width: 100, height: 60) + } + } +} + +// Run the application +ShaftHostingView.run(rootView: ContentView()) diff --git a/Package.resolved b/Package.resolved index 5c561d621..5a338f08c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "70d11bc750ff2cfa581c1eb54f9d597afbc1c36ff13574e89c840b85a29e1e5c", + "originHash" : "9022290e4dbc74a236c5d1d0df4fcc218be6c43bb5e3bc10ecc685f8f62b6a5f", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -46,6 +46,60 @@ "revision" : "ebed504f2785edfe500ebd0552a1bcf6d37071ba" } }, + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Rainbow", + "state" : { + "revision" : "16da5c62dd737258c6df2e8c430f8a3202f655a7", + "version" : "4.2.0" + } + }, + { + "identity" : "shaft", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/Shaft", + "state" : { + "branch" : "main", + "revision" : "ea52999447248fcb91096392dc95e2f1afece8ef" + } + }, + { + "identity" : "splash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/Splash", + "state" : { + "branch" : "master", + "revision" : "ed08785980b61de9b98306434410ce7fc10572ea" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark.git", + "state" : { + "branch" : "gfm", + "revision" : "924936d0427cb25a61169739a7660230bffa6ea6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/swift-collections", + "state" : { + "revision" : "52a1f698d5faa632df0e1219b1bbffa07cf65260", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-markdown", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-markdown.git", + "state" : { + "branch" : "main", + "revision" : "b2135f426fca19029430fbf26564e953b2d0f3d3" + } + }, { "identity" : "swift-numerics", "kind" : "remoteSourceControl", @@ -64,6 +118,33 @@ "version" : "601.0.1" } }, + { + "identity" : "swiftmath", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftMath", + "state" : { + "revision" : "29039462bcd88b9469041f2678b892d0dd7a4c6f", + "version" : "3.4.0" + } + }, + { + "identity" : "swiftreload", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftReload.git", + "state" : { + "revision" : "e0b67c14779b880c475de7c3c5e4778b23cf90fa", + "version" : "0.0.1" + } + }, + { + "identity" : "swiftsdl3", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftSDL3", + "state" : { + "revision" : "64fb16e7b2546cc33aefdad2304f909e08a5e54e", + "version" : "0.1.6" + } + }, { "identity" : "symbollocator", "kind" : "remoteSourceControl", @@ -72,6 +153,15 @@ "revision" : "546053c03f282df1a8270853da6692e1b078be09", "version" : "0.2.1" } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", + "version" : "5.3.1" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index a96724886..1901d657c 100644 --- a/Package.swift +++ b/Package.swift @@ -177,6 +177,7 @@ let openCombineCondition = envBoolValue("OPENCOMBINE", default: !buildForDarwinP let swiftLogCondition = envBoolValue("SWIFT_LOG", default: !buildForDarwinPlatform) let swiftCryptoCondition = envBoolValue("SWIFT_CRYPTO", default: !buildForDarwinPlatform) let renderGTKCondition = envBoolValue("RENDER_GTK", default: !buildForDarwinPlatform) +let shaftBackendCondition = envBoolValue("SHAFT_BACKEND") let swiftUIRenderCondition = envBoolValue("SWIFTUI_RENDER", default: buildForDarwinPlatform) @@ -673,6 +674,21 @@ let openSwiftUIBridgeTestTarget = Target.testTarget( swiftSettings: sharedSwiftSettings ) +// MARK: - OpenSwiftUIShaftBackend Target + +let openSwiftUIShaftBackendTarget = Target.target( + name: "OpenSwiftUIShaftBackend", + dependencies: [ + "OpenSwiftUI", + "OpenSwiftUICore", + .product(name: "Shaft", package: "Shaft"), + .product(name: "ShaftSetup", package: "Shaft"), + ], + cSettings: sharedCSettings, + cxxSettings: sharedCxxSettings, + swiftSettings: sharedSwiftSettings + [.interoperabilityMode(.Cxx)] +) + // MARK: - OpenSwiftUISymbolDualTests Target let openSwiftUISymbolDualTestsSupportTarget = Target.target( @@ -725,6 +741,9 @@ if supportMultiProducts { .library(name: "OpenSwiftUIBridge", targets: ["OpenSwiftUIBridge"]) ] } +if shaftBackendCondition { + products.append(.library(name: "OpenSwiftUIShaftBackend", targets: ["OpenSwiftUIShaftBackend"])) +} // MARK: - Package @@ -848,6 +867,13 @@ if useLocalDeps { package.dependencies += dependencies } +if shaftBackendCondition { + // Use relative path for easier local development + // package.dependencies.append(.package(path: "../../ShaftUI/Shaft")) + package.dependencies.append(.package(url: "https://github.com/ShaftUI/Shaft", branch: "main")) + package.targets.append(openSwiftUIShaftBackendTarget) +} + if openCombineCondition { package.dependencies.append( .package(url: "https://github.com/OpenSwiftUIProject/OpenCombine.git", from: "0.15.0") @@ -871,3 +897,5 @@ if swiftCryptoCondition { openSwiftUICoreTarget.addSwiftCryptoSettings() openSwiftUITarget.addSwiftCryptoSettings() } + +package.cxxLanguageStandard = .cxx17 // For building Shaft's Skia backend \ No newline at end of file diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift index 8970cf90b..267d85bdc 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift @@ -8,7 +8,7 @@ package import Foundation -protocol ViewRendererBase: AnyObject { +package protocol ViewRendererBase: AnyObject { var platform: DisplayList.ViewUpdater.Platform { get } var exportedObject: AnyObject? { get } func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time @@ -176,11 +176,11 @@ extension DisplayList { _openSwiftUIBaseClassAbstractMethod() } - var exportedObject: AnyObject? { + package var exportedObject: AnyObject? { platform.definition.getRBLayer(drawingView: drawingView!) } - func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time { + package func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time { // _openSwiftUIUnimplementedFailure() if printTree == nil { printTree = ProcessEnvironment.bool(forKey: "OPENSWIFTUI_PRINT_TREE") @@ -191,15 +191,15 @@ extension DisplayList { return .zero } - func renderAsync(to list: DisplayList, time: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time? { + package func renderAsync(to list: DisplayList, time: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time? { _openSwiftUIUnimplementedFailure() } - func destroy(rootView: AnyObject) { + package func destroy(rootView: AnyObject) { _openSwiftUIUnimplementedFailure() } - var viewCacheIsEmpty: Bool { + package var viewCacheIsEmpty: Bool { _openSwiftUIUnimplementedFailure() } } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift index 9bb8c50b1..537a7b1e4 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift @@ -31,7 +31,7 @@ extension DisplayList { _openSwiftUIUnimplementedFailure() } - func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time { + package func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time { // TODO if printTree == nil { printTree = ProcessEnvironment.bool(forKey: "OPENSWIFTUI_PRINT_TREE") @@ -42,24 +42,24 @@ extension DisplayList { return .zero } - func renderAsync(to list: DisplayList, time: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time? { + package func renderAsync(to list: DisplayList, time: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time? { nil } - func destroy(rootView: AnyObject) { + package func destroy(rootView: AnyObject) { } - var viewCacheIsEmpty: Bool { + package var viewCacheIsEmpty: Bool { // TODO false } - var platform: Platform { + package var platform: Platform { // TODO _openSwiftUIUnimplementedFailure() } - var exportedObject: AnyObject? { + package var exportedObject: AnyObject? { // TODO nil } diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift index 475df51c4..172708d1c 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift @@ -43,6 +43,16 @@ package protocol ViewRendererHost: ViewGraphDelegate { func updateFocusedValues() func updateAccessibilityEnvironment() + + func renderDisplayList( + _ list: DisplayList, + asynchronously: Bool, + time: Time, + nextTime: Time, + targetTimestamp: Time?, + version: DisplayList.Version, + maxVersion: DisplayList.Version + ) -> Time } // MARK: - ViewRendererHost + default implementation [6.5.4] @@ -292,7 +302,7 @@ extension ViewRendererHost { maxVersion: DisplayList.Version ) -> Time { guard let delegate = self.as(ViewGraphRenderDelegate.self), - let renderer = self.as(DisplayList.ViewRenderer.self) + let renderer = self.as(DisplayList.ViewRenderer.self) else { return .infinity } func renderOnMainThread() -> Time { diff --git a/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift b/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift new file mode 100644 index 000000000..1c9f28c79 --- /dev/null +++ b/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift @@ -0,0 +1,222 @@ +// +// DisplayListConverter.swift +// OpenSwiftUIShaftBackend +// +// Converts OpenSwiftUI DisplayList to Shaft widget tree +// + +import Foundation +import OpenSwiftUICore +import Shaft +import SwiftMath + +/// Converts OpenSwiftUI DisplayList structures into Shaft widgets +final class DisplayListConverter { + private var contentsScale: Float = 1.0 + + init() {} + + /// Convert a DisplayList to a Shaft widget tree + func convertDisplayList( + _ displayList: OpenSwiftUICore.DisplayList, + contentsScale: CGFloat + ) -> Widget { + self.contentsScale = Float(contentsScale) + + // Handle empty display list + guard !displayList.items.isEmpty else { + return SizedBox(width: 0, height: 0) + } + + // Convert all items + let widgets = displayList.items.compactMap { convertItem($0) } + + // If single item, return it directly + // if widgets.count == 1 { + // return widgets[0] + // } + + // Multiple items - stack them + return Stack { widgets } + } + + /// Convert a single DisplayList.Item to a Shaft widget + private func convertItem(_ item: OpenSwiftUICore.DisplayList.Item) -> Widget { + // Each item has a frame and a value + let frame = item.frame + + let result = + switch item.value { + case .empty: + SizedBox() + + case .content(let content): + // Actual renderable content + convertContent(content) + + case .effect(let effect, let childList): + // Effect applied to child display list + // let childWidget = + applyEffect(effect, to: convertDisplayList( + childList, contentsScale: CGFloat(contentsScale) + )) + + case .states(let states): + // State-dependent display lists + // For now, just render the first state if available + if let firstState = states.first { + convertDisplayList(firstState.1, contentsScale: CGFloat(contentsScale)) + } else { + Text("states not implemented") + } + } + + return Positioned( + left: Float(frame.minX), top: Float(frame.minY), + width: Float(frame.width), height: Float(frame.height) + ) { + result + } + } + + /// Convert Content to Shaft widget + private func convertContent( + _ content: OpenSwiftUICore.DisplayList.Content, + ) -> Widget { + switch content.value { + case .color(let resolvedColor): + return convertColor(resolvedColor) + + case .text(let textView, let size): + return convertText(textView, size: size) + + case .shape(let path, let paint, let fillStyle): + return convertShape(path: path, paint: paint, fillStyle: fillStyle) + + case .image(let graphicsImage): + return convertImage(graphicsImage) + + case .flattened(let displayList, let offset, _): + // Nested display list + let childWidget = convertDisplayList(displayList, contentsScale: CGFloat(contentsScale)) + if offset != .zero { + return Positioned( + left: Float(offset.x), + top: Float(offset.y) + ) { + childWidget + } + } + return childWidget + + default: + return Text("\(content.value) not implemented") + } + } + + /// Apply an effect to a widget + private func applyEffect( + _ effect: OpenSwiftUICore.DisplayList.Effect, + to widget: Widget, + ) -> Widget { + switch effect { + case .identity: + return widget + + case .opacity(let alpha): + // TODO: Shaft doesn't have Opacity widget - need to implement custom + // For now, just return the widget without opacity + return widget + + case .transform(let transform): + return applyTransform(transform, to: widget) + + case .clip(let path, _, _): + // TODO: Implement clipping with path + return widget + + case .mask(let maskList, _): + // TODO: Implement masking + return widget + + case .geometryGroup, .compositingGroup, .backdropGroup: + // Grouping effects - just return widget for now + return widget + + case .properties, .blendMode, .filter: + // Advanced effects - not supported initially + return widget + + case .archive, .platformGroup, .animation, .contentTransition, .view, .accessibility, + .platform, .state, .interpolatorRoot, .interpolatorLayer, .interpolatorAnimation: + // Complex features - not supported initially + return widget + } + } + + /// Apply transform to widget + private func applyTransform( + _ transform: OpenSwiftUICore.DisplayList.Transform, + to widget: Widget + ) -> Widget { + mark("transform: \(transform)") + switch transform { + case .affine(let affineTransform): + // TODO: affine transform + return widget + + case .rotation(let data): + return Transform( + transform: Matrix4x4f.rotate(z: .init(radians: Float(data.angle.radians))) + ) { + widget + } + + case .rotation3D, .projection: + // TODO: Implement 3D transforms + return widget + } + } + + // MARK: - Content Converters + + private func convertColor( + _ resolvedColor: OpenSwiftUICore.Color.Resolved, + ) -> Widget { + // Convert from linear color (0.0-1.0) to sRGB bytes (0-255) + let a = UInt8(resolvedColor.opacity * 255) + let r = UInt8(resolvedColor.linearRed * 255) + let g = UInt8(resolvedColor.linearGreen * 255) + let b = UInt8(resolvedColor.linearBlue * 255) + let shaftColor = Shaft.Color.argb(a, r, g, b) + + return DecoratedBox(decoration: .box(color: shaftColor)) + } + + private func convertText( + _ textView: StyledTextContentView, + size: CGSize, + ) -> Widget { + // TODO: Extract actual text content from StyledTextContentView + // This is a complex type that needs proper parsing + // For now, return a placeholder + return Text("TODO: Extract text") + } + + private func convertShape( + path: OpenSwiftUICore.Path, + paint: AnyResolvedPaint, + fillStyle: FillStyle, + ) -> Widget { + // TODO: Convert CGPath to Shaft Path + // TODO: Handle paint and fill styles + return Text("TODO: Extract shape") + } + + private func convertImage( + _ graphicsImage: GraphicsImage, + ) -> Widget { + // TODO: Extract and convert image data + return Text("TODO: Extract image") + } +} diff --git a/Sources/OpenSwiftUIShaftBackend/ShaftBridgeWidget.swift b/Sources/OpenSwiftUIShaftBackend/ShaftBridgeWidget.swift new file mode 100644 index 000000000..7b6ea3e6c --- /dev/null +++ b/Sources/OpenSwiftUIShaftBackend/ShaftBridgeWidget.swift @@ -0,0 +1,41 @@ +// +// ShaftBridgeWidget.swift +// OpenSwiftUIShaftBackend +// +// Bridge widget that holds OpenSwiftUI's converted widget tree +// + +import Foundation +import Shaft + +/// A Shaft StatelessWidget that bridges OpenSwiftUI DisplayList updates +/// +/// This widget uses a ValueNotifier to hold the current widget tree. +/// When the DisplayList changes and updates the notifier, Shaft's @Observable +/// system automatically triggers a rebuild. +final class ShaftBridgeWidget: StatelessWidget { + init(widgetNotifier: ValueNotifier) { + self.widgetNotifier = widgetNotifier + } + + /// ValueNotifier holding the current converted widget tree + /// When this changes, the widget automatically rebuilds thanks to @Observable + let widgetNotifier: ValueNotifier + + public func build(context: BuildContext) -> Widget { + print("widgetNotifier.value: \(widgetNotifier.value)") + + // Reading .value automatically subscribes to changes + return widgetNotifier.value + } +} + +/// Empty widget used as initial placeholder +final class EmptyWidget: StatelessWidget { + init() {} + + public func build(context: BuildContext) -> Widget { + SizedBox(width: 0, height: 0) + } +} + diff --git a/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift b/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift new file mode 100644 index 000000000..3ac7d3883 --- /dev/null +++ b/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift @@ -0,0 +1,164 @@ +// +// ShaftHostingView.swift +// OpenSwiftUIShaftBackend +// +// Public API for hosting OpenSwiftUI views in Shaft +// + +import Foundation +public import OpenSwiftUI +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore +import Shaft +import ShaftSetup + +/// A hosting view that renders OpenSwiftUI views using Shaft's rendering system +public enum ShaftHostingView { + /// Run an OpenSwiftUI view in a Shaft application + /// + /// This sets up Shaft's default backend, creates the rendering pipeline, + /// and starts the event loop. + public static func run(rootView: Content) { + // Set up Shaft's default backend (SDL3 + Skia) + ShaftSetup.useDefault() + + // Create the internal host implementation + let host = ShaftHostingViewImpl(rootView: rootView) + host.startShaftApp() + } +} + +// MARK: - Internal Implementation + +/// Internal implementation that bridges OpenSwiftUI and Shaft +final class ShaftHostingViewImpl: OpenSwiftUICore.ViewRendererHost { + let viewGraph: OpenSwiftUICore.ViewGraph + var currentTimestamp: Time = .zero + var propertiesNeedingUpdate: OpenSwiftUICore.ViewRendererHostProperties = .all + var renderingPhase: OpenSwiftUICore.ViewRenderingPhase = .none + var externalUpdateCount: Int = 0 + + private let shaftRenderer: ShaftRenderer + private var rootView: Content + + /// ValueNotifier that holds the current Shaft widget tree + private let widgetNotifier: Shaft.ValueNotifier + + init(rootView: Content) { + self.rootView = rootView + self.widgetNotifier = Shaft.ValueNotifier(EmptyWidget()) + + // Create our custom Shaft-compatible DisplayList renderer wrapper + self.shaftRenderer = ShaftRenderer(widgetNotifier: widgetNotifier) + + // Create ViewGraph with displayList output enabled + self.viewGraph = OpenSwiftUICore.ViewGraph( + rootViewType: Content.self, + requestedOutputs: [.displayList, .layout] + ) + + // Set up the view graph + viewGraph.delegate = self + viewGraph.setRootView(rootView) + } + + func startShaftApp() { + // Create the bridge widget with our notifier + let bridgeWidget = ShaftBridgeWidget(widgetNotifier: widgetNotifier) + + // Trigger initial render BEFORE runApp (which blocks) + viewGraph.updateOutputs(at: Time.zero) + + render(targetTimestamp: nil) + + // Run the Shaft app with our bridge widget (this blocks) + Shaft.runApp(bridgeWidget) + } + + func requestUpdate(after delay: Double) { + // Schedule an update after the specified delay + // TODO: Integrate with Shaft's scheduler + mark("requestUpdate(after: \(delay))") + SchedulerBinding.shared.scheduleFrame() + } + + func renderDisplayList( + _ list: OpenSwiftUICore.DisplayList, + asynchronously: Bool, + time: OpenSwiftUICore.Time, + nextTime: OpenSwiftUICore.Time, + targetTimestamp: OpenSwiftUICore.Time?, + version: OpenSwiftUICore.DisplayList.Version, + maxVersion: OpenSwiftUICore.DisplayList.Version + ) -> OpenSwiftUICore.Time { + return shaftRenderer.render( + rootView: self, + from: list, + time: time, + version: version, + maxVersion: maxVersion, + environment: DisplayList.ViewRenderer.Environment( + contentsScale: 1.0 + ) + ) + } + + // Required ViewRendererHost methods with stub implementations + func updateRootView() { + // TODO: Implement root view updates + mark("updateRootView()") + } + + func updateEnvironment() { + // TODO: Implement environment updates + mark("updateEnvironment()") + } + + func updateSize() { + // TODO: Implement size updates + // mark("updateSize()") + let windowSize = CGSize(width: 800, height: 600) // placeholder + viewGraph.setProposedSize(windowSize) + } + + func updateSafeArea() { + // TODO: Implement safe area updates + mark("updateSafeArea()") + } + + func updateContainerSize() { + // TODO: Implement container size updates + mark("updateContainerSize()") + } +} + +// MARK: - ViewGraphDelegate + +extension ShaftHostingViewImpl: OpenSwiftUICore.ViewGraphDelegate { + func updateEnvironment(_ environment: inout OpenSwiftUI.EnvironmentValues) { + mark("🔍 [ViewGraphDelegate.updateEnvironment] Called") + // Update environment values if needed + } +} + +// MARK: - ViewGraphRenderDelegate + +extension ShaftHostingViewImpl: OpenSwiftUICore.ViewGraphRenderDelegate { + var renderingRootView: AnyObject { + mark("🔍 [renderingRootView] Called") + return self + } + + func updateRenderContext(_ context: inout ViewGraphRenderContext) { + mark("🔍 [updateRenderContext] Called, setting contentsScale=1.0") + // Set the contents scale from Shaft's device pixel ratio + // TODO: Get this from Shaft's view + context.contentsScale = 1.0 + } + + func withMainThreadRender(wasAsync: Bool, _ body: () -> Time) -> Time { + mark("🔍 [withMainThreadRender] Called with wasAsync=\(wasAsync)") + let result = body() + mark("🔍 [withMainThreadRender] body() returned \(result)") + return result + } +} diff --git a/Sources/OpenSwiftUIShaftBackend/ShaftRenderer.swift b/Sources/OpenSwiftUIShaftBackend/ShaftRenderer.swift new file mode 100644 index 000000000..3858ff1de --- /dev/null +++ b/Sources/OpenSwiftUIShaftBackend/ShaftRenderer.swift @@ -0,0 +1,70 @@ +// +// ShaftRenderer.swift +// OpenSwiftUIShaftBackend +// +// Created by OpenSwiftUI integration with Shaft +// + +import Foundation +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore +import Shaft + +final class ShaftRenderer { + + /// Converter for DisplayList to Shaft widgets + private let converter = DisplayListConverter() + + /// Last rendered DisplayList version for incremental updates + private var lastVersion: OpenSwiftUICore.DisplayList.Version? + + /// ValueNotifier that holds the current widget tree + /// Updating this automatically triggers Shaft rebuilds via @Observable + private let widgetNotifier: ValueNotifier + + init(widgetNotifier: ValueNotifier) { + self.widgetNotifier = widgetNotifier + } + + func render( + rootView: AnyObject, + from list: OpenSwiftUICore.DisplayList, + time: OpenSwiftUICore.Time, + version: OpenSwiftUICore.DisplayList.Version, + maxVersion: OpenSwiftUICore.DisplayList.Version, + environment: OpenSwiftUICore.DisplayList.ViewRenderer.Environment + ) -> OpenSwiftUICore.Time { + mark( + "render(rootView: \(rootView), from: \(list), time: \(time), version: \(version), maxVersion: \(maxVersion), environment: \(environment))" + ) + + // Check if we need to update based on version + let needsFullRebuild = lastVersion == nil || lastVersion != version + + if needsFullRebuild { + // Convert DisplayList to Shaft widget tree + let shaftWidget = converter.convertDisplayList( + list, + contentsScale: environment.contentsScale + ) + + // Update the ValueNotifier - this automatically triggers rebuild + // thanks to Shaft's @Observable support + widgetNotifier.value = shaftWidget + + lastVersion = version + } + + return .zero + } + + func renderAsync( + to list: OpenSwiftUICore.DisplayList, + time: OpenSwiftUICore.Time, + targetTimestamp: OpenSwiftUICore.Time?, + version: OpenSwiftUICore.DisplayList.Version, + maxVersion: OpenSwiftUICore.DisplayList.Version + ) -> OpenSwiftUICore.Time? { + // Async rendering not supported initially + return nil + } +}