diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 27bddfb1b..0d449487a 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -34,6 +34,7 @@ private enum MountedElementKind { public class MountedElement { private var element: MountedElementKind + var type: Any.Type { element.type } public internal(set) var app: _AnyApp { get { @@ -117,20 +118,16 @@ public class MountedElement { updateEnvironment() } - @discardableResult - func updateEnvironment() -> TypeInfo { - // swiftlint:disable:next force_try - let info = try! typeInfo(of: element.type) + func updateEnvironment() { + let type = element.type switch element { case .app: - environmentValues = info.injectEnvironment(from: environmentValues, into: &app.app) + environmentValues.inject(into: &app.app, type) case .scene: - environmentValues = info.injectEnvironment(from: environmentValues, into: &scene.scene) + environmentValues.inject(into: &scene.scene, type) case .view: - environmentValues = info.injectEnvironment(from: environmentValues, into: &view.view) + environmentValues.inject(into: &view.view, type) } - - return info } func mount( @@ -163,59 +160,62 @@ public class MountedElement { } } -extension TypeInfo { - fileprivate func injectEnvironment( - from environmentValues: EnvironmentValues, - into element: inout Any - ) -> EnvironmentValues { - var modifiedEnv = environmentValues +extension EnvironmentValues { + mutating func inject(into element: inout Any, _ type: Any.Type) { + // swiftlint:disable:next force_try + let info = try! typeInfo(of: type) + // swiftlint:disable force_try // Extract the view from the AnyView for modification, apply Environment changes: - if genericTypes.contains(where: { $0 is EnvironmentModifier.Type }), - let modifier = try! property(named: "modifier").get(from: element) as? EnvironmentModifier + if info.genericTypes.contains(where: { $0 is EnvironmentModifier.Type }), + let modifier = try! info.property(named: "modifier") + .get(from: element) as? EnvironmentModifier { - modifier.modifyEnvironment(&modifiedEnv) + modifier.modifyEnvironment(&self) } // Inject @Environment values // swiftlint:disable force_cast // `DynamicProperty`s can have `@Environment` properties contained in them, // so we have to inject into them as well. - for dynamicProp in properties.filter({ $0.type is DynamicProperty.Type }) { + for dynamicProp in info.properties.filter({ $0.type is DynamicProperty.Type }) { let propInfo = try! typeInfo(of: dynamicProp.type) var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) { var wrapper = try! prop.get(from: propWrapper) as! EnvironmentReader - wrapper.setContent(from: modifiedEnv) + wrapper.setContent(from: self) try! prop.set(value: wrapper, on: &propWrapper) } try! dynamicProp.set(value: propWrapper, on: &element) } - for prop in properties.filter({ $0.type is EnvironmentReader.Type }) { + for prop in info.properties.filter({ $0.type is EnvironmentReader.Type }) { var wrapper = try! prop.get(from: element) as! EnvironmentReader - wrapper.setContent(from: modifiedEnv) + wrapper.setContent(from: self) try! prop.set(value: wrapper, on: &element) } // swiftlint:enable force_try // swiftlint:enable force_cast - - return modifiedEnv } +} +extension TypeInfo { /// Extract all `DynamicProperty` from a type, recursively. /// This is necessary as a `DynamicProperty` can be nested. /// `EnvironmentValues` can also be injected at this point. - func dynamicProperties(_ environment: EnvironmentValues, source: inout Any) -> [PropertyInfo] { + func dynamicProperties( + _ environment: inout EnvironmentValues, + source: inout Any + ) -> [PropertyInfo] { var dynamicProps = [PropertyInfo]() for prop in properties where prop.type is DynamicProperty.Type { dynamicProps.append(prop) // swiftlint:disable force_try let propInfo = try! typeInfo(of: prop.type) - _ = propInfo.injectEnvironment(from: environment, into: &source) + environment.inject(into: &source, prop.type) var extracted = try! prop.get(from: source) dynamicProps.append( contentsOf: propInfo.dynamicProperties( - environment, + &environment, source: &extracted ) ) diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index aeabe8c35..6e8484c44 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2020 Tokamak contributors +// Copyright 2018-2021 Tokamak contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -208,11 +208,12 @@ public final class StackReconciler { body bodyKeypath: ReferenceWritableKeyPath, Any>, result: KeyPath, (Any) -> T> ) -> T { - let info = compositeElement.updateEnvironment() + compositeElement.updateEnvironment() + let info = try! typeInfo(of: compositeElement.type) var stateIdx = 0 let dynamicProps = info.dynamicProperties( - compositeElement.environmentValues, + &compositeElement.environmentValues, source: &compositeElement[keyPath: bodyKeypath] ) diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index 9b39eb7aa..3747868db 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -1,4 +1,4 @@ -// Copyright 2020 Tokamak contributors +// Copyright 2020-2021 Tokamak contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,8 +24,11 @@ extension EnvironmentValues { /// Returns default settings for the DOM environment static var defaultEnvironment: Self { var environment = EnvironmentValues() + + // `.toggleStyle` property is internal environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle()) - environment[_ColorSchemeKey] = .init(matchMediaDarkScheme: matchMediaDarkScheme) + + environment.colorScheme = .init(matchMediaDarkScheme: matchMediaDarkScheme) environment._defaultAppStorage = LocalStorage.standard _DefaultSceneStorageProvider.default = SessionStorage.standard diff --git a/Sources/TokamakGTK/Views/NavigationView.swift b/Sources/TokamakGTK/Views/NavigationView.swift index e302e027c..ae4f16326 100644 --- a/Sources/TokamakGTK/Views/NavigationView.swift +++ b/Sources/TokamakGTK/Views/NavigationView.swift @@ -1,4 +1,4 @@ -// Copyright 2020 Tokamak contributors +// Copyright 2020-2021 Tokamak contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ protocol GtkStackProtocol {} // extension NavigationView: AnyWidget, ParentView, GtkStackProtocol { // var expand: Bool { true } -// func new(_ application: UnsafeMutablePointer) -> UnsafeMutablePointer { +// func new( +// _ application: UnsafeMutablePointer +// ) -> UnsafeMutablePointer { // let box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)! // let stack = gtk_stack_new()! // let sidebar = gtk_stack_sidebar_new()! @@ -77,7 +79,9 @@ extension NavigationLink: ViewDeferredToRenderer { } // extension NavigationLink: AnyWidget, ParentView { -// func new(_ application: UnsafeMutablePointer) -> UnsafeMutablePointer { +// func new( +// _ application: UnsafeMutablePointer +// ) -> UnsafeMutablePointer { // let btn = gtk_button_new()! // bindAction(to: btn) // return btn @@ -105,7 +109,9 @@ extension NavigationLink: ViewDeferredToRenderer { // } // extension NavigationLink: AnyWidget, ParentView { -// func new(_ application: UnsafeMutablePointer) -> UnsafeMutablePointer { +// func new( +// _ application: UnsafeMutablePointer +// ) -> UnsafeMutablePointer { // print("Creating NavLink widget") // let btn = gtk_button_new()! // bindAction(to: btn) diff --git a/Tests/TokamakTests/EnvironmentTests.swift b/Tests/TokamakTests/EnvironmentTests.swift new file mode 100644 index 000000000..7237e5fe5 --- /dev/null +++ b/Tests/TokamakTests/EnvironmentTests.swift @@ -0,0 +1,46 @@ +// Copyright 2021 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 XCTest + +@testable import TokamakCore + +private struct TestView: View { + @Environment(\.colorScheme) var colorScheme + + public var body: some View { + EmptyView() + } +} + +final class EnvironmentTests: XCTestCase { + func testInjection() { + var test: Any = TestView() + var values = EnvironmentValues() + values.colorScheme = .light + values.inject(into: &test, TestView.self) + // swiftlint:disable:next force_cast + XCTAssertEqual((test as! TestView).colorScheme, .light) + + values.colorScheme = .dark + values.inject(into: &test, TestView.self) + // swiftlint:disable:next force_cast + XCTAssertEqual((test as! TestView).colorScheme, .dark) + + let modifier = TestView().colorScheme(.light) + var anyModifier: Any = modifier + + values.inject(into: &anyModifier, type(of: modifier)) + XCTAssertEqual(values.colorScheme, .light) + } +} diff --git a/Tests/TokamakTests/ReconcilerTests.swift b/Tests/TokamakTests/ReconcilerTests.swift index 4391f2edd..63a87e2b6 100644 --- a/Tests/TokamakTests/ReconcilerTests.swift +++ b/Tests/TokamakTests/ReconcilerTests.swift @@ -1,4 +1,4 @@ -// Copyright 2020 Tokamak contributors +// Copyright 2020-2021 Tokamak contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,12 +20,12 @@ import XCTest @testable import TokamakCore -struct Counter: View { +private struct Counter: View { @State var count: Int let limit: Int - @ViewBuilder public var body: some View { + public var body: some View { if count < limit { VStack { Button("Increment") { count += 1 } @@ -37,7 +37,7 @@ struct Counter: View { } } -extension Text { +private extension Text { var verbatim: String? { guard case let .verbatim(text) = storage else { return nil } return text