From 08d1bec2245ccea6797dc5a23622ac7022808504 Mon Sep 17 00:00:00 2001 From: Signal Date: Mon, 22 Jun 2026 19:03:07 +0000 Subject: [PATCH] Provisional example. --- main.swift | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 main.swift diff --git a/main.swift b/main.swift new file mode 100644 index 0000000..141b2dd --- /dev/null +++ b/main.swift @@ -0,0 +1,202 @@ +// Example + +import stb_truetype +import Foundation + +class STBTextProvider: UITextProvider { + // TODO: Add a fallback font. + let font = try! Data(contentsOf: URL(filePath: "/path/to/Arial.ttf")) + var pixels = [UInt8](repeating: 0, count: 1024 * 1024) + var chars = [stbtt_bakedchar](repeating: .init(), count: 256) + + public init() { + let atlas = font.withUnsafeBytes { ptr in + pixels.withUnsafeMutableBufferPointer { pixels in + chars.withUnsafeMutableBufferPointer { chars in + stbtt_BakeFontBitmap(ptr.baseAddress, 0, 32, pixels.baseAddress, 1024, 1024, Int32("\0".utf8CString[0]), 256, chars.baseAddress) + } + } + } + pixels[0] = .max + } + + public func getQuadForChar(_ char: Character) -> UIRect { + var x: Float = 0 + var y: Float = 0 + var quad = stbtt_aligned_quad() + chars.withUnsafeMutableBufferPointer { chars in + stbtt_GetBakedQuad(chars.baseAddress, 1024, 1024, Int32(char.asciiValue ?? "?".first!.asciiValue!), &x, &y, &quad, 1) + } + return UIRect(origin: UIVec2(Double(x), Double(y)), size: UIVec2(Double(quad.x1 - quad.x0), Double(quad.y1 - quad.y0))) + } + + public func getCharInfo(_ char: Character) -> stbtt_bakedchar { + return chars[Int(char.asciiValue ?? "?".first!.asciiValue!)] + } + + public func measure(text: String, maxWidth: Double?) -> UIVec2 { + var out = UIVec2(0.0, 0.0) + var advance = UIVec2(0.0, 32.0) + for char in text { + let info = getCharInfo(char) + advance.x += Double(info.xadvance) + if let maxWidth = maxWidth, advance.x >= maxWidth { + out.x = maxWidth + advance.x = 0 + advance.y += 32 + } + } + out.x = max(out.x, advance.x) + out.y = advance.y + return out + } + + public func drawText(into list: inout UIDrawList, rect: UIRect, text: String, color: UIColor, fontName: String?, fontSize: Float, alignment: Alignment) { + var advance = 0.0 + for char in text { + let info = getCharInfo(char) + let cw = Double(info.x1 - info.x0) + let ch = Double(info.y1 - info.y0) + + let uvLeft = Double(info.x0) / 1024.0 + let uvRight = Double(info.x1) / 1024.0 + let uvTop = Double(info.y0) / 1024.0 + let uvBottom = Double(info.y1) / 1024.0 + + list.addQuad( + bounds: UIRect( + origin: rect.origin + (advance + Double(info.xoff), Double(fontSize + info.yoff)), + size: UIVec2(cw, ch) + ), + uv: UIRect( + origin: UIVec2(uvLeft, uvTop), + size: UIVec2(uvRight - uvLeft, uvBottom - uvTop) + ), + color: color + ) + advance += Double(info.xadvance) + } + } +} + +let atlas = STBTextProvider() + +let ctx = UIContext(STBTextProvider()) { + HStack { + VStack { + Text("Test 1") + Text("Test 3") + Text("Test 4") + } + .width(.grow(1)) + .backgroundColor(.white) + .withStyle(BorderRadiusStyle.self, 100) + .withStyle(BorderWidthStyle.self, 6) + + VStack { + ScrollView { + VStack { + for i in 1...50 { + Text("Item \(i)") + } + } + } + .height(.grow(1)) + Text("Test 2") + } + .alignment(.trailing) + .width(.grow(1)) + .backgroundColor(.init(60, 60, 60)) + } + .width(.relative(1)) + .height(.relative(1)) +} + +let window = GLFWWindow(width: 1024, height: 720) +let graphics = window.createGraphics(WebGPUGraphics.self) + +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 = glm_mat4() +glm_ortho(0, Float(window.width), Float(window.height), 0, -1, 1, &projection.0) + +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: mat4AsBytes(&projection)) + + +let list = ctx.getDrawList() + +print("Vertices: \(list.vertices)") + +verts.writeData(list.vertices) +drawable.vertexCount = list.vertexCount +inds.writeData(list.indices.withUnsafeBytes { Array($0) }) +drawable.indexCount = list.indices.count + +ctx.dump() + +window.registerOnResize { x, y in + ctx.viewportSize = UIVec2(Double(x), Double(y)) + glm_ortho(0, Float(window.width), Float(window.height), 0, -1, 1, &projection.0) + drawable.uniforms = mat4AsBytes(&projection) +} + +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() \ No newline at end of file