Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AndroidSwiftUICore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import PackageDescription
// ClassicUI-style desktop renderer it shares its design with).
let package = Package(
name: "AndroidSwiftUICore",
platforms: [.macOS(.v13)],
platforms: [.macOS(.v14)],
products: [
.library(name: "AndroidSwiftUICore", targets: ["AndroidSwiftUICore"])
],
Expand Down
82 changes: 82 additions & 0 deletions AndroidSwiftUICore/Sources/AndroidSwiftUICore/Environment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// Environment.swift
// AndroidSwiftUICore
//
// Object environment: values flow DOWN the evaluation (they shape body
// evaluation) and never cross the bridge. `@Environment(Model.self)` reads an
// object a parent injected with `.environment(model)`.
//

/// Per-subtree environment values, keyed by object type.
public struct EnvironmentStorage {

var objects: [ObjectIdentifier: AnyObject] = [:]

public init() {}

mutating func set(_ object: AnyObject) {
objects[ObjectIdentifier(type(of: object))] = object
}

func object<T: AnyObject>(of type: T.Type) -> T? {
objects[ObjectIdentifier(type)] as? T
}
}

/// Reads an object from the environment.
@propertyWrapper
public struct Environment<Value: AnyObject> {

internal let box = EnvironmentBox<Value>()

public init(_ type: Value.Type) {}

public var wrappedValue: Value {
guard let value = box.value else {
fatalError("@Environment(\(Value.self).self) read before injection — missing .environment(_:)?")
}
return value
}
}

internal final class EnvironmentBox<Value: AnyObject> {
var value: Value?
}

/// Type-erased injection hook, discovered via the reflection seam.
public protocol _EnvironmentProperty {
func _inject(from storage: EnvironmentStorage)
}

extension Environment: _EnvironmentProperty {
public func _inject(from storage: EnvironmentStorage) {
box.value = storage.object(of: Value.self)
}
}

/// The `.environment(_:)` wrapper: sets an object for the subtree.
public struct _EnvironmentWriterView<Content: View>: View {

internal let object: AnyObject
internal let content: Content

public typealias Body = Never
}

public extension View {
func environment<T: AnyObject>(_ object: T) -> _EnvironmentWriterView<Self> {
_EnvironmentWriterView(object: object, content: self)
}
}

/// Injects environment values into a view's `@Environment` properties.
public enum EnvironmentInjector {

public static func inject(_ storage: EnvironmentStorage, into view: any View) {
for child in Mirror(reflecting: view).children {
if let property = child.value as? _EnvironmentProperty {
property._inject(from: storage)
}
}
}
}
35 changes: 33 additions & 2 deletions AndroidSwiftUICore/Sources/AndroidSwiftUICore/Evaluator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,44 @@ public struct ResolveContext {

public let storage: StateStorage
public let callbacks: CallbackRegistry
public var environment: EnvironmentStorage
public var path: String
public var depth: Int

public init(storage: StateStorage, callbacks: CallbackRegistry, path: String = "", depth: Int = 0) {
public init(
storage: StateStorage,
callbacks: CallbackRegistry,
environment: EnvironmentStorage = EnvironmentStorage(),
path: String = "",
depth: Int = 0
) {
self.storage = storage
self.callbacks = callbacks
self.environment = environment
self.path = path
self.depth = depth
}

/// Context for a child at a structurally stable position.
public func descending(_ component: String) -> ResolveContext {
ResolveContext(storage: storage, callbacks: callbacks, path: path + "/" + component, depth: depth + 1)
var context = self
context.path += "/" + component
context.depth += 1
return context
}
}

/// Type-erased access to the environment-writer wrapper.
internal protocol _AnyEnvironmentWriter {
var _object: AnyObject { get }
var _content: any View { get }
}

extension _EnvironmentWriterView: _AnyEnvironmentWriter {
var _object: AnyObject { object }
var _content: any View { content }
}

// MARK: - Primitive / dispatch protocols

/// A view that resolves directly to a node (leaf or container).
Expand Down Expand Up @@ -78,9 +100,14 @@ public enum Evaluator {
return primitive._render(in: context)
case let anyView as AnyView:
return resolve(anyView.storage, context.descending("any"))
case let writer as _AnyEnvironmentWriter:
var child = context.descending("env")
child.environment.set(writer._object)
return resolve(writer._content, child)
default:
let child = context.descending("\(type(of: view))")
child.storage.install(in: view, path: child.path)
EnvironmentInjector.inject(child.environment, into: view)
return resolve(body(of: view), child)
}
}
Expand Down Expand Up @@ -113,6 +140,10 @@ public enum Evaluator {
}
case let anyView as AnyView:
flatten(anyView.storage, into: &nodes, context: context.descending("any"))
case let writer as _AnyEnvironmentWriter:
var child = context.descending("env")
child.environment.set(writer._object)
flatten(writer._content, into: &nodes, context: child)
default:
nodes.append(resolve(view, context))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,79 @@ public extension View {
modifier(_BackgroundColorModifier(color: color))
}
}

