Add `ColorScheme` environment (#136)
You can see the dark scheme environment text representation updated in `EnvironmentDemo`. I suggest adding default dark mode styles in a separate PR, I've created #237 as a reminder for that.
This commit is contained in:
parent
70d31b2e5b
commit
c7b5e75e1a
|
@ -32,7 +32,10 @@ public protocol App: _TitledApp {
|
|||
static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues)
|
||||
|
||||
/// Implemented by the renderer to update the `App` on `ScenePhase` changes
|
||||
var _phasePublisher: CurrentValueSubject<ScenePhase, Never> { get }
|
||||
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
|
||||
|
||||
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
|
||||
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
||||
|
||||
static func main()
|
||||
|
||||
|
|
|
@ -48,7 +48,11 @@ public struct _AnyApp: App {
|
|||
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
|
||||
}
|
||||
|
||||
public var _phasePublisher: CurrentValueSubject<ScenePhase, Never> {
|
||||
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
|
||||
}
|
||||
|
||||
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
|
||||
fatalError("`_AnyApp` cannot monitor colorScheme. Access underlying `app` value.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ protocol EnvironmentReader {
|
|||
case value(Value)
|
||||
}
|
||||
|
||||
var content: Content
|
||||
let keyPath: KeyPath<EnvironmentValues, Value>
|
||||
private var content: Content
|
||||
private let keyPath: KeyPath<EnvironmentValues, Value>
|
||||
public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
|
||||
content = .keyPath(keyPath)
|
||||
self.keyPath = keyPath
|
||||
|
|
|
@ -94,6 +94,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
while let child = mountedChildren.first, let firstChild = childrenViews.first {
|
||||
let newChild: MountedElement<R>
|
||||
if firstChild.typeConstructorName == mountedChildren[0].view.typeConstructorName {
|
||||
child.environmentValues = environmentValues
|
||||
child.view = firstChild
|
||||
child.updateEnvironment()
|
||||
child.update(with: reconciler)
|
||||
|
|
|
@ -94,12 +94,8 @@ public final class StackReconciler<R: Renderer> {
|
|||
|
||||
rootElement.mount(with: self)
|
||||
if let mountedApp = rootElement as? MountedApp<R> {
|
||||
app._phasePublisher.sink { [weak self] phase in
|
||||
if mountedApp.environmentValues.scenePhase != phase {
|
||||
mountedApp.environmentValues.scenePhase = phase
|
||||
self?.queueUpdate(for: mountedApp)
|
||||
}
|
||||
}.store(in: &mountedApp.subscriptions)
|
||||
setupSubscription(for: app._phasePublisher, to: \.scenePhase, of: mountedApp)
|
||||
setupSubscription(for: app._colorSchemePublisher, to: \.colorScheme, of: mountedApp)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,6 +170,22 @@ public final class StackReconciler<R: Renderer> {
|
|||
}.store(in: &compositeElement.subscriptions)
|
||||
}
|
||||
|
||||
private func setupSubscription<T: Equatable>(
|
||||
for publisher: AnyPublisher<T, Never>,
|
||||
to keyPath: WritableKeyPath<EnvironmentValues, T>,
|
||||
of mountedApp: MountedApp<R>
|
||||
) {
|
||||
publisher.sink { [weak self, weak mountedApp] value in
|
||||
guard
|
||||
let mountedApp = mountedApp,
|
||||
mountedApp.environmentValues[keyPath: keyPath] != value
|
||||
else { return }
|
||||
|
||||
mountedApp.environmentValues[keyPath: keyPath] = value
|
||||
self?.queueUpdate(for: mountedApp)
|
||||
}.store(in: &mountedApp.subscriptions)
|
||||
}
|
||||
|
||||
func render<T>(compositeElement: MountedCompositeElement<R>,
|
||||
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
|
||||
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>) -> T {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
public struct Color: Hashable, Equatable {
|
||||
// FIXME: This is not injected.
|
||||
@Environment(\.accentColor) static var envAccentColor: Color?
|
||||
@Environment(\.accentColor) static var envAccentColor
|
||||
|
||||
public enum RGBColorSpace {
|
||||
case sRGB
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// 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 enum ColorScheme: CaseIterable {
|
||||
case dark
|
||||
case light
|
||||
}
|
||||
|
||||
public struct _ColorSchemeKey: EnvironmentKey {
|
||||
public static var defaultValue: ColorScheme {
|
||||
fatalError("\(self) must have a renderer-provided default value")
|
||||
}
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var colorScheme: ColorScheme {
|
||||
get { self[_ColorSchemeKey.self] }
|
||||
set { self[_ColorSchemeKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func colorScheme(_ colorScheme: ColorScheme) -> some View {
|
||||
environment(\.colorScheme, colorScheme)
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ public struct DisclosureGroup<Label, Content>: View
|
|||
@State var isExpanded: Bool = false
|
||||
let isExpandedBinding: Binding<Bool>?
|
||||
|
||||
@Environment(\._outlineGroupStyle) var style: _OutlineGroupStyle
|
||||
@Environment(\._outlineGroupStyle) var style
|
||||
|
||||
let label: Label
|
||||
let content: () -> Content
|
||||
|
|
|
@ -25,7 +25,7 @@ public struct List<SelectionValue, Content>: View
|
|||
let selection: _Selection
|
||||
let content: Content
|
||||
|
||||
@Environment(\.listStyle) var style: ListStyle
|
||||
@Environment(\.listStyle) var style
|
||||
|
||||
public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content) {
|
||||
self.selection = .many(selection)
|
||||
|
|
|
@ -30,7 +30,7 @@ public struct NavigationView<Content>: View where Content: View {
|
|||
}
|
||||
|
||||
/// This is a helper class that works around absence of "package private" access control in Swift
|
||||
public struct _NavigationViewProxy<Content: View> {
|
||||
public struct _NavigationViewProxy<Content: View>: View {
|
||||
public let subject: NavigationView<Content>
|
||||
|
||||
public init(_ subject: NavigationView<Content>) { self.subject = subject }
|
||||
|
|
|
@ -16,7 +16,7 @@ public struct _PickerContainer<Label: View, SelectionValue: Hashable, Content: V
|
|||
@Binding public var selection: SelectionValue
|
||||
public let label: Label
|
||||
public let content: Content
|
||||
@Environment(\.pickerStyle) public var style: PickerStyle
|
||||
@Environment(\.pickerStyle) public var style
|
||||
|
||||
public init(
|
||||
selection: Binding<SelectionValue>,
|
||||
|
@ -36,7 +36,7 @@ public struct _PickerContainer<Label: View, SelectionValue: Hashable, Content: V
|
|||
public struct _PickerElement: View {
|
||||
public let valueIndex: Int?
|
||||
public let content: AnyView
|
||||
@Environment(\.pickerStyle) public var style: PickerStyle
|
||||
@Environment(\.pickerStyle) public var style
|
||||
|
||||
public var body: Never {
|
||||
neverBody("_PickerElement")
|
||||
|
|
|
@ -39,7 +39,7 @@ public struct TextField<Label>: View where Label: View {
|
|||
let textBinding: Binding<String>
|
||||
let onEditingChanged: (Bool) -> ()
|
||||
let onCommit: () -> ()
|
||||
@Environment(\.textFieldStyle) var style: TextFieldStyle
|
||||
@Environment(\.textFieldStyle) var style
|
||||
|
||||
public var body: Never {
|
||||
neverBody("TextField")
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
public struct Toggle<Label>: View where Label: View {
|
||||
@Binding var isOn: Bool
|
||||
var label: Label
|
||||
@Environment(\.toggleStyle) var toggleStyle: _AnyToggleStyle
|
||||
@Environment(\.toggleStyle) var toggleStyle
|
||||
|
||||
public init(isOn: Binding<Bool>, label: () -> Label) {
|
||||
_isOn = isOn
|
||||
|
|
|
@ -20,22 +20,6 @@ import JavaScriptKit
|
|||
import TokamakCore
|
||||
import TokamakStaticHTML
|
||||
|
||||
private enum ScenePhaseObserver {
|
||||
static var publisher = CurrentValueSubject<ScenePhase, Never>(.active)
|
||||
|
||||
static func observe() {
|
||||
_ = document.addEventListener!("visibilitychange", JSClosure { _ in
|
||||
let visibilityState = document.visibilityState.string
|
||||
if visibilityState == "visible" {
|
||||
publisher.send(.active)
|
||||
} else if visibilityState == "hidden" {
|
||||
publisher.send(.background)
|
||||
}
|
||||
return .undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension App {
|
||||
/// The default implementation of `launch` for a `TokamakDOM` app.
|
||||
///
|
||||
|
@ -60,6 +44,7 @@ extension App {
|
|||
_ = body.appendChild!(div)
|
||||
|
||||
ScenePhaseObserver.observe()
|
||||
ColorSchemeObserver.observe()
|
||||
}
|
||||
|
||||
public static func _setTitle(_ title: String) {
|
||||
|
@ -69,7 +54,11 @@ extension App {
|
|||
_ = head.appendChild!(titleTag)
|
||||
}
|
||||
|
||||
public var _phasePublisher: CurrentValueSubject<ScenePhase, Never> {
|
||||
ScenePhaseObserver.publisher
|
||||
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||
ScenePhaseObserver.publisher.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
|
||||
ColorSchemeObserver.publisher.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
|
@ -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 CombineShim
|
||||
import JavaScriptKit
|
||||
|
||||
enum ColorSchemeObserver {
|
||||
static var publisher = CurrentValueSubject<ColorScheme, Never>(
|
||||
.init(matchMediaDarkScheme: matchMediaDarkScheme)
|
||||
)
|
||||
|
||||
private static var closure: JSClosure?
|
||||
|
||||
static func observe() {
|
||||
let closure = JSClosure {
|
||||
publisher.value = .init(matchMediaDarkScheme: $0[0].object!)
|
||||
return .undefined
|
||||
}
|
||||
_ = matchMediaDarkScheme.addEventListener!("change", closure)
|
||||
Self.closure = closure
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// 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 CombineShim
|
||||
import JavaScriptKit
|
||||
|
||||
enum ScenePhaseObserver {
|
||||
static var publisher = CurrentValueSubject<ScenePhase, Never>(.active)
|
||||
|
||||
private static var closure: JSClosure?
|
||||
|
||||
static func observe() {
|
||||
let closure = JSClosure { _ in
|
||||
let visibilityState = document.visibilityState.string
|
||||
if visibilityState == "visible" {
|
||||
publisher.send(.active)
|
||||
} else if visibilityState == "hidden" {
|
||||
publisher.send(.background)
|
||||
}
|
||||
return .undefined
|
||||
}
|
||||
_ = document.addEventListener!("visibilitychange", closure)
|
||||
Self.closure = closure
|
||||
}
|
||||
}
|
|
@ -51,8 +51,14 @@ public typealias RadioGroupPickerStyle = TokamakCore.RadioGroupPickerStyle
|
|||
public typealias SegmentedPickerStyle = TokamakCore.SegmentedPickerStyle
|
||||
public typealias WheelPickerStyle = TokamakCore.WheelPickerStyle
|
||||
|
||||
public typealias ToggleStyle = TokamakCore.ToggleStyle
|
||||
public typealias ToggleStyleConfiguration = TokamakCore.ToggleStyleConfiguration
|
||||
|
||||
public typealias ButtonStyle = TokamakCore.ButtonStyle
|
||||
public typealias ButtonStyleConfiguration = TokamakCore.ButtonStyleConfiguration
|
||||
public typealias DefaultButtonStyle = TokamakCore.DefaultButtonStyle
|
||||
|
||||
public typealias ColorScheme = TokamakCore.ColorScheme
|
||||
|
||||
// MARK: Shapes
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ extension EnvironmentValues {
|
|||
static var defaultEnvironment: Self {
|
||||
var environment = EnvironmentValues()
|
||||
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
|
||||
environment[_ColorSchemeKey] = .init(matchMediaDarkScheme: matchMediaDarkScheme)
|
||||
environment._defaultAppStorage = LocalStorage.standard
|
||||
_DefaultSceneStorageProvider.default = SessionStorage.standard
|
||||
|
||||
|
@ -54,8 +55,11 @@ private extension AnyView {
|
|||
}
|
||||
}
|
||||
|
||||
let log = JSObjectRef.global.console.object!.log.function!
|
||||
let document = JSObjectRef.global.document.object!
|
||||
let global = JSObjectRef.global
|
||||
let window = global.window.object!
|
||||
let matchMediaDarkScheme = window.matchMedia!("(prefers-color-scheme: dark)").object!
|
||||
let log = global.console.object!.log.function!
|
||||
let document = global.document.object!
|
||||
let body = document.body.object!
|
||||
let head = document.head.object!
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// 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
|
||||
|
||||
extension ColorScheme {
|
||||
init(matchMediaDarkScheme: JSObjectRef) {
|
||||
self = matchMediaDarkScheme.matches.boolean == true ? .dark : .light
|
||||
}
|
||||
}
|
|
@ -32,13 +32,28 @@ struct EnvironmentObjectDemo: View {
|
|||
}
|
||||
}
|
||||
|
||||
extension ColorScheme: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .dark: return "dark"
|
||||
case .light: return "light"
|
||||
@unknown default: return "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EnvironmentDemo: View {
|
||||
@Environment(\.font) var font: Font?
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@Environment(\.font) var font
|
||||
@EnvironmentObject var testEnv: TestEnvironment
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(font == nil ? "`font` environment not set." : "\(String(describing: font!))")
|
||||
Text("`colorScheme` is \(colorScheme.description)")
|
||||
if let font = font {
|
||||
Text("`font` environment is \(String(describing: font))")
|
||||
}
|
||||
Text(testEnv.envTest)
|
||||
EnvironmentObjectDemo()
|
||||
}
|
||||
|
|
|
@ -27,7 +27,11 @@ extension App {
|
|||
StaticHTMLRenderer.title = title
|
||||
}
|
||||
|
||||
public var _phasePublisher: CurrentValueSubject<ScenePhase, Never> {
|
||||
CurrentValueSubject<ScenePhase, Never>(.active)
|
||||
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||
CurrentValueSubject<ScenePhase, Never>(.active).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
|
||||
CurrentValueSubject<ColorScheme, Never>(.light).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,16 @@
|
|||
|
||||
import TokamakCore
|
||||
|
||||
extension EnvironmentValues {
|
||||
/// Returns default settings for the static HTML environment
|
||||
static var defaultEnvironment: Self {
|
||||
var environment = EnvironmentValues()
|
||||
environment[_ColorSchemeKey] = .light
|
||||
|
||||
return environment
|
||||
}
|
||||
}
|
||||
|
||||
public final class HTMLTarget: Target {
|
||||
var html: AnyHTML
|
||||
var children: [HTMLTarget] = []
|
||||
|
@ -80,7 +90,7 @@ public final class StaticHTMLRenderer: Renderer {
|
|||
reconciler = StackReconciler(
|
||||
view: view,
|
||||
target: rootTarget,
|
||||
environment: EnvironmentValues(),
|
||||
environment: .defaultEnvironment,
|
||||
renderer: self,
|
||||
scheduler: { _ in
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
|
@ -94,7 +104,7 @@ public final class StaticHTMLRenderer: Renderer {
|
|||
reconciler = StackReconciler(
|
||||
app: app,
|
||||
target: rootTarget,
|
||||
environment: EnvironmentValues(),
|
||||
environment: .defaultEnvironment,
|
||||
renderer: self,
|
||||
scheduler: { _ in
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
|
|
|
@ -22,7 +22,7 @@ extension NavigationView: ViewDeferredToRenderer {
|
|||
width: 100%; height: 100%;
|
||||
""",
|
||||
]) {
|
||||
_NavigationViewProxy(self).body
|
||||
_NavigationViewProxy(self)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue