diff --git a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj index e98e14776..21998387c 100644 --- a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj +++ b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj @@ -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 */; }; @@ -107,6 +109,7 @@ D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = ""; }; D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = ""; }; D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = ""; }; + D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = ""; }; 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 = ""; }; D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = ""; }; @@ -168,6 +171,7 @@ 85ED189924AD425E0085DFA0 /* TokamakDemo */ = { isa = PBXGroup; children = ( + D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */, D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */, B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */, B56F22DF24BC89FD001738DF /* ColorDemo.swift */, @@ -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 */, @@ -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 */, diff --git a/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift b/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift index 532b5c693..9c65b91d3 100644 --- a/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift +++ b/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift @@ -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 } diff --git a/Sources/TokamakCore/Modifiers/LifecycleModifier.swift b/Sources/TokamakCore/Modifiers/LifecycleModifier.swift new file mode 100644 index 000000000..cd3fc1a87 --- /dev/null +++ b/Sources/TokamakCore/Modifiers/LifecycleModifier.swift @@ -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)) + } +} diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index e720a4ebb..3d9dce8ac 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -22,10 +22,6 @@ final class MountedCompositeView: MountedCompositeElement { override func mount(with reconciler: StackReconciler) { let childBody = reconciler.render(compositeView: self) - if let appearanceAction = view.view as? AppearanceActionType { - appearanceAction.appear?() - } - let child: MountedElement = childBody.makeMountedView(parentTarget, environmentValues) mountedChildren = [child] child.mount(with: reconciler) @@ -44,6 +40,13 @@ final class MountedCompositeView: MountedCompositeElement { 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) { diff --git a/Sources/TokamakCore/State/StateObject.swift b/Sources/TokamakCore/State/StateObject.swift new file mode 100644 index 000000000..07245557f --- /dev/null +++ b/Sources/TokamakCore/State/StateObject.swift @@ -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 diff --git a/Sources/TokamakCore/Tokens/UnitPoint.swift b/Sources/TokamakCore/Tokens/UnitPoint.swift index 17aa28215..cbdc0f440 100644 --- a/Sources/TokamakCore/Tokens/UnitPoint.swift +++ b/Sources/TokamakCore/Tokens/UnitPoint.swift @@ -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. // diff --git a/Sources/TokamakCore/Views/Layout/GeometryReader.swift b/Sources/TokamakCore/Views/Layout/GeometryReader.swift new file mode 100644 index 000000000..010bb2371 --- /dev/null +++ b/Sources/TokamakCore/Views/Layout/GeometryReader.swift @@ -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 { +// let box: AnchorValueBoxBase +// public struct Source { +// private var box: AnchorBoxBase +// } +// } + +// extension GeometryProxy { +// public let safeAreaInsets: EdgeInsets +// public func frame(in coordinateSpace: CoordinateSpace) -> CGRect +// public subscript(anchor: Anchor) -> T {} +// } + +public struct GeometryReader: 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") + } +} diff --git a/Sources/TokamakCore/Views/Toggle.swift b/Sources/TokamakCore/Views/Selectors/Toggle.swift similarity index 100% rename from Sources/TokamakCore/Views/Toggle.swift rename to Sources/TokamakCore/Views/Selectors/Toggle.swift diff --git a/Sources/TokamakDOM/Core.swift b/Sources/TokamakDOM/Core.swift index cfb874b46..733eb1323 100644 --- a/Sources/TokamakDOM/Core.swift +++ b/Sources/TokamakDOM/Core.swift @@ -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 diff --git a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift new file mode 100644 index 000000000..ddff5f766 --- /dev/null +++ b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift @@ -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: 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 + } + } +} diff --git a/Sources/TokamakDemo/GeometryReaderDemo.swift b/Sources/TokamakDemo/GeometryReaderDemo.swift new file mode 100644 index 000000000..a4a764378 --- /dev/null +++ b/Sources/TokamakDemo/GeometryReaderDemo.swift @@ -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))") + } + } +} diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 7249aeb0f..a05c1cb60 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -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()) diff --git a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift index f724bae5b..932a5ef91 100644 --- a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift +++ b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift @@ -31,7 +31,6 @@ public let tokamakStyles = """ height: 100%; padding: 0; } - ._tokamak-disclosuregroup-label { cursor: pointer; } @@ -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;