diff --git a/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift b/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift index a10399c22..cd050cb6c 100644 --- a/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift +++ b/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -protocol AppearanceActionProtocol { +protocol AppearanceActionType { var appear: (() -> ())? { get } var disappear: (() -> ())? { get } } @@ -29,7 +29,7 @@ struct _AppearanceActionModifier: ViewModifier { typealias Body = Never } -extension ModifiedContent: AppearanceActionProtocol +extension ModifiedContent: AppearanceActionType where Content: View, Modifier == _AppearanceActionModifier { var appear: (() -> ())? { modifier.appear } var disappear: (() -> ())? { modifier.disappear } diff --git a/Sources/TokamakCore/Modifiers/StyleModifiers.swift b/Sources/TokamakCore/Modifiers/StyleModifiers.swift index 817e810ab..f7d167f5b 100644 --- a/Sources/TokamakCore/Modifiers/StyleModifiers.swift +++ b/Sources/TokamakCore/Modifiers/StyleModifiers.swift @@ -32,7 +32,10 @@ public struct _BackgroundModifier: ViewModifier where Background: Vi extension _BackgroundModifier: Equatable where Background: Equatable {} extension View { - public func background(_ background: Background, alignment: Alignment = .center) -> some View where Background: View { + public func background( + _ background: Background, + alignment: Alignment = .center + ) -> some View where Background: View { modifier(_BackgroundModifier(background: background, alignment: alignment)) } } diff --git a/Sources/TokamakCore/Modifiers/zIndex.swift b/Sources/TokamakCore/Modifiers/ZIndexModifier.swift similarity index 100% rename from Sources/TokamakCore/Modifiers/zIndex.swift rename to Sources/TokamakCore/Modifiers/ZIndexModifier.swift diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index 11fbea064..e720a4ebb 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -22,19 +22,34 @@ final class MountedCompositeView: MountedCompositeElement { override func mount(with reconciler: StackReconciler) { let childBody = reconciler.render(compositeView: self) - if let appearanceAction = view.view as? AppearanceActionProtocol { + if let appearanceAction = view.view as? AppearanceActionType { appearanceAction.appear?() } let child: MountedElement = childBody.makeMountedView(parentTarget, environmentValues) mountedChildren = [child] child.mount(with: reconciler) + + // `_TargetRef` is a composite view, so it's enough to check for it only here + if var targetRef = view.view as? TargetRefType { + // `_TargetRef` body is not always a host view that has a target, need to traverse + // all descendants to find a `MountedHostView` instance. + var descendant: MountedElement? = child + while descendant != nil && !(descendant is MountedHostView) { + descendant = descendant?.mountedChildren.first + } + + guard let hostDescendant = descendant as? MountedHostView else { return } + + targetRef.target = hostDescendant.target + view.view = targetRef + } } override func unmount(with reconciler: StackReconciler) { mountedChildren.forEach { $0.unmount(with: reconciler) } - if let appearanceAction = view.view as? AppearanceActionProtocol { + if let appearanceAction = view.view as? AppearanceActionType { appearanceAction.disappear?() } } diff --git a/Sources/TokamakCore/MountedViews/MountedHostView.swift b/Sources/TokamakCore/MountedViews/MountedHostView.swift index cabfa7772..fe0828091 100644 --- a/Sources/TokamakCore/MountedViews/MountedHostView.swift +++ b/Sources/TokamakCore/MountedViews/MountedHostView.swift @@ -29,7 +29,7 @@ public final class MountedHostView: MountedElement { private let parentTarget: R.TargetType /// Target of this host view supplied by a renderer after mounting has completed. - private var target: R.TargetType? + private(set) var target: R.TargetType? init(_ view: AnyView, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget diff --git a/Sources/TokamakCore/Shapes/Shape.swift b/Sources/TokamakCore/Shapes/Shape.swift index 4e4bca4c8..582454087 100644 --- a/Sources/TokamakCore/Shapes/Shape.swift +++ b/Sources/TokamakCore/Shapes/Shape.swift @@ -82,7 +82,11 @@ extension Shape { OffsetShape(shape: self, offset: .init(width: x, height: y)) } - public func scale(x: CGFloat = 1, y: CGFloat = 1, anchor: UnitPoint = .center) -> ScaledShape { + public func scale( + x: CGFloat = 1, + y: CGFloat = 1, + anchor: UnitPoint = .center + ) -> ScaledShape { ScaledShape(shape: self, scale: CGSize(width: x, height: y), anchor: anchor) } @@ -121,7 +125,10 @@ extension Shape { } extension Shape { - public func fill(_ content: S, style: FillStyle = FillStyle()) -> some View where S: ShapeStyle { + public func fill( + _ content: S, + style: FillStyle = FillStyle() + ) -> some View where S: ShapeStyle { _ShapeView(shape: self, style: content, fillStyle: style) } diff --git a/Sources/TokamakCore/Shapes/ShapeModifiers.swift b/Sources/TokamakCore/Shapes/ShapeModifiers.swift index 28dcc8f0d..63c1a73e4 100644 --- a/Sources/TokamakCore/Shapes/ShapeModifiers.swift +++ b/Sources/TokamakCore/Shapes/ShapeModifiers.swift @@ -16,24 +16,35 @@ // extension InsettableShape { - public func strokeBorder(_ content: S, style: StrokeStyle, antialiased: Bool = true) -> some View where S: ShapeStyle { + public func strokeBorder( + _ content: S, + style: StrokeStyle, + antialiased: Bool = true + ) -> some View where S: ShapeStyle { inset(by: style.lineWidth / 2) .stroke(style: style) .fill(content, style: FillStyle(antialiased: antialiased)) } - @inlinable public func strokeBorder(style: StrokeStyle, antialiased: Bool = true) -> some View { + @inlinable + public func strokeBorder(style: StrokeStyle, antialiased: Bool = true) -> some View { inset(by: style.lineWidth / 2) .stroke(style: style) .fill(style: FillStyle(antialiased: antialiased)) } - @inlinable public func strokeBorder(_ content: S, lineWidth: CGFloat = 1, antialiased: Bool = true) -> some View where S: ShapeStyle { + @inlinable + public func strokeBorder( + _ content: S, + lineWidth: CGFloat = 1, + antialiased: Bool = true + ) -> some View where S: ShapeStyle { strokeBorder(content, style: StrokeStyle(lineWidth: lineWidth), antialiased: antialiased) } - @inlinable public func strokeBorder(lineWidth: CGFloat = 1, antialiased: Bool = true) -> some View { + @inlinable + public func strokeBorder(lineWidth: CGFloat = 1, antialiased: Bool = true) -> some View { strokeBorder(style: StrokeStyle(lineWidth: lineWidth), antialiased: antialiased) } diff --git a/Sources/TokamakCore/State/TargetRef.swift b/Sources/TokamakCore/State/TargetRef.swift new file mode 100644 index 000000000..0f82fa9da --- /dev/null +++ b/Sources/TokamakCore/State/TargetRef.swift @@ -0,0 +1,39 @@ +// 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. + +protocol TargetRefType { + var target: Target? { get set } +} + +public struct _TargetRef: View, TargetRefType { + let binding: Binding + + let view: V + + var target: Target? { + get { binding.wrappedValue as? Target } + + set { binding.wrappedValue = newValue as? T } + } + + public var body: V { view } +} + +extension View { + /** Allows capturing target instance of aclosest descendant host view. The resulting instance + is written to a given `binding`. */ + public func _targetRef(_ binding: Binding) -> _TargetRef { + .init(binding: binding, view: self) + } +} diff --git a/Sources/TokamakCore/Tokens/Edge.swift b/Sources/TokamakCore/Tokens/Edge.swift index bd120a6b6..9eb39ea64 100644 --- a/Sources/TokamakCore/Tokens/Edge.swift +++ b/Sources/TokamakCore/Tokens/Edge.swift @@ -48,10 +48,7 @@ public struct EdgeInsets: Equatable { public var bottom: CGFloat public var trailing: CGFloat - public init(top: CGFloat, - leading: CGFloat, - bottom: CGFloat, - trailing: CGFloat) { + public init(top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat) { self.top = top self.leading = leading self.bottom = bottom diff --git a/Sources/TokamakDOM/DOMRef.swift b/Sources/TokamakDOM/DOMRef.swift new file mode 100644 index 000000000..efbf2604a --- /dev/null +++ b/Sources/TokamakDOM/DOMRef.swift @@ -0,0 +1,30 @@ +// 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 + +extension View { + /** Allows capturing DOM references of host views. The resulting reference is written + to a given `binding`. + */ + public func _domRef(_ binding: Binding) -> some View { + // Convert `Binding` to `Binding` first. + let targetBinding = Binding( + get: { binding.wrappedValue.map(DOMNode.init) }, + set: { binding.wrappedValue = $0?.ref } + ) + return _targetRef(targetBinding) + } +} diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index b57f80d5f..afedb913c 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -63,7 +63,7 @@ let document = global.document.object! let body = document.body.object! let head = document.head.object! -let timeoutScheduler = { (closure: @escaping () -> ()) in +private let timeoutScheduler = { (closure: @escaping () -> ()) in let fn = JSClosure { _ in closure() return .undefined diff --git a/Sources/TokamakDemo/DOMRefDemo.swift b/Sources/TokamakDemo/DOMRefDemo.swift new file mode 100644 index 000000000..fea457d89 --- /dev/null +++ b/Sources/TokamakDemo/DOMRefDemo.swift @@ -0,0 +1,28 @@ +// 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. + +#if os(WASI) +import JavaScriptKit +import TokamakShim + +struct DOMRefDemo: View { + @State var button: JSObjectRef? + + var body: some View { + Button("Click me") { + button?.innerHTML = "This text was set directly through a DOM reference" + }._domRef($button) + } +} +#endif diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 3c796be55..92bd01eae 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -95,38 +95,45 @@ var redactDemo: NavItem { } } -var links: [NavItem] { - [ - NavItem("Counter", destination: - Counter(count: Count(value: 5), limit: 15) - .padding() - .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0)) - .border(Color.red, width: 3)), - NavItem("ZStack", destination: ZStack { - Text("I'm on bottom") - Text("I'm forced to the top") - .zIndex(1) - Text("I'm on top") - }.padding(20)), - NavItem("ButtonStyle", destination: ButtonStyleDemo()), - NavItem("ForEach", destination: ForEachDemo()), - NavItem("Text", destination: TextDemo()), - NavItem("Toggle", destination: ToggleDemo()), - NavItem("Path", destination: PathDemo()), - NavItem("TextField", destination: TextFieldDemo()), - NavItem("Spacer", destination: SpacerDemo()), - NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8))), - NavItem("Picker", destination: PickerDemo()), - NavItem("List", destination: listDemo), - sidebarDemo, - outlineGroupDemo, - NavItem("Color", destination: ColorDemo()), - appStorageDemo, - gridDemo, - redactDemo, - ] +var domRefDemo: NavItem { + #if os(WASI) + return NavItem("DOM reference", destination: DOMRefDemo()) + #else + return NavItem(unavailable: "DOM reference") + #endif } +let links = [ + NavItem("Counter", destination: + Counter(count: Count(value: 5), limit: 15) + .padding() + .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0)) + .border(Color.red, width: 3)), + NavItem("ZStack", destination: ZStack { + Text("I'm on bottom") + Text("I'm forced to the top") + .zIndex(1) + Text("I'm on top") + }.padding(20)), + NavItem("ButtonStyle", destination: ButtonStyleDemo()), + NavItem("ForEach", destination: ForEachDemo()), + NavItem("Text", destination: TextDemo()), + NavItem("Toggle", destination: ToggleDemo()), + NavItem("Path", destination: PathDemo()), + NavItem("TextField", destination: TextFieldDemo()), + NavItem("Spacer", destination: SpacerDemo()), + NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8))), + NavItem("Picker", destination: PickerDemo()), + NavItem("List", destination: listDemo), + sidebarDemo, + outlineGroupDemo, + NavItem("Color", destination: ColorDemo()), + appStorageDemo, + gridDemo, + redactDemo, + domRefDemo, +] + struct TokamakDemoView: View { var body: some View { NavigationView { () -> AnyView in