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:
parent
c7b5e75e1a
commit
b93be40a19
|
@ -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 }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue