Add `_targetRef` and `_domRef` modifiers (#240)

Resolves partially #231. `_targetRef` is a modifier that can be used by any renderer, while `_domRef` is an adaptation of that for `DOMRenderer`. Both are underscored as they are not available in SwiftUI, and also their stability is currently not so well known to us, we may consider changing this API in the future.

Example use:

```swift
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)
  }
}
```

I've also fixed all known line length warnings in this PR.
This commit is contained in:
Max Desiatov 2020-08-02 22:01:38 +01:00 committed by GitHub
parent c7b5e75e1a
commit b93be40a19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 47 deletions

View File

@ -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 }

View File

@ -32,7 +32,10 @@ public struct _BackgroundModifier<Background>: ViewModifier where Background: Vi
extension _BackgroundModifier: Equatable where Background: Equatable {}
extension View {
public func background<Background>(_ background: Background, alignment: Alignment = .center) -> some View where Background: View {
public func background<Background>(
_ background: Background,
alignment: Alignment = .center
) -> some View where Background: View {
modifier(_BackgroundModifier(background: background, alignment: alignment))
}
}

View File

@ -22,19 +22,34 @@ 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? AppearanceActionProtocol {
if let appearanceAction = view.view as? AppearanceActionType {
appearanceAction.appear?()
}
let child: MountedElement<R> = 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<R>` instance.
var descendant: MountedElement<R>? = child
while descendant != nil && !(descendant is MountedHostView<R>) {
descendant = descendant?.mountedChildren.first
}
guard let hostDescendant = descendant as? MountedHostView<R> else { return }
targetRef.target = hostDescendant.target
view.view = targetRef
}
}
override func unmount(with reconciler: StackReconciler<R>) {
mountedChildren.forEach { $0.unmount(with: reconciler) }
if let appearanceAction = view.view as? AppearanceActionProtocol {
if let appearanceAction = view.view as? AppearanceActionType {
appearanceAction.disappear?()
}
}

View File

@ -29,7 +29,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
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

View File

@ -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<Self> {
public func scale(
x: CGFloat = 1,
y: CGFloat = 1,
anchor: UnitPoint = .center
) -> ScaledShape<Self> {
ScaledShape(shape: self,
scale: CGSize(width: x, height: y), anchor: anchor)
}
@ -121,7 +125,10 @@ extension Shape {
}
extension Shape {
public func fill<S>(_ content: S, style: FillStyle = FillStyle()) -> some View where S: ShapeStyle {
public func fill<S>(
_ content: S,
style: FillStyle = FillStyle()
) -> some View where S: ShapeStyle {
_ShapeView(shape: self, style: content, fillStyle: style)
}

View File

@ -16,24 +16,35 @@
//
extension InsettableShape {
public func strokeBorder<S>(_ content: S, style: StrokeStyle, antialiased: Bool = true) -> some View where S: ShapeStyle {
public func strokeBorder<S>(
_ 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<S>(_ content: S, lineWidth: CGFloat = 1, antialiased: Bool = true) -> some View where S: ShapeStyle {
@inlinable
public func strokeBorder<S>(
_ 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)
}

View File

@ -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<V: View, T>: View, TargetRefType {
let binding: Binding<T?>
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<T: Target>(_ binding: Binding<T?>) -> _TargetRef<Self, T> {
.init(binding: binding, view: self)
}
}

View File

@ -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

View File

@ -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<JSObjectRef?>) -> some View {
// Convert `Binding<JSObjectRef?>` to `Binding<DOMNode?>` first.
let targetBinding = Binding(
get: { binding.wrappedValue.map(DOMNode.init) },
set: { binding.wrappedValue = $0?.ref }
)
return _targetRef(targetBinding)
}
}

View File

@ -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

View File

@ -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

View File

@ -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