commit 2f3600c84ffa99e2a77e2a5ff313562bbbfd8d8a Author: Signal Date: Mon Jun 22 16:23:33 2026 -0400 Initial Commit diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..8c645e8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TestGame", + platforms: [.macOS(.v14)], + products: [ + .executable(name: "TestGame", targets: ["TestGame"]) + ], + dependencies: [ + .package(path: "../ArtifactUI"), + .package(path: "../ArtifactPlatform", traits: ["SDL3"]), + .package(path: "../ArtifactGraphics"), + .package(path: "../ArtifactColor"), + .package(path: "../ArtifactSDL3WebGPU"), + .package(path: "../ArtifactWorld"), + .package(path: "../ArtifactFilesystem"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"), + ], + targets: [ + .executableTarget( + name: "TestGame", + dependencies: [ + "ArtifactSDL3WebGPU", + "ArtifactPlatform", + "ArtifactGraphics", + "ArtifactColor", + "ArtifactUI", + "ArtifactWorld", + "ArtifactFilesystem", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + linkerSettings: [ + .unsafeFlags(["-L\(Context.packageDirectory)/../GLFW/Artifacts/glfw/lib-universal", "-L\(Context.packageDirectory)/../WGPUNative/Artifacts/WGPUNative/lib/macOS"]), + .linkedLibrary("glfw3"), + .linkedLibrary("wgpu_native"), + .linkedFramework("CoreGraphics"), + .linkedFramework("IOKit"), + .linkedFramework("Cocoa"), + .linkedFramework("QuartzCore"), + .linkedFramework("Metal") + ] + ), + ] +) diff --git a/Sources/TestGame/TestGame.swift b/Sources/TestGame/TestGame.swift new file mode 100644 index 0000000..a0cc6ab --- /dev/null +++ b/Sources/TestGame/TestGame.swift @@ -0,0 +1,286 @@ + +import ArtifactPlatform +import ArtifactGraphics +import ArtifactUI +import ArtifactState +import ArtifactMath +import ArtifactFilesystem +import ArtifactColor +import Foundation + +import ArgumentParser + +@main +struct TestGame: AsyncParsableCommand { + @Option(name: [.short, .long], defaultAsFlag: "") + var config: String? + + struct AppSettings: Codable { + var name: String + } + + @MainActor + public mutating func run() async { + let atlas = STBTextProvider() + + @State var thing = 0 + + @Settings + var settings: AppSettings = AppSettings(name: "test") + + let scrollView2 = ScrollView { + VStack { + for i in 1...20 { + Text("Sub-item \(i)") + } + } + } + .height(120) + .backgroundColor(.gold) + .style(BorderWidthStyle.self, 1) + .style(BorderColorStyle.self, .blue) + + let scrollView: ScrollView = ScrollView { + VStack { + for i in 1...20 { + Text("Item \(i)") + } + + scrollView2 + + for i in 21...50 { + Text("Item \(i)") + } + + Text(""" + If the ScrollView has an absolute height, preferredSize correctly returns that height. + If the ScrollView has height: .auto (the default), preferredSize returns the sum of its children’s preferred sizes (or zero if it has no children yet / they haven’t been laid out). + The fatal interaction appears when the ScrollView is given an absolute size after it has already been inserted, or when the Stack’s first measurement pass sees .auto / .grow and therefore writes a zero (or very small) height into child.frame.size.y. Subsequent layout of the ScrollView itself then sees a zero-height content area and never recovers. + """) + } + } + .height(.grow(1)) + + let styles = StyleSheet { + StyleRule(.type(Stack.self) / (.type(Text.self) | .type(Stack.self))) { + BackgroundColorStyle.property(Color(21, 22, 24)) + BorderColorStyle.property(Color(64, 64, 64)) + TextColorStyle.property(Color(100, 100, 100)) + } + } + + let ctx = UIContext(atlas, styles) { + HStack { + VStack { + Text("Test 1") + Text("Test 3") + HStack { + Spacer(width: 50%, height: .absolute(50)) + .style(BorderRadiusStyle.self, 4) + .style(BackgroundColorStyle.self, .red) + .onClick { ev in + print("Click") + scrollView.style[BackgroundColorStyle.self] = .rgb(.random(in: 0...255), .random(in: 0...255), .random(in: 0...255)) + } + .onPointerDown { ev in + print("Pointer down") + } + .onPointerUp { ev in + print("Pointer up") + } +// .onPointerEnter { ev in +// scrollView.style[BackgroundColorStyle.self] = .blue +// } +// .onPointerLeave { ev in +// scrollView.style[BackgroundColorStyle.self] = .red +// } + } + .alignment(.center) + DerivedElement { + Text("Thing: \(thing)") + } + } + .groups("foo") + .width(.grow(1)) + .padding(50, 25) + .style(BorderRadiusStyle.self, 100) + .style(BorderWidthStyle.self, 6) + + VStack { + scrollView + Text("Test 2") + } + .alignment(.trailing) + .width(.grow(1)) + .padding(15) + } + .width(100%) + .height(100%) + } + + print(scrollView2.preferredSize()) + + let window = SDL3Window(width: 1024, height: 720) + + let graphics = WebGPUGraphics(window) + + let uniforms = graphics.createBuffer(label: "Uniform buffer", usage: [.uniform, .copyDest], size: 2048) + let texture = graphics.createTexture(label: "Texture", usage: [.texture, .copyDest], size: [1024, 1024]) + let sampler = graphics.createSampler(label: "Sampler") + + var rgba = [UInt8](repeating: 0, count: 1024*1024*4) + for i in 0..<1024*1024 { + let gray = atlas.pixels[i] + let idx = i * 4 + rgba[idx+0] = 255 // R + rgba[idx+1] = 255 // G + rgba[idx+2] = 255 // B + rgba[idx+3] = gray // A + } + texture.writeData(rgba) + + var projection = Mat4.ortho(left: 0, right: Float(window.width), bottom: Float(window.height), top: 0, near: -1, far: 1) + + let verts = graphics.createBuffer(label: "UI vertex buffer", usage: [.vertex, .copyDest], size: 1024 * 1024) + let inds = graphics.createBuffer(label: "UI index buffer", usage: [.index, .copyDest], size: 1024 * 1024) + var drawable = Drawable(passes: ["ui"], vertices: verts, vertexCount: 0, indices: inds, indexCount: 0, uniforms: projection.bytes) + + ctx.setViewportSize(to: Vec2(1024, 720)) + + let list = ctx.getDrawList() + + verts.writeData(list.vertices) + drawable.vertexCount = list.vertexCount + inds.writeData(list.indices.withUnsafeBytes { Array($0) }) + drawable.indexCount = list.indices.count + drawable.operations = list.operations.map { + switch $0 { + case .draw(let count): + .draw(count: count) + case .pushScissor(let rect): + .pushScissor(rect: rect) + case .popScissor: + .popScissor + case .pushTransform(let transformation): + .pushTransform(transformation) + case .popTransform: + .popTransform + } + } + + ctx.dump() + + window.onDraw { + ctx.update(deltaTime: $0) + + if ctx.needsDisplay { + let list = ctx.getDrawList() + + verts.writeData(list.vertices) + drawable.vertexCount = list.vertexCount + inds.writeData(list.indices.withUnsafeBytes { Array($0) }) + drawable.indexCount = list.indices.count + drawable.operations = list.operations.map { + switch $0 { + case .draw(let count): + .draw(count: count) + case .pushScissor(let rect): + .pushScissor(rect: rect) + case .popScissor: + .popScissor + case .pushTransform(let transformation): + .pushTransform(transformation) + case .popTransform: + .popTransform + } + } + } + } + + window.onResize { x, y in + ctx.setViewportSize(to: Vec2d(Double(x), Double(y))) + projection = Mat4.ortho(left: 0, right: Float(window.width), bottom: Float(window.height), top: 0, near: -1, far: 1) + drawable.uniforms = projection.bytes + } + + window.actions.onKeyDown(.escape) { + window.close() + } + + window.actions.onKeyDown(.enter) { + scrollView.animate(duration: 1000, easing: .easeInOut) { + HeightStyle.property(50%) + // BackgroundColorStyle.property(.red) + } to: { + HeightStyle.property(100%) + // BackgroundColorStyle.property(.blue) + } + } + + window.actions.onPointerDown { pointer in + ctx.handlePointerDown(pointer) + } + + window.actions.onPointerMove { pointer in + ctx.handlePointerMove(pointer) + } + + window.actions.onPointerUp { pointer in + ctx.handlePointerUp(pointer) + } + + window.actions.onWheel { pointer, delta in + ctx.handleWheel(pointer, delta: delta) +// thing += 1 +// print(thing) +// //ctx.input.scroll(amount: Vec2d(delta) * 2) +// scrollView.scrollPos += Vec2d(delta) * 4 + } + + graphics.registerPass( + RenderPass( + name: "ui", + label: "UI Pass", + bindings: [ + .buffer(stage: .vertex, type: .uniform, buffer: uniforms), + .texture(stage: .fragment, sampleType: .float, texture: texture), + .sampler(stage: .fragment, type: .filtering, sampler: sampler) + ], + vertexLayout: [.init(attributes: [.vec2f, .vec2f, .vec4n])], + vertexShader: Shader(entryPoint: "vs_main", source: """ + struct Uniforms { + ortho: mat4x4, + } + @group(0) @binding(0) var uniforms: Uniforms; + struct VertexOutput { + @builtin(position) pos: vec4, + @location(0) uv: vec2, + @location(1) color: vec4, + } + @vertex + fn vs_main(@location(0) pos: vec2, @location(1) uv: vec2, @location(2) color: vec4) -> VertexOutput { + var out: VertexOutput; + out.pos = uniforms.ortho * vec4(pos, 0.0, 1.0); + out.uv = uv; + out.color = color; + return out; + } + """), + colorTargets: [ColorTarget(channels: .all, colorBlendOperation: .add, colorBlendSrcFactor: .srcAlpha, colorBlendDestFactor: .oneMinusSrcAlpha, alphaBlendOperation: .add, alphaBlendSrcFactor: .one, alphaBlendDestFactor: .oneMinusSrcAlpha)], + fragmentShader: Shader(entryPoint: "fs_main", source: """ + @group(0) @binding(1) var texture: texture_2d; + @group(0) @binding(2) var sampler_: sampler; + @fragment + fn fs_main(@location(0) uv: vec2, @location(1) color: vec4) -> @location(0) vec4 { + return textureSample(texture, sampler_, uv) * color; + } + """), + clearColor: .init(0, 0, 128, .max) + ) + ) + + graphics.scene.append(drawable) + + window.run() + } +}