Add default dark styles for Views (#241)

This commit is contained in:
Carson Katri 2020-08-10 16:05:53 -04:00 committed by GitHub
parent 2a49b7808b
commit c43d2db1b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 769 additions and 241 deletions

View File

@ -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,

View File

@ -14,17 +14,23 @@
/// A value with a modifier applied to it.
public struct ModifiedContent<Content, Modifier> {
@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<View, ViewModifier>")

View File

@ -15,7 +15,10 @@
// Created by Carson Katri on 6/29/20.
//
public struct _BackgroundModifier<Background>: ViewModifier where Background: View {
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
where Background: View
{
public var environment: EnvironmentValues!
public var background: Background
public var alignment: Alignment
@ -27,9 +30,20 @@ public struct _BackgroundModifier<Background>: 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<Background>,
rhs: _BackgroundModifier<Background>
) -> Bool {
lhs.background == rhs.background
}
}
extension View {
public func background<Background>(
@ -40,7 +54,10 @@ extension View {
}
}
public struct _OverlayModifier<Overlay>: ViewModifier where Overlay: View {
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
where Overlay: View
{
public var environment: EnvironmentValues!
public var overlay: Overlay
public var alignment: Alignment
@ -55,9 +72,17 @@ public struct _OverlayModifier<Overlay>: 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<Overlay>, rhs: _OverlayModifier<Overlay>) -> Bool {
lhs.overlay == rhs.overlay
}
}
extension View {
public func overlay<Overlay>(_ overlay: Overlay, alignment: Alignment = .center) -> some View

View File

@ -16,6 +16,7 @@
//
public struct _StrokedShape<S>: Shape where S: Shape {
@Environment(\.self) public var environment
public var shape: S
public var style: StrokeStyle

View File

@ -47,6 +47,7 @@ public struct FillStyle: Equatable, ShapeStyle {
}
public struct _ShapeView<Content, Style>: View where Content: Shape, Style: ShapeStyle {
@Environment(\.self) public var environment
@Environment(\.foregroundColor) public var foregroundColor
public var shape: Content
public var style: Style

View File

@ -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<S: _NavigationLinkStyle>(_ 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<S: _NavigationLinkStyle>(_ style: S) -> some View {
environment(\._navigationLinkStyle, _AnyNavigationLinkStyle(style))
}
}

View File

@ -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
}

View File

@ -80,8 +80,49 @@ public extension ForEach where Data == Range<Int>, 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<Content, ID>: 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: ID) -> some View where ID: Hashable {
IDView(self, id: id)
}
}

View File

@ -85,6 +85,7 @@ public struct List<SelectionValue, Content>: View
listStack
.environment(\._outlineGroupStyle, _ListOutlineGroupStyle())
})
.frame(minHeight: 0, maxHeight: .infinity)
} else {
ScrollView {
HStack { Spacer() }

View File

@ -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)
)
}
}

View File

@ -15,14 +15,22 @@
// Created by Jed Fox on 06/30/2020.
//
final class NavigationLinkDestination {
let view: AnyView
init<V: View>(_ destination: V) {
view = AnyView(destination)
}
}
public struct NavigationLink<Label, Destination>: 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<S>(_ 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<Label, Destination> where Label: View, Destination: View {
public let subject: NavigationLink<Label, Destination>
public init(_ subject: NavigationLink<Label, Destination>) { self.subject = subject }
public init(_ subject: NavigationLink<Label, Destination>) {
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
}
}
}

View File

@ -15,10 +15,14 @@
// Created by Jed Fox on 06/30/2020.
//
final class NavigationContext: ObservableObject {
@Published var destination = NavigationLinkDestination(EmptyView())
}
public struct NavigationView<Content>: 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<Content>: View where Content: View {
}
/// This is a helper class that works around absence of "package private" access control in Swift
public struct _NavigationViewProxy<Content: View>: View {
public struct _NavigationViewProxy<Content: View> {
public let subject: NavigationView<Content>
public init(_ subject: NavigationView<Content>) { 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)
}
}

View File

@ -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")

View File

@ -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),
]))
}
}

View File

@ -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) {

View File

@ -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)")
}
}
}

View File

@ -16,27 +16,39 @@
//
import TokamakCore
import TokamakStaticHTML
extension _Button: ViewDeferredToRenderer where Label == Text {
public var deferredBody: AnyView {
let attributes: [String: String]
if buttonStyle.type == DefaultButtonStyle.self {
attributes = [:]
} else {
attributes = ["class": "_tokamak-buttonstyle-reset"]
}
return AnyView(DynamicHTML("button", attributes, listeners: [
"click": { _ in action() },
let listeners: [String: Listener] = [
"pointerdown": { _ in isPressed = true },
"pointerup": { _ in isPressed = false },
]) {
buttonStyle.makeBody(
configuration: _ButtonStyleConfigurationProxy(
label: AnyView(label),
isPressed: isPressed
).subject
)
})
"pointerup": { _ in
isPressed = false
action()
},
]
if buttonStyle.type == DefaultButtonStyle.self {
return AnyView(DynamicHTML(
"button",
["class": "_tokamak-buttonstyle-default"],
listeners: listeners
) {
HTML("span", content: label.innerHTML ?? "")
})
} else {
return AnyView(DynamicHTML(
"button",
["class": "_tokamak-buttonstyle-reset"],
listeners: listeners
) {
buttonStyle.makeBody(
configuration: _ButtonStyleConfigurationProxy(
label: AnyView(label),
isPressed: isPressed
).subject
)
.colorScheme(.light)
})
}
}
}

View File

@ -25,12 +25,35 @@ protocol AnyDynamicHTML: AnyHTML {
var listeners: [String: Listener] { get }
}
public struct DynamicHTML<Content>: View, AnyDynamicHTML where Content: View {
public struct DynamicHTML<Content>: 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<Content>: 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)]
}
}

View File

@ -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
}
)
}
}

View File

@ -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

View File

@ -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

View File

@ -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 ""
}
}
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) },

View File

@ -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)
}

View File

@ -35,7 +35,7 @@ struct NavItem: View {
init<V>(_ 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())
}

View File

@ -15,11 +15,19 @@
import TokamakCore
private extension DOMViewModifier {
func unwrapToStyle<T>(_ key: KeyPath<Self, T?>, property: String) -> String {
func unwrapToStyle<T>(
_ key: KeyPath<Self, T?>,
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;

View File

@ -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))"]
}
}

View File

@ -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 = """

View File

@ -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;
"""]
}

View File

@ -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
}

View File

@ -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)
}
}
}
}

View File

@ -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))"
}
}

View File

@ -18,16 +18,67 @@ extension PlainListStyle: ListStyleDeferredToRenderer {
public func sectionHeader<Header>(_ 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<Footer>(_ footer: Footer) -> AnyView where Footer: View {
AnyView(VStack {
Divider()
_ListRow.listRow(footer, self, isLast: true)
})
AnyView(
VStack(alignment: .leading) {
Divider()
_ListRow.listRow(footer, self, isLast: true)
}
.padding(.leading)
.frame(minWidth: 0, maxWidth: .infinity)
)
}
public func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
AnyView(section.padding(.leading).frame(minWidth: 0, maxWidth: .infinity))
}
public func listRow<Row>(_ row: Row) -> AnyView where Row: View {
AnyView(row.padding(.vertical))
}
}
extension InsetListStyle: ListStyleDeferredToRenderer {
public func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View {
AnyView(
header
.font(.system(size: 17, weight: .medium))
.padding(.vertical, 4)
.padding(.leading, 24)
.background(Color.listSectionHeader)
.frame(minWidth: 0, maxWidth: .infinity)
)
}
public func sectionFooter<Footer>(_ footer: Footer) -> AnyView where Footer: View {
AnyView(
VStack(alignment: .leading) {
Divider()
_ListRow.listRow(footer, self, isLast: true)
}
.padding(.leading, 24)
.frame(minWidth: 0, maxWidth: .infinity)
)
}
public func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
AnyView(
section
.padding(.leading, 24)
.frame(minWidth: 0, maxWidth: .infinity)
)
}
public func listRow<Row>(_ row: Row) -> AnyView where Row: View {
AnyView(row.padding(.vertical))
}
}
@ -35,8 +86,7 @@ extension GroupedListStyle: ListStyleDeferredToRenderer {
public func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
AnyView(
content
.padding(.top, 20)
.background(Color(0xEEEEEE))
.background(Color.groupedListBackground)
)
}
@ -44,15 +94,18 @@ extension GroupedListStyle: ListStyleDeferredToRenderer {
AnyView(
header
.font(.caption)
.padding(.leading, 20)
.padding([.top, .leading])
.frame(minWidth: 0, maxWidth: .infinity)
)
}
public func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
AnyView(
section
.background(Color.white)
.padding(.leading)
.background(Color.listGroupBackground)
.padding(.top)
.frame(minWidth: 0, maxWidth: .infinity)
)
}
@ -60,41 +113,38 @@ extension GroupedListStyle: ListStyleDeferredToRenderer {
AnyView(
footer
.font(.caption)
.padding(.leading, 20)
.padding([.top, .leading])
)
}
public func listRow<Row>(_ row: Row) -> AnyView where Row: View {
AnyView(row.padding(.vertical))
}
}
extension InsetGroupedListStyle: ListStyleDeferredToRenderer {
public func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
AnyView(
content
.padding(.top, 20)
.background(Color(0xEEEEEE))
)
}
public func listRow<Row>(_ row: Row) -> AnyView where Row: View {
AnyView(
row
.padding([.leading, .trailing, .top, .bottom])
)
AnyView(content.background(Color.groupedListBackground))
}
public func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View {
AnyView(
header
.font(.caption)
.padding(.leading, 20)
.padding([.top, .leading])
.padding(.leading)
.frame(minWidth: 0, maxWidth: .infinity)
)
}
public func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
AnyView(
section
.background(Color.white)
.padding(.leading)
.background(Color.listGroupBackground)
.cornerRadius(10)
.padding(.all)
.padding([.horizontal, .top])
.frame(minWidth: 0, maxWidth: .infinity)
)
}
@ -102,18 +152,66 @@ extension InsetGroupedListStyle: ListStyleDeferredToRenderer {
AnyView(
footer
.font(.caption)
.padding(.leading, 20)
.padding([.top, .leading])
.padding(.leading)
)
}
public func listRow<Row>(_ row: Row) -> AnyView where Row: View {
AnyView(row.padding(.vertical))
}
}
// TODO: Make sections collabsible (see Section.swift for more impl. details)
extension SidebarListStyle: ListStyleDeferredToRenderer {
public func sectionHeader<Header>(_ 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: Row) -> AnyView where Row: View {
AnyView(row.frame(minWidth: 0, maxWidth: .infinity))
}
public func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
AnyView(
content
._navigationLinkStyle(_SidebarNavigationLinkStyle())
.padding([.horizontal, .top], 6)
.background(Color.sidebarBackground)
)
}
}
extension SidebarListStyle: ListStyleDeferredToRenderer {
public func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
AnyView(
content
.padding(.all)
.padding(.leading, 20)
.background(Color(0xF2F2F7))
)
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)
}
}
}

