Skip to content

Commit 4939464

Browse files
authored
Merge pull request #22 from PureSwift/feature/controls
Controls, environment, observation, and the first gallery screens
2 parents cd4bd7e + a18ce33 commit 4939464

20 files changed

Lines changed: 1094 additions & 21 deletions

File tree

AndroidSwiftUICore/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import PackageDescription
88
// ClassicUI-style desktop renderer it shares its design with).
99
let package = Package(
1010
name: "AndroidSwiftUICore",
11-
platforms: [.macOS(.v13)],
11+
platforms: [.macOS(.v14)],
1212
products: [
1313
.library(name: "AndroidSwiftUICore", targets: ["AndroidSwiftUICore"])
1414
],
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// Environment.swift
3+
// AndroidSwiftUICore
4+
//
5+
// Object environment: values flow DOWN the evaluation (they shape body
6+
// evaluation) and never cross the bridge. `@Environment(Model.self)` reads an
7+
// object a parent injected with `.environment(model)`.
8+
//
9+
10+
/// Per-subtree environment values, keyed by object type.
11+
public struct EnvironmentStorage {
12+
13+
var objects: [ObjectIdentifier: AnyObject] = [:]
14+
15+
public init() {}
16+
17+
mutating func set(_ object: AnyObject) {
18+
objects[ObjectIdentifier(type(of: object))] = object
19+
}
20+
21+
func object<T: AnyObject>(of type: T.Type) -> T? {
22+
objects[ObjectIdentifier(type)] as? T
23+
}
24+
}
25+
26+
/// Reads an object from the environment.
27+
@propertyWrapper
28+
public struct Environment<Value: AnyObject> {
29+
30+
internal let box = EnvironmentBox<Value>()
31+
32+
public init(_ type: Value.Type) {}
33+
34+
public var wrappedValue: Value {
35+
guard let value = box.value else {
36+
fatalError("@Environment(\(Value.self).self) read before injection — missing .environment(_:)?")
37+
}
38+
return value
39+
}
40+
}
41+
42+
internal final class EnvironmentBox<Value: AnyObject> {
43+
var value: Value?
44+
}
45+
46+
/// Type-erased injection hook, discovered via the reflection seam.
47+
public protocol _EnvironmentProperty {
48+
func _inject(from storage: EnvironmentStorage)
49+
}
50+
51+
extension Environment: _EnvironmentProperty {
52+
public func _inject(from storage: EnvironmentStorage) {
53+
box.value = storage.object(of: Value.self)
54+
}
55+
}
56+
57+
/// The `.environment(_:)` wrapper: sets an object for the subtree.
58+
public struct _EnvironmentWriterView<Content: View>: View {
59+
60+
internal let object: AnyObject
61+
internal let content: Content
62+
63+
public typealias Body = Never
64+
}
65+
66+
public extension View {
67+
func environment<T: AnyObject>(_ object: T) -> _EnvironmentWriterView<Self> {
68+
_EnvironmentWriterView(object: object, content: self)
69+
}
70+
}
71+
72+
/// Injects environment values into a view's `@Environment` properties.
73+
public enum EnvironmentInjector {
74+
75+
public static func inject(_ storage: EnvironmentStorage, into view: any View) {
76+
for child in Mirror(reflecting: view).children {
77+
if let property = child.value as? _EnvironmentProperty {
78+
property._inject(from: storage)
79+
}
80+
}
81+
}
82+
}

AndroidSwiftUICore/Sources/AndroidSwiftUICore/Evaluator.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,44 @@ public struct ResolveContext {
1414

1515
public let storage: StateStorage
1616
public let callbacks: CallbackRegistry
17+
public var environment: EnvironmentStorage
1718
public var path: String
1819
public var depth: Int
1920

20-
public init(storage: StateStorage, callbacks: CallbackRegistry, path: String = "", depth: Int = 0) {
21+
public init(
22+
storage: StateStorage,
23+
callbacks: CallbackRegistry,
24+
environment: EnvironmentStorage = EnvironmentStorage(),
25+
path: String = "",
26+
depth: Int = 0
27+
) {
2128
self.storage = storage
2229
self.callbacks = callbacks
30+
self.environment = environment
2331
self.path = path
2432
self.depth = depth
2533
}
2634

2735
/// Context for a child at a structurally stable position.
2836
public func descending(_ component: String) -> ResolveContext {
29-
ResolveContext(storage: storage, callbacks: callbacks, path: path + "/" + component, depth: depth + 1)
37+
var context = self
38+
context.path += "/" + component
39+
context.depth += 1
40+
return context
3041
}
3142
}
3243

44+
/// Type-erased access to the environment-writer wrapper.
45+
internal protocol _AnyEnvironmentWriter {
46+
var _object: AnyObject { get }
47+
var _content: any View { get }
48+
}
49+
50+
extension _EnvironmentWriterView: _AnyEnvironmentWriter {
51+
var _object: AnyObject { object }
52+
var _content: any View { content }
53+
}
54+
3355
// MARK: - Primitive / dispatch protocols
3456

3557
/// A view that resolves directly to a node (leaf or container).
@@ -78,9 +100,14 @@ public enum Evaluator {
78100
return primitive._render(in: context)
79101
case let anyView as AnyView:
80102
return resolve(anyView.storage, context.descending("any"))
103+
case let writer as _AnyEnvironmentWriter:
104+
var child = context.descending("env")
105+
child.environment.set(writer._object)
106+
return resolve(writer._content, child)
81107
default:
82108
let child = context.descending("\(type(of: view))")
83109
child.storage.install(in: view, path: child.path)
110+
EnvironmentInjector.inject(child.environment, into: view)
84111
return resolve(body(of: view), child)
85112
}
86113
}
@@ -113,6 +140,10 @@ public enum Evaluator {
113140
}
114141
case let anyView as AnyView:
115142
flatten(anyView.storage, into: &nodes, context: context.descending("any"))
143+
case let writer as _AnyEnvironmentWriter:
144+
var child = context.descending("env")
145+
child.environment.set(writer._object)
146+
flatten(writer._content, into: &nodes, context: child)
116147
default:
117148
nodes.append(resolve(view, context))
118149
}

