Skip to content

Commit

Permalink
Add GeometryReader implementation (#239)
Browse files Browse the repository at this point in the history
This is just an empty API at the moment. I hope it can be implemented purely in the `deferredBody` of `GeometryReader` with [the ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) without requiring any tweaks in the `Renderer` protocol or the reconciler.

Seems like I need the `domRef` modifier that writes `JSObjectRef` to a given binding working first, as discussed in #231.
  • Loading branch information
MaxDesiatov authored Aug 11, 2020
1 parent c43d2db commit b7434a2
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 11 deletions.
6 changes: 6 additions & 0 deletions NativeDemo/TokamakDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; };
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; };
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
Expand Down Expand Up @@ -107,6 +109,7 @@
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; };
D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; };
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -168,6 +171,7 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup;
children = (
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */,
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
Expand Down Expand Up @@ -332,6 +336,7 @@
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */,
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */,
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
Expand All @@ -358,6 +363,7 @@
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */,
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */,
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
Expand Down
4 changes: 0 additions & 4 deletions Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ protocol AppearanceActionType {
struct _AppearanceActionModifier: ViewModifier {
var appear: (() -> ())?
var disappear: (() -> ())?
init(appear: (() -> ())? = nil, disappear: (() -> ())? = nil) {
self.appear = appear
self.disappear = disappear
}

typealias Body = Never
}
Expand Down
24 changes: 24 additions & 0 deletions Sources/TokamakCore/Modifiers/LifecycleModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// FIXME: these should have standalone implementations
extension View {
public func _onMount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(appear: action))
}

public func _onUnmount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(disappear: action))
}
}
11 changes: 7 additions & 4 deletions Sources/TokamakCore/MountedViews/MountedCompositeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
override func mount(with reconciler: StackReconciler<R>) {
let childBody = reconciler.render(compositeView: self)

if let appearanceAction = view.view as? AppearanceActionType {
appearanceAction.appear?()
}

let child: MountedElement<R> = childBody.makeMountedView(parentTarget, environmentValues)
mountedChildren = [child]
child.mount(with: reconciler)
Expand All @@ -44,6 +40,13 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
targetRef.target = hostDescendant.target
view.view = targetRef
}

// FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to
// `_onMount` and `_onUnmount` at the moment,
// see https://github.com/swiftwasm/Tokamak/issues/175 for more details
if let appearanceAction = view.view as? AppearanceActionType {
appearanceAction.appear?()
}
}

override func unmount(with reconciler: StackReconciler<R>) {
Expand Down
15 changes: 15 additions & 0 deletions Sources/TokamakCore/State/StateObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

public typealias StateObject = ObservedObject
13 changes: 12 additions & 1 deletion Sources/TokamakCore/Tokens/UnitPoint.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
// Copyright 2020 Tokamak contributors
//
// File.swift
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//
// Created by Carson Katri on 6/28/20.
//
Expand Down
52 changes: 52 additions & 0 deletions Sources/TokamakCore/Views/Layout/GeometryReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

public struct GeometryProxy {
public let size: CGSize
}

public func makeProxy(from size: CGSize) -> GeometryProxy {
.init(size: size)
}

// FIXME: to be implemented
// public enum CoordinateSpace {
// case global
// case local
// case named(AnyHashable)
// }

// public struct Anchor<Value> {
// let box: AnchorValueBoxBase<Value>
// public struct Source {
// private var box: AnchorBoxBase<Value>
// }
// }

// extension GeometryProxy {
// public let safeAreaInsets: EdgeInsets
// public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
// public subscript<T>(anchor: Anchor<T>) -> T {}
// }

public struct GeometryReader<Content>: View where Content: View {
public let content: (GeometryProxy) -> Content
public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
self.content = content
}

public var body: Never {
neverBody("GeometryReader")
}
}
File renamed without changes.
1 change: 1 addition & 0 deletions Sources/TokamakDOM/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public typealias Button = TokamakCore.Button
public typealias DisclosureGroup = TokamakCore.DisclosureGroup
public typealias Divider = TokamakCore.Divider
public typealias ForEach = TokamakCore.ForEach
public typealias GeometryReader = TokamakCore.GeometryReader
public typealias GridItem = TokamakCore.GridItem
public typealias Group = TokamakCore.Group
public typealias HStack = TokamakCore.HStack
Expand Down
80 changes: 80 additions & 0 deletions Sources/TokamakDOM/Views/Layout/GeometryReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import JavaScriptKit
import TokamakCore
import TokamakStaticHTML

private let ResizeObserver = JSObjectRef.global.ResizeObserver.function!

extension GeometryReader: ViewDeferredToRenderer {
public var deferredBody: AnyView {
AnyView(_GeometryReader(content: content))
}
}

struct _GeometryReader<Content: View>: View {
final class State: ObservableObject {
/** Holds a strong reference to a `JSClosure` instance that has to stay alive as long as
the `_GeometryReader` owner is alive.
*/
var closure: JSClosure?

/// A reference to a DOM node being observed for size updates.
var observedNodeRef: JSObjectRef?

/// A reference to a `ResizeObserver` instance.
var observerRef: JSObjectRef?

/// The last known size of the `observedNodeRef` DOM node.
@Published var size: CGSize?
}

let content: (GeometryProxy) -> Content

@StateObject private var state = State()

var body: some View {
HTML("div", ["class": "_tokamak-geometryreader"]) {
if let size = state.size {
content(makeProxy(from: size))
} else {
EmptyView()
}
}
._domRef($state.observedNodeRef)
._onMount {
let closure = JSClosure { [weak state] args in
// FIXME: `JSArrayRef` is not a `RandomAccessCollection` for some reason, which forces
// us to use a string subscript
guard
let rect = args[0].object?[dynamicMember: "0"].object?.contentRect.object,
let width = rect.width.number,
let height = rect.height.number
else { return .undefined }

state?.size = .init(width: width, height: height)

return .undefined
}
state.closure = closure

let observerRef = ResizeObserver.new(closure)

_ = observerRef.observe!(state.observedNodeRef!)

state.observerRef = observerRef
}
}
}
23 changes: 23 additions & 0 deletions Sources/TokamakDemo/GeometryReaderDemo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import TokamakShim

struct GeometryReaderDemo: View {
var body: some View {
GeometryReader {
Text("\(String(describing: $0.size))")
}
}
}
1 change: 1 addition & 0 deletions Sources/TokamakDemo/TokamakDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ struct TokamakDemoView: View {
.zIndex(1)
Text("I'm on top")
}.padding(20))
NavItem("GeometryReader", destination: GeometryReaderDemo())
}
Section(header: Text("Selectors")) {
NavItem("Picker", destination: PickerDemo())
Expand Down
9 changes: 7 additions & 2 deletions Sources/TokamakStaticHTML/Resources/TokamakStyles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public let tokamakStyles = """
height: 100%;
padding: 0;
}
._tokamak-disclosuregroup-label {
cursor: pointer;
}
Expand Down Expand Up @@ -76,7 +75,13 @@ public let tokamakStyles = """
height: 1.2em;
border-radius: .1em;
}
._tokamak-geometryreader {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
._tokamak-navigationview {
display: flex;
flex-direction: row;
Expand Down

0 comments on commit b7434a2

Please sign in to comment.