View File

@ -34,11 +34,32 @@ extension AnyHTML {
}
}
public struct HTML<Content>: View, AnyHTML where Content: View {
public struct HTML<Content>: View, AnyHTML {
public let tag: String
public let attributes: [String: String]
let content: Content
public let innerHTML: String?
public var body: Never {
neverBody("HTML")
}
}
extension HTML where Content: StringProtocol {
public init(
_ tag: String,
_ attributes: [String: String] = [:],
content: Content
) {
self.tag = tag
self.attributes = attributes
self.content = content
innerHTML = String(content)
}
}
extension HTML: ParentView where Content: View {
public init(
_ tag: String,
_ attributes: [String: String] = [:],
@ -47,12 +68,11 @@ public struct HTML<Content>: View, AnyHTML where Content: View {
self.tag = tag
self.attributes = attributes
self.content = content()
innerHTML = nil
}
public var innerHTML: String? { nil }
public var body: Never {
neverBody("HTML")
public var children: [AnyView] {
[AnyView(content)]
}
}
@ -65,12 +85,6 @@ extension HTML where Content == EmptyView {
}
}
extension HTML: ParentView {
public var children: [AnyView] {
[AnyView(content)]
}
}
public protocol StylesConvertible {
var styles: [String: String] { get }
}

View File

@ -17,12 +17,14 @@ import TokamakCore
extension NavigationView: ViewDeferredToRenderer {
public var deferredBody: AnyView {
AnyView(HTML("div", [
"style": """
display: flex; flex-direction: row; align-items: stretch;
width: 100%; height: 100%;
""",
"class": "_tokamak-navigationview",
]) {
_NavigationViewProxy(self)
_NavigationViewProxy(self).content
HTML("div", [
"class": "_tokamak-navigationview-content",
]) {
_NavigationViewProxy(self).destination
}
})
}
}

View File

@ -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;
""",
]

View File

@ -89,14 +89,32 @@ 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 +122,19 @@ 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 +144,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
@ -135,30 +165,27 @@ extension Text: AnyHTML {
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": """
\(font?.styles.filter {
if weight != nil {
return $0.key != "font-weight"
} else {
return true
}
}.inlineStyles ?? "")
\(font?.styles.filter { weight != nil ? $0.key != "font-weight" : true }.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
?? "inherit")
text-decoration-color: \(decorationColor)
""",
"class": isRedacted ? "_tokamak-text-redacted" : "",
]

View File

@ -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
}