// MARK: - Corner radius

public struct _CornerRadiusModifier: RenderModifier {
let radius: Double
public var _modifierNode: ModifierNode {
ModifierNode(kind: "cornerRadius", args: ["radius": .double(radius)])
}
}

public extension View {
func cornerRadius(_ radius: Double) -> ModifiedContent<Self, _CornerRadiusModifier> {
modifier(_CornerRadiusModifier(radius: radius))
}
}

// MARK: - Offset

public struct _OffsetModifier: RenderModifier {
let x: Double
let y: Double
public var _modifierNode: ModifierNode {
ModifierNode(kind: "offset", args: ["x": .double(x), "y": .double(y)])
}
}

public extension View {
func offset(x: Double = 0, y: Double = 0) -> ModifiedContent<Self, _OffsetModifier> {
modifier(_OffsetModifier(x: x, y: y))
}
}

// MARK: - Rotation

public struct _RotationModifier: RenderModifier {
let degrees: Double
public var _modifierNode: ModifierNode {
ModifierNode(kind: "rotation", args: ["degrees": .double(degrees)])
}
}

public extension View {
func rotationEffect(_ angle: Angle) -> ModifiedContent<Self, _RotationModifier> {
modifier(_RotationModifier(degrees: angle.degrees))
}
}

// MARK: - Scale

public struct _ScaleModifier: RenderModifier {
let scale: Double
public var _modifierNode: ModifierNode {
ModifierNode(kind: "scale", args: ["scale": .double(scale)])
}
}

public extension View {
func scaleEffect(_ scale: Double) -> ModifiedContent<Self, _ScaleModifier> {
modifier(_ScaleModifier(scale: scale))
}
}

// MARK: - Opacity

public struct _OpacityModifier: RenderModifier {
let opacity: Double
public var _modifierNode: ModifierNode {
ModifierNode(kind: "opacity", args: ["opacity": .double(opacity)])
}
}

public extension View {
func opacity(_ opacity: Double) -> ModifiedContent<Self, _OpacityModifier> {
modifier(_OpacityModifier(opacity: opacity))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// Picker.swift
// AndroidSwiftUICore
//

/// Associates a selection value with a picker row.
public struct _TagModifier: RenderModifier {
let value: String
public var _modifierNode: ModifierNode {
ModifierNode(kind: "tag", args: ["value": .string(value)])
}
}

public extension View {
func tag<V: Hashable>(_ value: V) -> ModifiedContent<Self, _TagModifier> {
modifier(_TagModifier(value: identityString(value)))
}
}

/// A control for selecting one of several tagged options.
///
/// Selection values round-trip the bridge as strings, so they must be
/// `LosslessStringConvertible` (String and Int both are).
public struct Picker<SelectionValue: Hashable & LosslessStringConvertible, Content: View>: View {

internal let title: String
internal let selection: Binding<SelectionValue>
internal let content: Content

public init<S: StringProtocol>(
_ title: S,
selection: Binding<SelectionValue>,
@ViewBuilder content: () -> Content
) {
self.title = String(title)
self.selection = selection
self.content = content()
}

public typealias Body = Never
}

extension Picker: PrimitiveView {
public func _render(in context: ResolveContext) -> RenderNode {
let binding = selection
let callbackID = context.callbacks.register(.string { raw in
guard let value = SelectionValue(raw) else { return }
binding.wrappedValue = value
})
return RenderNode(
type: "Picker",
id: context.path,
props: [
"title": .string(title),
"selection": .string(identityString(selection.wrappedValue)),
"onChange": .int(Int(callbackID)),
],
children: Evaluator.resolveChildren(content, context.descending("content"))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public struct Text: View {
self.content = String(content)
}

public init(verbatim content: String) {
self.content = content
}

public typealias Body = Never
}

Expand Down
Loading
Loading