TokamakUI / Tokamak

SwiftUI-compatible framework for building browser apps with WebAssembly and native apps for other platforms
Apache License 2.0
2.62k stars 111 forks source link

Add Canvas and TimelineView to DOM renderer #449

Closed carson-katri closed 3 years ago

carson-katri commented 3 years ago

This is built on the HTML canvas, and is compatible with the iOS 15 Canvas view. TimelineView with an .animation schedule only runs with Canvas at the moment with a special implementation using requestAnimationFrame, but could be adjusted to work with other views as well.

Here's a demo of confetti using Canvas:

https://user-images.githubusercontent.com/13581484/133929292-4b98926c-3e0a-468c-ad23-28d8a87e3c77.mov

MaxDesiatov commented 3 years ago

Disregard the snapshot test comment, I realize this won't be possible right now because we can't test TokamakDOM directly, and this is implemented in TokamakDOM.

carson-katri commented 3 years ago

Haven't done much cleanup yet, but did add support for symbols:

Canvas { context, size in
  // Draw axes on the canvas.
  context.stroke(Rectangle().path(in: .init(x: size.width / 2, y: 0, width: 1, height: size.height)), with: .color(.green))
  context.stroke(Rectangle().path(in: .init(x: 0, y: size.height / 2, width: size.width, height: 1)), with: .color(.green))

  // Resolve and draw the symbols, looking them up by tag.
  guard let aSymbol = context.resolveSymbol(id: "a"),
        let bSymbol = context.resolveSymbol(id: "b") else { return }
  context.draw(aSymbol, at: .init(x: size.width / 2, y: size.height / 2), anchor: .topLeading)
  context.draw(bSymbol, at: .init(x: size.width / 2, y: size.height / 2), anchor: .bottomTrailing)
} symbols: {
  Text("A")
    .frame(width: 100, height: 100)
    .background(Color.red)
    .cornerRadius(10)
    .tag("a") // Tagged with "a"
  Text("B")
    .frame(width: 100, height: 100)
    .background(Color.blue)
    .tag("b") // Tagged with "b"
}
Screen Shot 2021-09-23 at 2 59 15 PM

To look up Views by their tag, I added support for _VariadicView (matching SwiftUI's) which provides access to child views as a RandomAccessCollection, as well as access to _ViewTraitKeys. The basic usage in Canvas is like so:

private struct SymbolResolverLayout<ID: Hashable>: _VariadicView.ViewRoot {
  let id: ID

  func body(children: _VariadicView.Children) -> some View {
    ForEach(children) {
      if case let .tagged(tag) = $0[TagValueTraitKey<ID>.self],
         tag == id
      {
        $0
      }
    }
  }
}

// Then used like so:
_VariadicView.Tree(SymbolResolverLayout(id: id)) {
  _storage.symbols
}

The views are rendered in the canvas using SVG+foreignObject.

carson-katri commented 3 years ago

Off-topic, but _VariadicView could be used to solve some issues with Picker. It can allow for the use of tag(_:) to specify items alongside ForEach:

Picker("Choose...", selection: $selection) {
  ForEach(items) { Text($0.title) }
  Text("Clear").tag(Item.clear)
}

or for re-tagging:

Picker("Choose...", selection: $value) {
  ForEach(items) {
    Text($0.title)
      .tag($0.value)
  }
}
MaxDesiatov commented 3 years ago

Would this be ready for review soon? It would be great to keep the PR relatively small, and then additional features could be implemented in a separate PR, for easier review, and to have separate commits for such features in the main branch history.

carson-katri commented 3 years ago

Yes, it can be reviewed now.