AndroidSwiftUICore/Sources/AndroidSwiftUICore/Modifiers/Modifiers.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,79 @@ public extension View {
104104
modifier(_BackgroundColorModifier(color: color))
105105
}
106106
}
107+
108+
// MARK: - Corner radius
109+
110+
public struct _CornerRadiusModifier: RenderModifier {
111+
let radius: Double
112+
public var _modifierNode: ModifierNode {
113+
ModifierNode(kind: "cornerRadius", args: ["radius": .double(radius)])
114+
}
115+
}
116+
117+
public extension View {
118+
func cornerRadius(_ radius: Double) -> ModifiedContent<Self, _CornerRadiusModifier> {
119+
modifier(_CornerRadiusModifier(radius: radius))
120+
}
121+
}
122+
123+
// MARK: - Offset
124+
125+
public struct _OffsetModifier: RenderModifier {
126+
let x: Double
127+
let y: Double
128+
public var _modifierNode: ModifierNode {
129+
ModifierNode(kind: "offset", args: ["x": .double(x), "y": .double(y)])
130+
}
131+
}
132+
133+
public extension View {
134+
func offset(x: Double = 0, y: Double = 0) -> ModifiedContent<Self, _OffsetModifier> {
135+
modifier(_OffsetModifier(x: x, y: y))
136+
}
137+
}
138+
139+
// MARK: - Rotation
140+
141+
public struct _RotationModifier: RenderModifier {
142+
let degrees: Double
143+
public var _modifierNode: ModifierNode {
144+
ModifierNode(kind: "rotation", args: ["degrees": .double(degrees)])
145+
}
146+
}
147+
148+
public extension View {
149+
func rotationEffect(_ angle: Angle) -> ModifiedContent<Self, _RotationModifier> {
150+
modifier(_RotationModifier(degrees: angle.degrees))
151+
}
152+
}
153+
154+
// MARK: - Scale
155+
156+
public struct _ScaleModifier: RenderModifier {
157+
let scale: Double
158+
public var _modifierNode: ModifierNode {
159+
ModifierNode(kind: "scale", args: ["scale": .double(scale)])
160+
}
161+
}
162+
163+
public extension View {
164+
func scaleEffect(_ scale: Double) -> ModifiedContent<Self, _ScaleModifier> {
165+
modifier(_ScaleModifier(scale: scale))
166+
}
167+
}
168+
169+
// MARK: - Opacity
170+
171+
public struct _OpacityModifier: RenderModifier {
172+
let opacity: Double
173+
public var _modifierNode: ModifierNode {
174+
ModifierNode(kind: "opacity", args: ["opacity": .double(opacity)])
175+
}
176+
}
177+
178+
public extension View {
179+
func opacity(_ opacity: Double) -> ModifiedContent<Self, _OpacityModifier> {
180+
modifier(_OpacityModifier(opacity: opacity))
181+
}
182+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Picker.swift
3+
// AndroidSwiftUICore
4+
//
5+
6+
/// Associates a selection value with a picker row.
7+
public struct _TagModifier: RenderModifier {
8+
let value: String
9+
public var _modifierNode: ModifierNode {
10+
ModifierNode(kind: "tag", args: ["value": .string(value)])
11+
}
12+
}
13+
14+
public extension View {
15+
func tag<V: Hashable>(_ value: V) -> ModifiedContent<Self, _TagModifier> {
16+
modifier(_TagModifier(value: identityString(value)))
17+
}
18+
}
19+
20+
/// A control for selecting one of several tagged options.
21+
///
22+
/// Selection values round-trip the bridge as strings, so they must be
23+
/// `LosslessStringConvertible` (String and Int both are).
24+
public struct Picker<SelectionValue: Hashable & LosslessStringConvertible, Content: View>: View {
25+
26+
internal let title: String
27+
internal let selection: Binding<SelectionValue>
28+
internal let content: Content
29+
30+
public init<S: StringProtocol>(
31+
_ title: S,
32+
selection: Binding<SelectionValue>,
33+
@ViewBuilder content: () -> Content
34+
) {
35+
self.title = String(title)
36+
self.selection = selection
37+
self.content = content()
38+
}
39+
40+
public typealias Body = Never
41+
}
42+
43+
extension Picker: PrimitiveView {
44+
public func _render(in context: ResolveContext) -> RenderNode {
45+
let binding = selection
46+
let callbackID = context.callbacks.register(.string { raw in
47+
guard let value = SelectionValue(raw) else { return }
48+
binding.wrappedValue = value
49+
})
50+
return RenderNode(
51+
type: "Picker",
52+
id: context.path,
53+
props: [
54+
"title": .string(title),
55+
"selection": .string(identityString(selection.wrappedValue)),
56+
"onChange": .int(Int(callbackID)),
57+
],
58+
children: Evaluator.resolveChildren(content, context.descending("content"))
59+
)
60+
}
61+
}

AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Text.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ public struct Text: View {
1616
self.content = String(content)
1717
}
1818

19+
public init(verbatim content: String) {
20+
self.content = content
21+
}
22+
1923
public typealias Body = Never
2024
}
2125

0 commit comments

Comments
 (0)