From 26bb1ef83cc55a0839d855a88331fdfd064910e9 Mon Sep 17 00:00:00 2001 From: tyxu Date: Sat, 22 Nov 2025 21:28:54 +0800 Subject: [PATCH 1/4] Initial Shaft render backend impl --- .vscode/settings.json | 6 + Example/ShaftExample/.gitignore | 1 + Example/ShaftExample/Package.resolved | 159 ++++++++++++ Example/ShaftExample/Package.swift | 31 +++ .../Sources/ShaftExample/main.swift | 29 +++ Package.resolved | 92 ++++++- Package.swift | 28 +++ .../DisplayList/DisplayListViewRenderer.swift | 12 +- .../DisplayList/DisplayListViewUpdater.swift | 12 +- .../View/Graph/ViewRendererHost.swift | 12 +- .../DisplayListConverter.swift | 235 ++++++++++++++++++ .../ShaftBridgeWidget.swift | 41 +++ .../ShaftHostingView.swift | 160 ++++++++++++ .../ShaftRenderer.swift | 70 ++++++ 14 files changed, 874 insertions(+), 14 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 Example/ShaftExample/.gitignore create mode 100644 Example/ShaftExample/Package.resolved create mode 100644 Example/ShaftExample/Package.swift create mode 100644 Example/ShaftExample/Sources/ShaftExample/main.swift create mode 100644 Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift create mode 100644 Sources/OpenSwiftUIShaftBackend/ShaftBridgeWidget.swift create mode 100644 Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift create mode 100644 Sources/OpenSwiftUIShaftBackend/ShaftRenderer.swift 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..139a6981e --- /dev/null +++ b/Example/ShaftExample/Sources/ShaftExample/main.swift @@ -0,0 +1,29 @@ +// +// main.swift +// ShaftExample +// +// Example application demonstrating OpenSwiftUI rendering via Shaft +// + +import Foundation +import OpenSwiftUI +import OpenSwiftUIShaftBackend + + +// Define a simple OpenSwiftUI view +struct ContentView: OpenSwiftUI.View { + var body: some OpenSwiftUI.View { + BlueColor() + } +} + +struct BlueColor: OpenSwiftUI.View { + var id: String { "BlueColor" } + + var body: some OpenSwiftUI.View { + Color.blue._identified(by: id) + } +} + +// 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..c74839327 --- /dev/null +++ b/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift @@ -0,0 +1,235 @@ +// +// DisplayListConverter.swift +// OpenSwiftUIShaftBackend +// +// Converts OpenSwiftUI DisplayList to Shaft widget tree +// + +import Foundation +import OpenSwiftUICore +import Shaft + +/// 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 + + switch item.value { + case .empty: + return nil + + case .content(let content): + // Actual renderable content + return convertContent(content, frame: frame) + + case .effect(let effect, let childList): + // Effect applied to child display list + let childWidget = convertDisplayList(childList, contentsScale: CGFloat(contentsScale)) + return applyEffect(effect, to: childWidget, frame: frame) + + case .states(let states): + // State-dependent display lists + // For now, just render the first state if available + if let firstState = states.first { + return convertDisplayList(firstState.1, contentsScale: CGFloat(contentsScale)) + } + return nil + } + } + + /// Convert Content to Shaft widget + private func convertContent( + _ content: OpenSwiftUICore.DisplayList.Content, + frame: CGRect + ) -> Widget? { + switch content.value { + case .color(let resolvedColor): + return convertColor(resolvedColor, frame: frame) + + case .text(let textView, let size): + return convertText(textView, size: size, frame: frame) + + case .shape(let path, let paint, let fillStyle): + return convertShape(path: path, paint: paint, fillStyle: fillStyle, frame: frame) + + case .image(let graphicsImage): + return convertImage(graphicsImage, frame: frame) + + 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 + + case .platformView, .platformLayer, .view, .drawing: + // Platform-specific or complex cases - not supported initially + return nil + + case .backdrop, .chameleonColor, .shadow, .placeholder: + // Advanced features - not supported initially + return nil + } + } + + /// Apply an effect to a widget + private func applyEffect( + _ effect: OpenSwiftUICore.DisplayList.Effect, + to widget: Widget, + frame: CGRect + ) -> 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 { + switch transform { + case .affine(let affineTransform): + // Extract translation for now + let tx = Float(affineTransform.tx) + let ty = Float(affineTransform.ty) + if tx != 0 || ty != 0 { + return Positioned(left: tx, top: ty) { widget } + } + // TODO: Handle rotation and scale + return widget + + case .rotation, .rotation3D, .projection: + // TODO: Implement 3D transforms + return widget + } + } + + // MARK: - Content Converters + + private func convertColor( + _ resolvedColor: OpenSwiftUICore.Color.Resolved, + frame: CGRect + ) -> 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 SizedBox( + width: Float(frame.width), + height: Float(frame.height) + ) { + ColoredBox(color: shaftColor) + } + } + + private func convertText( + _ textView: StyledTextContentView, + size: CGSize, + frame: CGRect + ) -> Widget { + // TODO: Extract actual text content from StyledTextContentView + // This is a complex type that needs proper parsing + // For now, return a placeholder + return SizedBox( + width: Float(frame.width), + height: Float(frame.height) + ) { + Text("TODO: Extract text") + } + } + + private func convertShape( + path: OpenSwiftUICore.Path, + paint: AnyResolvedPaint, + fillStyle: FillStyle, + frame: CGRect + ) -> Widget { + // TODO: Convert CGPath to Shaft Path + // TODO: Handle paint and fill styles + return SizedBox( + width: Float(frame.width), + height: Float(frame.height) + ) + } + + private func convertImage( + _ graphicsImage: GraphicsImage, + frame: CGRect + ) -> Widget { + // TODO: Extract and convert image data + return SizedBox( + width: Float(frame.width), + height: Float(frame.height) + ) + } +} 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..00ea605af --- /dev/null +++ b/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift @@ -0,0 +1,160 @@ +// +// 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] + ) + + // 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) + + // 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))") + } + + 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()") + } + + 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 + } +} From facaa1866be14a12015927d9bb5aacffe206a394 Mon Sep 17 00:00:00 2001 From: tyxu Date: Sat, 22 Nov 2025 21:35:28 +0800 Subject: [PATCH 2/4] Add missing render() call --- Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift b/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift index 00ea605af..e139d7b12 100644 --- a/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift +++ b/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift @@ -67,6 +67,8 @@ final class ShaftHostingViewImpl: OpenSwiftUICore.Vie // 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) From 15c1695b41a680d129e62d9cbac675d081926ae2 Mon Sep 17 00:00:00 2001 From: tyxu Date: Sat, 22 Nov 2025 22:13:52 +0800 Subject: [PATCH 3/4] Update example --- .../Sources/ShaftExample/main.swift | 24 +++---- .../DisplayListConverter.swift | 62 ++++++------------- .../ShaftHostingView.swift | 42 +++++++------ 3 files changed, 53 insertions(+), 75 deletions(-) diff --git a/Example/ShaftExample/Sources/ShaftExample/main.swift b/Example/ShaftExample/Sources/ShaftExample/main.swift index 139a6981e..deab8307c 100644 --- a/Example/ShaftExample/Sources/ShaftExample/main.swift +++ b/Example/ShaftExample/Sources/ShaftExample/main.swift @@ -9,19 +9,19 @@ import Foundation import OpenSwiftUI import OpenSwiftUIShaftBackend - // Define a simple OpenSwiftUI view -struct ContentView: OpenSwiftUI.View { - var body: some OpenSwiftUI.View { - BlueColor() - } -} - -struct BlueColor: OpenSwiftUI.View { - var id: String { "BlueColor" } - - var body: some OpenSwiftUI.View { - Color.blue._identified(by: id) +struct ContentView: View { + var body: some View { + VStack { + Color.red + .frame(width: 50, height: 30) + Spacer() + Color.blue + .frame(width: 50, height: 30) + Spacer() + Color.green + .frame(width: 50, height: 30) + } } } diff --git a/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift b/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift index c74839327..16ada43c9 100644 --- a/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift +++ b/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift @@ -40,22 +40,25 @@ final class DisplayListConverter { } /// Convert a single DisplayList.Item to a Shaft widget - private func convertItem(_ item: OpenSwiftUICore.DisplayList.Item) -> Widget? { + private func convertItem(_ item: OpenSwiftUICore.DisplayList.Item) -> Widget { // Each item has a frame and a value let frame = item.frame switch item.value { case .empty: - return nil + return SizedBox() case .content(let content): // Actual renderable content - return convertContent(content, frame: frame) + mark("frame: \(frame)") + return Positioned(left: Float(frame.minX), top: Float(frame.minY), width: Float(frame.width), height: Float(frame.height)) { + convertContent(content) + } case .effect(let effect, let childList): // Effect applied to child display list let childWidget = convertDisplayList(childList, contentsScale: CGFloat(contentsScale)) - return applyEffect(effect, to: childWidget, frame: frame) + return applyEffect(effect, to: childWidget) case .states(let states): // State-dependent display lists @@ -63,27 +66,26 @@ final class DisplayListConverter { if let firstState = states.first { return convertDisplayList(firstState.1, contentsScale: CGFloat(contentsScale)) } - return nil + return Text("states not implemented") } } /// Convert Content to Shaft widget private func convertContent( _ content: OpenSwiftUICore.DisplayList.Content, - frame: CGRect - ) -> Widget? { + ) -> Widget { switch content.value { case .color(let resolvedColor): - return convertColor(resolvedColor, frame: frame) + return convertColor(resolvedColor) case .text(let textView, let size): - return convertText(textView, size: size, frame: frame) + return convertText(textView, size: size) case .shape(let path, let paint, let fillStyle): - return convertShape(path: path, paint: paint, fillStyle: fillStyle, frame: frame) + return convertShape(path: path, paint: paint, fillStyle: fillStyle) case .image(let graphicsImage): - return convertImage(graphicsImage, frame: frame) + return convertImage(graphicsImage) case .flattened(let displayList, let offset, _): // Nested display list @@ -98,13 +100,8 @@ final class DisplayListConverter { } return childWidget - case .platformView, .platformLayer, .view, .drawing: - // Platform-specific or complex cases - not supported initially - return nil - - case .backdrop, .chameleonColor, .shadow, .placeholder: - // Advanced features - not supported initially - return nil + default: + return Text("\(content.value) not implemented") } } @@ -112,7 +109,6 @@ final class DisplayListConverter { private func applyEffect( _ effect: OpenSwiftUICore.DisplayList.Effect, to widget: Widget, - frame: CGRect ) -> Widget { switch effect { case .identity: @@ -175,7 +171,6 @@ final class DisplayListConverter { private func convertColor( _ resolvedColor: OpenSwiftUICore.Color.Resolved, - frame: CGRect ) -> Widget { // Convert from linear color (0.0-1.0) to sRGB bytes (0-255) let a = UInt8(resolvedColor.opacity * 255) @@ -184,52 +179,33 @@ final class DisplayListConverter { let b = UInt8(resolvedColor.linearBlue * 255) let shaftColor = Shaft.Color.argb(a, r, g, b) - return SizedBox( - width: Float(frame.width), - height: Float(frame.height) - ) { - ColoredBox(color: shaftColor) - } + return DecoratedBox(decoration: .box(color: shaftColor)) } private func convertText( _ textView: StyledTextContentView, size: CGSize, - frame: CGRect ) -> Widget { // TODO: Extract actual text content from StyledTextContentView // This is a complex type that needs proper parsing // For now, return a placeholder - return SizedBox( - width: Float(frame.width), - height: Float(frame.height) - ) { - Text("TODO: Extract text") - } + return Text("TODO: Extract text") } private func convertShape( path: OpenSwiftUICore.Path, paint: AnyResolvedPaint, fillStyle: FillStyle, - frame: CGRect ) -> Widget { // TODO: Convert CGPath to Shaft Path // TODO: Handle paint and fill styles - return SizedBox( - width: Float(frame.width), - height: Float(frame.height) - ) + return Text("TODO: Extract shape") } private func convertImage( _ graphicsImage: GraphicsImage, - frame: CGRect ) -> Widget { // TODO: Extract and convert image data - return SizedBox( - width: Float(frame.width), - height: Float(frame.height) - ) + return Text("TODO: Extract image") } } diff --git a/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift b/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift index e139d7b12..3ac7d3883 100644 --- a/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift +++ b/Sources/OpenSwiftUIShaftBackend/ShaftHostingView.swift @@ -20,7 +20,7 @@ public enum ShaftHostingView { 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() @@ -36,48 +36,49 @@ final class ShaftHostingViewImpl: OpenSwiftUICore.Vie 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] + 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( @@ -101,28 +102,29 @@ final class ShaftHostingViewImpl: OpenSwiftUICore.Vie ) } - // 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()") + // 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()") @@ -145,14 +147,14 @@ extension ShaftHostingViewImpl: OpenSwiftUICore.ViewGraphRenderDelegate { 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() From f276b1ed197851b404cb7a18530d392b2015aa87 Mon Sep 17 00:00:00 2001 From: tyxu Date: Sat, 22 Nov 2025 22:29:29 +0800 Subject: [PATCH 4/4] Update example --- .../Sources/ShaftExample/main.swift | 7 +- .../DisplayListConverter.swift | 137 ++++++++++-------- 2 files changed, 78 insertions(+), 66 deletions(-) diff --git a/Example/ShaftExample/Sources/ShaftExample/main.swift b/Example/ShaftExample/Sources/ShaftExample/main.swift index deab8307c..4ad2f6b7f 100644 --- a/Example/ShaftExample/Sources/ShaftExample/main.swift +++ b/Example/ShaftExample/Sources/ShaftExample/main.swift @@ -14,13 +14,14 @@ struct ContentView: View { var body: some View { VStack { Color.red - .frame(width: 50, height: 30) + .frame(width: 100, height: 60) Spacer() Color.blue - .frame(width: 50, height: 30) + .frame(width: 100, height: 60) + .rotationEffect(.degrees(45)) Spacer() Color.green - .frame(width: 50, height: 30) + .frame(width: 100, height: 60) } } } diff --git a/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift b/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift index 16ada43c9..1c9f28c79 100644 --- a/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift +++ b/Sources/OpenSwiftUIShaftBackend/DisplayListConverter.swift @@ -8,68 +8,77 @@ 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] - } - + // 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 - - switch item.value { - case .empty: - return SizedBox() - - case .content(let content): - // Actual renderable content - mark("frame: \(frame)") - return Positioned(left: Float(frame.minX), top: Float(frame.minY), width: Float(frame.width), height: Float(frame.height)) { + + 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") + } } - - case .effect(let effect, let childList): - // Effect applied to child display list - let childWidget = convertDisplayList(childList, contentsScale: CGFloat(contentsScale)) - return applyEffect(effect, to: childWidget) - - case .states(let states): - // State-dependent display lists - // For now, just render the first state if available - if let firstState = states.first { - return convertDisplayList(firstState.1, contentsScale: CGFloat(contentsScale)) - } - return 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, @@ -77,16 +86,16 @@ final class DisplayListConverter { 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)) @@ -99,12 +108,12 @@ final class DisplayListConverter { } } return childWidget - + default: return Text("\(content.value) not implemented") } } - + /// Apply an effect to a widget private func applyEffect( _ effect: OpenSwiftUICore.DisplayList.Effect, @@ -113,62 +122,64 @@ final class DisplayListConverter { 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: + .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): - // Extract translation for now - let tx = Float(affineTransform.tx) - let ty = Float(affineTransform.ty) - if tx != 0 || ty != 0 { - return Positioned(left: tx, top: ty) { widget } - } - // TODO: Handle rotation and scale + // TODO: affine transform return widget - - case .rotation, .rotation3D, .projection: + + 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 { @@ -178,10 +189,10 @@ final class DisplayListConverter { 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, @@ -191,7 +202,7 @@ final class DisplayListConverter { // For now, return a placeholder return Text("TODO: Extract text") } - + private func convertShape( path: OpenSwiftUICore.Path, paint: AnyResolvedPaint, @@ -201,7 +212,7 @@ final class DisplayListConverter { // TODO: Handle paint and fill styles return Text("TODO: Extract shape") } - + private func convertImage( _ graphicsImage: GraphicsImage, ) -> Widget {