diff --git a/Sources/TokamakCore/Modifiers/FlexFrameLayout.swift b/Sources/TokamakCore/Modifiers/FlexFrameLayout.swift index 0d1884632..817fd5b34 100644 --- a/Sources/TokamakCore/Modifiers/FlexFrameLayout.swift +++ b/Sources/TokamakCore/Modifiers/FlexFrameLayout.swift @@ -21,6 +21,16 @@ public struct _FlexFrameLayout: ViewModifier { public let maxHeight: CGFloat? public let alignment: Alignment + // These are special cases in SwiftUI, where the child + // will request the entire width/height of the parent. + public var fillWidth: Bool { + minWidth == 0 && maxWidth == .infinity + } + + public var fillHeight: Bool { + minHeight == 0 && maxHeight == .infinity + } + init( minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, diff --git a/Sources/TokamakCore/Modifiers/ModifiedContent.swift b/Sources/TokamakCore/Modifiers/ModifiedContent.swift index 53b083248..4de795fcc 100644 --- a/Sources/TokamakCore/Modifiers/ModifiedContent.swift +++ b/Sources/TokamakCore/Modifiers/ModifiedContent.swift @@ -14,17 +14,23 @@ /// A value with a modifier applied to it. public struct ModifiedContent { + @Environment(\.self) public var environment public typealias Body = Never - public let content: Content - public let modifier: Modifier + public private(set) var content: Content + public private(set) var modifier: Modifier - @inlinable public init(content: Content, modifier: Modifier) { self.content = content self.modifier = modifier } } +extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader { + mutating func setContent(from values: EnvironmentValues) { + modifier.setContent(from: values) + } +} + extension ModifiedContent: View, ParentView where Content: View, Modifier: ViewModifier { public var body: Body { neverBody("ModifiedContent") diff --git a/Sources/TokamakCore/Modifiers/StyleModifiers.swift b/Sources/TokamakCore/Modifiers/StyleModifiers.swift index 78980f445..1104d506b 100644 --- a/Sources/TokamakCore/Modifiers/StyleModifiers.swift +++ b/Sources/TokamakCore/Modifiers/StyleModifiers.swift @@ -15,7 +15,10 @@ // Created by Carson Katri on 6/29/20. // -public struct _BackgroundModifier: ViewModifier where Background: View { +public struct _BackgroundModifier: ViewModifier, EnvironmentReader + where Background: View +{ + public var environment: EnvironmentValues! public var background: Background public var alignment: Alignment @@ -27,9 +30,20 @@ public struct _BackgroundModifier: ViewModifier where Background: Vi public func body(content: Content) -> some View { content } + + mutating func setContent(from values: EnvironmentValues) { + environment = values + } } -extension _BackgroundModifier: Equatable where Background: Equatable {} +extension _BackgroundModifier: Equatable where Background: Equatable { + public static func == ( + lhs: _BackgroundModifier, + rhs: _BackgroundModifier + ) -> Bool { + lhs.background == rhs.background + } +} extension View { public func background( @@ -40,7 +54,10 @@ extension View { } } -public struct _OverlayModifier: ViewModifier where Overlay: View { +public struct _OverlayModifier: ViewModifier, EnvironmentReader + where Overlay: View +{ + public var environment: EnvironmentValues! public var overlay: Overlay public var alignment: Alignment @@ -55,9 +72,17 @@ public struct _OverlayModifier: ViewModifier where Overlay: View { overlay } } + + mutating func setContent(from values: EnvironmentValues) { + environment = values + } } -extension _OverlayModifier: Equatable where Overlay: Equatable {} +extension _OverlayModifier: Equatable where Overlay: Equatable { + public static func == (lhs: _OverlayModifier, rhs: _OverlayModifier) -> Bool { + lhs.overlay == rhs.overlay + } +} extension View { public func overlay(_ overlay: Overlay, alignment: Alignment = .center) -> some View diff --git a/Sources/TokamakCore/Shapes/ModifiedShapes.swift b/Sources/TokamakCore/Shapes/ModifiedShapes.swift index 68bf1db55..19317e987 100644 --- a/Sources/TokamakCore/Shapes/ModifiedShapes.swift +++ b/Sources/TokamakCore/Shapes/ModifiedShapes.swift @@ -16,6 +16,7 @@ // public struct _StrokedShape: Shape where S: Shape { + @Environment(\.self) public var environment public var shape: S public var style: StrokeStyle diff --git a/Sources/TokamakCore/Shapes/Shape.swift b/Sources/TokamakCore/Shapes/Shape.swift index 4c30b92c8..b7c318315 100644 --- a/Sources/TokamakCore/Shapes/Shape.swift +++ b/Sources/TokamakCore/Shapes/Shape.swift @@ -47,6 +47,7 @@ public struct FillStyle: Equatable, ShapeStyle { } public struct _ShapeView: View where Content: Shape, Style: ShapeStyle { + @Environment(\.self) public var environment @Environment(\.foregroundColor) public var foregroundColor public var shape: Content public var style: Style diff --git a/Sources/TokamakCore/Styles/NavigationLinkStyle.swift b/Sources/TokamakCore/Styles/NavigationLinkStyle.swift new file mode 100644 index 000000000..4e35d5674 --- /dev/null +++ b/Sources/TokamakCore/Styles/NavigationLinkStyle.swift @@ -0,0 +1,74 @@ +// 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. +// +// Created by Carson Katri on 8/2/20. +// + +public struct _NavigationLinkStyleConfiguration: View { + public let body: AnyView + public let isSelected: Bool +} + +public protocol _NavigationLinkStyle { + associatedtype Body: View + typealias Configuration = _NavigationLinkStyleConfiguration + func makeBody(configuration: Configuration) -> Self.Body +} + +public struct _DefaultNavigationLinkStyle: _NavigationLinkStyle { + public func makeBody(configuration: Configuration) -> some View { + configuration.foregroundColor(.accentColor) + } +} + +public struct _AnyNavigationLinkStyle: _NavigationLinkStyle { + public typealias Body = AnyView + + private let bodyClosure: (_NavigationLinkStyleConfiguration) -> AnyView + public let type: Any.Type + + public init(_ style: S) { + type = S.self + bodyClosure = { configuration in + AnyView(style.makeBody(configuration: configuration)) + } + } + + public func makeBody(configuration: Configuration) -> AnyView { + bodyClosure(configuration) + } +} + +public enum _NavigationLinkStyleKey: EnvironmentKey { + public static var defaultValue: _AnyNavigationLinkStyle { + _AnyNavigationLinkStyle(_DefaultNavigationLinkStyle()) + } +} + +extension EnvironmentValues { + var _navigationLinkStyle: _AnyNavigationLinkStyle { + get { + self[_NavigationLinkStyleKey.self] + } + set { + self[_NavigationLinkStyleKey.self] = newValue + } + } +} + +extension View { + public func _navigationLinkStyle(_ style: S) -> some View { + environment(\._navigationLinkStyle, _AnyNavigationLinkStyle(style)) + } +} diff --git a/Sources/TokamakCore/Tokens/Color.swift b/Sources/TokamakCore/Tokens/Color.swift index 480aac7d1..87180f13b 100644 --- a/Sources/TokamakCore/Tokens/Color.swift +++ b/Sources/TokamakCore/Tokens/Color.swift @@ -16,8 +16,18 @@ // public struct Color: Hashable, Equatable { - // FIXME: This is not injected. - @Environment(\.accentColor) static var envAccentColor + public static func == (lhs: Self, rhs: Self) -> Bool { + var lightEnv = EnvironmentValues() + lightEnv.colorScheme = .light + var darkEnv = EnvironmentValues() + darkEnv.colorScheme = .dark + return lhs._evaluate(lightEnv) == rhs._evaluate(lightEnv) && + lhs._evaluate(darkEnv) == rhs._evaluate(darkEnv) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(evaluator(EnvironmentValues())) + } public enum RGBColorSpace { case sRGB @@ -25,11 +35,19 @@ public struct Color: Hashable, Equatable { case displayP3 } - public let red: Double - public let green: Double - public let blue: Double - public let opacity: Double - public let space: RGBColorSpace + public struct _RGBA: Hashable, Equatable { + public let red: Double + public let green: Double + public let blue: Double + public let opacity: Double + public let space: RGBColorSpace + } + + let evaluator: (EnvironmentValues) -> _RGBA + + private init(_ evaluator: @escaping (EnvironmentValues) -> _RGBA) { + self.evaluator = evaluator + } public init( _ colorSpace: RGBColorSpace = .sRGB, @@ -38,34 +56,35 @@ public struct Color: Hashable, Equatable { blue: Double, opacity: Double = 1 ) { - self.red = red - self.green = green - self.blue = blue - self.opacity = opacity - space = colorSpace + self.init { _ in + _RGBA(red: red, green: green, blue: blue, opacity: opacity, space: colorSpace) + } } public init(_ colorSpace: RGBColorSpace = .sRGB, white: Double, opacity: Double = 1) { - red = white - green = white - blue = white - self.opacity = opacity - space = colorSpace + self.init(colorSpace, red: white, green: white, blue: white, opacity: opacity) } // Source for the formula: // https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative public init(hue: Double, saturation: Double, brightness: Double, opacity: Double = 1) { let a = saturation * min(brightness / 2, 1 - (brightness / 2)) - let f: (Int) -> Double = { n in + let f = { (n: Int) -> Double in let k = Double((n + Int(hue * 12)) % 12) return brightness - (a * max(-1, min(k - 3, 9 - k, 1))) } - red = f(0) - green = f(8) - blue = f(4) - self.opacity = opacity - space = .sRGB + self.init(.sRGB, red: f(0), green: f(8), blue: f(4), opacity: opacity) + } + + /// Create a `Color` dependent on the current `ColorScheme`. + public static func _withScheme(_ evaluator: @escaping (ColorScheme) -> Self) -> Self { + .init { + evaluator($0.colorScheme)._evaluate($0) + } + } + + public func _evaluate(_ environment: EnvironmentValues) -> _RGBA { + evaluator(environment) } } @@ -81,9 +100,19 @@ extension Color { public static let yellow: Self = .init(red: 1.00, green: 0.84, blue: 0.04) public static let pink: Self = .init(red: 1.00, green: 0.22, blue: 0.37) public static let purple: Self = .init(red: 0.75, green: 0.36, blue: 0.95) - // FIXME: Switch to use colorScheme - public static let primary: Self = .black + public static let primary: Self = .init { + switch $0.colorScheme { + case .light: + return .init(red: 0, green: 0, blue: 0, opacity: 1, space: .sRGB) + case .dark: + return .init(red: 1, green: 1, blue: 1, opacity: 1, space: .sRGB) + } + } + public static let secondary: Self = .gray + public static let accentColor: Self = .init { + ($0.accentColor ?? Self.blue)._evaluate($0) + } public init(_ color: UIColor) { self = color.color @@ -93,11 +122,13 @@ extension Color { extension Color: ExpressibleByIntegerLiteral { /// Allows initializing value of `Color` type from hex values public init(integerLiteral bitMask: UInt32) { - red = Double((bitMask & 0xFF0000) >> 16) / 255 - green = Double((bitMask & 0x00FF00) >> 8) / 255 - blue = Double(bitMask & 0x0000FF) / 255 - opacity = 1 - space = .sRGB + self.init( + .sRGB, + red: Double((bitMask & 0xFF0000) >> 16) / 255, + green: Double((bitMask & 0x00FF00) >> 8) / 255, + blue: Double(bitMask & 0x0000FF) / 255, + opacity: 1 + ) } } @@ -114,11 +145,13 @@ extension Color { else { return nil } - self.red = Double(red) / 255 - self.green = Double(green) / 255 - self.blue = Double(blue) / 255 - opacity = 1 - space = .sRGB + self.init( + .sRGB, + red: Double(red) / 255, + green: Double(green) / 255, + blue: Double(blue) / 255, + opacity: 1 + ) } } @@ -150,12 +183,6 @@ extension View { } } -extension Color { - public static var accentColor: Self { - envAccentColor ?? .blue - } -} - struct ForegroundColorKey: EnvironmentKey { static let defaultValue: Color? = nil } diff --git a/Sources/TokamakCore/Views/Containers/ForEach.swift b/Sources/TokamakCore/Views/Containers/ForEach.swift index c5807bd86..17164cf89 100644 --- a/Sources/TokamakCore/Views/Containers/ForEach.swift +++ b/Sources/TokamakCore/Views/Containers/ForEach.swift @@ -80,8 +80,49 @@ public extension ForEach where Data == Range, ID == Int { extension ForEach: ParentView { public var children: [AnyView] { - data.map { AnyView(content($0)) } + data.map { AnyView(IDView(content($0), id: $0[keyPath: id])) } } } extension ForEach: GroupView {} + +struct _IDKey: EnvironmentKey { + static let defaultValue: AnyHashable? = nil +} + +extension EnvironmentValues { + public var _id: AnyHashable? { + get { + self[_IDKey.self] + } + set { + self[_IDKey.self] = newValue + } + } +} + +public protocol _AnyIDView { + var anyId: AnyHashable { get } +} + +struct IDView: View, _AnyIDView where Content: View, ID: Hashable { + let content: Content + let id: ID + var anyId: AnyHashable { AnyHashable(id) } + + init(_ content: Content, id: ID) { + self.content = content + self.id = id + } + + var body: some View { + content + .environment(\._id, AnyHashable(id)) + } +} + +extension View { + public func id(_ id: ID) -> some View where ID: Hashable { + IDView(self, id: id) + } +} diff --git a/Sources/TokamakCore/Views/Containers/List.swift b/Sources/TokamakCore/Views/Containers/List.swift index 2c39f8d12..2966f53df 100644 --- a/Sources/TokamakCore/Views/Containers/List.swift +++ b/Sources/TokamakCore/Views/Containers/List.swift @@ -85,6 +85,7 @@ public struct List: View listStack .environment(\._outlineGroupStyle, _ListOutlineGroupStyle()) }) + .frame(minHeight: 0, maxHeight: .infinity) } else { ScrollView { HStack { Spacer() } diff --git a/Sources/TokamakCore/Views/Containers/Section.swift b/Sources/TokamakCore/Views/Containers/Section.swift index 2bf25aeac..11a120796 100644 --- a/Sources/TokamakCore/Views/Containers/Section.swift +++ b/Sources/TokamakCore/Views/Containers/Section.swift @@ -77,11 +77,14 @@ extension Section: View, SectionView where Parent: View, Content: View, Footer: } func listRow(_ style: ListStyle) -> AnyView { - AnyView(VStack(alignment: .leading) { - headerView(style) - sectionContent(style) - footerView(style) - }) + AnyView( + VStack(alignment: .leading) { + headerView(style) + sectionContent(style) + footerView(style) + } + .frame(minWidth: 0, maxWidth: .infinity) + ) } } diff --git a/Sources/TokamakCore/Views/NavigationLink.swift b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift similarity index 71% rename from Sources/TokamakCore/Views/NavigationLink.swift rename to Sources/TokamakCore/Views/Navigation/NavigationLink.swift index 62e50b607..024892eee 100644 --- a/Sources/TokamakCore/Views/NavigationLink.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift @@ -15,14 +15,22 @@ // Created by Jed Fox on 06/30/2020. // +final class NavigationLinkDestination { + let view: AnyView + init(_ destination: V) { + view = AnyView(destination) + } +} + public struct NavigationLink: View where Label: View, Destination: View { - let destination: Destination + @State var destination: NavigationLinkDestination let label: Label - @Environment(_navigationDestinationKey) var navigationContext + @EnvironmentObject var navigationContext: NavigationContext + @Environment(\._navigationLinkStyle) var style public init(destination: Destination, @ViewBuilder label: () -> Label) { - self.destination = destination + _destination = State(wrappedValue: NavigationLinkDestination(destination)) self.label = label() } @@ -46,8 +54,7 @@ extension NavigationLink where Label == Text { /// Creates an instance that presents `destination`, with a `Text` label /// generated from a title string. public init(_ title: S, destination: Destination) where S: StringProtocol { - self.destination = destination - label = Text(title) + self.init(destination: destination) { Text(title) } } /// Creates an instance that presents `destination` when active, with a @@ -69,11 +76,25 @@ extension NavigationLink where Label == Text { public struct _NavigationLinkProxy where Label: View, Destination: View { public let subject: NavigationLink - public init(_ subject: NavigationLink) { self.subject = subject } + public init(_ subject: NavigationLink) { + self.subject = subject + } - public var label: Label { subject.label } + public var label: AnyView { + subject.style.makeBody(configuration: .init( + body: AnyView(subject.label), + isSelected: isSelected + )) + } + + public var style: _AnyNavigationLinkStyle { subject.style } + public var isSelected: Bool { + ObjectIdentifier(subject.destination) == ObjectIdentifier(subject.navigationContext.destination) + } public func activate() { - subject.navigationContext!.wrappedValue = AnyView(subject.destination) + if !isSelected { + subject.navigationContext.destination = subject.destination + } } } diff --git a/Sources/TokamakCore/Views/NavigationView.swift b/Sources/TokamakCore/Views/Navigation/NavigationView.swift similarity index 77% rename from Sources/TokamakCore/Views/NavigationView.swift rename to Sources/TokamakCore/Views/Navigation/NavigationView.swift index 9eca0f14c..dd96d4cae 100644 --- a/Sources/TokamakCore/Views/NavigationView.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationView.swift @@ -15,10 +15,14 @@ // Created by Jed Fox on 06/30/2020. // +final class NavigationContext: ObservableObject { + @Published var destination = NavigationLinkDestination(EmptyView()) +} + public struct NavigationView: View where Content: View { let content: Content - @State var destination = AnyView(EmptyView()) + @ObservedObject var context = NavigationContext() public init(@ViewBuilder content: () -> Content) { self.content = content() @@ -30,17 +34,19 @@ public struct NavigationView: View where Content: View { } /// This is a helper class that works around absence of "package private" access control in Swift -public struct _NavigationViewProxy: View { +public struct _NavigationViewProxy { public let subject: NavigationView public init(_ subject: NavigationView) { self.subject = subject } - public var content: Content { subject.content } - public var body: some View { - HStack { - content - subject.destination - }.environment(\.navigationDestination, subject.$destination) + public var content: some View { + subject.content + .environmentObject(subject.context) + } + + public var destination: some View { + subject.context.destination.view + .environmentObject(subject.context) } } diff --git a/Sources/TokamakCore/Views/Spacers/Divider.swift b/Sources/TokamakCore/Views/Spacers/Divider.swift index 680470ef7..33e25a08f 100644 --- a/Sources/TokamakCore/Views/Spacers/Divider.swift +++ b/Sources/TokamakCore/Views/Spacers/Divider.swift @@ -17,6 +17,7 @@ /// A horizontal line for separating content. public struct Divider: View { + @Environment(\.self) public var environment public init() {} public var body: Never { neverBody("Divider") diff --git a/Sources/TokamakCore/Views/Text/Text.swift b/Sources/TokamakCore/Views/Text/Text.swift index 7a4cbd115..9c40ab1d6 100644 --- a/Sources/TokamakCore/Views/Text/Text.swift +++ b/Sources/TokamakCore/Views/Text/Text.swift @@ -33,13 +33,11 @@ public struct Text: View { let storage: _Storage let modifiers: [_Modifier] - @Environment(\.font) var font - @Environment(\.foregroundColor) var foregroundColor - @Environment(\.redactionReasons) var redactionReasons + @Environment(\.self) var environment public enum _Storage { case verbatim(String) - case segmentedText([Text]) + case segmentedText([(_Storage, [_Modifier])]) } public enum _Modifier: Equatable { @@ -58,7 +56,7 @@ public struct Text: View { init(storage: _Storage, modifiers: [_Modifier] = []) { if case let .segmentedText(segments) = storage { self.storage = .segmentedText(segments.map { - Self(storage: $0.storage, modifiers: modifiers + $0.modifiers) + ($0.0, modifiers + $0.1) }) } else { self.storage = storage @@ -79,6 +77,19 @@ public struct Text: View { } } +extension Text._Storage { + public var rawText: String { + switch self { + case let .segmentedText(segments): + return segments + .map(\.0.rawText) + .reduce("", +) + case let .verbatim(text): + return text + } + } +} + /// This is a helper class that works around absence of "package private" access control in Swift public struct _TextProxy { public let subject: Text @@ -87,24 +98,17 @@ public struct _TextProxy { public var storage: Text._Storage { subject.storage } public var rawText: String { - switch subject.storage { - case let .segmentedText(segments): - return segments - .map { Self($0).rawText } - .reduce("", +) - case let .verbatim(text): - return text - } + subject.storage.rawText } public var modifiers: [Text._Modifier] { [ - .font(subject.font), - .color(subject.foregroundColor), + .font(subject.environment.font), + .color(subject.environment.foregroundColor), ] + subject.modifiers } - public var redactionReasons: RedactionReasons { subject.redactionReasons } + public var environment: EnvironmentValues { subject.environment } } public extension Text { @@ -147,6 +151,9 @@ public extension Text { extension Text { public static func _concatenating(lhs: Self, rhs: Self) -> Self { - .init(storage: .segmentedText([lhs, rhs])) + .init(storage: .segmentedText([ + (lhs.storage, lhs.modifiers), + (rhs.storage, rhs.modifiers), + ])) } } diff --git a/Sources/TokamakDOM/App/App.swift b/Sources/TokamakDOM/App/App.swift index b22177193..27c07b43e 100644 --- a/Sources/TokamakDOM/App/App.swift +++ b/Sources/TokamakDOM/App/App.swift @@ -30,7 +30,7 @@ extension App { /// public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) { let body = TokamakDOM.body - if body.style == .undefined { + if body.style.object!.all == "" { body.style = "margin: 0;" } let rootStyle = document.createElement!("style").object! @@ -44,7 +44,7 @@ extension App { _ = body.appendChild!(div) ScenePhaseObserver.observe() - ColorSchemeObserver.observe() + ColorSchemeObserver.observe(div) } public static func _setTitle(_ title: String) { diff --git a/Sources/TokamakDOM/App/ColorSchemeObserver.swift b/Sources/TokamakDOM/App/ColorSchemeObserver.swift index 7c05e2193..bd21def9d 100644 --- a/Sources/TokamakDOM/App/ColorSchemeObserver.swift +++ b/Sources/TokamakDOM/App/ColorSchemeObserver.swift @@ -21,13 +21,22 @@ enum ColorSchemeObserver { ) private static var closure: JSClosure? + private static var cancellable: AnyCancellable? - static func observe() { + static func observe(_ rootElement: JSObjectRef) { let closure = JSClosure { publisher.value = .init(matchMediaDarkScheme: $0[0].object!) return .undefined } _ = matchMediaDarkScheme.addListener!(closure) Self.closure = closure + Self.cancellable = Self.publisher.sink { colorScheme in + let systemBackground: String + switch colorScheme { + case .light: systemBackground = "#FFFFFF" + case .dark: systemBackground = "rgb(38, 38, 38)" + } + rootElement.style.object!.backgroundColor = .string("\(systemBackground)") + } } } diff --git a/Sources/TokamakDOM/Views/Buttons/Button.swift b/Sources/TokamakDOM/Views/Buttons/Button.swift index 3caf067a2..3f18024b3 100644 --- a/Sources/TokamakDOM/Views/Buttons/Button.swift +++ b/Sources/TokamakDOM/Views/Buttons/Button.swift @@ -16,27 +16,39 @@ // import TokamakCore +import TokamakStaticHTML extension _Button: ViewDeferredToRenderer where Label == Text { public var deferredBody: AnyView { - let attributes: [String: String] + let listeners: [String: Listener] = [ + "pointerdown": { _ in isPressed = true }, + "pointerup": { _ in + isPressed = false + action() + }, + ] if buttonStyle.type == DefaultButtonStyle.self { - attributes = [:] + return AnyView(DynamicHTML( + "button", + ["class": "_tokamak-buttonstyle-default"], + listeners: listeners + ) { + HTML("span", content: label.innerHTML ?? "") + }) } else { - attributes = ["class": "_tokamak-buttonstyle-reset"] + return AnyView(DynamicHTML( + "button", + ["class": "_tokamak-buttonstyle-reset"], + listeners: listeners + ) { + buttonStyle.makeBody( + configuration: _ButtonStyleConfigurationProxy( + label: AnyView(label), + isPressed: isPressed + ).subject + ) + .colorScheme(.light) + }) } - - return AnyView(DynamicHTML("button", attributes, listeners: [ - "click": { _ in action() }, - "pointerdown": { _ in isPressed = true }, - "pointerup": { _ in isPressed = false }, - ]) { - buttonStyle.makeBody( - configuration: _ButtonStyleConfigurationProxy( - label: AnyView(label), - isPressed: isPressed - ).subject - ) - }) } } diff --git a/Sources/TokamakDOM/Views/DynamicHTML.swift b/Sources/TokamakDOM/Views/DynamicHTML.swift index 4d8ac37c8..54bd7a330 100644 --- a/Sources/TokamakDOM/Views/DynamicHTML.swift +++ b/Sources/TokamakDOM/Views/DynamicHTML.swift @@ -25,12 +25,35 @@ protocol AnyDynamicHTML: AnyHTML { var listeners: [String: Listener] { get } } -public struct DynamicHTML: View, AnyDynamicHTML where Content: View { +public struct DynamicHTML: View, AnyDynamicHTML { public let tag: String public let attributes: [String: String] public let listeners: [String: Listener] let content: Content + public var innerHTML: String? + + public var body: Never { + neverBody("HTML") + } +} + +extension DynamicHTML where Content: StringProtocol { + public init( + _ tag: String, + _ attributes: [String: String] = [:], + listeners: [String: Listener] = [:], + content: Content + ) { + self.tag = tag + self.attributes = attributes + self.listeners = listeners + self.content = content + innerHTML = String(content) + } +} + +extension DynamicHTML: ParentView where Content: View { public init( _ tag: String, _ attributes: [String: String] = [:], @@ -41,12 +64,11 @@ public struct DynamicHTML: View, AnyDynamicHTML where Content: View { self.attributes = attributes self.listeners = listeners self.content = content() + innerHTML = nil } - public var innerHTML: String? { nil } - - public var body: Never { - neverBody("HTML") + public var children: [AnyView] { + [AnyView(content)] } } @@ -59,9 +81,3 @@ extension DynamicHTML where Content == EmptyView { self = DynamicHTML(tag, attributes, listeners: listeners) { EmptyView() } } } - -extension DynamicHTML: ParentView { - public var children: [AnyView] { - [AnyView(content)] - } -} diff --git a/Sources/TokamakDOM/Views/Navigation/NavigationLink.swift b/Sources/TokamakDOM/Views/Navigation/NavigationLink.swift index da179da49..6089a946c 100644 --- a/Sources/TokamakDOM/Views/Navigation/NavigationLink.swift +++ b/Sources/TokamakDOM/Views/Navigation/NavigationLink.swift @@ -13,6 +13,7 @@ // limitations under the License. import TokamakCore +import TokamakStaticHTML extension NavigationLink: ViewDeferredToRenderer { public var deferredBody: AnyView { @@ -20,11 +21,16 @@ extension NavigationLink: ViewDeferredToRenderer { return AnyView( DynamicHTML("a", [ "href": "javascript:void%200", + "style": proxy.style.type == _SidebarNavigationLinkStyle.self ? + "width: 100%; text-decoration: none;" + : "", ], listeners: [ // FIXME: Focus destination or something so assistive // technology knows where to look when clicking the link. "click": { _ in proxy.activate() }, - ]) { proxy.label } + ]) { + proxy.label + } ) } } diff --git a/Sources/TokamakDOM/Views/Selectors/Picker.swift b/Sources/TokamakDOM/Views/Selectors/Picker.swift index 7289a7d8a..cfd3892e5 100644 --- a/Sources/TokamakDOM/Views/Selectors/Picker.swift +++ b/Sources/TokamakDOM/Views/Selectors/Picker.swift @@ -21,7 +21,7 @@ extension _PickerContainer: ViewDeferredToRenderer { AnyView(HTML("label") { label Text(" ") - DynamicHTML("select", listeners: ["change": { + DynamicHTML("select", ["class": "_tokamak-picker"], listeners: ["change": { guard let valueString = $0.target.object!.value.string, let value = Int(valueString) as? SelectionValue diff --git a/Sources/TokamakDOM/Views/Text/SecureField.swift b/Sources/TokamakDOM/Views/Text/SecureField.swift index 176f517e8..667211ea2 100644 --- a/Sources/TokamakDOM/Views/Text/SecureField.swift +++ b/Sources/TokamakDOM/Views/Text/SecureField.swift @@ -24,6 +24,7 @@ extension SecureField: ViewDeferredToRenderer where Label == Text { "type": "password", "value": proxy.textBinding.wrappedValue, "placeholder": proxy.label.rawText, + "class": "_tokamak-securefield", ], listeners: [ "keypress": { event in if event.key == "Enter" { proxy.onCommit() } }, "input": { event in diff --git a/Sources/TokamakDOM/Views/Text/TextField.swift b/Sources/TokamakDOM/Views/Text/TextField.swift index 71d820fcc..5df82c88a 100644 --- a/Sources/TokamakDOM/Views/Text/TextField.swift +++ b/Sources/TokamakDOM/Views/Text/TextField.swift @@ -17,18 +17,29 @@ import TokamakCore -func css(for style: TextFieldStyle) -> String { - if style is PlainTextFieldStyle { - return """ - background: transparent; - border: none; - """ - } else { - return "" +extension TextField: ViewDeferredToRenderer where Label == Text { + func css(for style: TextFieldStyle) -> String { + if style is PlainTextFieldStyle { + return """ + background: transparent; + border: none; + """ + } else { + return "" + } + } + + func className(for style: TextFieldStyle) -> String { + switch style { + case is DefaultTextFieldStyle: + return "_tokamak-textfield-default" + case is RoundedBorderTextFieldStyle: + return "_tokamak-textfield-roundedborder" + default: + return "" + } } -} -extension TextField: ViewDeferredToRenderer where Label == Text { public var deferredBody: AnyView { let proxy = _TextFieldProxy(self) @@ -37,6 +48,7 @@ extension TextField: ViewDeferredToRenderer where Label == Text { "value": proxy.textBinding.wrappedValue, "placeholder": proxy.label.rawText, "style": css(for: proxy.textFieldStyle), + "class": className(for: proxy.textFieldStyle), ], listeners: [ "focus": { _ in proxy.onEditingChanged(true) }, "blur": { _ in proxy.onEditingChanged(false) }, diff --git a/Sources/TokamakDemo/ColorDemo.swift b/Sources/TokamakDemo/ColorDemo.swift index faf6e0c93..e4e0af34d 100644 --- a/Sources/TokamakDemo/ColorDemo.swift +++ b/Sources/TokamakDemo/ColorDemo.swift @@ -37,20 +37,20 @@ public struct ColorDemo: View { case rgb, hsb } - let colors: [Color] = [ - .clear, - .black, - .white, - .gray, - .red, - .green, - .blue, - .orange, - .yellow, - .pink, - .purple, - .primary, - .secondary, + let colors: [(String, Color)] = [ + ("Clear", .clear), + ("Black", .black), + ("White", .white), + ("Gray", .gray), + ("Red", .red), + ("Green", .green), + ("Blue", .blue), + ("Orange", .orange), + ("Yellow", .yellow), + ("Pink", .pink), + ("Purple", .purple), + ("Primary", .primary), + ("Secondary", .secondary), ] @State private var colorForm: ColorForm = .hsb @@ -72,16 +72,16 @@ public struct ColorDemo: View { .bold() .padding() .background(color) - Text("Accent Color: \(Color.accentColor.description)") + Text("Accent Color: \(String(describing: Color.accentColor))") .bold() .padding() .background(Color.accentColor) - ForEach(colors, id: \.self) { - Text($0.description) + ForEach(colors, id: \.0) { + Text($0.0) .font(.caption) .bold() .padding() - .background($0) + .background($0.1) } }.padding(.horizontal) } diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index e78c0e414..7249aeb0f 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -35,7 +35,7 @@ struct NavItem: View { init(_ id: String, destination: V) where V: View { self.id = id - self.destination = title(destination.frame(minWidth: 300), title: id) + self.destination = title(destination, title: id) } init(unavailable id: String) { @@ -43,13 +43,10 @@ struct NavItem: View { destination = nil } - @ViewBuilder var body: some View { + @ViewBuilder + var body: some View { if let dest = destination { - NavigationLink(id, destination: HStack { - Spacer(minLength: 0) - dest - Spacer(minLength: 0) - }) + NavigationLink(id, destination: dest) } else { #if os(WASI) Text(id) @@ -79,6 +76,7 @@ struct TokamakDemoView: View { .padding() .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0)) .border(Color.red, width: 3) + .foregroundColor(.black) ) NavItem("ButtonStyle", destination: ButtonStyleDemo()) } diff --git a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift index 1047b1baa..fb4678f2f 100644 --- a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift +++ b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift @@ -15,11 +15,19 @@ import TokamakCore private extension DOMViewModifier { - func unwrapToStyle(_ key: KeyPath, property: String) -> String { + func unwrapToStyle( + _ key: KeyPath, + property: String? = nil, + defaultValue: String = "" + ) -> String { if let val = self[keyPath: key] { - return "\(property): \(val)px;" + if let property = property { + return "\(property): \(val)px;" + } else { + return "\(val)px;" + } } else { - return "" + return defaultValue } } } @@ -42,10 +50,10 @@ extension _FlexFrameLayout: DOMViewModifier { public var attributes: [String: String] { ["style": """ \(unwrapToStyle(\.minWidth, property: "min-width")) - \(unwrapToStyle(\.idealWidth, property: "width")) + width: \(unwrapToStyle(\.idealWidth, defaultValue: fillWidth ? "100%" : "auto")); \(unwrapToStyle(\.maxWidth, property: "max-width")) \(unwrapToStyle(\.minHeight, property: "min-height")) - \(unwrapToStyle(\.idealHeight, property: "height")) + height: \(unwrapToStyle(\.idealHeight, defaultValue: fillHeight ? "100%" : "auto")); \(unwrapToStyle(\.maxHeight, property: "max-height")) overflow: hidden; text-overflow: ellipsis; diff --git a/Sources/TokamakStaticHTML/Modifiers/_ViewModifier_Content.swift b/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift similarity index 100% rename from Sources/TokamakStaticHTML/Modifiers/_ViewModifier_Content.swift rename to Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift diff --git a/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift b/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift index 53ff12631..d48ee0ebb 100644 --- a/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift +++ b/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift @@ -48,6 +48,6 @@ extension _ZIndexModifier: DOMViewModifier { extension _BackgroundModifier: DOMViewModifier where Background == Color { public var isOrderDependent: Bool { true } public var attributes: [String: String] { - ["style": "background-color: \(background.description)"] + ["style": "background-color: \(background.cssValue(environment))"] } } diff --git a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift index 04d387e3d..f724bae5b 100644 --- a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift +++ b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift @@ -76,6 +76,38 @@ public let tokamakStyles = """ height: 1.2em; border-radius: .1em; } + +._tokamak-navigationview { + display: flex; + flex-direction: row; + align-items: stretch; + width: 100%; + height: 100%; +} +._tokamak-navigationview-content { + display: flex; flex-direction: column; + align-items: center; justify-content: center; + flex-grow: 1; + height: 100%; +} + +._tokamak-securefield, +._tokamak-textfield-default, +._tokamak-textfield-roundedborder, +._tokamak-picker { + color-scheme: light dark; +} + +@media (prefers-color-scheme:dark) { + ._tokamak-text-redacted::after { + background-color: rgb(100, 100, 100); + } + + ._tokamak-disclosuregroup-chevron { + border-right-color: rgba(255, 255, 255, 0.25); + border-top-color: rgba(255, 255, 255, 0.25); + } +} """ public let rootNodeStyles = """ diff --git a/Sources/TokamakStaticHTML/Shapes/Shape.swift b/Sources/TokamakStaticHTML/Shapes/Shape.swift index 02ad2a042..cbc0d80e1 100644 --- a/Sources/TokamakStaticHTML/Shapes/Shape.swift +++ b/Sources/TokamakStaticHTML/Shapes/Shape.swift @@ -26,7 +26,7 @@ extension _OverlayModifier: DOMViewModifier return ["style": """ border-style: \(style); border-width: \(overlay.shape.style.lineWidth); - border-color: \(overlay.style.description); + border-color: \(overlay.style.cssValue(environment)); border-radius: inherit; """] } diff --git a/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift b/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift index f7ee5bb06..0ad8cd5f2 100644 --- a/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift +++ b/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift @@ -24,7 +24,7 @@ protocol ShapeAttributes { extension _StrokedShape: ShapeAttributes { func attributes(_ style: ShapeStyle) -> [String: String] { if let color = style as? Color { - return ["style": "stroke: \(color); fill: none;"] + return ["style": "stroke: \(color.cssValue(environment)); fill: none;"] } else { return ["style": "stroke: black; fill: none;"] } @@ -37,9 +37,13 @@ extension _ShapeView: ViewDeferredToRenderer { if let shapeAttributes = shape as? ShapeAttributes { return AnyView(HTML("div", shapeAttributes.attributes(style)) { path }) } else if let color = style as? Color { - return AnyView(HTML("div", ["style": "fill: \(color);"]) { path }) + return AnyView(HTML("div", [ + "style": "fill: \(color.cssValue(environment));", + ]) { path }) } else if let foregroundColor = foregroundColor { - return AnyView(HTML("div", ["style": "fill: \(foregroundColor);"]) { path }) + return AnyView(HTML("div", [ + "style": "fill: \(foregroundColor.cssValue(environment));", + ]) { path }) } else { return path } diff --git a/Sources/TokamakStaticHTML/Tokens/BuiltinColors.swift b/Sources/TokamakStaticHTML/Tokens/BuiltinColors.swift new file mode 100644 index 000000000..1fee3e957 --- /dev/null +++ b/Sources/TokamakStaticHTML/Tokens/BuiltinColors.swift @@ -0,0 +1,58 @@ +// 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. +// +// Created by Carson Katri on 8/4/20. +// + +import TokamakCore + +// MARK: List Colors + +extension Color { + static var listSectionHeader: Self { + Color._withScheme { + switch $0 { + case .light: return Color(0xDDDDDD) + case .dark: return Color(0x323234) + } + } + } + + static var groupedListBackground: Self { + Color._withScheme { + switch $0 { + case .light: return Color(0xEEEEEE) + case .dark: return .clear + } + } + } + + static var listGroupBackground: Self { + Color._withScheme { + switch $0 { + case .light: return .white + case .dark: return Color(0x444444) + } + } + } + + static var sidebarBackground: Self { + Color._withScheme { + switch $0 { + case .light: return Color(0xF2F2F7) + case .dark: return Color(0x2D2B30) + } + } + } +} diff --git a/Sources/TokamakStaticHTML/Tokens/Tokens.swift b/Sources/TokamakStaticHTML/Tokens/Tokens.swift index 7de73cc83..23deaa1cd 100644 --- a/Sources/TokamakStaticHTML/Tokens/Tokens.swift +++ b/Sources/TokamakStaticHTML/Tokens/Tokens.swift @@ -14,9 +14,10 @@ import TokamakCore -extension Color: CustomStringConvertible { - public var description: String { - "rgb(\(red * 255), \(green * 255), \(blue * 255), \(opacity * 255))" +extension Color { + func cssValue(_ environment: EnvironmentValues) -> String { + let rgba = _evaluate(environment) + return "rgba(\(rgba.red * 255), \(rgba.green * 255), \(rgba.blue * 255), \(rgba.opacity))" } } diff --git a/Sources/TokamakStaticHTML/Views/Containers/List.swift b/Sources/TokamakStaticHTML/Views/Containers/List.swift index d7c750f38..d4be21259 100644 --- a/Sources/TokamakStaticHTML/Views/Containers/List.swift +++ b/Sources/TokamakStaticHTML/Views/Containers/List.swift @@ -18,16 +18,67 @@ extension PlainListStyle: ListStyleDeferredToRenderer { public func sectionHeader
(_ header: Header) -> AnyView where Header: View { AnyView( header - .padding(.vertical, 5) - .background(Color(0xDDDDDD)) + .font(.system(size: 17, weight: .medium)) + .padding(.vertical, 4) + .padding(.leading) + .background(Color.listSectionHeader) + .frame(minWidth: 0, maxWidth: .infinity) ) } public func sectionFooter