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
30 changes: 30 additions & 0 deletions .github/workflows/ci-embedded.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build Embedded Wasm

on:
push:
branches: [main]
paths:
- "**.swift"
- "**.yml"
pull_request:
branches: [main]
workflow_dispatch:

jobs:
linux:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
image: ["swiftlang/swift:nightly-main"]

container:
image: ${{ matrix.image }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build
run: |
JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true \
swift build --triple wasm32-unknown-none-wasm
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [main]
paths:
- "**.swift"
- "**.yml"
pull_request:
branches: [main]
workflow_dispatch:

jobs:
linux:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
image: ["swift:6.0.1"]

container:
image: ${{ matrix.image }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build
run: swift build --build-tests

- name: Run Tests
run: swift test
38 changes: 38 additions & 0 deletions Examples/Basic/Sources/EmbeddedApp/Views.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import ElementaryDOM

extension EnvironmentValues {
@Entry var myText = ""
}

@View
struct App {
@State var counters: [Int] = [1]
@State var nextCounterName = 1
@State var data = SomeData()

var content: some View {
for (index, counter) in counters.enumerated() {
Expand All @@ -21,6 +26,12 @@ struct App {
nextCounterName += 1
counters.append(nextCounterName)
}
hr()
TextField(value: Binding(get: { data.name }, set: { data.name = $0 }))
div {
p { "Via Binding: \(data.name)" }
p { TestView() }
}.environment(#Key(\.myText), data.name)
}
}

Expand All @@ -44,3 +55,30 @@ struct Counter {
}
}
}

@View
struct TextField {
@Binding var value: String

var content: some View {
input(.type(.text), .value("Hello"))
.onInput { event in
value = event.targetValue ?? ""
}
}
}

@Reactive
final class SomeData {
var name: String = ""
var age: Int = 0
}

@View
struct TestView {
@Environment(#Key(\.myText)) var key

var content: some View {
span { "Via Environment: \(key)" }
}
}
35 changes: 19 additions & 16 deletions Examples/Swiftle/Sources/EmbeddedApp/Views.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ struct GameView {
game.handleKey(key)
}

func onRestart() {
game = Game()
}

var content: some View {
main(.class("flex flex-col gap-5 items-center h-screen bg-black text-white")) {
div(.class("flex gap-4 items-center pt-5")) {
Expand All @@ -28,7 +24,7 @@ struct GameView {

div(.class("relative")) {
KeyboardView(keyboard: game.keyboard, onKeyPressed: onKeyPressed)
GameEndOverlay(game: game, onRestart: onRestart)
GameEndOverlay(game: $game)
}

footer {
Expand All @@ -50,13 +46,15 @@ struct GameView {
}
}

struct SwiftLogo: View {
@View
struct SwiftLogo {
var content: some View {
img(.src("swift-bird.svg"), .class("h-10"))
}
}

struct GuessView: View {
@View
struct GuessView {
var guess: Guess

var content: some View {
Expand All @@ -68,7 +66,8 @@ struct GuessView: View {
}
}

struct LetterView: View {
@View
struct LetterView {
var guess: LetterGuess?

var content: some View {
Expand All @@ -85,7 +84,8 @@ struct LetterView: View {
}
}

struct KeyboardView: View {
@View
struct KeyboardView {
var keyboard: Keyboard
var onKeyPressed: (EnteredKey) -> Void

Expand All @@ -112,7 +112,8 @@ struct KeyboardView: View {
}
}

struct KeyboardLetterView: View {
@View
struct KeyboardLetterView {
var guess: LetterGuess
var onKeyPressed: (EnteredKey) -> Void

Expand All @@ -133,7 +134,8 @@ struct KeyboardLetterView: View {
}
}

struct EnterKeyView: View {
@View
struct EnterKeyView {
var onKeyPressed: (EnteredKey) -> Void

var content: some View {
Expand All @@ -148,7 +150,8 @@ struct EnterKeyView: View {
}
}

struct BackspaceKeyView: View {
@View
struct BackspaceKeyView {
var onKeyPressed: (EnteredKey) -> Void

var content: some View {
Expand All @@ -163,9 +166,9 @@ struct BackspaceKeyView: View {
}
}

struct GameEndOverlay: View {
var game: Game
var onRestart: () -> Void
@View
struct GameEndOverlay {
@Binding var game: Game

var content: some View {
if game.state != .playing {
Expand All @@ -177,7 +180,7 @@ struct GameEndOverlay: View {
button(.class("bg-orange-500 py-2 px-6 rounded-md shadow-lg")) {
"Restart"
}.onClick { _ in
onRestart()
game = Game()
}
}
}
Expand Down
6 changes: 0 additions & 6 deletions Examples/Swiftle/Sources/EmbeddedApp/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,3 @@ struct App {
}

App().mount(in: JSObject.global.document.body.object!)

// // this should probably go in an "onMounted" closure or similar
// Document.onKeyDown { event in
// guard let key = EnteredKey(event) else { return }
// store.onKeyPressed(key)
// }
69 changes: 69 additions & 0 deletions Sources/ElementaryDOM/Data/Environment/Environment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@propertyWrapper
public struct Environment<V> {
enum Storage {
case value(V)
case accessor(_StorageKey<EnvironmentValues, V>)
}

var storage: Storage

public init(_ accessor: _StorageKey<EnvironmentValues, V>) {
storage = .accessor(accessor)
}

public var wrappedValue: V {
switch storage {
case let .value(value):
return value
case let .accessor(accessor):
return accessor.defaultValue()
}
}

public mutating func __load(from context: _ViewRenderingContext) {
__load(from: context.environment)
}

mutating func __load(from values: borrowing EnvironmentValues) {
switch storage {
case let .accessor(accessor):
storage = .value(values[accessor])
default:
fatalError("Cannot load environment value twice")
}
}
}

extension EnvironmentValues: _ValueStorage {
public subscript<Value>(key: _StorageKey<EnvironmentValues, Value>) -> Value {
get {
if let value = values[key.propertyID] {
return value[]
} else {
return key.defaultValue()
}
}
set {
values[key.propertyID] = StoredValue(newValue)
}
}
}

public protocol _ValueStorage {
typealias _Key<Value> = _StorageKey<Self, Value>
subscript<Value>(key: _Key<Value>) -> Value { get set }
}

public struct _StorageKey<Storage: _ValueStorage, Value>: Sendable {
let propertyID: PropertyID
let defaultValue: @Sendable () -> sending Value

public init(_ propertyID: PropertyID, defaultValue: @autoclosure @Sendable @escaping () -> sending Value) {
self.propertyID = propertyID
self.defaultValue = defaultValue
}
}

public struct EnvironmentValues {
var values: [PropertyID: StoredValue] = [:]
}
17 changes: 17 additions & 0 deletions Sources/ElementaryDOM/Data/Environment/View+Envionment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
public extension View {
consuming func environment<V>(_ key: EnvironmentValues._Key<V>, _ value: V) -> _EnvironmentView<V, Self> {
_EnvironmentView(wrapped: self, key: key, value: value)
}
}

public struct _EnvironmentView<V, Wrapped: View>: View {
public typealias Tag = Wrapped.Tag
let wrapped: Wrapped
let key: EnvironmentValues._Key<V>
let value: V

public static func _renderView(_ view: consuming sending Self, context: consuming _ViewRenderingContext) -> _RenderedView {
context.environment[view.key] = view.value
return Wrapped._renderView(view.wrapped, context: context)
}
}
45 changes: 45 additions & 0 deletions Sources/ElementaryDOM/Data/State/Binding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@propertyWrapper
@dynamicMemberLookup
public struct Binding<V> {
enum Storage {
case stateAccessor(StateAccessor<V>)
case getSet(() -> V, (V) -> Void)
}

let storage: Storage

public init(get: @escaping () -> V, set: @escaping (V) -> Void) {
storage = .getSet(get, set)
}

init(accessor: StateAccessor<V>) {
storage = .stateAccessor(accessor)
}

public var wrappedValue: V {
get {
switch storage {
case let .stateAccessor(accessor):
return accessor.value
case let .getSet(get, _):
return get()
}
}
nonmutating set {
switch storage {
case let .stateAccessor(accessor):
accessor.value = newValue
case let .getSet(_, set):
set(newValue)
}
}
}

@_unavailableInEmbedded
public subscript<P>(dynamicMember keypath: WritableKeyPath<V, P>) -> Binding<P> {
Binding<P>(
get: { self.wrappedValue[keyPath: keypath] },
set: { self.wrappedValue[keyPath: keypath] = $0 }
)
}
}
Loading