From cdc452b7a5b39e598242e9acae73302eddf75833 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Sun, 2 Aug 2020 20:22:52 -0400 Subject: [PATCH 01/16] Add dark styles for most elements --- .../Modifiers/ModifiedContent.swift | 13 +- .../Modifiers/StyleModifiers.swift | 30 ++++- .../TokamakCore/Shapes/ModifiedShapes.swift | 1 + Sources/TokamakCore/Shapes/Shape.swift | 1 + Sources/TokamakCore/Tokens/Color.swift | 125 ++++++++++++------ .../TokamakCore/Views/Spacers/Divider.swift | 1 + Sources/TokamakCore/Views/Text/Text.swift | 1 + Sources/TokamakDOM/App/App.swift | 4 +- .../TokamakDOM/App/ColorSchemeObserver.swift | 12 +- Sources/TokamakDOM/Views/Buttons/Button.swift | 2 +- .../TokamakDOM/Views/Selectors/Picker.swift | 2 +- .../TokamakDOM/Views/Text/SecureField.swift | 1 + Sources/TokamakDOM/Views/Text/TextField.swift | 32 +++-- Sources/TokamakDemo/ColorDemo.swift | 36 ++--- ...er_Content.swift => ModifiedContent.swift} | 0 .../Modifiers/ViewModifier.swift | 2 +- .../Resources/TokamakStyles.swift | 28 ++++ Sources/TokamakStaticHTML/Shapes/Shape.swift | 2 +- .../TokamakStaticHTML/Shapes/_ShapeView.swift | 6 +- Sources/TokamakStaticHTML/Tokens/Tokens.swift | 7 +- .../Views/Containers/List.swift | 7 +- .../Views/Spacers/Divider.swift | 7 +- .../TokamakStaticHTML/Views/Text/Text.swift | 4 +- Tests/TokamakTests/ColorTests.swift | 11 +- 24 files changed, 237 insertions(+), 98 deletions(-) rename Sources/TokamakStaticHTML/Modifiers/{_ViewModifier_Content.swift => ModifiedContent.swift} (100%) diff --git a/Sources/TokamakCore/Modifiers/ModifiedContent.swift b/Sources/TokamakCore/Modifiers/ModifiedContent.swift index eb79fa9f5..4de795fcc 100644 --- a/Sources/TokamakCore/Modifiers/ModifiedContent.swift +++ b/Sources/TokamakCore/Modifiers/ModifiedContent.swift @@ -14,16 +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) { + 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 817e810ab..4de12b53a 100644 --- a/Sources/TokamakCore/Modifiers/StyleModifiers.swift +++ b/Sources/TokamakCore/Modifiers/StyleModifiers.swift @@ -15,7 +15,9 @@ // 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 +29,18 @@ 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(_ background: Background, alignment: Alignment = .center) -> some View where Background: View { @@ -37,7 +48,9 @@ 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 @@ -52,9 +65,18 @@ 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, 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 4e4bca4c8..23381599a 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/Tokens/Color.swift b/Sources/TokamakCore/Tokens/Color.swift index cfbc88f95..06aa98f71 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,32 +35,42 @@ 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, red: Double, green: Double, 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: @@ -60,15 +80,26 @@ public struct Color: Hashable, Equatable { 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) } } @@ -84,9 +115,27 @@ 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 @@ -96,11 +145,11 @@ 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) } } @@ -117,11 +166,11 @@ 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) } } @@ -153,12 +202,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/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 5cf50d5e3..5ff9d32e1 100644 --- a/Sources/TokamakCore/Views/Text/Text.swift +++ b/Sources/TokamakCore/Views/Text/Text.swift @@ -33,6 +33,7 @@ public struct Text: View { let storage: _Storage let modifiers: [_Modifier] + @Environment(\.self) public var environment @Environment(\.font) var font @Environment(\.foregroundColor) var foregroundColor @Environment(\.redactionReasons) var redactionReasons 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 f5c18bf7e..262acba6d 100644 --- a/Sources/TokamakDOM/App/ColorSchemeObserver.swift +++ b/Sources/TokamakDOM/App/ColorSchemeObserver.swift @@ -21,13 +21,23 @@ 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.addEventListener!("change", closure) Self.closure = closure + Self.cancellable = Self.publisher.sink { colorScheme in + let systemBackground = { () -> String in + switch colorScheme { + case .light: return "#FFFFFF" + case .dark: return "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..a02c6dd82 100644 --- a/Sources/TokamakDOM/Views/Buttons/Button.swift +++ b/Sources/TokamakDOM/Views/Buttons/Button.swift @@ -21,7 +21,7 @@ extension _Button: ViewDeferredToRenderer where Label == Text { public var deferredBody: AnyView { let attributes: [String: String] if buttonStyle.type == DefaultButtonStyle.self { - attributes = [:] + attributes = ["class": "_tokamak-buttonstyle-default"] } else { attributes = ["class": "_tokamak-buttonstyle-reset"] } 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 abc6707df..aa67695bf 100644 --- a/Sources/TokamakDemo/ColorDemo.swift +++ b/Sources/TokamakDemo/ColorDemo.swift @@ -36,20 +36,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 @@ -71,16 +71,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/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 d4c6e0794..062d47c6a 100644 --- a/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift +++ b/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift @@ -47,6 +47,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..c1fb00b6f 100644 --- a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift +++ b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift @@ -76,6 +76,34 @@ public let tokamakStyles = """ height: 1.2em; border-radius: .1em; } + +@media (prefers-color-scheme:dark) { + ._tokamak-buttonstyle-default { + background-color: rgb(99, 95, 98); + } + + ._tokamak-securefield { + background-color: rgb(99, 95, 98); + color: #FFFFFF; + } + ._tokamak-textfield-default { + background-color: rgb(99, 95, 98); + color: #FFFFFF; + } + ._tokamak-textfield-roundedborder { + background-color: rgb(99, 95, 98); + color: #FFFFFF; + } + + ._tokamak-picker { + background-color: rgb(99, 95, 98); + color: #FFFFFF; + } + + ._tokamak-text-redacted::after { + background-color: rgb(100, 100, 100); + } +} """ public let rootNodeStyles = """ diff --git a/Sources/TokamakStaticHTML/Shapes/Shape.swift b/Sources/TokamakStaticHTML/Shapes/Shape.swift index 8b26a03bb..0d73d4272 100644 --- a/Sources/TokamakStaticHTML/Shapes/Shape.swift +++ b/Sources/TokamakStaticHTML/Shapes/Shape.swift @@ -25,7 +25,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..bdfdcc6cd 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,9 @@ 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/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 716b15ef3..721387a06 100644 --- a/Sources/TokamakStaticHTML/Views/Containers/List.swift +++ b/Sources/TokamakStaticHTML/Views/Containers/List.swift @@ -94,7 +94,12 @@ extension SidebarListStyle: ListStyleDeferredToRenderer { AnyView(content .padding(.all) .padding(.leading, 20) - .background(Color(0xF2F2F7)) + .background(Color._withScheme { + switch $0 { + case .light: return Color(0xF2F2F7) + case .dark: return Color(.sRGB, red: 45 / 255, green: 43 / 255, blue: 48 / 255) + } + }) ) } } diff --git a/Sources/TokamakStaticHTML/Views/Spacers/Divider.swift b/Sources/TokamakStaticHTML/Views/Spacers/Divider.swift index 57358bdda..1a17d06b0 100644 --- a/Sources/TokamakStaticHTML/Views/Spacers/Divider.swift +++ b/Sources/TokamakStaticHTML/Views/Spacers/Divider.swift @@ -23,7 +23,12 @@ extension Divider: AnyHTML { width: 100%; height: 0; margin: 0; border-top: none; border-right: none; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); + border-bottom: 1px solid \(Color._withScheme { + switch $0 { + case .light: return .init(.sRGB, white: 0, opacity: 0.2) + case .dark: return .init(.sRGB, white: 1, opacity: 0.2) + } + }.cssValue(environment)); border-left: none; """, ] diff --git a/Sources/TokamakStaticHTML/Views/Text/Text.swift b/Sources/TokamakStaticHTML/Views/Text/Text.swift index b315de203..8cd83cb6c 100644 --- a/Sources/TokamakStaticHTML/Views/Text/Text.swift +++ b/Sources/TokamakStaticHTML/Views/Text/Text.swift @@ -151,13 +151,13 @@ extension Text: AnyHTML { } }.inlineStyles ?? "") \(font == nil ? "font-family: \(Font.Design.default.description);" : "") - color: \(color?.description ?? "inherit"); + color: \((color ?? .primary).cssValue(environment)); font-style: \(italic ? "italic" : "normal"); font-weight: \(weight?.value ?? font?._weight.value ?? 400); letter-spacing: \(kerning); vertical-align: \(baseline == nil ? "baseline" : "\(baseline!)em"); text-decoration: \(textDecoration); - text-decoration-color: \(strikethrough?.1?.description ?? underline?.1?.description + text-decoration-color: \(strikethrough?.1?.cssValue(environment) ?? underline?.1?.cssValue(environment) ?? "inherit") """, "class": isRedacted ? "_tokamak-text-redacted" : "", diff --git a/Tests/TokamakTests/ColorTests.swift b/Tests/TokamakTests/ColorTests.swift index 166ce80a0..6d4c2f5fc 100644 --- a/Tests/TokamakTests/ColorTests.swift +++ b/Tests/TokamakTests/ColorTests.swift @@ -17,7 +17,8 @@ import XCTest final class ColorTests: XCTestCase { func testHexColors() { - guard let color = Color(hex: "#FF00FF") else { + let env = EnvironmentValues() + guard let color = Color(hex: "#FF00FF")?._evaluate(env) else { XCTFail("Hexadecimal decoding failed") return } @@ -28,11 +29,11 @@ final class ColorTests: XCTestCase { XCTAssertEqual( color, - Color(hex: "FF00FF"), + Color(hex: "FF00FF")?._evaluate(env), "The '#' before a hex code produced a different output than without it" ) - guard let red = Color(hex: "#FF0000") else { + guard let red = Color(hex: "#FF0000")?._evaluate(env) else { XCTFail("Hexadecimal decoding failed") return } @@ -41,7 +42,7 @@ final class ColorTests: XCTestCase { XCTAssertEqual(red.green, 0) XCTAssertEqual(red.blue, 0) - guard let green = Color(hex: "#00FF00") else { + guard let green = Color(hex: "#00FF00")?._evaluate(env) else { XCTFail("Hexadecimal decoding failed") return } @@ -50,7 +51,7 @@ final class ColorTests: XCTestCase { XCTAssertEqual(green.green, 1) XCTAssertEqual(green.blue, 0) - guard let blue = Color(hex: "#0000FF") else { + guard let blue = Color(hex: "#0000FF")?._evaluate(env) else { XCTFail("Hexadecimal decoding failed") return } From abc59f048a46691edb26681dd94fe01f48e67e91 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Mon, 3 Aug 2020 17:07:44 -0400 Subject: [PATCH 02/16] Style fixes for NavigationLink, DisclosureGroup, Button, TextField, Picker, and more --- .../Styles/NavigationLinkStyle.swift | 74 +++++++++++++++++++ .../TokamakCore/Views/NavigationLink.swift | 16 +++- .../Views/Navigation/NavigationLink.swift | 8 +- Sources/TokamakDemo/TokamakDemo.swift | 3 +- .../Resources/TokamakStyles.swift | 35 ++++----- .../Views/Containers/List.swift | 33 ++++++++- 6 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 Sources/TokamakCore/Styles/NavigationLinkStyle.swift 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/Views/NavigationLink.swift b/Sources/TokamakCore/Views/NavigationLink.swift index 62e50b607..480c3e0de 100644 --- a/Sources/TokamakCore/Views/NavigationLink.swift +++ b/Sources/TokamakCore/Views/NavigationLink.swift @@ -19,7 +19,8 @@ public struct NavigationLink: View where Label: View, Destin let destination: Destination let label: Label - @Environment(_navigationDestinationKey) var navigationContext + @Environment(\.navigationDestination) var navigationContext + @Environment(\._navigationLinkStyle) var style public init(destination: Destination, @ViewBuilder label: () -> Label) { self.destination = destination @@ -46,8 +47,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 @@ -71,7 +71,15 @@ public struct _NavigationLinkProxy where Label: View, Destin 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 { + true + } public func activate() { subject.navigationContext!.wrappedValue = AnyView(subject.destination) 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/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 3c796be55..b8a5ee786 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -101,7 +101,8 @@ var links: [NavItem] { Counter(count: Count(value: 5), limit: 15) .padding() .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0)) - .border(Color.red, width: 3)), + .border(Color.red, width: 3) + .foregroundColor(.black)), NavItem("ZStack", destination: ZStack { Text("I'm on bottom") Text("I'm forced to the top") diff --git a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift index c1fb00b6f..cc06fee50 100644 --- a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift +++ b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift @@ -77,32 +77,23 @@ public let tokamakStyles = """ border-radius: .1em; } -@media (prefers-color-scheme:dark) { - ._tokamak-buttonstyle-default { - background-color: rgb(99, 95, 98); - } - - ._tokamak-securefield { - background-color: rgb(99, 95, 98); - color: #FFFFFF; - } - ._tokamak-textfield-default { - background-color: rgb(99, 95, 98); - color: #FFFFFF; - } - ._tokamak-textfield-roundedborder { - background-color: rgb(99, 95, 98); - color: #FFFFFF; - } - - ._tokamak-picker { - background-color: rgb(99, 95, 98); - color: #FFFFFF; - } +._tokamak-buttonstyle-default, +._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); + } } """ diff --git a/Sources/TokamakStaticHTML/Views/Containers/List.swift b/Sources/TokamakStaticHTML/Views/Containers/List.swift index 721387a06..3eaffe723 100644 --- a/Sources/TokamakStaticHTML/Views/Containers/List.swift +++ b/Sources/TokamakStaticHTML/Views/Containers/List.swift @@ -90,16 +90,43 @@ extension InsetGroupedListStyle: ListStyleDeferredToRenderer { } extension SidebarListStyle: ListStyleDeferredToRenderer { + public func listRow(_ row: Row) -> AnyView where Row: View { + AnyView(row) + } + public func listBody(_ content: ListBody) -> AnyView where ListBody: View { AnyView(content - .padding(.all) - .padding(.leading, 20) + ._navigationLinkStyle(_SidebarNavigationLinkStyle()) + .padding([.horizontal, .top], 6) .background(Color._withScheme { switch $0 { case .light: return Color(0xF2F2F7) - case .dark: return Color(.sRGB, red: 45 / 255, green: 43 / 255, blue: 48 / 255) + case .dark: return Color(0x2D2B30) } }) ) } } + +public struct _SidebarNavigationLinkStyle: _NavigationLinkStyle { + @ViewBuilder + public func makeBody(configuration: _NavigationLinkStyleConfiguration) -> some View { + if configuration.isSelected { + configuration + .padding(6) + .font(.footnote) + .background(Color._withScheme { + switch $0 { + case .light: return Color(.sRGB, white: 0, opacity: 0.1) + case .dark: return Color(.sRGB, white: 1, opacity: 0.1) + } + }) + .cornerRadius(5) + } else { + configuration + .padding(6) + .foregroundColor(.primary) + .font(.footnote) + } + } +} From cb6359431df4ea626edc846eaa5386ac9cd7ebf8 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Mon, 3 Aug 2020 18:09:40 -0400 Subject: [PATCH 03/16] Add _NavigationLinkStyle for sidebar lists --- .../Views/Containers/ForEach.swift | 43 ++++++++++++++++++- .../TokamakCore/Views/NavigationLink.swift | 23 +++++++--- .../TokamakCore/Views/NavigationView.swift | 11 +++-- .../Modifiers/LayoutModifiers.swift | 20 ++++++--- .../Views/Containers/List.swift | 2 +- 5 files changed, 82 insertions(+), 17 deletions(-) diff --git a/Sources/TokamakCore/Views/Containers/ForEach.swift b/Sources/TokamakCore/Views/Containers/ForEach.swift index 4a587c44f..c59a1d783 100644 --- a/Sources/TokamakCore/Views/Containers/ForEach.swift +++ b/Sources/TokamakCore/Views/Containers/ForEach.swift @@ -79,8 +79,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/NavigationLink.swift b/Sources/TokamakCore/Views/NavigationLink.swift index 480c3e0de..baccf5e11 100644 --- a/Sources/TokamakCore/Views/NavigationLink.swift +++ b/Sources/TokamakCore/Views/NavigationLink.swift @@ -15,15 +15,24 @@ // Created by Jed Fox on 06/30/2020. // +import OpenCombine + +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(\.navigationDestination) 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() } @@ -69,7 +78,9 @@ 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: AnyView { subject.style.makeBody(configuration: .init(body: AnyView(subject.label), @@ -78,10 +89,10 @@ public struct _NavigationLinkProxy where Label: View, Destin public var style: _AnyNavigationLinkStyle { subject.style } public var isSelected: Bool { - true + ObjectIdentifier(subject.destination) == ObjectIdentifier(subject.navigationContext.destination) } public func activate() { - subject.navigationContext!.wrappedValue = AnyView(subject.destination) + subject.navigationContext.destination = subject.destination } } diff --git a/Sources/TokamakCore/Views/NavigationView.swift b/Sources/TokamakCore/Views/NavigationView.swift index 9eca0f14c..c496c3246 100644 --- a/Sources/TokamakCore/Views/NavigationView.swift +++ b/Sources/TokamakCore/Views/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() @@ -39,8 +43,9 @@ public struct _NavigationViewProxy: View { public var body: some View { HStack { content - subject.destination - }.environment(\.navigationDestination, subject.$destination) + subject.context.destination.view + } + .environmentObject(subject.context) } } diff --git a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift index 1047b1baa..2f1e99ceb 100644 --- a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift +++ b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift @@ -15,11 +15,17 @@ 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 } } } @@ -40,12 +46,14 @@ extension _FrameLayout: DOMViewModifier { extension _FlexFrameLayout: DOMViewModifier { public var attributes: [String: String] { - ["style": """ + let flexibleWidth = minWidth == 0 && maxWidth == .infinity + let flexibleHeight = minHeight == 0 && maxHeight == .infinity + return ["style": """ \(unwrapToStyle(\.minWidth, property: "min-width")) - \(unwrapToStyle(\.idealWidth, property: "width")) + width: \(unwrapToStyle(\.idealWidth, defaultValue: flexibleWidth ? "100%" : "auto")); \(unwrapToStyle(\.maxWidth, property: "max-width")) \(unwrapToStyle(\.minHeight, property: "min-height")) - \(unwrapToStyle(\.idealHeight, property: "height")) + height: \(unwrapToStyle(\.idealHeight, defaultValue: flexibleHeight ? "100%" : "auto")); \(unwrapToStyle(\.maxHeight, property: "max-height")) overflow: hidden; text-overflow: ellipsis; diff --git a/Sources/TokamakStaticHTML/Views/Containers/List.swift b/Sources/TokamakStaticHTML/Views/Containers/List.swift index 3eaffe723..1118668a2 100644 --- a/Sources/TokamakStaticHTML/Views/Containers/List.swift +++ b/Sources/TokamakStaticHTML/Views/Containers/List.swift @@ -91,7 +91,7 @@ extension InsetGroupedListStyle: ListStyleDeferredToRenderer { extension SidebarListStyle: ListStyleDeferredToRenderer { public func listRow(_ row: Row) -> AnyView where Row: View { - AnyView(row) + AnyView(row.frame(minWidth: 0, maxWidth: .infinity)) } public func listBody(_ content: ListBody) -> AnyView where ListBody: View { From c0a580653af4009be78f7337387da66b1d26a109 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Mon, 3 Aug 2020 18:27:27 -0400 Subject: [PATCH 04/16] Fix segmented Text --- Sources/TokamakCore/Views/Text/Text.swift | 43 +++++++++++-------- .../TokamakStaticHTML/Views/Text/Text.swift | 30 +++++++++++-- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/Sources/TokamakCore/Views/Text/Text.swift b/Sources/TokamakCore/Views/Text/Text.swift index 5ff9d32e1..bec06191b 100644 --- a/Sources/TokamakCore/Views/Text/Text.swift +++ b/Sources/TokamakCore/Views/Text/Text.swift @@ -33,14 +33,11 @@ public struct Text: View { let storage: _Storage let modifiers: [_Modifier] - @Environment(\.self) public var environment - @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 { @@ -59,8 +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 @@ -81,6 +77,19 @@ public struct Text: View { } } +extension Text._Storage { + public var rawText: String { + switch self { + case let .segmentedText(segments): + return segments + .map { $0.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 @@ -89,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 { @@ -149,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/TokamakStaticHTML/Views/Text/Text.swift b/Sources/TokamakStaticHTML/Views/Text/Text.swift index 8cd83cb6c..11fa2c6d5 100644 --- a/Sources/TokamakStaticHTML/Views/Text/Text.swift +++ b/Sources/TokamakStaticHTML/Views/Text/Text.swift @@ -89,14 +89,28 @@ extension Font: StylesConvertible { } } +private struct TextSpan: AnyHTML { + let content: String + let attributes: [String: String] + + var innerHTML: String? { content } + var tag: String { "span" } +} + extension Text: AnyHTML { public var innerHTML: String? { - switch _TextProxy(self).storage { + let proxy = _TextProxy(self) + switch proxy.storage { case let .verbatim(text): return text case let .segmentedText(segments): return segments - .map(\.outerHTML) + .map { + TextSpan(content: $0.0.rawText, + attributes: Self.attributes(from: $0.1, + environment: proxy.environment)) + .outerHTML + } .reduce("", +) } } @@ -104,7 +118,15 @@ extension Text: AnyHTML { public var tag: String { "span" } public var attributes: [String: String] { let proxy = _TextProxy(self) - let isRedacted = proxy.redactionReasons.contains(.placeholder) + return Self.attributes(from: proxy.modifiers, + environment: proxy.environment) + } +} + +extension Text { + static func attributes(from modifiers: [_Modifier], + environment: EnvironmentValues) -> [String: String] { + let isRedacted = environment.redactionReasons.contains(.placeholder) var font: Font? var color: Color? @@ -114,7 +136,7 @@ extension Text: AnyHTML { var baseline: CGFloat? var strikethrough: (Bool, Color?)? var underline: (Bool, Color?)? - for modifier in proxy.modifiers { + for modifier in modifiers { switch modifier { case let .color(_color): color = _color From fff9909a5a17bd48fb8a76a02423436e92fe20bc Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 4 Aug 2020 14:25:07 -0400 Subject: [PATCH 05/16] Fix linter and layout issues --- .../Views/{ => Navigation}/NavigationLink.swift | 4 +++- .../Views/{ => Navigation}/NavigationView.swift | 13 +++++++------ Sources/TokamakDemo/TokamakDemo.swift | 8 ++------ Sources/TokamakStaticHTML/Shapes/_ShapeView.swift | 8 ++++++-- .../Views/{ => Navigation}/NavigationView.swift | 12 +++++++++++- Sources/TokamakStaticHTML/Views/Text/Text.swift | 7 +++++-- 6 files changed, 34 insertions(+), 18 deletions(-) rename Sources/TokamakCore/Views/{ => Navigation}/NavigationLink.swift (97%) rename Sources/TokamakCore/Views/{ => Navigation}/NavigationView.swift (90%) rename Sources/TokamakStaticHTML/Views/{ => Navigation}/NavigationView.swift (74%) diff --git a/Sources/TokamakCore/Views/NavigationLink.swift b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift similarity index 97% rename from Sources/TokamakCore/Views/NavigationLink.swift rename to Sources/TokamakCore/Views/Navigation/NavigationLink.swift index baccf5e11..01db7c67a 100644 --- a/Sources/TokamakCore/Views/NavigationLink.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift @@ -93,6 +93,8 @@ public struct _NavigationLinkProxy where Label: View, Destin } public func activate() { - subject.navigationContext.destination = 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 90% rename from Sources/TokamakCore/Views/NavigationView.swift rename to Sources/TokamakCore/Views/Navigation/NavigationView.swift index c496c3246..e29abb767 100644 --- a/Sources/TokamakCore/Views/NavigationView.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationView.swift @@ -39,13 +39,14 @@ public struct _NavigationViewProxy: View { public init(_ subject: NavigationView) { self.subject = subject } - public var content: Content { subject.content } + public var content: some View { + subject.content + .environmentObject(subject.context) + } + public var body: some View { - HStack { - content - subject.context.destination.view - } - .environmentObject(subject.context) + subject.context.destination.view + .environmentObject(subject.context) } } diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 5dfae5459..96ff88c12 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -35,7 +35,7 @@ struct NavItem: Identifiable { 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) { @@ -141,11 +141,7 @@ struct TokamakDemoView: View { let list = title( List(links) { link in if let dest = link.destination { - NavigationLink(link.id, destination: HStack { - Spacer() - dest - Spacer() - }) + NavigationLink(link.id, destination: dest) } else { #if os(WASI) Text(link.id) diff --git a/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift b/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift index bdfdcc6cd..0ad8cd5f2 100644 --- a/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift +++ b/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift @@ -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.cssValue(environment));"]) { path }) + return AnyView(HTML("div", [ + "style": "fill: \(color.cssValue(environment));", + ]) { path }) } else if let foregroundColor = foregroundColor { - return AnyView(HTML("div", ["style": "fill: \(foregroundColor.cssValue(environment));"]) { path }) + return AnyView(HTML("div", [ + "style": "fill: \(foregroundColor.cssValue(environment));", + ]) { path }) } else { return path } diff --git a/Sources/TokamakStaticHTML/Views/NavigationView.swift b/Sources/TokamakStaticHTML/Views/Navigation/NavigationView.swift similarity index 74% rename from Sources/TokamakStaticHTML/Views/NavigationView.swift rename to Sources/TokamakStaticHTML/Views/Navigation/NavigationView.swift index aad783610..19439bccb 100644 --- a/Sources/TokamakStaticHTML/Views/NavigationView.swift +++ b/Sources/TokamakStaticHTML/Views/Navigation/NavigationView.swift @@ -22,7 +22,17 @@ extension NavigationView: ViewDeferredToRenderer { width: 100%; height: 100%; """, ]) { - _NavigationViewProxy(self) + _NavigationViewProxy(self).content + HTML("div", [ + "style": """ + display: flex; flex-direction: column; + align-items: center; justify-content: center; + flex-grow: 1; + height: 100%; + """, + ]) { + _NavigationViewProxy(self) + } }) } } diff --git a/Sources/TokamakStaticHTML/Views/Text/Text.swift b/Sources/TokamakStaticHTML/Views/Text/Text.swift index 11fa2c6d5..60d718575 100644 --- a/Sources/TokamakStaticHTML/Views/Text/Text.swift +++ b/Sources/TokamakStaticHTML/Views/Text/Text.swift @@ -157,11 +157,15 @@ extension Text { underline = (active, color) } } + let hasStrikethrough = strikethrough?.0 ?? false let hasUnderline = underline?.0 ?? false let textDecoration = !hasStrikethrough && !hasUnderline ? "none" : "\(hasStrikethrough ? "line-through" : "") \(hasUnderline ? "underline" : "")" + let decorationColor = strikethrough?.1?.cssValue(environment) + ?? underline?.1?.cssValue(environment) + ?? "inherit" return [ "style": """ @@ -179,8 +183,7 @@ extension Text { letter-spacing: \(kerning); vertical-align: \(baseline == nil ? "baseline" : "\(baseline!)em"); text-decoration: \(textDecoration); - text-decoration-color: \(strikethrough?.1?.cssValue(environment) ?? underline?.1?.cssValue(environment) - ?? "inherit") + text-decoration-color: \(decorationColor) """, "class": isRedacted ? "_tokamak-text-redacted" : "", ] From fd57bf33289d2d137d26902019ff346a95814831 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 4 Aug 2020 14:30:02 -0400 Subject: [PATCH 06/16] Move flexible frame check to TokamakCore --- Sources/TokamakCore/Modifiers/FlexFrameLayout.swift | 10 ++++++++++ .../TokamakStaticHTML/Modifiers/LayoutModifiers.swift | 8 +++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/TokamakCore/Modifiers/FlexFrameLayout.swift b/Sources/TokamakCore/Modifiers/FlexFrameLayout.swift index dfb19fd66..631c1c201 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 + // This is a special case in SwiftUI, where the child + // will request the entire width 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, maxWidth: CGFloat? = nil, diff --git a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift index 2f1e99ceb..4bbb6205f 100644 --- a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift +++ b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift @@ -46,14 +46,12 @@ extension _FrameLayout: DOMViewModifier { extension _FlexFrameLayout: DOMViewModifier { public var attributes: [String: String] { - let flexibleWidth = minWidth == 0 && maxWidth == .infinity - let flexibleHeight = minHeight == 0 && maxHeight == .infinity - return ["style": """ + ["style": """ \(unwrapToStyle(\.minWidth, property: "min-width")) - width: \(unwrapToStyle(\.idealWidth, defaultValue: flexibleWidth ? "100%" : "auto")); + width: \(unwrapToStyle(\.idealWidth, defaultValue: fillWidth ? "100%" : "auto")); \(unwrapToStyle(\.maxWidth, property: "max-width")) \(unwrapToStyle(\.minHeight, property: "min-height")) - height: \(unwrapToStyle(\.idealHeight, defaultValue: flexibleHeight ? "100%" : "auto")); + height: \(unwrapToStyle(\.idealHeight, defaultValue: fillHeight ? "100%" : "auto")); \(unwrapToStyle(\.maxHeight, property: "max-height")) overflow: hidden; text-overflow: ellipsis; From ccd20350265ed40f1a72242bf881693b8cc9cda7 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 4 Aug 2020 14:54:35 -0400 Subject: [PATCH 07/16] Fix Sidebar section headers to match macOS --- Sources/TokamakCore/Views/Containers/Section.swift | 3 ++- Sources/TokamakDemo/TokamakDemo.swift | 12 +++++------- .../TokamakStaticHTML/Views/Containers/List.swift | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Sources/TokamakCore/Views/Containers/Section.swift b/Sources/TokamakCore/Views/Containers/Section.swift index 2bf25aeac..52f298255 100644 --- a/Sources/TokamakCore/Views/Containers/Section.swift +++ b/Sources/TokamakCore/Views/Containers/Section.swift @@ -81,7 +81,8 @@ extension Section: View, SectionView where Parent: View, Content: View, Footer: headerView(style) sectionContent(style) footerView(style) - }) + } + .frame(minWidth: 0, maxWidth: .infinity)) } } diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index b66c003a3..87823ab5d 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -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) @@ -76,7 +73,8 @@ struct TokamakDemoView: View { Counter(count: Count(value: 5), limit: 15) .padding() .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0)) - .border(Color.red, width: 3)) + .border(Color.red, width: 3) + .foregroundColor(.black)) NavItem("ButtonStyle", destination: ButtonStyleDemo()) } Section(header: Text("Containers")) { diff --git a/Sources/TokamakStaticHTML/Views/Containers/List.swift b/Sources/TokamakStaticHTML/Views/Containers/List.swift index 1118668a2..e936c10f5 100644 --- a/Sources/TokamakStaticHTML/Views/Containers/List.swift +++ b/Sources/TokamakStaticHTML/Views/Containers/List.swift @@ -89,7 +89,21 @@ extension InsetGroupedListStyle: ListStyleDeferredToRenderer { } } +// TODO: Make sections collabsible (see Section.swift for more impl. details) extension SidebarListStyle: ListStyleDeferredToRenderer { + public func sectionHeader
(_ header: Header) -> AnyView where Header: View { + AnyView(header + .font(.system(size: 11, weight: .medium)) + .foregroundColor(Color._withScheme { + switch $0 { + case .light: return Color(.sRGB, white: 0, opacity: 0.4) + case .dark: return Color(.sRGB, white: 1, opacity: 0.4) + } + }) + .padding(.vertical, 2) + .padding(.leading, 4)) + } + public func listRow(_ row: Row) -> AnyView where Row: View { AnyView(row.frame(minWidth: 0, maxWidth: .infinity)) } From 208c260f812d3f10e47bb609b750e5a4ad23c64f Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 4 Aug 2020 15:41:56 -0400 Subject: [PATCH 08/16] Fix List --- .../TokamakCore/Views/Containers/List.swift | 1 + .../Views/Containers/List.swift | 131 +++++++++++++++--- 2 files changed, 111 insertions(+), 21 deletions(-) diff --git a/Sources/TokamakCore/Views/Containers/List.swift b/Sources/TokamakCore/Views/Containers/List.swift index 461bd4e0f..778930e0d 100644 --- a/Sources/TokamakCore/Views/Containers/List.swift +++ b/Sources/TokamakCore/Views/Containers/List.swift @@ -90,6 +90,7 @@ public struct List: View listStack .environment(\._outlineGroupStyle, _ListOutlineGroupStyle()) }) + .frame(minHeight: 0, maxHeight: .infinity) } else { ScrollView { HStack { diff --git a/Sources/TokamakStaticHTML/Views/Containers/List.swift b/Sources/TokamakStaticHTML/Views/Containers/List.swift index e936c10f5..40c44486f 100644 --- a/Sources/TokamakStaticHTML/Views/Containers/List.swift +++ b/Sources/TokamakStaticHTML/Views/Containers/List.swift @@ -17,75 +17,164 @@ import TokamakCore 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._withScheme { + switch $0 { + case .light: return Color(0xDDDDDD) + case .dark: return Color(0x323234) + } + }) + .frame(minWidth: 0, maxWidth: .infinity) + ) } public func sectionFooter