From 44aa42402d819fbdd6afa1aa917e3d0d5edca175 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 2 Aug 2020 13:49:06 +0100 Subject: [PATCH 01/11] Add `GeometryReader` implementation --- Sources/TokamakCore/Tokens/UnitPoint.swift | 13 ++++- .../Views/{ => Buttons}/NavigationLink.swift | 0 .../{ => Containers}/NavigationView.swift | 0 .../Views/Layout/GeometryReader.swift | 48 +++++++++++++++++++ .../Views/{ => Selectors}/Toggle.swift | 0 .../Views/Layout/GeometryReader.swift | 25 ++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) rename Sources/TokamakCore/Views/{ => Buttons}/NavigationLink.swift (100%) rename Sources/TokamakCore/Views/{ => Containers}/NavigationView.swift (100%) create mode 100644 Sources/TokamakCore/Views/Layout/GeometryReader.swift rename Sources/TokamakCore/Views/{ => Selectors}/Toggle.swift (100%) create mode 100644 Sources/TokamakDOM/Views/Layout/GeometryReader.swift 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/NavigationLink.swift b/Sources/TokamakCore/Views/Buttons/NavigationLink.swift similarity index 100% rename from Sources/TokamakCore/Views/NavigationLink.swift rename to Sources/TokamakCore/Views/Buttons/NavigationLink.swift diff --git a/Sources/TokamakCore/Views/NavigationView.swift b/Sources/TokamakCore/Views/Containers/NavigationView.swift similarity index 100% rename from Sources/TokamakCore/Views/NavigationView.swift rename to Sources/TokamakCore/Views/Containers/NavigationView.swift diff --git a/Sources/TokamakCore/Views/Layout/GeometryReader.swift b/Sources/TokamakCore/Views/Layout/GeometryReader.swift new file mode 100644 index 000000000..e6ea25741 --- /dev/null +++ b/Sources/TokamakCore/Views/Layout/GeometryReader.swift @@ -0,0 +1,48 @@ +// 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 +} + +// 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/Views/Layout/GeometryReader.swift b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift new file mode 100644 index 000000000..3cee94b65 --- /dev/null +++ b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift @@ -0,0 +1,25 @@ +// 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 TokamakCore +import TokamakStaticHTML + +extension GeometryReader: ViewDeferredToRenderer { + public var deferredBody: AnyView { + AnyView( + HTML("div") + .onAppear {} + ) + } +} From 382bca17ab9011deedf924059f0c9c7c6da509a0 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 2 Aug 2020 15:41:35 +0100 Subject: [PATCH 02/11] Add `ResizeObserver` reference --- Sources/TokamakDOM/Views/Layout/GeometryReader.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift index 3cee94b65..c8e1d177f 100644 --- a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift +++ b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift @@ -12,9 +12,12 @@ // 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 + extension GeometryReader: ViewDeferredToRenderer { public var deferredBody: AnyView { AnyView( From c31e412943c70130e1d3a4b8a969b889528264a4 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 2 Aug 2020 19:13:56 +0100 Subject: [PATCH 03/11] Add `_targetRef` and `_domRef` modifiers --- .../Modifiers/AppearanceActionModifier.swift | 4 +- .../Modifiers/StyleModifiers.swift | 5 ++- .../{zIndex.swift => ZIndexModifier.swift} | 0 .../MountedViews/MountedCompositeView.swift | 19 +++++++- .../MountedViews/MountedHostView.swift | 2 +- Sources/TokamakCore/Shapes/Shape.swift | 11 ++++- .../TokamakCore/Shapes/ShapeModifiers.swift | 19 ++++++-- Sources/TokamakCore/State/TargetRef.swift | 45 +++++++++++++++++++ Sources/TokamakCore/Tokens/Edge.swift | 10 +++-- Sources/TokamakDOM/DOMRef.swift | 33 ++++++++++++++ Sources/TokamakDOM/DOMRenderer.swift | 2 +- Sources/TokamakDemo/DOMRefDemo.swift | 29 ++++++++++++ Sources/TokamakDemo/TokamakDemo.swift | 8 +++- 13 files changed, 169 insertions(+), 18 deletions(-) rename Sources/TokamakCore/Modifiers/{zIndex.swift => ZIndexModifier.swift} (100%) create mode 100644 Sources/TokamakCore/State/TargetRef.swift create mode 100644 Sources/TokamakDOM/DOMRef.swift create mode 100644 Sources/TokamakDemo/DOMRefDemo.swift 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..10c106d36 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 travers + // 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 071643835..1afb0a901 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..007096df5 --- /dev/null +++ b/Sources/TokamakCore/State/TargetRef.swift @@ -0,0 +1,45 @@ +// 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..55cfad967 100644 --- a/Sources/TokamakCore/Tokens/Edge.swift +++ b/Sources/TokamakCore/Tokens/Edge.swift @@ -48,10 +48,12 @@ 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..ec984394d --- /dev/null +++ b/Sources/TokamakDOM/DOMRef.swift @@ -0,0 +1,33 @@ +// 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: { + print("setting binding to \($0)") + binding.wrappedValue = $0?.ref + } + ) + return _targetRef(targetBinding) + } +} diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index 359dec1f0..d6b7a493d 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -59,7 +59,7 @@ let document = JSObjectRef.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..366bb28df --- /dev/null +++ b/Sources/TokamakDemo/DOMRefDemo.swift @@ -0,0 +1,29 @@ +// 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") { + print(button!) + 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..e38409cb6 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -96,7 +96,7 @@ var redactDemo: NavItem { } var links: [NavItem] { - [ + var result = [ NavItem("Counter", destination: Counter(count: Count(value: 5), limit: 15) .padding() @@ -125,6 +125,12 @@ var links: [NavItem] { gridDemo, redactDemo, ] + + #if os(WASI) + result.append(NavItem("DOM manipulation", destination: DOMRefDemo())) + #endif + + return result } struct TokamakDemoView: View { From d64221a8b760ff8ae3063e62fa16f521ea51cdfe Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 2 Aug 2020 19:18:47 +0100 Subject: [PATCH 04/11] Fix formatting, remove redundant `print` --- Sources/TokamakCore/State/TargetRef.swift | 12 +++--------- Sources/TokamakCore/Tokens/Edge.swift | 7 +------ Sources/TokamakDOM/DOMRef.swift | 5 +---- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Sources/TokamakCore/State/TargetRef.swift b/Sources/TokamakCore/State/TargetRef.swift index 007096df5..0f82fa9da 100644 --- a/Sources/TokamakCore/State/TargetRef.swift +++ b/Sources/TokamakCore/State/TargetRef.swift @@ -22,18 +22,12 @@ public struct _TargetRef: View, TargetRefType { let view: V var target: Target? { - get { - binding.wrappedValue as? Target - } + get { binding.wrappedValue as? Target } - set { - binding.wrappedValue = newValue as? T - } + set { binding.wrappedValue = newValue as? T } } - public var body: V { - view - } + public var body: V { view } } extension View { diff --git a/Sources/TokamakCore/Tokens/Edge.swift b/Sources/TokamakCore/Tokens/Edge.swift index 55cfad967..9eb39ea64 100644 --- a/Sources/TokamakCore/Tokens/Edge.swift +++ b/Sources/TokamakCore/Tokens/Edge.swift @@ -48,12 +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 index ec984394d..efbf2604a 100644 --- a/Sources/TokamakDOM/DOMRef.swift +++ b/Sources/TokamakDOM/DOMRef.swift @@ -23,10 +23,7 @@ extension View { // Convert `Binding` to `Binding` first. let targetBinding = Binding( get: { binding.wrappedValue.map(DOMNode.init) }, - set: { - print("setting binding to \($0)") - binding.wrappedValue = $0?.ref - } + set: { binding.wrappedValue = $0?.ref } ) return _targetRef(targetBinding) } From 095152595c7251ae6ee9e4e4b274dc5cca075450 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 2 Aug 2020 19:26:15 +0100 Subject: [PATCH 05/11] Remove redundant `print` --- Sources/TokamakDemo/DOMRefDemo.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/TokamakDemo/DOMRefDemo.swift b/Sources/TokamakDemo/DOMRefDemo.swift index 366bb28df..fea457d89 100644 --- a/Sources/TokamakDemo/DOMRefDemo.swift +++ b/Sources/TokamakDemo/DOMRefDemo.swift @@ -21,7 +21,6 @@ struct DOMRefDemo: View { var body: some View { Button("Click me") { - print(button!) button?.innerHTML = "This text was set directly through a DOM reference" }._domRef($button) } From 9a7b670f48ad6863e75371accd588df0e8f99fca Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 2 Aug 2020 21:18:04 +0100 Subject: [PATCH 06/11] Use `_domRef` in `GeometryReader` of TokamakDOM --- .../Modifiers/AppearanceActionModifier.swift | 4 ---- .../Views/Layout/GeometryReader.swift | 4 ++++ .../Views/Layout/GeometryReader.swift | 23 +++++++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift b/Sources/TokamakCore/Modifiers/AppearanceActionModifier.swift index cd050cb6c..8c6a9a5b1 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/Views/Layout/GeometryReader.swift b/Sources/TokamakCore/Views/Layout/GeometryReader.swift index e6ea25741..010bb2371 100644 --- a/Sources/TokamakCore/Views/Layout/GeometryReader.swift +++ b/Sources/TokamakCore/Views/Layout/GeometryReader.swift @@ -16,6 +16,10 @@ 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 diff --git a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift index c8e1d177f..bff83dac1 100644 --- a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift +++ b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift @@ -20,9 +20,24 @@ private let ResizeObserver = JSObjectRef.global.ResizeObserver extension GeometryReader: ViewDeferredToRenderer { public var deferredBody: AnyView { - AnyView( - HTML("div") - .onAppear {} - ) + AnyView(_GeometryReader(content: content)) + } +} + +struct _GeometryReader: View { + public let content: (GeometryProxy) -> Content + + @State var ref: JSObjectRef? + @State var size: CGSize? + + var body: some View { + HTML("div") { + if let size = size { + content(makeProxy(from: size)) + } else { + EmptyView() + } + } + ._domRef($ref) } } From 29561f1ee290409fa7e63ea4309dc56f80f683f3 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 2 Aug 2020 22:12:41 +0100 Subject: [PATCH 07/11] Add `_GeometryReader.State` --- .../MountedViews/MountedCompositeView.swift | 8 +-- Sources/TokamakCore/State/StateObject.swift | 15 ++++++ Sources/TokamakDOM/Core.swift | 1 + .../Views/Layout/GeometryReader.swift | 52 ++++++++++++++++--- Sources/TokamakDemo/main.swift | 4 +- 5 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 Sources/TokamakCore/State/StateObject.swift diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index 10c106d36..648d8d68c 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,10 @@ final class MountedCompositeView: MountedCompositeElement { targetRef.target = hostDescendant.target view.view = targetRef } + + 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/TokamakDOM/Core.swift b/Sources/TokamakDOM/Core.swift index 13b3fce1d..9501e922e 100644 --- a/Sources/TokamakDOM/Core.swift +++ b/Sources/TokamakDOM/Core.swift @@ -81,6 +81,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 index bff83dac1..2eb0841f0 100644 --- a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift +++ b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift @@ -16,7 +16,7 @@ import JavaScriptKit import TokamakCore import TokamakStaticHTML -private let ResizeObserver = JSObjectRef.global.ResizeObserver +private let ResizeObserver = JSObjectRef.global.ResizeObserver.function! extension GeometryReader: ViewDeferredToRenderer { public var deferredBody: AnyView { @@ -25,19 +25,59 @@ extension GeometryReader: ViewDeferredToRenderer { } struct _GeometryReader: View { - public let content: (GeometryProxy) -> Content + 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? - @State var ref: JSObjectRef? - @State var size: CGSize? + /// 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") { - if let size = size { + if let size = state.size { content(makeProxy(from: size)) } else { EmptyView() } } - ._domRef($ref) + ._domRef($state.observedNodeRef) + .onAppear { + let closure = JSClosure { [weak state] args in + log(args[0].object!.contentRect) + + // 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) + log(observerRef) + + _ = observerRef.observe!(state.observedNodeRef!) + + state.observerRef = observerRef + } } } diff --git a/Sources/TokamakDemo/main.swift b/Sources/TokamakDemo/main.swift index c29cbe20e..706ad967a 100644 --- a/Sources/TokamakDemo/main.swift +++ b/Sources/TokamakDemo/main.swift @@ -21,7 +21,9 @@ struct CustomScene: Scene { var body: some Scene { print("In CustomScene.body scenePhase is \(scenePhase)") return WindowGroup("Tokamak Demo") { - TokamakDemoView() + GeometryReader { + Text("\(String(describing: $0))") + } } } } From de7688683886a8a19711607405fe30ca52777880 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 3 Aug 2020 10:59:39 +0100 Subject: [PATCH 08/11] Fix `GeometryReader` sizing style, add demo view --- .../TokamakDemo.xcodeproj/project.pbxproj | 6 +++++ .../Modifiers/LifecycleModifier.swift | 24 +++++++++++++++++++ .../MountedViews/MountedCompositeView.swift | 3 +++ .../Views/Layout/GeometryReader.swift | 7 ++---- Sources/TokamakDemo/GeometryReaderDemo.swift | 23 ++++++++++++++++++ Sources/TokamakDemo/TokamakDemo.swift | 1 + Sources/TokamakDemo/main.swift | 4 +--- 7 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 Sources/TokamakCore/Modifiers/LifecycleModifier.swift create mode 100644 Sources/TokamakDemo/GeometryReaderDemo.swift 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/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 971470221..3d9dce8ac 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -41,6 +41,9 @@ final class MountedCompositeView: MountedCompositeElement { 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?() } diff --git a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift index 2eb0841f0..de90857dc 100644 --- a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift +++ b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift @@ -46,7 +46,7 @@ struct _GeometryReader: View { @StateObject private var state = State() var body: some View { - HTML("div") { + HTML("div", ["style": "width: 100%; height: 100%"]) { if let size = state.size { content(makeProxy(from: size)) } else { @@ -54,10 +54,8 @@ struct _GeometryReader: View { } } ._domRef($state.observedNodeRef) - .onAppear { + ._onMount { let closure = JSClosure { [weak state] args in - log(args[0].object!.contentRect) - // FIXME: `JSArrayRef` is not a `RandomAccessCollection` for some reason, which forces // us to use a string subscript guard @@ -73,7 +71,6 @@ struct _GeometryReader: View { state.closure = closure let observerRef = ResizeObserver.new(closure) - log(observerRef) _ = observerRef.observe!(state.observedNodeRef!) 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 92bd01eae..19c8f9486 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -122,6 +122,7 @@ let links = [ NavItem("Path", destination: PathDemo()), NavItem("TextField", destination: TextFieldDemo()), NavItem("Spacer", destination: SpacerDemo()), + NavItem("GeometryReader", destination: GeometryReaderDemo()), NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8))), NavItem("Picker", destination: PickerDemo()), NavItem("List", destination: listDemo), diff --git a/Sources/TokamakDemo/main.swift b/Sources/TokamakDemo/main.swift index 706ad967a..c29cbe20e 100644 --- a/Sources/TokamakDemo/main.swift +++ b/Sources/TokamakDemo/main.swift @@ -21,9 +21,7 @@ struct CustomScene: Scene { var body: some Scene { print("In CustomScene.body scenePhase is \(scenePhase)") return WindowGroup("Tokamak Demo") { - GeometryReader { - Text("\(String(describing: $0))") - } + TokamakDemoView() } } } From 007d3e2c263d6748909300969a0adb061654731e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 4 Aug 2020 14:38:24 +0100 Subject: [PATCH 09/11] Fix compilation error --- Sources/TokamakDemo/TokamakDemo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 0edcb83ee..b80a0cdb5 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -114,7 +114,7 @@ struct TokamakDemoView: View { .zIndex(1) Text("I'm on top") }.padding(20)) - NavItem("GeometryReader", destination: GeometryReaderDemo()), + NavItem("GeometryReader", destination: GeometryReaderDemo()) } Section(header: Text("Selectors")) { NavItem("Picker", destination: PickerDemo()) From d783141c3c8997c47a4e9822068c684ed8c3e38f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 4 Aug 2020 16:54:02 +0100 Subject: [PATCH 10/11] Try suggested layout CSS --- Sources/TokamakDOM/Views/Layout/GeometryReader.swift | 2 +- Sources/TokamakStaticHTML/Resources/TokamakStyles.swift | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift index de90857dc..ddff5f766 100644 --- a/Sources/TokamakDOM/Views/Layout/GeometryReader.swift +++ b/Sources/TokamakDOM/Views/Layout/GeometryReader.swift @@ -46,7 +46,7 @@ struct _GeometryReader: View { @StateObject private var state = State() var body: some View { - HTML("div", ["style": "width: 100%; height: 100%"]) { + HTML("div", ["class": "_tokamak-geometryreader"]) { if let size = state.size { content(makeProxy(from: size)) } else { diff --git a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift index 04d387e3d..7a4b3fd03 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,6 +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; +} """ public let rootNodeStyles = """ From 47115cf5be65d1dead35f3b9673f28d05ab81518 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 10 Aug 2020 21:48:38 +0100 Subject: [PATCH 11/11] Fix styles --- Sources/TokamakStaticHTML/Resources/TokamakStyles.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift index d87c91901..932a5ef91 100644 --- a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift +++ b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift @@ -81,7 +81,7 @@ public let tokamakStyles = """ display: flex; align-items: center; justify-content: center; - +} ._tokamak-navigationview { display: flex; flex-direction: row;